#define min(a,b) ((a) < (b) ? (a) : (b))
#define max(a,b) ((a) > (b) ? (a) : (b))
我常常會用這兩個宏,一般來說,宏比函數更高效,但是如果里面元素是函數的話,就不是這樣。因為宏只是在編譯預處理階段執行簡單替換,如果你有兩個函數f和g,代碼中寫了min(f(a),g(a)),那么就會被展開為 ((f(a)) < (g(a)) ? (f(a)) : (g(a))),這是4次調用,而不是想象中的先算f(a),再算g(a),然后傳給min——那是函數的行為,不是宏!
如果f和g是遞歸函數的話,結果更糟糕
實際上,C++的STL的algorithm中有min,max的模板函數,大概這個樣子:
template <class T> const T& min ( const T& a, const T& b ) { return !(b<a)?a:b; // or: return !comp(b,a)?a:b; for the comp version }
看上去和我們的宏很像嘛,只是多了一層函數的包裝——但是它是函數,是要傳參的,而不是像宏那樣執行替換,所以min(f(a),g(a))完全沒有問題,只會調用f和g各一次。
其實說完全沒有問題還是有點心虛的,如果f是遞歸函數,f函數中有調用min(f(a), f(b))的話,遞歸過程中會有很多個min等待傳值(在遞歸棧中),調用深度似乎是增加了一倍,不過可能這不是很大的問題,不過考慮效率的話,可以這么寫
int f(int a){ //some code.....可能是處理遞歸邊界什么的 r1 = f(a - 1) * 3;//遞歸調用 r2 = f(a - 1) * 2 + 12; return min(r1,r2); }
也就是說,先算出來,再用min,這樣無論min是函數還是宏都沒問題,不過不像直接把函數寫在min里那么簡潔……簡潔有時是要付出一點點效率上的代價的,而追求效率有時候會很羅嗦……
STL中的min/max還有一個好處是支持自定義類型,可以通過重載<運算符,也可以通過提供比較函數
因此,我決定暫時不使用這個min/max宏了,直接用STL中的min/max模板函數,還省得敲這么兩行代碼
下面是一個例子,展示了我是如何犯了這個錯誤,然后突然醒悟的(肯定有書上講過關於宏的這個注意點,貌似我還看到過,但是……人要犯錯,你怎么能攔得住?)
原始的錯誤記錄在這里http://www.cnblogs.com/fstang/archive/2012/12/23/2829946.html (這是一篇挺長的文章,且包含新的內容(Topcoder上的一道題),如果你只想了解關於本文宏的相關內容就不必跳過去了)
我還保留了原來錯誤的地方,為了方便閱讀,我把對比代碼貼在這里,主要觀察f和f2的寫法上的區別(其實這個在前面也講過了,一個是直接把函數放到min宏里,一個是先算,然后用min……別看代碼了,直接看最后的函數調用次數的統計結果吧)
#include <string> #include <iostream> #include <vector> #include <algorithm> #include <map> using namespace std; #define min(a,b) ((a) < (b) ? (a) : (b)) class MonstersValley2{ public: int cnt; MonstersValley2():cnt(0){} int minimumPrice(vector <int> dread, vector <int> price){ int r1 = f(0, 0, dread, price); cout << "function f called " << cnt << "times\n"; cnt = 0; int r2 = f2(0, 0, dread, price); cout << "function f2 called " << cnt << "times\n"; if (r1 != r2) { cerr << "error\n"; } return r1; } int f(long long sum, int idx, vector <int> &dread, vector <int> &price){ cnt++;//統計調用次數 if (idx >= dread.size()) { return 0; } if (sum >= dread[idx]) { int m = min(price[idx] + f(sum + dread[idx], idx + 1, dread, price), f(sum, idx + 1, dread, price)); return m; } else { int m = price[idx] + f(sum + dread[idx], idx + 1, dread, price); return m; } } int f2(long long sum, int idx, vector <int> &dread, vector <int> &price){ cnt++;//統計調用次數 if (idx >= dread.size()) { return 0; } if (sum >= dread[idx]) { int m1 = price[idx] + f2(sum + dread[idx], idx + 1, dread, price);//先算,再調用min int m2 = f2(sum, idx + 1, dread, price); return min(m1,m2); } else { int m = price[idx] + f2(sum + dread[idx], idx + 1, dread, price); return m; } } }; int main(int argc, char* argv[]) { int a[] = {200, 107, 105, 206, 307, 400}; int b[] = {1, 2, 1, 1, 1, 2}; MonstersValley2 m; cout << m.minimumPrice(vector<int>(a, a+sizeof(a)/sizeof(int)), vector<int>(b, b+sizeof(b)/sizeof(int))) << endl; return 0; }
輸出:
function f called 221times
function f2 called 53times
2
如果換一個更大的輸入實例:
int a[] = {5216, 12512, 613, 1256, 66, 17202, 30000, 23512, 2125, 33333}; int b[] = {2, 2, 1, 1, 1, 1, 2, 1, 2, 1};
輸出:
function f called 7683times
function f2 called 393times
5
前一個相差4倍,后一個接近20倍,因為這本來就是一個(隨問題規模增大,復雜度指數增長的)遞歸函數,每個規模為n的問題可能變為2個規模為n-1的問題,也可能變為1個規模為n-1的問題,所以最壞情況復雜度是O(2^n),最好情況時間復雜度為O(n)
但是由於宏的不慎使用,每個規模為n的問題可能變為4個規模為n-1的問題,或變為1個規模為n-1的問題,最壞情況復雜度是O(4^n),最好情況時間復雜度為O(n)