到目前為止,我們已經了解了 C++ STL 標準庫中 vector、deque 和 list 這 3 個容器的功能和具體用法。學習過程中,讀者是否想過一個問題,即這些容器的模板類中都提供了 empty() 成員方法和 size() 成員方法,它們都可以用來判斷容器是否為空。
換句話說,假設有一個容器 cont,則此程式碼:
if(cont.size() == 0)
本質上和如下程式碼是等價的:
if(cont.empty())
那麼,在實際場景中,到底應該使用哪一種呢?
建議使用 empty() 成員方法。理由很簡單,無論是哪種容器,只要其模板類中提供了 empty() 成員方法,使用此方法都可以保證在 O(1) 時間複雜度內完成對“容器是否為空”的判斷;但對於 list 容器來說,使用 size() 成員方法判斷“容器是否為空”,可能要消耗 O(n) 的時間複雜度。
注意,這個結論不僅適用於 vector、deque 和 list 容器,後續還會講解更多容器的用法,該結論也依然適用。
那麼,為什麼 list 容器這麼特殊呢?這和 list 模板類提供了獨有的 splice() 成員方法有關。
深度剖析選用empty()的原因
做一個大膽的設想,假設我們自己就是 list 容器的設計者。從始至終,我們的目標都是讓 list 成為標準容器,並被廣泛使用,因此打造“高效率的 list”成為我們奮鬥的目標。
在實現 list 容器的過程中我們發現,使用者經常需要知道當前 list 容器中存有多少個元素,於是我們想讓 size() 成員方法的執行效率達到 O(1)。為了實現這個目的,我們必須重新設計 list,使它總能以最快的效率知道自己含有多少個元素。
要想令 size() 的執行效率達到 O(1),最直接的實現方式是:在 list 模板類中設定一個專門用於統計儲存元素數量的 size 變數,其位於所有成員方法的外部。與此同時,在每一個可用來為容器新增或刪除元素的成員方法中,新增對 size 變數值的更新操作。由此,size() 成員方法只需返回 size 這個變數即可(時間複雜度為 O(1))。
不僅如此,由於 list 容器底層採用的是鏈式儲存結構(也就是連結串列),該結構最大的特點就是,某一連結串列中存有元素的節點,無需經過拷貝就可以直接連結到其它連結串列中,且整個過程只需要消耗 O(1) 的時間複雜度。考慮到很多使用者之所以選用 list 容器,就是看中了其底層儲存結構的這一特性。因此,作為 list 容器設計者的我們,自然也想將 splice() 方法的時間複雜度設計為 O(1)。
這裡就產生了一個矛盾,即如果將 size() 設計為 O(1) 時間複雜度,則由於 splice() 成員方法會修改 list 容器儲存元素的個數,因此該方法中就需要新增更新 size 變數的程式碼(更新方式無疑是通過遍歷連結串列來實現),這也就意味著 splice() 成員方法的執行效率將無法達到 O(1);反之,如果將 splice() 成員方法的執行效率提高到 O(1),則 size() 成員方法將無法實現 O(1) 的時間複雜度。
也就是說,list 容器中的 size() 和 splice() 總有一個要做出讓步,即只能實現其中一個方法的執行效率達到 O(1)。
值得一提的是,不同版本的 STL 標準庫,其底層解決此衝突的抉擇是不同的。以本教學所用的 C++ STL 標準模板庫為例,其選擇將 splice() 成員方法的執行效率達到 O(1),而 size() 成員方法的執行效率為 O(n)。當然,有些版本的 STL 標準庫中,選擇將 size() 方法的執行效率設計為 O(1)。
但不論怎樣,選用 empty() 判斷容器是否為空,效率總是最高的。所以,如果程式中需要判斷當前容器是否為空,應優先考慮使用 empty()。