C++ 學習筆記(1):STL、Vector 與 Set

AurLemon發表於2024-11-19

背景

最近在嘗試入坑藍橋杯,於是先從 C++ 開始學起,這裡記個筆記。這裡我的筆記是跟著這個教程來的。沙比學校天天整些屁事都沒什麼空折騰。

前言

筆者是 JS / TS 寫的比較多,以前寫過 C 但是有點忘了,所以文章裡都是和 JS 進行對比著方便快速理解。

同時其實我還有幾個小問題,嘻嘻。沒有解答,就是寫文章的時候提醒一下自己要全面一點,複習哈。

  • 指標的 *& 是什麼用?指標的本質是?
  • 和 JS 比有沒有類似的,異同點是?
  • C++ 和 C 有什麼區別?
  • 本質上迭代器和陣列索引遍歷是類似的,但是為什麼要單獨抽象一個迭代器出來?

複習

指標

  • 宣告指標:int *p; 表示一個指向 int 型別的指標。
  • 取地址:&x 獲取變數 x 的記憶體地址。
  • 解引用:*p 訪問指標 p 指向的記憶體地址上的資料。
#include <iostream>
using namespace std;

int main() {
    int x = 10;
    int *p = &x; // p 儲存的是 x 的地址
    cout << "x 的值是: " << x << endl;  // 輸出 10
    cout << "p 指向的值是: " << *p << endl; // 解引用,輸出 10
    return 0;
}

其實就是記憶體和值互相指來指去的,JS 裡面沒有可以類比的,要說比較像的就是物件賦值,舉個例子:

let obj = { value: 10 };
let ref = obj; // ref 是 obj 的引用
ref.value = 20;
console.log(obj.value); // 輸出 20

JS 裡面如果要避免這種情況只能深複製了~ 因為預設的賦值方式(如 let ref = obj)只是將物件的引用賦值給新的變數,並沒有建立新的獨立物件。

STL

概念

STL 是一個 C++ 的工具庫,分為三大元件,分別是容器、演算法、迭代器。

容器

常見容器型別有三種,第一種是序列式容器(即元素按順序儲存,支援高效的插入和刪除)。常見的有這幾個:vector,動態陣列,支援隨機訪問;deque,雙端佇列,支援高效的兩端操作;list,雙向連結串列,支援高效的任意位置插入和刪除。

第二種是關聯式容器(基於鍵值對儲存,元素通常自動排序)。常見的有這幾個:set,集合,不允許重複元素;map,鍵值對對映,每個鍵唯一;multisetmultimap,允許重複鍵。

最後一種是無序容器(基於雜湊表儲存,元素無序)。常見的有unordered_set,無序集合;unordered_map,無序對映。

其實換句話說 JS 基本都有類似功能的方法/庫,比如說 Map 和 Set 都是自帶的,JS 裡面直接例項化後即可使用。雖然實現方式還是有差異,STL 的 Map 和 Set 底層通常用平衡二叉樹實現,預設是有序的;而 JS 是基於 HashMap 實現的。

另外 C++ 還存在 auto 這個型別,即編譯器自動推斷變數型別,被叫做“型別推導關鍵字”,意思是讓編譯器推導變數的具體型別。如果你是 JS 或者 Python 寫的比較多的應該馬上覺得很熟悉了,這不是動態語言麼?

雖然差距很大,C++ 的 auto 只是編譯器編譯時推到,執行後其實已經是靜態了(auto x = 4編譯完會變成int x = 4,某種程度上和#define接近);而 JS / Python 這類動態語言在 Runtime 的型別就是動態的。

演算法

演算法是用來操作容器的工具集,包括排序、搜尋、變換等常見操作。STL 的演算法庫與任何容器相容。

常見演算法有很多,如下。

  • 排序類:sort()stable_sort()
  • 查詢類:find()binary_search()
  • 變換類:transform()replace()
  • 集合操作:set_union()set_intersection()

演算法使用迭代器(見下文)作為操作物件,不直接與容器繫結,因此非常靈活。

其實看到這也非常眼熟,就比如 sort() 這個在 JS 的 Array 物件裡最常見的方法,原來這個在 C++ 這種老登(bushi)語言裡被叫統稱為演算法啊!不過 STL 和 JS 的 sort() 還是有些許不同的。STL 的 sort() 是一個函式,可以對任意範圍內的元素排序,不僅限於容器,比如陣列、列表都可以用。

而 JS 的 sort() 僅僅只是陣列的一個方法,只針對陣列排序;STL 的 sort() 的實現底層使用了快速排序 + 插入排序,比 JS 的 sort() 更快。

迭代器

迭代器是一種指標類似的物件,用於在容器中遍歷元素。迭代器分類如下:

  • 輸入迭代器:只讀訪問,例如 std::istream_iterator
  • 輸出迭代器:只寫訪問,例如 std::ostream_iterator
  • 前向迭代器:支援單向遍歷,例如 std::forward_list 的迭代器。
  • 雙向迭代器:支援前後遍歷,例如 std::list 的迭代器。
  • 隨機訪問迭代器:支援隨機訪問,例如 std::vector 的迭代器。

一般常見的就兩種,一種是拿 begin() 和 end() 用來返回容器的起點和終點迭代器,另外一種是用 *it 訪問迭代器指向的值,++it 進行移動。

迭代器可以理解為一種“指標”,它用來在容器中遍歷元素,其實這不就是遍歷陣列裡面的值啊!但為什麼弄這麼複雜?因為 STL 容器不能像陣列索引那樣直接遍歷,STL 容器都是各種各樣的資料結構,不像陣列那樣連續,所以只能使用指標來遍歷裡面的值。

深入一點來說,陣列在記憶體中的資料是連續的,你透過陣列索引訪問的時候,其實就是arr[i] = *(arr + i),arr 是這個陣列的基準地址,i 就是往後推幾位,陣列索引本質上就是最開始的地址 + 索引數;而 STL 本身就是高層抽象的東西,STL 容器包含了很多種資料結構,每種資料結構的訪問方式都是不一樣的,所以 STL 庫把迭代器給抽象了出來,最終作為一種庫給開發人員呼叫。

迭代器其實就是一種抽象。它的實現完全依賴於容器的底層資料結構。如果容器是連續的(如 vector),迭代器可以直接封裝指標操作;如果容器是非連續的(如 list、set),迭代器內部會包含指向當前元素的指標,同時封裝了訪問下一個/上一個元素的邏輯。對於複雜的資料結構(如紅黑樹),迭代器會依賴樹的中序遍歷演算法來實現移動。

另外,STL 中的迭代器不僅僅是用來遍歷的工具,還能透過它修改容器內容,甚至對任意範圍的元素進行操作。

所以迭代器其實就是面向容器的“智慧索引”,它不是簡單的指標,而是高度抽象和適配不同資料結構的工具。

Vector

Vector 是 STL 容器的一種,全稱叫可變陣列,但如果你熟悉 JS 你就會發現這不就是裡面的陣列嗎。STL 裡面的 vector 的特性和 JS 裡面很像哦,支援動態擴容(陣列大小可以自動調整)、連續記憶體儲存(可以高效地進行隨機訪問)、高效尾部操作(尾部插入和刪除操作非常快)。感覺這裡沒啥好說的就純看語法寫寫就好了,放一段 Example。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v = {3, 1, 4, 1, 5};
    v.push_back(9); // 新增元素 9
    sort(v.begin(), v.end()); // 排序

    cout << "排序後的元素: ";
    for (auto num : v) {
        cout << num << " ";
    }
    cout << endl;

    v.pop_back(); // 刪除最後一個元素
    cout << "大小: " << v.size() << endl;

    return 0;
}

問了下 AI,vector 一般在演算法裡有啥地方用得到?他說這三個:

  • 在需要動態增長的陣列時使用,例如儲存所有輸入資料。
  • 排序和去重,常用 sort + unique 去重。
vector<int> v = {1, 2, 2, 3, 3, 4};
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
  • 二分查詢,用 lower_bound 和 upper_bound 高效查詢元素位置。
vector<int> v = {1, 2, 4, 4, 5};
int pos = lower_bound(v.begin(), v.end(), 4) - v.begin(); // 找到第一個 >= 4 的位置

然後這個是語法:

操作 說明 示例程式碼
vector<T> v; 定義一個儲存型別為 T 的動態陣列 vector<int> v;
v.push_back(x); 在尾部新增元素 x v.push_back(10);
v.pop_back(); 刪除尾部元素 v.pop_back();
v.size(); 獲取陣列大小 int n = v.size();
v[i] / v.at(i) 訪問第 i 個元素(at 帶邊界檢查) cout << v[0];
v.clear(); 清空所有元素 v.clear();
v.begin() / v.end() 返回首迭代器和尾迭代器,用於遍歷 for (auto it = v.begin(); it != v.end(); ++it)
v.insert(pos, x); 在迭代器位置 pos 插入元素 x v.insert(v.begin(), 5);
v.erase(pos); 刪除迭代器位置 pos 的元素 v.erase(v.begin());
sort(v.begin(), v.end()) v 進行升序排序 sort(v.begin(), v.end());

Set

set 是 STL 中的 集合容器,JS 裡也有,特性一樣:儲存唯一值(不允許重複元素)、自動排序(預設以升序儲存)、基於紅黑樹實現(插入、刪除、查詢的時間複雜度為 O(log n))。還是丟一段 Example。

#include <iostream>
#include <set>
using namespace std;

int main() {
    set<int> s = {10, 20, 10, 30}; // 自動去重和排序
    s.insert(40); // 插入元素 40
    s.erase(20);  // 刪除元素 20

    cout << "集合中的元素: ";
    for (auto num : s) {
        cout << num << " ";
    }
    cout << endl;

    if (s.count(30)) {
        cout << "30 存在於集合中" << endl;
    }

    return 0;
}

Set 常用方法有這幾個:

  • 高效查詢和去重,集合自動去重,插入後元素總是唯一。
  • 範圍查詢,用迭代器找到範圍內的值。
auto it = s.lower_bound(15); // 找到第一個 >= 15 的元素
  • 動態集合,用於實時儲存和查詢,如滑動視窗。

這個是語法:

操作 說明 示例程式碼
set<T> s; 定義一個儲存型別為 T 的集合 set<int> s;
s.insert(x); 插入元素 x,如果已存在則插入失敗 s.insert(10);
s.erase(x); 刪除值為 x 的元素 s.erase(10);
s.count(x); 檢查是否存在值為 x 的元素(返回 1 或 0) if (s.count(5))
s.find(x); 返回指向值為 x 的迭代器(找不到返回 s.end() auto it = s.find(10);
s.size(); 獲取集合大小 int n = s.size();
s.begin() / s.end() 返回首迭代器和尾迭代器,用於遍歷 for (auto it = s.begin(); it != s.end(); ++it)
s.clear(); 清空集合中的所有元素 s.clear();
s.lower_bound(x); 返回指向第一個 >= x 的迭代器 auto it = s.lower_bound(15);
s.upper_bound(x); 返回指向第一個 > x 的迭代器 auto it = s.upper_bound(20);

相關文章