[技術] OIer的C++標准庫 : STL入門


注: 本文主要摘取STL在OI中的常用技巧應用, 所以可能會重點說明容器部分和算法部分, 且不會討論所有支持的函數/操作並主要討論 C++11 前支持的特性. 如果需要詳細完整的介紹請自行查閱標准文檔.

原始資料源於各大C++參考信息網站/C++標准文檔和Wikipedia.

博主可能會寫一個系列的博文來闡述C++標准庫在OI中的應用, 本文為第一篇.

(表示打這個好累的說OwO博主表示手打了好幾天才碼完這么多字)

1.概述

首先, 什么是STL?

STL, 即標准模板庫, 全稱Standard Template Library , 主要包含4個組件, 即算法, 函數, 容器, 迭代器. 這里的函數似乎主要指函數式編程(FP)中的函數而不是平常概念中的函數...標准模板庫具有強大的可擴展性, 包含很多常用算法/數據結構/常用操作, 極大地增加了代碼的重用, 幾年前在NOI系列賽中解禁后為C++語言選手提供了很多便利.

但是需要注意的是, STL並不等同於C++標准庫. C++標准庫除了STL中的容器/迭代器/算法之外還包含語言支持/輸入輸出/字符串/診斷/一般工具/數值操作/本地化等內容, 雖然其中很多也會用到模板, 但是它們並不屬於STL. STL應為C++標准庫的子集.

為了與用戶的命名區分, STL中的內容都在  namespace std 中, 可能許多人會偷懶使用 using namespace std; , 但是博主個人不建議大家使用這種做法. 因為這樣可能會造成潛在的訪問元素歧義. 因為在使用該語句后, 如果在全局也有聲明同名變量/函數且調用時未顯式指定命名空間為全局或 std 的話就可以同時解釋為調用標准庫或訪問自己定義的標識符. 容易出現歐洲合格認證(CE)的情況w

(順便說一句其實C++里的模板是圖靈完全的, 據說有dalao可以用這個在編譯期可以打出表來)

2.內容

STL的邏輯是"將待操作的數據與要進行的操作分開", 於是便有了如下的三個概念:

1.容器: 用於存放數據的

2.迭代器: 用於指出數據的位置, 或成對使用來划定一個區間 (注: STL區間一般都是左閉右開的半開區間.)

3.算法: 要執行的操作.

2.1 容器

STL中, 容器是"包含/放置數據的地方". STL中的容器使用的都是堆內存, 而且大多數情況下支持動態分配(不會寫指針和動態內存的選手の福利)所以內存利用率多數都很高.

多說幾句, 對於STL容器的對象數組堅決不能memset.....STL 容器所存儲的靜態內存內部數據都是指針或者其他標志位, memset之后直接亂掉然后沒法用了w

容器又分以下幾類:

2.1.1 順序容器

順序容器中保存的是一個有序的數據集合, 實現能按順序訪問的數據結構.

也就是說你放進順序容器的數據是保持原來放進去時的順序的. 關聯容器內部實現是平衡樹, 只能按元素從大到小訪問.(當然也有時候我們確實也希望這么做w所以要分兩類嘛(霧))

STL中的順序容器包含以下5個:

 std::vector  std::list  std::forward_list  std::deque  std::array 

由於 std::list 和 std::forward_list 就是鏈表平常手寫一個也不費事功能過於簡單而且時間復雜度常數偏大OI中一般不使用所以略(其實是博主懶癌發作)

然后 std::array 是C++11起支持的特性, 提供一個更加安全的C風格數組(然而這個是定長的...除了比較安全以及可以快速獲取長度/剩余空間等等信息之外和普通數組似乎沒卵區別)所以也略(逃)

2.1.1.1 std::vector

 std::vector 定義於頭文件 <vector> , 在OI中一般用於作為變長數組. 元素在其中連續存儲, 也就是說不僅按下標隨機訪問是嚴格 $O(1)$ 時間復雜度, 而且除了迭代器外, 常規指針也可以用於訪問它的內容. 最重要的是在尾部插入/刪除元素的均攤時間復雜度為 $O(1)$ ,在OI中無需擔心時間復雜度問題(雖然可能有喪病出題人卡你常數233).

其實它也支持在首部和中間插入數據, 但是時間復雜度 $O(n)$ ...

vector似乎是用了一些啟發式的奇技淫巧內存管理方式, 每次預分配一定的空間, 當預分配的這部分被用完時開一塊新的連續內存並將所有原來的元素copy過去. 當預分配大小經過仔細計算后就可以做到尾部插入元素操作均攤 $O(1)$ 的時間復雜度.

模板參數如下

1 template<
2     class T,
3     class Allocator = std::allocator<T>
4 > class vector;

第一個參數 class T 指定元素類型, 第二個參數 class Allocator 指定內存分配方式(這個參數有默認值所以平常一般不用管, 除非手寫了個內存池出來讓STL用)

例:聲明一個成員為 int , 內存分配方式默認的 std::vector 的代碼如下:

std::vector<int> v;

然后是 std::vector 所支持的操作(成員函數):

1. std::vector::operator=  $O(n)$

將另一個vector中的所有值copy進該vector(平常似乎沒卵用)

2. std::vector::at  $O(1)$

這個函數用來按下標訪問vector中的值(返回引用), 參數為一個下標. 與用方括號的效果基本上是等價的. 但是用這個函數來訪問vector中的值之前會先檢查一下下標是否越界, 如果越界則拋出異常 std::out_of_range , 控制台也會輸出相關信息比如所在行號所在文件等等. 如果用方括號的話就可能出現各種玄學錯誤比如修改了其他變量的值或者破壞內存結構影響以后對內存的訪問, 再或者直接崩潰RE... 

3. std::vector::operator[]  $O(1)$

最常用的操作, 按下標訪問vector中的值. 當做數組來用233  復雜度$O(1)$

4. std::vector::front  $O(1)$

返回vector首部元素的引用. 用於對首部元素進行修改/讀取. 注意如果是空vector的話訪問結果是未定義行為(也就是天曉得會發生什么, 因平台和編譯器而異)

5. std::vector::back  $O(1)$

返回vector的尾部元素的引用, 用於對尾部元素進行修改/讀取. 注意這次STL沒有日常左閉右開.

6. std::vector::data  $O(1)$

這個函數返回的是指向vector首部元素的指針(不是迭代器. 不是迭代器. 重要的事情說三遍, 不是迭代器) , 比如vector內部元素的類型為 $T$ , 則此函數的返回值類型為 $T*$.

7. std::vector::begin  $O(1)$

返回指向vector首部元素的迭代器. 迭代器類型為隨機訪問迭代器(具體參見下文).

8. std::vector::end  $O(1)$

返回指向vector尾部元素的后一個元素. STL的日常左閉右開區間的結果=w=

9. std::vector::rbegin  $O(1)$

返回指向vector尾部元素的反向迭代器. 倒序枚舉專用(霧) (其實貌似等於一個自增時實際下標自減的向前迭代器?)

10. std::vector::empty  $O(1)$

如果vector為空的話返回true, 否則為false. 常用於各種while循環里 (誒好像stack/queue/priority_queue的empty操作更常用在while里吧)

11. std::vector::size  $O(1)$

返回vector中包含的元素個數.

12. std::vector::resize $O(n)$

這個函數參數為一個整數, 可以調整vector的大小使其可以容納參數指定的元素.

13. std::vector::clear $O(n)$

將vector清空.

14. std::vector::push_back  均攤 $O(1)$

向vector尾部插入一個值. 除了下標訪問操作之外最常用的233

15. std::vector::pop_back  均攤 $O(1)$

刪除vector尾部的值.

16. std::vector::push_front  $O(n)$

向vector首部插入一個值. 因為要移動整個vector中的元素所以復雜度為 $O(n)$. 很少使用. 對應的 std::vector::pop_front 也是同一個時間復雜度, 原因一樣.

17.六種大小比較運算符 != == > >= < <=  $O(n)$

按字典序比較兩個vector中的內容.

2.1.1.2 std::deque

 std::deque , 定義於頭文件 <deque> , 相當於一個普通的雙端隊列. 但是與  std::queue 不同的是, 它不僅僅可以訪問兩端的元素, 內部的元素也可以訪問與修改. 而與 std::vector 的不同之處在於, deque內部的存儲是不連續的. 這給了deque在兩端插入/刪除元素時的嚴格 $O(1)$ 時間復雜度. 但是非連續的存儲結構也使得deque中的隨機訪問與插入刪除的復雜度猛增至 $O(n)$ . 所以deque在OI中基本上只是用於作為 std::queue 的一個擴充(畢竟這個可以在不彈出所有元素的情況下遍歷內部的元素而且兩端都可以插入/刪除).

1. std::deque::front  $O(1)$

返回deque首部元素的引用, 用於對首部元素進行修改/讀取. 同vector.

2. std::deque::back  $O(1)$

返回尾部元素的引用, 同vector.

3. std::deque::operator[]  $O(n)$

(港真一直以為deque和queue很像的我第一次看見這個操作表示震驚) 按下標訪問元素, 返回引用, 同vector.

(實際上deque也支持通過 std::deque::at 來訪問內部元素, 復雜度 $O(n)$, 用法和特性同vector)

4. std::deque::begin  $O(1)$

返回指向首部元素的迭代器, 同vector.

5. std::deque::end  $O(1)$

返回指向尾部元素后一個元素的迭代器. 同vector.

6. std::deque::rbegin  $O(1)$

(博主突然發現到目前為止一直都和vector一樣誒...)

(突然懶癌發作) 同vector.

7. std::deque::insert  $O(n)$

插入若干值. 第一個參數為下標, 可以是代表下標的整數也可以是迭代器. 后面的參數是要插入的信息. 可以是一個值, 也可以是一個區間.

(vector其實也支持, 復雜度一樣)

8. std::deque::erase  $O(n)$

刪除若干值. 可以通過一對下標/迭代器指定區間, 也可以通過一個下標/迭代器和長度來指定一個區間.

(vector其實也支持, 復雜度也是一樣=w=但是估計並沒人用)

9. std::deque::push_back  $O(1)$

在尾部插入一個值. 同vector

10. std::deque::push_front  $O(1)$

在首部插入一個值. 同vector.

11. std::deque::pop_front  $O(1)$

刪除首部的值. 同vector.

12. std::deque::pop_back  $O(1)$

刪除尾部的值. 同vector.

13.六種大小比較運算符 != == > >= < <=  $O(n)$

按字典序比較兩個deque的內容或者判斷二者中的數據是否相同.

2.1.2 關聯容器

關聯容器的內部實現一般都是一個平衡樹. 可以實現 $O(log(n))$ 時間復雜度的查找操作. 包括 std::set  std::map  std::multiset  std::multimap . 其中前面兩個容器自帶去重buff, 后面的是給那些可能有重復元素或者要計數的應用場所設計的.

關聯容器的組合有時候可以得到非常強的效果. 畢竟內部有一個封裝好的平衡樹(

 std::set / std::map 分別和 std::multiset / std::multimap 對應, 支持的操作完全相同, 只是內部存儲時的區別而已. 故省略multiset和multimap (懶癌再次發作)

2.1.2.1 std::set

STL提供的集合容器. 具有自動去重/自動排序等buff性質, 內部實現為平衡樹所以可以拿來當樹套樹的次級樹來偷懶(霧), 自動去重與排序后的結果可以使用迭代器在 $O(nlog(n))$ 的總時間復雜度內遍歷. 配合迭代器與算法庫食用可滋磁的操作還有查前驅/后繼/K大等等操作.

常見操作如下:

1. std::set::begin  $O(1)$

返回指向首部元素的迭代器. 與多數STL容器相同.

2. std::set::end  $O(1)$

返回指向尾部元素的下一個元素的迭代器. STL日常左閉右開區間.

3. std::set::rbegin  $O(1)$

返回指向翻轉后的區間的首部元素的迭代器. 可以用於快速訪問尾部元素.

4. std::set::empty  $O(1)$

返回set是否為空

5. std::set::size  $O(1)$

返回set中的元素個數.

6. std::set::clear  $O(n)$

清空set中的數據.

7. std::set::insert  插入一個值為$O(log(n))$, 傳入位置參考迭代器且位置參考有效則為均攤 $O(1)$ , 插入區間為 $O(dis*log(dis+n))$ (dis為區間長度)

向set中插入數據. 可以是一個值或者一個區間. 對於插入一個值還可以傳入一個位置參考迭代器. 若插入剛好發生在位置參考迭代器的左側或右側則可以將復雜度優化至均攤 $O(1)$. 

8. std::set::erase  刪除指定值為$O(log(n))$ , 刪除指定迭代器指向的結點為均攤 $O(1)$ , 刪除區間為 $O(log(n)+dis)$ (dis為區間長度)

從set中刪除數據. 可以通過值/迭代器來定位一個元素. 或者通過兩個迭代器來指定一個區間. 需要注意的是對於multiset, erase一個值代表的是erase掉所有等於這個值的數據而不是只刪除一個. 對於multiset若要刪除一個元素只能選擇傳入迭代器.

9. std::set::count  $O(log(n))$

在set中查找傳入的值並返回該值在set中的個數. 對於set來說可以判某值是否存在於set中.

10. std::set::find  $O(log(n))$

在set中查找傳入的值病返回指向查找到的值的迭代器. 若未找到則返回該set的 end() 

11. std::set::lower_bound  $O(log(n))$

返回指向第一個不小於給定值的迭代器. 或者說指向第一個"可插入位置".

這里要記住不要用形如 std::lower_bound(s.begin(), s.end(), value); 來調用算法庫里的相應函數. 因為迭代器移動需要 $O(log(n))$ 的時間復雜度, 而二分查找又要 $O(log(n))$ 的復雜度, 然而set的迭代器不是隨機訪問迭代器所以二分的時候變成了 $O(n)$ 的...然后你的復雜度就多乘了一大坨東西w目測搜索一次的時間復雜度是 $O(nlog^2(n))$  ...(還記得某學長胡策的時候就被這個坑了OwO歡聲笑語中打出GG)

12. std::set::upper_bound  $O(log(n))$

返回指向第一個大於給定值的迭代器. 或者說指向最后一個"可插入位置".

注意事項同lower_bound

13. 六種大小比較運算符 != == > >= < <=  $O(n)$

按字典序比較兩個set中的內容.

2.1.2.2 std::map

STL映射.必須指定的模板參數有兩個: 下標的類型與數據的類型. 由於重載了 operator[] 所以在代碼字面上可以當做一個下標不連續/下標可以是任何可比較類型數組來使用(下標是 std::string 都沒人管你), 有時用於離散化.

內部實現是 std::pair 加上一個平衡樹. 下標為第一關鍵字, 值為第二關鍵字扔在平衡樹里. 因為底層元素是pair用迭代器查值時要用形如  (*it).first   it->first 這樣的語句獲取該結點對應的下標,   (*it).second  it->second 來獲取該結點存儲的值.

1. std::map::operator[]  $O(log(n))$

按下標訪問map中的元素. 若該下標不存在則自動新建結點保存數據, 若存在則返回對對應數據的引用供讀取/修改.

2. std::map::begin  $O(1)$

標准容器函數(霧) 返回指向首元素的迭代器

3. std::map::end  $O(1)$

返回指向尾元素下一位置的迭代器

4. std::map::rbegin  $O(1)$

返回指向翻轉后區間的首元素(即原區間的尾元素) 的迭代器

5. std::map::empty  $O(1)$

返回容器是否為空.

6. std::map::size  $O(1)$

返回容器中元素的個數.

7. std::map::clear  $O(n)$

清空map中的所有數據.

8. std::map::insert  插入一個值為$O(log(n))$, 傳入位置參考迭代器且位置參考有效則為均攤 $O(1)$ , 插入區間為 $O(dis*log(dis+n))$ (dis為區間長度)

插入一個值. 這個值為下標, 整個insert的意義可以解釋為"為參數所提供的下標分配映射所需空間". 支持單點/區間.

9. std::map::erase  刪除指定值為$O(log(n))$ , 刪除指定迭代器指向的結點為均攤 $O(1)$ , 刪除區間為 $O(log(n)+dis)$ (dis為區間長度)

刪除一個值與它對應的映射值. 可以刪除單點/區間.

10. std::map::count  $O(log(n))$

11. std::map::find  $O(log(n))$

12.六種大小比較運算符  != == > >= < <=  $O(n)$

2.1.3 無序關聯容器

包括 std::unordered_set  std::unordered_map  std::unordered_multiset  std::unordered_multimap 四個, 具體用法和與之對應的關聯容器基本相同. 內部基於Hash來進行元素查找, 均攤時間復雜度 $O(1)$ , 最壞時間復雜度 $O(n)$ , C++11起可用. C++11前G++的STL實現了 std::hash_set  std::hash_map 這樣的非標准規定的容器, 當數據隨機時可以用這些來將查找優化到均攤 $O(1)$ (但願不會有喪病出題人專卡STL的Hash算法吧qwq)

2.1.4 容器適配器

包括棧/隊列/優先隊列這樣的數據結構, 提供順序容器的不同接口(也就是說基於順序容器構建). 這三種數據結構分別對應於 std::stack  std::queue  std::priority_queue .

和手寫棧/隊列相比有自動管理內存的優勢. 優先隊列基本上就拿來當堆用了w

容器適配器都基於順序容器作為底層容器, 所以各種操作的時間復雜度與底層容器的對應操作相等. 以下未特殊說明的時間復雜度均為底層容器默認時的時間復雜度

 std::stack 和 std::queue 默認使用 std::deque 作為底層容器. 而 std::priority_queue 則默認使用 std::vector 作為底層容器.

然而這三類容器適配器都喪心病狂地不支持 clear (清空操作) 

2.1.4.1 std::stack

字面意思, 就是個棧. 除了各種基本操作外還非常"良心"地支持了兩個棧之間的賦值操作=w=. 

話不多說, 直接上成員好了w

1. std::stack::top  $O(1)$

用於訪問棧頂元素. 返回的是引用所以理論上也可以用於修改棧頂元素.

2. std::stack::empty  $O(1)$

STL容器常見用法, 返回該棧是否為空.

3. std::stack::size  $O(1)$

STL容器常見用法, 返回該棧中所存儲的元素個數.

4. std::stack::push  $O(1)$

將一個作為參數的值壓入棧頂.

5. std::stack::pop  $O(1)$

將棧頂元素彈出(不返回棧頂元素的值)

6. std::stack::swap  與交換底層容器一致, 當容器是 std::array 時 $O(n)$, 否則 $O(1)$

將該stack中的內容與另一個stack交換.

7.六種大小比較運算符( != == > >= < <= ) $O(n)$

按照字典序比較兩個棧中的內容

2.1.4.2 std::queue

字面意思, 就是個隊列. 同樣除了push/pop這些基本操作外"良心"地支持了兩個queue間的賦值與字典序比較.

1. std::queue::front  $O(1)$

訪問隊首元素. 返回引用, 一般也可用於修改.

2. std::queue::back  $O(1)$

訪問隊尾元素(這次也不是左閉右開), 日常引用.

3. std::queue::empty  $O(1)$

熟悉的操作, 判定隊列是否為空.

4. std::queue::size  $O(1)$

依然是熟悉的操作, 返回隊列中元素的個數.

5. std::queue::push  $O(1)$

向隊尾插入一個元素.

6. std::queue::pop  $O(1)$

刪除隊首元素(不返回隊首的元素值)

7. std::queue::swap  <同stack> (博主懶癌再次發作)

交換兩個隊列中的所有信息.

8.六種大小比較運算符 != == > >= < <=  $O(n)$

按字典序比較兩個隊列中的信息.

2.1.4.3 std::priority_queue

字面意思, 優先隊列. OI里一般當做一個封裝好的堆來用. 模板參數按順序是:元素類型/底層容器類型(默認std::vector<T>)/比較方式 , 在這時可以自定義比較函數, 但是要先指定底層容器w. 對於如何擴展它的功能請看最下面的STL使用技巧. (突然發現優先隊列原生滋磁的操作是容器適配器里最少的OwO)

1. std::priority_queue::top  $O(1)$

訪問堆頂元素(優先隊列默認為大端堆, 即最大的元素在隊首), 不過要注意是top不是front...

2. std::priority_queue::empty  $O(1)$

熟悉操作++, 判斷隊列是否為空.

3. std::priority_queue::size  $O(1)$

返回隊列中的元素數量.

4. std::priority_queue::push  $O(log(n))$

將一個值插入優先隊列中.

5. std::priority_queue::pop  $O(log(n))$

將優先隊列隊首元素彈出(同樣不返回隊首元素的值/迭代器)

6. std::priority_queue::swap  <同stack>

交換兩個優先隊列中的信息.

 

2.2 迭代器

迭代器是用於指向容器中的數據的類, (雖然平常的操作似乎和指針沒啥區別, 也支持 operator* 和 operator-> w) 不同的容器根據其內部結構與實現原理的不同也會提供功能不同的迭代器. 迭代器有五種類型(C++17標准及以后是六種, 增加了一個連續迭代器的概念), 分別是輸入迭代器 輸出迭代器 向前迭代器 雙向迭代器 隨機訪問迭代器. 這些迭代器按照支持的操作的不同來分類.

STL中某種容器對應的迭代器類型可以直接在容器類型名(帶有模板參數)后加 ::iterator 來使用. 比如對於 std::set<int> , 則對應的迭代器類型名為 std::set<int>::iterator .

同時滿足輸出迭代器和四種迭代器之一二者的定義的迭代器也被稱為可變迭代器. 不滿足輸出迭代器定義的也被稱為不可變迭代器常迭代器.

似乎可以認為迭代器是個容器中專用的指針...?

畢竟容器中的存儲結構不一定連續, 所以當指針的位置發生移動時實際的下一個元素所在的內存可能並非剛好在上一個元素之后, 而是經過一定的算法計算出來的. 這也就造成普通指針在容器中無法使用. 這時我們可以選擇使用運算符重載特性來在偏移迭代器時執行對應的算法, 保證指向內存的合法性.

注:迭代器都可以支持形如這樣的操作:  *it  ++it 

至於分類可以理解為不同類型的迭代器一般有不同的定位自由度. 下面我們從限制最多的一直到最自由的開始說.

2.2.1 輸入迭代器

輸入迭代器只能一直自增, 並通過這樣的方式遍歷容器. 不過輸入迭代器比迭代器的基礎定義多了一個功能: 判等. 但是有些喪病的是在這個迭代器自增之后, 原來指向的元素可能失效. 這種情況可能發生在你試圖復制一個迭代器作為備份, 並將原來的迭代器自增, 則自增后這個備份所指向的內容不保證合法性. 而且不保證可以根據這個迭代器合法地向這個迭代器所指向的內容進行寫入.

2.2.2 向前迭代器

向前迭代器和輸入迭代器一樣也只能進行自增且可以判等, 但是向前迭代器比輸入迭代器多了一層保證: 迭代器自增后原位置依然合法.

嚴格來說這個東西叫做多趟保證, 具體定義摘一下:

多趟保證

給定 a 和 b ,類型 It 的可解引用迭代器

  • 若 a 與 b 比較相等( a == b 可語境轉換到 true ),則要么都是不可解引用,要么 *a 與 *b 是綁定到同一對象的引用
  • 通過可變 ForwardIterator 迭代器賦值不能非法化該迭代器(隱含地因為 reference 定義為真引用)
  • 自增 a 的副本不改變從 a 讀取的值(正式而言,要么 It 是無修飾指針類型,要么表達式 (void)++It(a), *a 等價於表達式 *a )
  • a == b 隱含 ++a == ++b

2.2.3 雙向迭代器

雙向迭代器首先滿足向前迭代器作為前置條件, 然后如字面意思所言, 雙向迭代器不僅可以支持單方向的自增操作, 還支持反方向定位的自減操作. 上面所述的三種迭代器都是一次只能定位一個元素的距離.

2.2.4 隨機訪問迭代器

隨機訪問迭代器是自由度最大的, 可以指定整數偏移量來任意移動定位迭代器並訪問元素. 而且對於兩個隨機訪問迭代器還可以相減求二者之間相距的元素個數, 判斷兩個迭代器的前后位置關系(大於/小於/等於/不大於/不小於等運算符)

2.2.5 輸出迭代器

輸出迭代器的概念其實只要滿足是迭代器而且這個迭代器所指向的內容可以寫入數據即可.

2.2.6 迭代器綜述

雖然不同類型的迭代器有不同的限制, 但是這些限制並不是沒有理由的, 比如對於 std::set , 它的內部實現是一棵平衡樹, 而且沒有實現名次樹的功能, 所以能保證時間復雜度為 $O(log(n))$ 的操作只有查詢該迭代器的前驅與后繼. 所以它的迭代器是一個雙向迭代器. 而 std::vector 由於它連續存儲的特性所以可以支持隨機定位, 比如位置 $+5$ 或者 $-7$ .

其實與其說不同種類的迭代器有不同的限制, 不如說不同種類的迭代器提供了自己對某種操作是否支持的保證. 比如需要用到二分查找的函數需要隨機訪問迭代器作為參數, 因為只有這樣才能取到最中間的元素位置, 當傳入的迭代器不滿足隨機訪問迭代器的要求時查找就會退化為線性時間復雜度; 而像 std::for_each 這樣的函數則只需要一個滿足前向迭代器的輸出迭代器就行了. (不過二分查找好像不必是輸出迭代器233)

附:迭代器使用示例:

 1 #include <vector>
 2 #include <iostream>
 3 
 4 int main(){
 5     std::vector<int> v={1,9,2,4,7,5,6,8};
 6     for(std::vector<int>::iterator i=v.begin();i!=v.end();++i){
 7         std::cout<<*i<<std::endl;
 8     }
 9     return 0;
10 }

 

2.3 算法庫

C++算法庫中提供大量用途的函數(例如查找、排序、計數、操作),它們在元素范圍上操作. 雖然算法庫中的很多函數都可以比較快地手寫出來, 但是有時候正確使用C++算法庫可以大幅精簡OI代碼並減少在這些基礎部分的無意義低錯(而這些錯誤常常需要很長時間的debug才能發現.), 而且算法庫中的算法往往進行過一些特殊優化使得庫函數常常比手寫快(STL和編譯器可能有不為人知的py交易). 注意范圍定義為左閉右開區間 [first, last) , 其中 last 指向要查詢或修改的最后元素的后一個元素.

庫函數大多都能吊打手寫, 就算你自以為用了一些卡常的奇技淫巧也依然沒有任何卵用. (畢竟人家有交易嘛(大霧))

多數涉及比較或者判定的庫函數都支持自定義比較函數或者判定函數, 一般應該選擇傳const引用或者傳值. 傳值因為要復制所以一般有額外開銷, 建議使用const引用.

下面僅介紹算法庫中的部分OI常用函數(不然太多根本沒法寫QAQ)

1. std::for_each 

接受三個參數: 兩個迭代器用於指定操作區間, 最后一個函數對象用於執行操作. FP專屬操作的味道

2. std::count  std::count_if 

分別接受三個參數, 前兩個都是兩個迭代器用於指定區間, 前者的第三個參數為一個值, 返回該值在指定區間內出現的次數; 后者的第三個參數為一個函數對象, 接受一個對應的值並返回 bool 值. count_if 對於指定區間內令指定函數返回 true 的值進行計數.

3. std::fill 

三個參數, 前兩個迭代器划分左閉右開區間, 第三個為一個類型對應的值. 作用為將指定區間內的對象全部設置為指定值. 據說內部有多路優化所以比自己寫循環要快而且似乎不會像memset一樣破壞STL及其他類的數據結構?

4. std::fill_n 

三個參數, 第一個為一個迭代器指定區間左端, 第二個為要修改的元素數目, 第三個為要修改的值. 與 std::fill 功能類似.

5. std::transform 

將一個或兩個區間內的值分別傳給一個指定的函數並將計算結果保存在另一個區間內的對應位置.

兩個重載, 分別對應一元函數與二元函數. 先是兩個迭代器指定參數區間, 二元函數還要再加一個迭代器指定第二個參數的區間左端. 然后是一個迭代器指定要保存結果的區間的左端, 最后是函數對象.

6. std::generate 

用一個函數的返回值填充一個區間. 先是兩個迭代器划分區間, 第三個參數為一個函數. 函數的返回值將被用於一個一個地寫入指定區間內.

7. std::generate_n 

與 std::generate 功能類似, 不同的是由兩個迭代器划分區間改為由一個迭代器和一個計數器划分區間.

8. std::remove  std::remove_if 

分別接受三個參數, 先是照例兩個迭代器划分區間, 第三個參數前者是一個值, 后者是一個函數. 前者將值與參數相等的元素刪除掉, 后者將使函數返回 true 的元素刪除掉. 由於刪除后區間會變短所以返回刪除部分元素后新的右端點(依然遵循STL的左閉右開原則).

9. std::swap 

接受兩個類型相同的參數, 並將它們的值交換. 這個庫函數跑得比誰都快, 不要想着自己搞個奇技淫巧就能卡常.

10. std::iter_swap 

和 std::swap 功能類似, 不同的是本函數接受的參數是指向待交換變量的迭代器.

11. std::reverse 

接受一對迭代器指定的區間然后暴力翻轉w

12. std::rotate 

以輪轉的方式旋轉指定區間. (即滑動該區間, 並將滑出去的元素補到開頭OwO) 第三個參數為一個迭代器, 指定要旋轉到左端的元素位置.

13. std::random_shuffle 

接受一對迭代器指定的區間, 對區間里的值進行隨機打亂, 防Hack&隨機化&造數據時的好幫手OwO

14. std::unique 

給區間去重. 但是只去重連續的重復元素. 即區間 8 2 5 4 4 4 6 3 中該函數會將三個 $4$ 去重為 $1$ 個, 但是區間 8 2 4 5 4 6 4 中的三個 $4$ 不會被去重.

可以先 std::sort 排個序把相等的元素集中起來再去重.

15. std::partition 

重排區間. 接受一對指定區間的迭代器與一個函數. 區間內所有使一元函數返回 true 的值都排在返回 false 的值之后從而將其分為兩組. 此重排過程不穩定.

16. std::stable_partition 

 std::partition 的穩定版本, 保證同一組內的元素的相對次序保持不變.

17. std::sort 

STL里極其強勁的函數... 可以根據數據自我調整排序方式...

指定一個區間即可. 要求區間內的元素重載了 <  運算符或者傳入一個自定義比較函數.

數據量小, 快排優勢體現不出來? 沒事, 我們用插入排序.

數據量大而且比較隨機? 快排走起.

數據量過大遞歸過多? 上堆排序

數據原始位置不好結果把快排卡到了接近 $O(n^2)$ ? 接着堆排上.

不可能有哪個手寫排序能打敗這個庫函數的.

此排序不穩定.

18. std::stable_sort 

 std::sort 的穩定版本, 內部在內存足夠的情況下使用 $O(nlog(n))$ 的多路歸並排序, 內存不足時使用 $O(nlog^2(n))$ 的次優算法.

保證相等的元素相對次序不變.

19. std::lower_bound 

只能在有序區間上操作.

指定一個區間和一個待查找的值, 可選參數為自定義比較函數, 返回指向第一個不小於給定值的元素的迭代器. 或者說指向"第一個可插入位置".

20. std::upper_bound 

只能在有序區間上操作.

指定一個區間和一個待查找的值, 可選參數為自定義比較函數, 返回指向第一個大於給定值的元素的迭代器. 或者說指向"最后一個可插入位置".

21. std::nth_element 

指定一個區間和值 $n$ , 該函數將會以第 $n$ 大的值為分割點, 將所有大於第 $n$ 個值的元素放在第 $n$ 個元素之前, 所有小於第 $n$ 個值的元素放在它之后, 第 $n$ 大的元素將剛好在第 $n$ 個值的位置上.

可傳入自定義比較函數.

22. std::merge 

合並兩個有序區間.

前四個參數為指定操作區間的兩對迭代器, 第五個參數為輸出區間的左端迭代器. 可傳入自定義比較函數.

23. std::is_heap 

判斷一個區間里的元素是否滿足堆性質.(大端堆)

STL對區間大端堆的定義:

對於區間 $[f,l)$, 設 $N=l-f$, 則:

\[\forall \: i \in (0,N) , f[\left \lfloor \frac{i-1}{2} \right \rfloor] \geq f[i]\]

可傳入自定義比較函數.

24. std::make_heap 

在 $O(n)$ 時間復雜度內構造一個大端堆. 參數為指定操作區間的一對迭代器. 可傳入自定義比較函數.

25. std::push_heap 

在 $O(log(n))$ 時間內插入一個數到建好的堆中. 待插入的值應該置於區間尾部, 原來的堆應位於 $[l,r-1)$

26. std::pop_heap 

在 $O(log(n))$ 時間內將堆頂元素彈出並放到原來堆的尾部.

27. std::sort_heap 

在 $O(n)$ 時間內建立一個堆並通過反復調用 $pop_heap$ 實現堆排序. 總時間復雜度 $O(nlog(n))$

28. std::max 

參數為兩個類型相同的變量, 返回其中的較大值.

可傳入自定義比較函數.

比自己用三目運算符手寫再inline快.

29. std::max_element 

參數為一對迭代器指定的區間, 返回區間中最大元素的值.

可傳入自定義比較函數.

30. std::min 

參數為兩個類型相同的變量, 返回其中的較小值.

可傳入自定義比較函數.

比手寫快系列.

31. std::min_element

參數為指定操作區間的一對迭代器,  返回區間中最小元素的值.

可傳入自定義比較函數.

32. std::next_permutation 

參數為指定操作區間的一對迭代器, 這個函數會計算區間內數據的下一個排列.

若有下一個排列返回 true , 否則返回 false 

33. std::prev_permutation 

基本同 std::next_permutation , 不同的是這個函數計算上一個排列並返回上一個排列是否存在.

3. 技巧

STL中的各種元素按照一定的方式配合起來使用功能可以變得十分強大, 下面介紹一些博主見到或者使用過的奇技淫巧使用方式.

3.1 離散化

我們可以使用 std::sort  std::unique 和 std::lower_bound 配合使用來實現離散化. 首先復制一份待離散化的數據到另一個數組, 然后對該數組進行排序並去重, 然后可以選擇 $O(nlog(n))$ 預處理至一個數組保存並在 $O(1)$ 時間內相應查詢, 或者不預處理每次直接 $O(log(n))$ 查詢. 查詢與預處理的過程都是拿數據在排序並去重后的數組中二分查找, 然后將返回的指針減去數組起始位置的指針來獲得下標. 這個下標的值即為離散化后的值.

3.2 快速清空容器適配器

如上文所述, 容器適配器都**喪病地不支持清空操作, 這時很多OIer為了清空就會寫出醬嬸的袋馬:

while(!q.empty()){
    q.pop();
}

看起來就很低效的樣子(雖然對於棧和隊列來說時間復雜度達到了理論下界, 但是常數顯然比直接清空要大, 至於優先隊列就直接從 $O(n)$ 掛成 $O(nlog(n))$ 了...)

這種時候就應該上我們的萬能STL容器清空函數(霧)

template<class T>
void Clear(T& x){
    T tmp;
    std::swap(tmp,x);
}

簡潔, 清晰(大霧)

可以看到我們在函數內聲明了一個局部對象 $tmp$, 該對象類型與參數相同. 這個新建的容器必定為空, 所以我們交換了兩個對象的值. (注:由於STL容器都特化了 std::swap 的實現所以不用擔心時間復雜度問題) 然后巧妙的地方在於, $tmp$ 在函數執行結束后自動析構, 也就是帶着 $x$ 原來的值自動析構了...

然后由於我們傳的是引用所以我們華麗麗地獲得了一個新的空的容器 $x$

3.3 快速刪除堆/優先隊列隊中的元素

有時候我們會使用堆或者優先隊列來進行一些掃描/貪心/優化等過程, 然而有時候我們可能會需要刪除堆中的元素而非堆頂. 然而STL的堆並不提供這些函數, 而手寫堆似乎也不好實現. 這時我們可以采取一些奇怪的思路: 建立一個 "垃圾堆" , 每次要刪除堆中的某個元素時直接壓入垃圾堆頂, 而從原堆堆頂取元素時判斷一下:

1.如果兩堆堆頂相等則直接同時彈出;

2.如果原堆堆頂小於垃圾堆堆頂則說明曾經有一個試圖刪除的值在原堆中不存在, 彈掉垃圾堆堆頂;

3.如果原堆堆頂大於垃圾堆堆頂, 原堆堆頂保證存在, 該值即為真正的堆頂.

以上算法正確性應該不難理解. 此處不再給出詳細證明(其實就是懶)

3.4 STL性能相關

可能許多OIer都有一個錯誤的觀念: STL慢出翔, 除了一個 std::sort 可以吊打手寫之外經常被卡常

卡常出題人本就該被掛起來裱, 神特么有理有據的底層優化(大霧)

然而真實結果是: 不.

為什么STL平常看起來那么慢?

STL容器,只要會給你迭代器用的,都會在debug模式下內置一個線程安全的鏈表,對內置一個鏈表,用來保存送出去的迭代器,每次你的迭代器操作都會進這個鏈表里線性掃描一遍,檢查下你的迭代器是否合法,線程安全的哦,有鎖的哦,這個鏈表還自帶allocator的哦

STL算法,只要用了比較器的,debug運行時,每次調用之前,都會對你的比較器進行幾次余先驗證,驗證比較器對於這一組數據是不是真滿足偏序關系/等價關系

STL容器本身用了各種traits技術和函數重載進行轉發,高度依賴編譯內聯優化

作者:匿名用戶
鏈接:https://www.zhihu.com/question/51650118/answer/126855487
來源:知乎

(攤手)

關掉Debug並開-O2之后基本不可能有誰的手寫代碼能做到STL的速度.

還好, 現在各路OI比賽都開始向前看並滋磁O2優化了而且優化之后跑得比港記快到不知哪里去了

配合HE省超神評測姬基本無敵的存在233333 ( $10^{10}$ 循環只跑 $4s$ 的六代i5就問你慫不慫....)

以及一些必定吊打手寫的庫函數:

 std::sort  std::swap  std::min  std::max 

上面幾個簡單函數不要想着手寫, 直接調庫...

對於 std::swap , 可能有的OIer學習 lrj dalao使用這樣的寫法:

void Swap(int& a,int& b){
    a^=b^=a^=b;
}

然后你發現你UB了...非常容易炸的寫法.

就算你拆開每一行讓它不UB:

void Swap(int& a,int& b){
    a^=b;
    b^=a;
    a^=b;
}

然而只是憑空拖慢速度罷了...

為啥呢? 正是因為多了那三次看起來很奇技淫巧的位運算...因為庫函數優化后直接在寄存器里mov然后扔RAM完事w

而且還會存在一個嚴重的問題: Swap同一個變量的時候會把它Swap成 $0$ ......(突然滑稽.png)

 

4.結語

STL是個非常有用的C++特性, 只要正確使用, 不僅可以提高代碼精簡程度, 還可以將你的精力聚焦到核心算法而不是一些其他的細節, 並由此提高做題速度與算法能力. 本文僅僅做了很簡單的介紹就已經長到了這樣的地步, 可想而知STL還有更深的水. 比如庫函數的多個重載/自定義內存分配器/自定義執行模式/迭代器和容器的高級用法與嵌套等等. 如果想要深入了解STL的用法與高級特性, 可以參考 cplusplus.com zh.cppreference.com wikipedia 或者直接參考標准文檔. 如果為了防止版權爭議可以查閱C++14標准發布前的最后一個Working Draft (n4140.pdf)

本文依然不定期更新.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM