STL 容器用法簡要整理(未完成)

DengStar發表於2024-10-11

STL 容器用法簡要整理

本文將簡要介紹 C++14 中可以使用的 STL 容器的用法。根據 CCF 規定,這些容器都可以在比賽中使用。

本文中的程式碼均為 C++14。

本文中的程式碼均已引入了相關的庫,並 using namespace std

共同點

  • 特性:所有能用下標訪問的 STL 容器,下標都是從 0 開始,到 size - 1 結束,如 vectorarraystring

  • 初始化:STL 容器的初始化都是形如 container<T> name 的形式,其中 T 是資料型別,如 intchar,結構體或其它 STL 容器;name 是你起的容器的名字。例如,建立一個空的 intvector 可以這麼寫:vector<int> v

  • =:賦值運算子。可以把某個容器的值賦值給另一個同類容器。以 vector 為例:

    int main()
    {
    	vector<int> v1{1, 2, 3, 4, 5};
    	vector<int> v2 = v1;
        // 兩個容器的型別必須相同。vector<int> 和 vector<long long> 也是不能互相賦值的。
    	for(int x: v2) cout << x << ' ';
    	cout << endl; 
    	return 0;
    }
    

    輸出:1 2 3 4 5

  • size():返回容器內的元素個數。(如無特別說明,提到的函式都是成員函式。)

  • empty()bool 型,返回容器是否為空。

  • swap()a.swap(b) 表示交換容器 a 和容器 b 的值。時間複雜度常數。但是有一個例外:arrayswap() 是線性的!(參見此處。作為對比,vectormap 的時間複雜度都是常數(constant)。)

    int main()
    {
    	vector<int> v1{1, 2, 3, 4, 5};
    	vector<int> v2{6, 7, 8, 9, 10};
    	v1.swap(v2);
    	cout << "v1: "; for(int x: v1) cout << x << ' '; cout << endl; 
    	cout << "v2: "; for(int x: v2) cout << x << ' '; cout << endl; 
    	return 0;
    }
    

    輸出:

    v1: 6 7 8 9 10
    v2: 1 2 3 4 5
    
  • > / >= / < / <= / == / !=:按字典序比較兩個容器。無序容器(如 unordered_map)只支援 ==!=

    int main()
    {
    	vector<int> v1{1, 2, 3, 4, 5}, v2{1, 2, 4, 2, 1}, v3{1, 2, 3, 4, 5, 6};
    	cout << "v1 < v2: " << boolalpha << (v1 < v2) << endl;
    	cout << "v1 < v3: " << boolalpha << (v1 < v3) << endl;
    	string s1("cat"), s2("cat"), s3("cap");
    	cout << "s1 == s2: " << boolalpha << (s1 == s2) << endl;
    	cout << "s1 < s3: " << boolalpha << (s1 < s3) << endl;
    	return 0;
    }
    

    輸出:

    v1 < v2: true
    v1 < v3: true
    s1 == s2: true
    s1 < s3: false
    
  • 迭代器(iterator):迭代器類似指標。用 * 可以解引用。

    宣告時可以用 autoauto it = v1.begin(),也可以用 container::iteratorvector<int>::iterator it

    begin() 返回指向容器開頭的迭代器,end() 返回指向容器最後一個元素的後繼的迭代器。(所以實際上 end() 不指向任何一個元素,元素的地址範圍是 [begin(), end())。)

    對於 vectorarray 等可以用下標訪問的容器,可以用 *(it + c) 的形式來訪問元素,其中 it 是迭代器,c 是整數。也可以用這種形式修改元素,就像指標一樣。這裡以 array 為例:

    int main()
    {
    	array<int, 5> a{1, 2, 3, 4, 5};
    	auto it = a.begin(), it2 = a.end();
    	cout << *it << ' ' << *(it + 1) << ' ' << *(it2 - 1) << endl;
    	*it = 2, *(it + 1) = 10;
    	for(int x: a) cout << x << ' '; cout << endl; 
    	return 0;
    }
    

    輸出:

    1 2 5
    2 10 3 4 5
    

    更多關於迭代器的知識,參見此處

vector(向量)

std::vector - cppreference.com

可變長度陣列。解決了 C 風格陣列長度只能是常數的問題。它支援如下操作:\(O(1)\) 的隨機訪問,\(O(n)\) 的插入/刪除元素。

我們經常能看到這樣的題目(尤其是在 CF 上):有一個 \(n \times m\) 的矩陣,我們要儲存每個元素的資訊,而 \(nm \le 10^6\),但對於 \(n\)\(m\) 的大小沒有單獨限定。這時候用陣列就不能保證既不爆空間,又能存下所有資料。vector 的作用就體現出來了。

注意,不要使用 vector<bool>vector<bool> 不是 vector,使用它可能會出現意外的錯誤。如必須使用,可以用 vector<char> 代替,二者空間相同。

需要注意的是,vector 的常數有時劣於陣列(待驗證)。

  • 初始化vector 的初始化方式有許多種,使用合理的方式可以為我們提供便利。參見此處

    下面是 5 種常用的初始化方法:

    #define vec_print(x) cout << #x": ", print(x)
    void print(vector<int> &v)
    {
    	for(int x: v) cout << x << ' ';
    	cout << endl; 
    }
    
    int main()
    {
    	vector<int> v1; // 1. 建立空 vector,時間複雜度常數
    	int n = 5;
    	vector<int> v2(n); // 2. 建立長度為n的vector,時間複雜度線性。
    	// 元素的初值都為0。下面會討論這個初值是怎麼得來的。 
    	// 這體現了與陣列的不同之處:長度可以是變數。
    	vector<int> v3(n, 42); // 3. 建立長度為n的vector,每個元素都是42。時間複雜度線性。
    	vector<int> v4(v3); // 4. 建立一個與v3相同的vector。時間複雜度與v3的大小線性相關。
    	vector<int> v5(v4.begin() + 1, v4.end() - 1);
    	// 5. 把[v4[1] ~ v4[4])中的元素複製到v5中(注意是左閉右開區間!) 
    	// 時間複雜度和複製的元素數量線性相關。 
        vector<int> v6{1, 2, 3, 4, 5}; // 6. 用列表初始化,時間複雜度? 
        // (我不知道這個方法的時間複雜度,但是元素太多的時候一般不會用列表初始化,因此時間可以忽略不計)
    	
    	vec_print(v1), vec_print(v2), vec_print(v3), vec_print(v4), vec_print(v5), vec_print(v6);
    	
    	return 0;
    }
    

    輸出:

    v1:
    v2: 0 0 0 0 0
    v3: 42 42 42 42 42
    v4: 42 42 42 42 42
    v5: 42 42 42
    v6: 1 2 3 4 5
    

    關於方法 2 中,元素的初值:

    好吧,我沒有完全搞懂。cppreference 上的原文表示元素的值是 default-inserted instance of T,但我不知道什麼叫 "default-inserted"。我猜測應該是元素預設的值:例如整型預設是 0

    如果元素型別是結構體,而結構體有建構函式,那麼元素的初值透過建構函式得來:

    struct Node
    {
    	int x;
    	Node(): x(42) {}
    };
    
    int main()
    {
    	vector<Node> v(5);
    	for(auto nd: v) cout << nd.x << ' ';
    	cout << endl;
    	return 0;
    }
    

    輸出:42 42 42 42 42

    關於方法 5:

    用某個 vector 中一段元素的值初始化另一個 vector,兩個 vector 的元素型別可以不相同,但要滿足可以互相轉換。例如 int 可以轉成 long long。或者,如果被初始化的 vector 的元素是結構體,要有對應的建構函式。

    需要注意的是,如果兩個 vector 的元素型別不同,就不能用 vector<int> v2(v1) 之類的形式來初始化,即使兩種元素型別可以互相轉換或者有建構函式也不行。

    struct Node
    {
    	string str;
    	Node(int x): str(to_string(x * 10) + "str") {}
    };
    
    int main()
    {
    	vector<int> v1{1, 2, 3, 4, 5};
    	vector<long long> v2(v1.begin(), v1.end()); // ok,int 和 long long 可以轉換 
    //	vector<string> v3(v1.begin(), v1.end()); // wrong,int 不能轉成 long long 
    	vector<Node> v4(v1.begin(), v1.end()); // ok,存在 int 到 Node 的建構函式 
    //	vector<Node> v5(v1); // wrong,不同元素型別的 vector 不能互相初始化
        
    	cout << "v1: "; for(auto x: v1) cout << x << ' '; cout << endl;
    	cout << "v2: "; for(auto x: v2) cout << x << ' '; cout << endl;
    	cout << "v4: "; for(auto x: v4) cout << x.str << ' '; cout << endl;
    	return 0;
    }
    

    輸出:

    v1: 1 2 3 4 5
    v2: 1 2 3 4 5
    v4: 10str 20str 30str 40str 50str
    

以下是一些常用的成員函式(STL 共有的成員函式不再列出):

  • 訪問元素的方式:

    1. 透過 [] 訪問。
    2. 透過 at() 訪問。與前者的區別是用 at 訪問時會檢測有沒有越界。at() 的效率低於 [],所以一般情況下都用 [],但是如果覺得自己可能會 RE,可以用 at() 以便於除錯。
    3. front() 訪問首元素,back() 訪問尾元素。(注意與 begin()end() 區分,它們是迭代器。)
    4. 用迭代器訪問:*it 表示 it 指向的元素的值,例如*begin() 就表示首元素的值。
    int main()
    {
    	vector<int> v{1, 2, 3, 4, 5};
    	cout << v.front() << ' ' << v[1] << ' ' << v.at(2) << ' ' << *(v.begin() + 3) << ' ' << v.back() << endl;
    	return 0;
    }
    

    輸出:1 2 3 4 5

    at() 訪問時,如果越界,會輸出錯誤資訊。

    int main()
    {
    	vector<int> v{1, 2, 3, 4, 5};
    	cout << v.at(5) << endl;
    	return 0;
    }
    

    輸出:

    terminate called after throwing an instance of 'std::out_of_range'
      what():  vector::_M_range_check: __n (which is 5) >= this->size() (which is 5)
    
  • 新增元素:push_back():向 vector 的末尾增加元素。這麼做會增加 vector 的長度。注意我們沒有 push_front(),要實現類似操作,得用 deque

  • 刪除元素:pop_back()。同理,沒有 pop_front()

  • 改變 vector 的大小:resize()resize(n) 表示把大小改為 n。如果原先的長度大於 n,會刪除多餘的元素;否則:

    • 如果呼叫 resize(n),會在末尾新增元素的預設值;
    • 如果呼叫 resize(n, val),會在末尾補上 val

    時間複雜度和 nvector 原來大小的差線性相關。

    int main()
    {
    	vector<int> v{1, 2, 3, 4, 5};
    	v.resize(3);
    	for(int x: v) cout << x << ' '; cout << endl;
    	v.resize(10, 42);
    	for(int x: v) cout << x << ' '; cout << endl;
    	cout << "v.size() is " << v.size() << endl;
    	return 0;
    }
    

    輸出:

    1 2 3
    1 2 3 42 42 42 42 42 42 42
    v.size() is 10
    
  • assign():給 vector 填充某個元素,有點像 fill()。用法:

    1. assign(n, val):把 vector 替換nval。時間複雜度 \(O(n)\)vector 的大小多退少補。“替換”的意思是會覆蓋原有的元素。
    2. 咕咕咕
  • inserterase():插入刪除操作。通常情況下不使用這個函式,因為時間複雜度是線性。如果要做到常數的插入和刪除,應該使用連結串列。這裡不做介紹。

array(陣列)

std::array - cppreference.com

定長陣列。和 C 中的陣列一樣,長度必須為常數,效率優於 vector,和 C 中的陣列相當。個人建議用 array 代替所有的定長陣列,用 vector 代替所有的不定長陣列。

大部分使用方法與 vector 相同,除了不能加入/刪除末尾元素(push_back()/pop_back()),當然也不能 insert()erase()

下面是一些(個?)特有的函式:

  • fill()fill(x) 表示把 array 全部填充為 x。時間複雜度與 array 大小線性相關。

    int main()
    {
    	array<char, 5> a{'a', 'a', 'a', 'a', 'a'};
    	a.fill('b');
    	for(char ch: a) cout << ch; cout << endl;
    	return 0;
    }
    

    輸出:bbbbb(覆蓋了原先的值)

deque(雙端佇列/雙端陣列)

std::deque - cppreference.com

咕咕咕

string(字串)

(嚴格來說 string 不算 container,但它很重要,這裡一併整理用法。)

在 C 中,字串是透過 char 陣列實現的。而在 C++ 中,我們終於有了原生的字串型別——string。它自帶許多高效的函式,可以為我們寫題(特別是字串相關的模擬題)帶來極大便利。與此同時,它的常數也十分優秀。

  • 初始化

    int main()
    {
    	string s1; // 1. 建立空字串
    	string s2("test"); // 2. 建立特定字串
    	int n = 5;
    	string s3(n, '='); // 3. 建立含n個'='的字串,時間複雜度線性
    	auto print = [&](string name, string str) {cout << name << ": " << str << endl;};
    	print("s1", s1), print("s2", s2), print("s3", s3);
    	return 0;
    }
    

    輸出:

    s1:
    s2: test
    s3: =====
    
  • find():查詢某個子串/字元。如果存在該子串,返回第一個子串的第一個字元的下標;否則,返回 string::npos(本質上是一個 size_t 型的數)。

    int main()
    {
    	string str("This is a string."); /*
    	              ^  ^         ^
    	              2  5         15     */
    	cout << str.find("This") << ' ' << str.find("is") << endl;
    	cout << str.find('g') << endl; // 也可以找 char 
    	cout << str.find("is", 3) << endl; // find(s, pos):從下標pos處尋找s 
    	cout << str.find("1234") << ' ' << boolalpha << (str.find("1234") == string::npos) << endl; // 找不到示例 
    	return 0;
    }
    

    輸出:

    0 2
    15
    5
    18446744073709551615 true
    

    其中 18446744073709551615 就是 string::npos,具體的值由編譯器決定。

  • substr()

相關文章