:桶思想、桶排序與map的關聯(lián)與應用)
1. 項目概述從“桶”到“排序”再到“映射”的算法工具箱在C的算法世界里我們常常會遇到一些看似基礎但組合起來威力巨大的概念。今天要聊的這三個關鍵詞——桶、桶排序和map就是這樣一個典型的組合。它們分別代表了數(shù)據(jù)處理的不同維度桶是一種思想一種將數(shù)據(jù)分而治之的抽象容器桶排序是這種思想在排序領域最直接、最經(jīng)典的應用而map映射則是C標準庫提供的一個強大工具它本身就可以看作是一種高級的、自動化的“桶”管理機制。很多初學者在刷題或者做項目時對這三者的關系和應用場景感到模糊要么死記硬背模板要么面對具體問題不知該用哪個。這篇文章我就以一個老碼農(nóng)的視角帶大家徹底捋清這三者的來龍去脈、內(nèi)在聯(lián)系和實戰(zhàn)用法。我會用最直白的語言結合具體的例題和詳盡的注釋讓你不僅知道怎么寫更明白為什么這么寫以及在不同場景下如何做出最合適的選擇。無論你是正在準備面試還是希望在項目中寫出更高效的代碼這篇文章都能給你帶來實實在在的收獲。2. 核心概念拆解桶、排序與映射2.1 “桶”的哲學分而治之的數(shù)據(jù)容器“桶”這個概念在算法中并非特指某個數(shù)據(jù)結構而是一種策略或思想。它的核心邏輯非常簡單當你要處理一大批數(shù)據(jù)時如果直接處理很困難或效率低下不妨先根據(jù)數(shù)據(jù)的某個特征比如數(shù)值范圍、首字母、狀態(tài)等將它們分門別類地放入不同的“桶”中。然后對每個桶內(nèi)部的數(shù)據(jù)進行單獨處理可能是排序、統(tǒng)計或其他操作最后將所有桶的結果合并起來。舉個例子假設你要對全公司員工的年齡進行排序。如果直接用快速排序當然可以。但如果你知道員工年齡都在20-60歲之間你可以準備41個桶分別標號20, 21, 22, ..., 60。然后遍歷員工列表將年齡為25的員工放入標號25的桶中。遍歷結束后你只需要按桶標號順序從20到60依次輸出每個桶里的員工自然就得到了按年齡排序的列表。這個過程甚至不需要對桶內(nèi)元素進行排序因為一個年齡值對應的桶里所有員工年齡都相同?!巴啊彼枷氲膬?yōu)勢化整為零將大規(guī)模問題分解為多個小規(guī)模問題降低單個問題的復雜度。利用數(shù)據(jù)分布如果數(shù)據(jù)分布均勻或已知范圍可以設計出時間復雜度接近O(n)的算法。并行處理潛力各個桶之間的處理通常是獨立的非常適合并行計算。“桶”思想的實現(xiàn)關鍵映射函數(shù) (Hash Function)決定一個數(shù)據(jù)項應該放入哪個桶。這是桶思想的核心一個好的映射函數(shù)應該盡可能均勻地將數(shù)據(jù)分散到各個桶中避免某些桶過滿退化而另一些桶空著。桶的數(shù)據(jù)結構通常使用數(shù)組vector或鏈表list來實現(xiàn)取決于是否需要頻繁的中間插入。注意這里說的“桶”和哈希表Hash Table中的“桶”在思想上是同源的。哈希表通過哈希函數(shù)將鍵映射到數(shù)組桶數(shù)組的特定索引每個索引位置可能掛載一個鏈表一個桶來處理哈希沖突。2.2 桶排序桶思想的經(jīng)典排序?qū)嵺`桶排序是“桶”思想在排序問題上的直接應用。它是一種分配式排序算法其性能依賴于數(shù)據(jù)的分布。當輸入數(shù)據(jù)服從均勻分布時它的平均時間復雜度可以達到O(n)。標準桶排序的步驟設置桶確定桶的數(shù)量和范圍。例如對于范圍在[0, 1)的浮點數(shù)可以設置n個桶第i個桶的范圍是[i/n, (i1)/n)。數(shù)據(jù)入桶遍歷原始數(shù)組根據(jù)每個元素的數(shù)值通過映射函數(shù)將其放入對應的桶中。桶內(nèi)排序?qū)γ總€非空桶內(nèi)的元素進行排序。這里可以使用任何排序算法如快速排序、插入排序等。由于數(shù)據(jù)被分桶后每個桶內(nèi)數(shù)據(jù)量較小插入排序在這種小數(shù)據(jù)量場景下往往表現(xiàn)不錯。合并結果按桶的順序從小到大依次將每個桶內(nèi)排序好的元素取出放回原數(shù)組即完成排序。C簡單實現(xiàn)框架void bucketSort(vectorfloat arr) { int n arr.size(); if (n 0) return; // 1. 創(chuàng)建n個空桶 vectorvectorfloat buckets(n); // 2. 將數(shù)組元素放入不同的桶中 for (int i 0; i n; i) { int bucketIndex n * arr[i]; // 映射函數(shù)假設arr[i]在[0,1)內(nèi) buckets[bucketIndex].push_back(arr[i]); } // 3. 對每個桶進行排序 for (int i 0; i n; i) { sort(buckets[i].begin(), buckets[i].end()); // 使用標準庫排序 } // 4. 將排序后的桶元素依次放回原數(shù)組 int index 0; for (int i 0; i n; i) { for (float num : buckets[i]) { arr[index] num; } } }桶排序的適用場景與局限適用數(shù)據(jù)分布均勻且易于劃分到有限數(shù)量的桶中。例如對大量0-100的考試成績進行排序。不適用數(shù)據(jù)分布極度不均勻?qū)е滤袛?shù)據(jù)都集中在少數(shù)幾個桶內(nèi)這時桶排序退化為單純的桶內(nèi)排序且額外增加了桶管理的開銷?;蛘邤?shù)據(jù)范圍非常大但數(shù)據(jù)量很小導致桶空間浪費嚴重。2.3 C STL 中的 map一個強大的有序“桶”管理器如果說我們手動實現(xiàn)“桶”和“桶排序”是在造輪子那么C標準模板庫STL中的std::map就是給我們提供了一輛現(xiàn)成的、功能強大的“分類管理車”。map是一種關聯(lián)容器它存儲的元素是鍵值對key-value并且會根據(jù)鍵key自動進行排序默認是升序。你可以把map理解為一個自動維護的、排序好的“桶”集合鍵Key相當于我們?yōu)椤巴啊辟N上的唯一標簽。map保證鍵的唯一性。值Value相當于這個“桶”里存放的內(nèi)容。自動排序map通?;诩t黑樹實現(xiàn)它會在你插入或刪除元素時自動維護所有鍵的排序順序。這意味著你不需要像手動實現(xiàn)桶排序那樣最后再去按順序收集桶。map的基本操作#include iostream #include map #include string using namespace std; int main() { // 聲明一個map鍵是string類型值是int類型 mapstring, int studentScore; // 插入元素三種方式 studentScore[Alice] 95; // 使用下標運算符如果鍵不存在則創(chuàng)建 studentScore.insert({Bob, 88}); // 使用insert方法 studentScore.emplace(Charlie, 92); // 使用emplace高效構造 // 查找元素 auto it studentScore.find(Alice); if (it ! studentScore.end()) { cout Alices score: it-second endl; // 輸出 95 } // 遍歷自動按鍵的字典序排序 for (const auto pair : studentScore) { cout pair.first : pair.second endl; } // 輸出 // Alice: 95 // Bob: 88 // Charlie: 92 // 刪除元素 studentScore.erase(Bob); // 判斷鍵是否存在 if (studentScore.count(David) 0) { cout David not found. endl; } return 0; }map與桶思想的關聯(lián)當你的“桶”的標簽鍵是離散的、需要動態(tài)增刪、并且你希望隨時能按標簽順序訪問時map是絕佳的選擇。它省去了你手動管理桶數(shù)組、處理哈希沖突、維護順序的麻煩。例如統(tǒng)計一篇文章中每個單詞出現(xiàn)的頻率單詞就是鍵頻率就是值mapstring, int完美契合。unordered_map的抉擇STL中還有一個unordered_map它基于哈希表實現(xiàn)不維護元素的順序但平均插入和查找的時間復雜度是O(1)。選擇map還是unordered_map根本在于你是否需要有序的鍵。需要順序遍歷或進行范圍查詢?nèi)缯掖笥谀硞€鍵的所有元素選map。只需要快速的查找、插入、刪除不關心順序選unordered_map。在大多數(shù)只做統(tǒng)計、查找的場景下unordered_map性能通常優(yōu)于map。3. 從理論到實戰(zhàn)例題精講與代碼剖析理解了概念我們通過兩道經(jīng)典的LeetCode例題來看看如何靈活運用桶思想和map。3.1 例題一前 K 個高頻元素LeetCode 347題目描述給你一個整數(shù)數(shù)組nums和一個整數(shù)k請你返回其中出現(xiàn)頻率前k高的元素。你可以按任意順序返回答案。思路分析 這個問題可以清晰地分解為幾個步驟完美串聯(lián)了map和“桶”的思想。統(tǒng)計頻率我們需要知道每個數(shù)字出現(xiàn)的次數(shù)。這顯然是一個鍵值對映射數(shù)字 - 次數(shù)并且我們只需要快速查找和更新暫時不需要順序。因此使用unordered_mapint, int是最合適的。按頻率排序目標是找出頻率最高的前k個。傳統(tǒng)思路是對unordered_map的鍵值對按值頻率排序但排序復雜度是 O(m log m)其中m是不同數(shù)字的個數(shù)。桶思想優(yōu)化這里可以引入“桶”。我們創(chuàng)建一個“桶數(shù)組”桶的索引代表頻率桶內(nèi)存儲具有該頻率的所有數(shù)字。由于頻率最高不會超過數(shù)組長度n所以我們只需要 n1 個桶索引從0到n。映射函數(shù)bucket[frequency] list of numbers with this frequency創(chuàng)建好這樣的桶之后從后向前從高頻到低頻遍歷桶數(shù)組依次取出數(shù)字直到取滿k個。這一步的時間復雜度是 O(n)。C實現(xiàn)與詳細注釋#include vector #include unordered_map using namespace std; class Solution { public: vectorint topKFrequent(vectorint nums, int k) { // 步驟1使用 unordered_map 統(tǒng)計每個數(shù)字出現(xiàn)的頻率 unordered_mapint, int frequencyMap; for (int num : nums) { frequencyMap[num]; // 如果num不存在會默認初始化為0后 } // 步驟2創(chuàng)建“桶”。桶下標是頻率桶內(nèi)是該頻率的所有數(shù)字。 // 最大頻率不會超過數(shù)組大小所以桶的數(shù)量為 nums.size() 1 vectorvectorint buckets(nums.size() 1); // 遍歷頻率哈希表將數(shù)字放入對應的頻率桶中 for (const auto pair : frequencyMap) { int num pair.first; int freq pair.second; buckets[freq].push_back(num); // 數(shù)字num放入第freq個桶 } // 步驟3從高頻到低頻從后向前遍歷桶收集前k個高頻元素 vectorint result; // 從最大的可能頻率nums.size()開始向下遍歷 for (int i buckets.size() - 1; i 0 result.size() k; --i) { // 如果當前桶不為空將其中的所有數(shù)字加入結果集 for (int num : buckets[i]) { result.push_back(num); if (result.size() k) { // 已收集夠k個立即返回 return result; } } } return result; // 理論上一定會提前返回這里為了語法完整 } };解題心得這道題是map此處用unordered_map和“桶”思想結合的典范。unordered_map負責高效統(tǒng)計而“桶”負責將“按值排序”的問題轉化為“按索引遍歷”的 O(n) 操作。它避免了全排序是典型的“空間換時間”策略。注意桶的結構是vectorvectorint因為同一頻率可能有多個數(shù)字。3.2 例題二存在重復元素 IIILeetCode 220題目描述給你一個整數(shù)數(shù)組nums和兩個整數(shù)k和t。請你判斷是否存在兩個不同的下標i和j使得abs(nums[i] - nums[j]) t并且滿足abs(i - j) k。思路分析 這道題難度較大它要求數(shù)值差在一定范圍(t)且下標差也在一定范圍(k)。暴力解法是 O(nk) 的復雜度。高效的解法需要結合滑動窗口和“桶”的思想?;瑒哟翱诰S護下標距離我們維護一個大小為k的滑動窗口使用set或map存儲窗口內(nèi)的元素當窗口超過k個元素時移除最舊的那個。這保證了窗口中任意兩個元素的下標差絕對值不超過k。桶思想判斷數(shù)值距離如何快速判斷窗口內(nèi)是否存在一個元素其值與當前元素x的差 t遍歷窗口是 O(k)。我們可以用“桶”來優(yōu)化。我們將數(shù)值空間劃分為若干個寬度為(t 1)的桶。例如t2則桶寬度為3。數(shù)值0,1,2落入桶03,4,5落入桶1以此類推。關鍵性質(zhì)如果兩個數(shù)在同一個桶內(nèi)那么它們差的絕對值一定 t。如果兩個數(shù)在相鄰桶內(nèi)它們差的絕對值也可能 t需要額外檢查。如果兩個數(shù)相隔超過一個桶差的絕對值必然 t。映射函數(shù)bucket_id floor(num / (t 1))。對于負數(shù)需要特殊處理例如-1 / 3在C中向0取整得0與2 / 3得0在同一個桶這不符合邏輯。因此我們采用bucket_id (num 0) ? ((num 1) / w - 1) : (num / w)其中w t 1。數(shù)據(jù)結構選擇我們需要一個能根據(jù)bucket_id快速查找是否存在對應元素的數(shù)據(jù)結構并且要能動態(tài)增刪滑動窗口。unordered_maplong long, long long很合適鍵是桶ID值是落入該桶的數(shù)值由于桶內(nèi)最多只需保存一個代表元素即可判斷。C實現(xiàn)與詳細注釋#include vector #include unordered_map #include cmath using namespace std; class Solution { public: bool containsNearbyAlmostDuplicate(vectorint nums, int k, int t) { if (t 0 || k 0) return false; // 根據(jù)題意負數(shù)參數(shù)無意義 unordered_maplong long, long long bucketMap; // 桶映射桶ID - 桶內(nèi)元素值 long long width (long long)t 1; // 桶的寬度 for (int i 0; i nums.size(); i) { long long num (long long)nums[i]; long long bucketId getBucketId(num, width); // 獲取當前元素所屬桶ID // 情況1當前桶已存在元素說明窗口內(nèi)有兩個數(shù)差t if (bucketMap.find(bucketId) ! bucketMap.end()) { return true; } // 情況2檢查左側相鄰桶 auto itLeft bucketMap.find(bucketId - 1); if (itLeft ! bucketMap.end() abs(num - itLeft-second) t) { return true; } // 情況3檢查右側相鄰桶 auto itRight bucketMap.find(bucketId 1); if (itRight ! bucketMap.end() abs(num - itRight-second) t) { return true; } // 將當前元素放入其桶中 bucketMap[bucketId] num; // 維護滑動窗口大小不超過k if (i k) { // 移除窗口最左側的元素 long long oldNum (long long)nums[i - k]; long long oldBucketId getBucketId(oldNum, width); bucketMap.erase(oldBucketId); } } return false; } private: // 獲取數(shù)值num所屬的桶ID正確處理負數(shù) long long getBucketId(long long num, long long width) { // 對于非負數(shù)桶ID num / width // 對于負數(shù)需要偏移使得 -1 落入 -1 桶而不是和 0,1,2 落入同一個桶 // 例如 width3: ... [-3,-2,-1] - -1桶, [0,1,2] - 0桶 ... return num 0 ? num / width : ((num 1) / width) - 1; } };解題心得與避坑指南整數(shù)溢出這是本題最大的坑。nums[i] - nums[j]可能超出int范圍必須使用long long。負數(shù)桶ID計算C的整數(shù)除法向0取整對于負數(shù)-1/3 0這與正數(shù)2/30混同。必須實現(xiàn)自定義的getBucketId函數(shù)來保證負數(shù)落入正確的桶。一個簡單的記憶方法是對于負數(shù)n其桶ID為(n1)/w - 1。桶內(nèi)存儲每個桶我們只需要存儲一個元素通常是最近放入的那個因為如果同一個桶里有兩個元素我們已經(jīng)直接返回true了。這保證了算法的正確性和空間效率。t0的特殊情況此時桶寬度為1算法退化為判斷窗口內(nèi)是否有重復元素這正是 LeetCode 219 題存在重復元素 II的解法。4. 進階技巧與性能考量4.1 如何為桶排序設計高效的映射函數(shù)映射函數(shù)是桶排序的靈魂它直接決定了數(shù)據(jù)分布的均勻性從而影響性能。設計時需考慮數(shù)據(jù)范圍已知如果數(shù)據(jù)明確在[min, max]之間桶索引可以計算為int bucketIndex (int)((num - min) / (max - min 1.0) * bucketCount);。數(shù)據(jù)范圍未知可以先遍歷一遍數(shù)據(jù)找出min和max或者采用動態(tài)調(diào)整桶的策略如使用map而非vector來管理桶但會失去O(1)的桶訪問。非數(shù)值數(shù)據(jù)對于字符串等數(shù)據(jù)需要設計哈希函數(shù)將其映射到有限的桶索引上這本質(zhì)上就是構建一個哈希表。4.2 map 的迭代器失效與性能陷阱使用map和unordered_map時必須小心迭代器失效問題。插入操作對于map插入元素不會使任何迭代器失效除了被刪除元素的迭代器。刪除操作刪除元素只會使指向被刪除元素的迭代器失效其他迭代器仍然有效。這是map基于樹相對于vector的一大優(yōu)勢。mapint, string m {{1, a}, {2, b}, {3, c}}; auto it m.find(2); if (it ! m.end()) { m.erase(it); // it 現(xiàn)在失效不能再使用 // 但 it_other m.find(1) 獲取的迭代器仍然有效 }[]運算符 vsinsert/emplacemap[key]如果key不存在會插入一個具有默認值的鍵值對。而insert或emplace只有在鍵不存在時才會插入。在只需要查找、不希望意外插入的場景應使用find方法。遍歷中修改在基于范圍的for循環(huán)或使用迭代器遍歷時直接插入或刪除元素可能導致未定義行為。安全的做法是先收集需要修改的鍵遍歷結束后再統(tǒng)一操作。4.3 桶排序 vs 其他排序算法場景選擇桶排序并非萬能理解其優(yōu)劣才能正確選擇。算法平均時間復雜度最壞時間復雜度空間復雜度穩(wěn)定性適用場景桶排序O(n k)O(n2)O(n k)穩(wěn)定數(shù)據(jù)分布均勻易于分桶快速排序O(n log n)O(n2)O(log n)不穩(wěn)定通用平均性能好歸并排序O(n log n)O(n log n)O(n)穩(wěn)定需要穩(wěn)定性鏈表排序堆排序O(n log n)O(n log n)O(1)不穩(wěn)定原地排序?qū)彺娌挥押糜嫈?shù)排序O(n k)O(n k)O(k)穩(wěn)定數(shù)據(jù)范圍k較小如0-100選擇建議當數(shù)據(jù)是浮點數(shù)且范圍已知如[0,1)分布均勻桶排序是極佳選擇。當數(shù)據(jù)是小范圍整數(shù)計數(shù)排序可視為桶大小為1的桶排序更簡單高效。對于通用排序std::sort通常為內(nèi)省排序是首選。當需要穩(wěn)定排序且數(shù)據(jù)量大考慮std::stable_sort通常為歸并排序。4.4 利用 auto 關鍵字簡化 map 相關代碼C11 引入的auto關鍵字能極大簡化迭代器聲明讓代碼更清晰。// 傳統(tǒng)方式類型名冗長 std::mapstd::string, std::vectorint::iterator it myMap.begin(); // 使用auto編譯器自動推導類型 auto it myMap.begin(); // 在基于范圍的for循環(huán)中尤其方便 for (const auto keyValuePair : myMap) { // keyValuePair 是 std::pairconst Key, Value std::cout keyValuePair.first : keyValuePair.second std::endl; } // 結構化綁定 (C17)更直觀 for (const auto [key, value] : myMap) { std::cout key : value std::endl; }使用auto不僅能減少打字錯誤還能使代碼更專注于邏輯而不是復雜的類型名。特別是在模板編程或嵌套容器中優(yōu)勢更加明顯。5. 常見問題排查與調(diào)試技巧5.1 桶排序結果錯誤或崩潰問題訪問桶數(shù)組時發(fā)生越界。排查檢查映射函數(shù)。確保對于所有可能的輸入num計算出的bucketIndex滿足0 bucketIndex bucketCount。特別是邊界值min和max要正確處理。打印bucketIndex和bucketCount進行調(diào)試??紤]使用vector.at(index)替代operator[]at()會進行邊界檢查并拋出std::out_of_range異常便于定位問題。問題排序結果不正確部分元素順序錯亂。排查確認桶內(nèi)排序算法是否穩(wěn)定如果穩(wěn)定性是要求的應使用穩(wěn)定排序算法如std::stable_sort或插入排序。檢查合并結果的邏輯。確保是按桶的索引順序從小到大依次取出桶內(nèi)元素。如果數(shù)據(jù)是浮點數(shù)注意浮點數(shù)精度問題可能導致映射到錯誤的桶??梢钥紤]給映射結果加上一個小的 epsilon 偏移或者使用整數(shù)運算來模擬。5.2 map 查找或插入行為不符合預期問題使用map[key]訪問不存在的鍵后map 的大小增加了。原因map的operator[]在鍵不存在時會插入一個具有默認值的鍵值對。這不是一個只讀操作解決如果只想檢查鍵是否存在而不想插入應使用find()方法。mapstring, int m; if (m.find(unknown) ! m.end()) { // 正確只查找不插入 int val m[unknown]; } // 錯誤int val m[unknown]; // 這會插入 {unknown, 0}問題自定義類型作為map的鍵時編譯失敗或運行時排序錯誤。原因map需要根據(jù)鍵來排序因此鍵類型必須支持嚴格弱序的比較通常是重載運算符或提供自定義的比較函數(shù)對象。解決struct MyKey { int id; string name; // 方法1重載 運算符 bool operator(const MyKey other) const { if (id ! other.id) return id other.id; return name other.name; } }; mapMyKey, int myMap1; // 方法2提供自定義比較器 struct MyKeyComparator { bool operator()(const MyKey a, const MyKey b) const { return tie(a.id, a.name) tie(b.id, b.name); } }; mapMyKey, int, MyKeyComparator myMap2;對于unordered_map則需要為自定義鍵類型提供哈希函數(shù)和相等比較函數(shù)。5.3 內(nèi)存與性能問題問題桶排序或使用超大map時內(nèi)存占用過高。優(yōu)化桶的數(shù)量桶的數(shù)量并非越多越好。過多的桶會導致大量空桶浪費內(nèi)存增加遍歷開銷。通常桶數(shù)量取sqrt(n)或與數(shù)據(jù)范圍成比例的一個合理值。桶的數(shù)據(jù)結構如果桶內(nèi)元素極少使用vector可能因預分配空間造成浪費??梢钥紤]使用list或forward_list但會犧牲一些緩存局部性。需要根據(jù)實際數(shù)據(jù)分布權衡。map的預分配unordered_map可以預先調(diào)用reserve(n)預留足夠桶數(shù)減少重建哈希表的開銷。問題map的插入、刪除、查找操作變慢。排查對于map紅黑樹操作是 O(log n)數(shù)據(jù)量極大時可能成為瓶頸??紤]是否可以用unordered_mapO(1) 平均替代。對于unordered_map如果哈希沖突嚴重所有元素都擠在少數(shù)幾個桶里性能會退化到 O(n)。檢查哈希函數(shù)的質(zhì)量或考慮使用標準庫提供的針對基本類型的特化哈希。使用性能分析工具如perf,Valgrind, VS Profiler定位熱點代碼。5.4 多線程環(huán)境下的安全問題無論是手動實現(xiàn)的桶數(shù)組還是 STL 的map它們在默認情況下都不是線程安全的。競態(tài)條件如果多個線程同時讀寫同一個桶或同一個map元素會導致未定義行為。迭代器失效一個線程在遍歷容器時另一個線程進行了插入或刪除可能導致迭代器失效引發(fā)崩潰。解決方案最直接使用互斥鎖std::mutex在訪問共享容器前加鎖。注意鎖的粒度過粗影響性能過細增加復雜度。讀寫鎖如果讀多寫少可以使用std::shared_mutexC17。并發(fā)容器考慮使用 TBBIntel Threading Building Blocks或 folly 等庫提供的并發(fā)哈希表。避免共享設計上盡可能讓每個線程擁有自己的數(shù)據(jù)副本最后再合并這是最理想的并行模式。調(diào)試這類問題通常比較困難可以使用線程消毒工具如ThreadSanitizer來幫助檢測數(shù)據(jù)競爭。一個基本原則是除非有明確的同步機制否則不要在多線程間共享可變的 STL 容器。