實用編程技巧匯總,讓代碼效率提高一個檔次

老九學堂 發佈 2020-01-16T23:47:16+00:00

在編程過程中有小夥伴說我敲代碼又不好看還慢怎麼辦?

在編程過程中

有小夥伴說我敲代碼又不好看還慢

怎麼辦?


今天大雄給大家介紹幾個編程小技巧

讓你的代碼迅速提高檔次


for循環


1

for循環變量初始化

在c語言中,我們常常這樣使用for語句:


for (int i = 0; i < strlen(s); i++)


這看起來似乎很完美,代碼也很漂亮,讓我們再看看另一種寫法:


for (int i = 0, len = strlen(s); i < len; i++)


二者唯一的不同在於後者用len變量將字符串s的長度保存了,在條件判斷時直接將i與len比較。


第二種方法用一個額外變量len避免了每次條件判斷都要重複執行函數strlen(s),而執行該函數是非常耗時的(假設字符串的長度為n,函數執行的複雜度為O(n)),尤其是當for循環體的語句比較少,字符串比較長的時候。


在很多leetcode題目中,兩種不同的寫法需要的運行時間相差巨大。


同樣在C++、Java中,這種寫法for (int i = 0; i < s.length(); i++),也是不值得推薦的。


儘管C++編譯時期有的編譯器會將length()函數用內聯或者一個確定的變量來替代,Java也會將其用「屬性」來替代,但很多小夥伴仍然傾向於使用後者。


有意思的是,在Python的語法中,for循環用這種方式來表示:


for i in range(len(s))


這就避免了重複去求字符串s的長度,這種方法既有語義感,又獲得了高性能。


2


變量定義位置(for循環內部還是外部)


//內部for (int i = 0; i < 10; i++){ string s = ss[i]; ...}//外部string s;for (int i = 0; i < 10; i++){ s = ss[i]; ...}


如果定義在內部,每次循環都要重新定義string變量s,意味著每次循環都要調用構造和析構函數;而定義在外部每次循環只需要調用複製構造函數。


一般建議將大的對象定義到外部,提高運行效率,把小的對象定義在裡面,提高程序可讀性。


基本運算和函數


1

在乘以2(或2的整數次冪)或除以2(或2的整數次冪)的時候儘量用位運算來替代。


2

儘量減少使用除法運算(可以適當轉換為乘法,如條件判斷時將if (a == b / c)替換為if (a * c == b)。除法運算需要更多的移位和轉換操作,往往需要的時間是相應乘法的兩倍)


3

多使用+=、-=、*=、/=等複合運算符,以加一為例,效率由高到低是(i++ 、 i += 1 、 i = i + 1)


4

多掌握一些小巧的庫函數,例如:swap, max, min, sort, qsort, ati, stoi...它們用起來方便,效率更是比一般人寫的代碼高。


inline、const、&修飾符


inline讓函數內聯,建議編譯器將函數體代碼「複製粘貼」到函數調用處,在函數體短小,函數調用又比較頻繁的時候能有效避免因函數調用帶來的內存開銷(因為每一次調用函數系統都會生成許多額外的變量)。


const不僅僅可以保證其修飾的變量不被修改,提高程序的穩定性,同時也讓編譯器更好地為我們優化代碼。


舉個例子:我們如果用const修飾某一個常量,那麼程序中所有用到該常量的地方都會用其值來代替,這樣就避免了讀取其地址而浪費時間。


&修飾返回值類型和參數類型表示採取引用的方式傳遞,避免了對象賦值構造所需的時間和內存。


緩存(cache)和寄存器(register)


除了CPU,就是寄存器和緩存的訪問速度最高了,一般不建議我們自己定義寄存器變量和控制數據緩存,編譯器會自動幫我們把經常用到的一些數據放到緩存和寄存器中。


但是,了解一些編譯器控制數據的依據對編程也有極大幫助。


一般來說,放到寄存器/緩存的數據優先級為:用register修飾的變量,循環控制變量,auto局部變量,靜態變量,用戶自己分配的內存數據。


疊代器(iterator)


1

訪問容器中元素的時候儘量使用疊代器而不是下標或者指針。


首先,疊代器訪問元素類似與指針,相對於下標訪問不用根據下標值計算地址,這在循環中能夠節省不少時間。


其次,疊代器作為指針一種延拓,能更好的代表並操作其所指的對象,而在下標訪問中我們往往用一個int值pos來表示pos下標下的元素,沒有面向對象編程的直觀。


再次,疊代器為我們訪問各種容器(數組,vector,list,map,queue,deque,set …)中的元素提供了統一的方法,其作用類似於「語法糖」,讓編程更加簡單、方便。


2

另外在使用疊代器的自增和自減運算符需要注意,iterator++,和++iterator的效率有天壤之別。兩種自增方式的運算符重載如下:


iterator & operator++(){  // 前增 ++*this; return (*this);}iterator operator++(int){  // 後增 iterator temp = *this; ++*this; return (temp);}


  • 後增(iterator++)相對於前增(++iterator)創建了一個臨時疊代器temp,並將其返回,而前增直接返回原來疊代器的引用。
  • 在for循環中的頻繁自增操作中,創建臨時疊代器temp,以及返回temp時調用的複製構造函數所需的時間不容忽視。


vector容器


vector容器毫無疑問是C++STL使用最為頻繁的容器了,當然這個強大容器的使用也包含了很多的小技巧。


1

在適當時候使用emplace和emplace_back函數來替代insert和push_back函數。


它們之間的區別很明顯,insert和push_back函數參數是vector容器裡面的模板對象,而帶emplace的函數參數是模板對象的構造函數的參數。


這意味著後者將模板對象插入到vector容器的過程中不用先生成好對象,而是可以直接利用參數構造。


當然如果模板對象已經是生成好的,那就沒有必要用emplace函數了。


在很多循環遞歸疊代中,往往需要反覆向vector容器中添加對象,這時候額外構造一個對象所需要的時間和空間就不容忽視了,因此這是一個vector進階用法的好trick。


2

vector容器的底層實現是數組,並且在當元素大於最大容量的時候會重新生成一個更大的數組,將原來數組中的對象複製構造到新數組中。


由於要重新分配大量內存以及反覆調用複製構造函數,這對時間和空間的開銷是巨大的。


為了減少內存的重新分配,我們可以適當的估計我們需要保存的元素數量,並在vector初始化的時候指定其capacity。


這種方法很直接但也有其缺點,就是我們往往很難在開始的時候就估計準確我們要保存的元素數量(如果能,我們就直接用數組得了)。


一個很好的解決辦法是:將vector中保存的元素改為指針,指針指向我們真正想要保存的對象。


由於指針相對於其所指向的對象來說占用內存很小,而且在複製的時候不用調用複製構造函數,因此以上提到的一些缺點都能很好的克服。


事實上,對於能夠熟練控制內存分配的老碼農來說,這種vector + 指針的方式是十分完美的。


if條件判斷


在進入討論之前,我們先思考下面這個例子:


一個班的數學成績如下:74、76、78、94、97、68、77、65、54、89…,總共有50個數據。


要求用程序將分數為優秀(>=80)、良好(>=70)、及格(>=60)、不及格(>=0)四個分數段。


for 所有學生分數 if 分數 < 60   歸為不及格段 else if 分數 < 70   歸為及格段 else if 分數 < 80   歸為良好段 else    歸為優秀段


這個偽代碼邏輯沒有問題,但是就這個數據來看這段代碼運行效率糟透了。


由於這個班的數學成績絕大多數是良好和優秀,而這個程序需要三次if判斷才能將分數歸為良好,三次if判斷加上一個else才能將分數歸為優秀,所以絕大多數前兩個if判斷是不必要的。


我們將if判斷語句的順序變換下:


for 所有學生分數 if 分數 >= 80   歸為優秀段 else if 分數 >= 70   歸為良好段 else if 分數 >= 60   歸為及格段 else    歸為不及格段


在這個偽代碼中絕大多數分數都在前兩個if語句中完成了分段。兩者的時間效率相差巨大,實際運行也發現,前者是後者運行時間的兩倍多。


switch分支判斷


switch語句的底層實現主要有三種方式:轉換為if else 語句,跳轉表,樹形結構。


當分支比較小時,編譯器傾向於轉換為if else語句,當分支比較多,分支範圍很廣時,用樹形結構,當分支數量不算多,分支範圍緊湊時,用跳轉表。


跳轉表的底層實現是數組映射,對條件轉換的效率為O(1),相比於另外兩種方式優勢明顯,因此我們應該儘量控制分支的數量,以及讓各個分支的int型數據緊湊。


這些技巧小夥伴們都了解了嗎?

#你在編程過程中還有什麼小技巧#



更多乾貨關注公眾號【老九學堂】(づ ̄3 ̄)づ╭❤~

關鍵字: