1. 程式人生 > >這幾個程式設計小技巧,讓你程式碼效率提高一個檔次

這幾個程式設計小技巧,讓你程式碼效率提高一個檔次

轉載自程式人生公眾號 2018/9/3

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的長度,這種方法既有語義感,又獲得了高效能。

1.變數定義位置(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.訪問容器中元素的時候儘量使用迭代器而不是下標或者指標。在剛從C語言轉到C++的那段時間,我非常不適應迭代器的用法,總覺得下標訪問多好,與for迴圈搭配在一起簡直是無敵的存在。後來我才慢慢發掘出迭代器的眾多優勢。首先,迭代器訪問元素類似與指標,相對於下標訪問不用根據下標值計算地址,這在迴圈中能夠節省不少時間。其次,迭代器作為指標一種延拓,能更好的代表並操作其所指的物件,而在下標訪問中我們往往用一個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型資料緊湊。