1. 程式人生 > >讀c++ primer有感——stl效能leetcode刷題直觀測試

讀c++ primer有感——stl效能leetcode刷題直觀測試

用leetcode刷題對stl效率的感性認識。期望證實即使對stl(或某些c++特性)即使在不熟練的應用下也和裸寫c沒有效能代差(就是有差距也是小系數線性的),後面的文章會用string原始碼為例剖析下stl的實現細節和效能,而本文只是個簡單的事實堆砌。
ps:stl網上有不少說法,這裡就取standard template library的本意,用模板實現的那部分容器演算法迭代器和各種trait都算,string也算,因為string也是基於模板的。
說明:
1.leetcode測試比較完備方便,即改即傳即比;
2.刷leetcode的有不少有志之士,得到的效率分佈資料相對靠譜;
3.刷的全是leetcode中easy的題,因為本文目的不是看演算法效率,而是看程式碼優化效率;
4.是一口氣全寫完後,string原始碼看完後再寫的部落格,所以知道的有爛的地方沒有改,因為目的是希望得到stl實現和純c實現沒有代差;
5.以前也做過不多的leetcode,和同學一起刷,發生過leetcode說某個程式碼效率低但一起分析和自測後發現該程式碼更優的情況,所以leetcode的結果是作參考用的;
6.到寫的時候發現......!!!不同語言的效率對比沒有了!!!好吧,和c++最優的比估計和c最優解比差不多,畢竟前者對後者的完全相容。

415. Add Strings
題意:字串模擬大數相加;string addStrings(string num1, string num2);
思路:
1.如果num2比num1長,swap二者,從而保證num1不比num2短;
2.num1和num2都和string("0")相加,string("0") + num1,從而進位不用特殊處理;
3.用std::transform,遍歷num1和num2,把結果寫回num1。加操作用lamda函式,進位與否用函式區域性變數up表示;
4.最後看第一位是否進位(為1?),不是就把0給剪掉。
可能的敗筆:
1.和string("0")相加導致string總體向後移位;
效率對比:
前85%,比最優速度差25%,如果在自己的實際工作中無論何時都可以忽略不計。

121. Best Time to Buy and Sell Stock
題意:int maxProfit(vector<int>& prices);對於prices,找到prices[j] - prices[i]的最大值,其中i<=j;
思路:
1.從陣列第一個元素開始遍歷陣列,找到區域性最高最低點push_back到一個新陣列,比如76543212321,那麼從7開始一路下降到的1就是一個最低點,再往後一路上升到的3是最高點;
2.呵呵,從第一個元素開始遍歷暴力破解...,std::max_element查詢之後的最大元素與當前元素做差值。
可能的敗筆:
1.暴力破解...
效率對比:
前35%,比最優速度差25%,實際工作中忽略不計即可。

202. Happy Number
題意:“開心數”這種數要麼能夠迴圈回某個非開心數迴圈,要麼就各位數平方和為1。
思路:
1.用set<int>存已經開心過的數,如果某一步迴圈回開心過的數,那麼返回false。
敗筆:
1.求各位數平方和說不定有啥非除10求餘再相加的解法,比如構造一個array儲存;
效率對比:
前98%,但是存在0ms的解,實際工作中如果此步驟重要且重複需要進一步查詢深究。不過這個效率差距大概率在演算法。

326. Power of Three
題意:檢查一個數是否是3的某次方;bool isPowerOfThree(int n)。
思路:
1.暴力破解,3一直乘下去直到大於等於該數n;
2.注意資料溢位;
3.對於int可以取中位的數,如果大於該數,從該數開始乘;
敗筆:
1.int想當然認為是32位並且闖對了,可以用static_cast<unsigned int>(-1) / 2或者climits下面的INT_MAX;
效率對比:
前百分之80,這個倒和stl無關。

231. Power of Two
題意:檢查一個數是否是2的某次方;bool isPowerOfTwo(int n);
思路:
1.2每次乘方從位角度看是唯一的置位左移一位;
2.bitset<64>(n).count() == 1。
敗筆:
暫無。
效率對比:
前百分之100。

83. Remove Duplicates from Sorted List
題意:從排序連結串列中刪除重複結點;ListNode* deleteDuplicates(ListNode* head);
思路:
1.注意各種情況的邊界條件;
2.delete結點;
敗筆:
1.可以嘗試定義class lnIter{ListNode * ln;},過載lnIter的前置++操作符和*、==,再用std::unique把重複的移動到連結串列最後,在手動刪除或者remove_if。實現細節不少,但是std::unique保證無bug。
效率對比:
前百分之75,比最快的慢25%,實際工作可以忽略。

35. Search Insert Position
題意:int searchInsert(vector<int>& nums, int target);對於給定排好序的陣列nums,找到最後一個不大於等於target的nums中數的下標;
思路:
1.return std::lower_bound(nums.cbegin(),nums.cend(),target) - nums.cbegin();用lower_bound找到下標;
敗筆:
想不到。
效率對比:
前百分之百。

70. Climbing Stairs
題意:爬梯子;int climbStairs(int n);其實是對於n,找到一個數列1 2 1 1 2 1......只能有2或者1,並且數列之和為n,問這種組合的個數;
思路:
1.只能想到遞迴了,對於偶數的n = 2m,對第m階臺階來說,有剛好走到m階和走過m階走到m+1階兩種情況:對於剛好走到m階的情況,下一次不能走一步,因為走1步就會走到m+1階,那麼和情況2重複了,所以情況1有climbStairs(m) * climbStairs(m - 2)種走法;剛好走到m+1階的情況2有climbStairs(m+1) * climbStairs(m-1)種走法;情況1和2之和就可以遞迴;
敗筆:
1.非遞迴解法,需要查證。
效率對比:
前百分之100,不過此題和stl無關。

437. Path Sum III
題意:int pathSum(TreeNode* root, int sum);找到二叉樹結點和等於sum的所有路徑數量和;
思路:
1.遞迴,但是寫得很麻煩,無法總結思路;
2.set<pair<TreeNode*,int>> tSet,用一個set來儲存遍歷過的結點和到該結點的當前數之和的二元組;
敗筆:
1.此題
效率對比:
前百分之一,效率比最好的差了十倍。但是估計是演算法差距。

53. Maximum Subarray
題意:int maxSubArray(vector<int>& nums);求最大連續子序列和;
思路:
1.先斬頭去尾,把頭尾的負數或0全部掉;
2.從vector最後一位開始向前加一直到前一位是負數,把和tempSum按情況賦值給max,再把tempSum往前加一直加到下一位是正數,如果tempSum此時是正數則加入下一回合計算,否則tempSum清0;
3.返回tempSum。
敗筆:
1.可以從第一位開始加,並且可以用find和accumlate計算。
效率對比:
前百分之百。

191. Number of 1 Bits
題意:int hammingWeight(uint32_t n);返回n二進位制1的數量;
思路:
1.bitset<64>(n).count();
敗筆:
1.可以用查表方式逐位元組試試;也許還有某種位運算玩法;
效率對比:
前百分之5,效率是最快解法三分之一,如果實際工作這裡不是關鍵重複處,可以接受,畢竟寫法優雅呵呵。

263. Ugly Number
題意:bool isUgly(int num);如果一個數只能被2 3 5整除,那麼num就是醜陋數;
思路:
1.除2一直到餘數為0,再除3,除5,如果剩下1,那麼就是醜陋的;
敗筆:
1.暴力破解,可以嘗試用素數去除;
效率對比:
前百分之35,是最好效率的一半,效率可接受,但解法太醜。

217. Contains Duplicate
題意:bool containsDuplicate(vector<int>& nums);陣列中是否有重複數;
思路:
1.std::set<int> numSet,通過insert判斷是否insert成功;
敗筆:
1.unordered_multiset,雜湊表應該更好;雜湊表大小和哈系函式優化;這道題後面值得深入;
效率:
前百分之80,比最快慢了10%。

206. Reverse Linked List
題意:ListNode* reverseList(ListNode* head);反轉連結串列;
思路:
1.直接反轉;
敗筆:
1.和unique要求的ForwardIterator不同,std::reverse需要雙向迭代器,封裝ListNode *呼叫stl恐怕不行;
效率:
前百分之85,比最快慢33%,但是和stl無關。

350. Intersection of Two Arrays II
題意:vector<int> intersect(vector<int>& nums1, vector<int>& nums2),求nums1和nums2的交集;
思路:
1.先sort重新排序兩個陣列,再std::set_intersection求交集;
敗筆:
效率:
前百分之百,也證明了stl的高效。

551. Student Attendance Record I
題意:bool checkRecord(string s),如果字串多餘一個A,或者有兩個以上連續的L,返回false;
思路:
1.用find_first_not_of分析出現A或者L的具體情況;
敗筆:
效率:
前百分之100。

268. Missing Number
題意:int missingNumber(vector<int>& nums),nums含有0-n,n+1個數中的n個,找出少的那個數;
思路:
1.std::accumlate求和,再用公式求0-n的總和,返回差值;
敗筆:
效率:
前百分之50,比最快慢五分之一。

541. Reverse String II
題意:string reverseStr(string s, int k),s按照k,先反轉k個,再跳過k個。
思路:
1.用std::reverse;
敗筆:
1.應該使用string自帶的reverse,std不會比string更懂自己;
效率:
前百分之百。

543. Diameter of Binary Tree
題意:int diameterOfBinaryTree(TreeNode* root),求二叉樹相差最遠的兩個結點距離;
思路:
1.遞迴,int findMax(TreeNode *root),返回樹的最大深度,二叉樹的周長等於左右子樹深度之和,在求左右子樹最大深度的同時更新周長;
效率:
前百分之40,比最快的慢三倍,此題和stl無關。

371. Sum of Two Integers
題意:求兩個整數之和,不能用+-號;int getSum(int a, int b);
思路:
1.用個intWrapper,定義一個減法操作實際執行加法,int到intWrapper的建構函式,intWrapper到int的隱式轉化;
敗筆:
1.可以看下編譯結果看是否最終程式碼沒有呼叫任何函式;
效率:
前百分之40,儘管和stl無關,知識相通。

258. Add Digits
題意:返回數字各位數字之和,直到之和為各位數為止;
思路:
1.遞迴呼叫;
2.每一次遞迴,用to_string把int轉換為string,再呼叫accumulate相加,再減去'0'*str.length();
效率:
前百分之65,比最快慢一倍。

283. Move Zeroes
題意:void moveZeroes(vector<int>& nums),把nums中的0移到最後,其他的相對位置不變;
思路:
1.建議一個和nums等長初始值為0的陣列;
2.copy_if拷貝;
效率:
前百分之90,額,其實此題要求不能拷貝陣列,呵呵。

530. Minimum Absolute Difference in BST
題意:求二叉搜尋樹中兩個相鄰node的最大差值;相鄰的node B是比當前node A大或者等於的第一個數;
思路:
1.遍歷二叉樹得到所有的數,sort,transform得到相鄰數字差陣列,再用min_element找到最小差值返回。
效率:
前百分之50,比最快慢25%。

506. Relative Ranks
題意:vector<string> findRelativeRanks(vector<int>& nums),求nums前三大的數;
思路:
1.std::sort,再遍歷nums,用std::lower_bound找num對應的rank;
效率:
前百分之60。

時間緊張,下面的就寫效率了。
167. Two Sum II - Input array is sorted
80%。

383. Ransom Note
先統計note中的字元頻率,再find_if。
60%。

349. Intersection of Two Arrays
和350比起來多一個unique和resize。
100%。

453. Minimum Moves to Equal Array Elements
一個move等價於一個數減1。先min_element求到最小值min,再遍歷求差值之和。
可以accumulate求和再求差值。
5%,比最快的慢60%。

455. Assign Cookies
80%。

387. First Unique Character in a String
用一個int cCount[26]計數。
90%。

100. Same Tree
遍歷樹,用字串作為遍歷的路線圖,比如7l8m9r,前序遍歷,7左子樹結點(l),最後比較路線圖字串是否相同。路線圖相同==數相同,需要證明。
65%。

237. Delete Node in a Linked List
85%。

169. Majority Element
先sort排序,返回nums[nums.size()/2]。
60%。慢30%。

409. Longest Palindrome
85%,慢一倍。

504. Base 7
10%,慢三倍。

242. Valid Anagram
先sort,再==比較。
其實統計字元數就可以了。
3%,慢4倍。

389. Find the Difference
accumlate之後求差值,需要注意溢位。
70%。

508. Most Frequent Subtree Sum
50%,和stl無關。

448. Find All Numbers Disappeared in an Array
std::iota填充滿一個1-n 的陣列,在sort源陣列,再set_difference求得差集。
30%,慢50%。

520. Detect Capital
用any_of和find_if。
75%。

136. Single Number
75%。

442. Find All Duplicates in an Array
20%,和stl無關。

485. Max Consecutive Ones
for_each,lamda函式,加區域性變數。
30%,慢25%。

406. Queue Reconstruction by Height
8%,和stl無關。

463. Island Perimeter
70%,和stl無關。

344. Reverse String
直接調std::reverse。
應該調string::reverse。
30%,慢80%。

412. Fizz Buzz
generate_n和lamda函式生成。
80%。

500. Keyboard Row
用到了find_if。
70%。

557. Reverse Words in a String III
用到了istringstream和reverse。
50%,慢40%。

461. Hamming Distance
40%,和c++新特性無關。

最後,從這些例子,我自己得到初步結論,使用stl不熟練時應該根據使用場景來測試程式碼效能選擇是否使用;熟練使用後可以減少測試工作,通過分析來看是否用stl。一般來說,直接用,stl沒bug,文件清晰,拋異常規範,介面通用,功能全面,這是優點;原始碼晦澀,不知道在幹什麼,這是不足(程式碼不易讀,知乎和stackoverflow不少人都這麼說,《stl原始碼剖析》頭一章也顯而易見);寫stl的人是業界最牛的人,他們的程式碼可以放心使用,如果不好用,那說不定是自己用不好,這也是優點。