數據結構的選型中,“高效查找與操作”始終是核心需求。當面對海量數據的插入、查詢場景時,基於紅黑樹實現的map/set雖能保證有序性,卻受限於O(log n)的時間複雜度,難以突破性能瓶頸。而哈希表及其衍生的unordered_map/unordered_set,憑藉“平均O(1)”的極致效率,成為解決這類問題的最優解之一。
為什麼哈希表能實現遠超紅黑樹的操作速度?unordered系列容器的底層是如何依託哈希表實現高效運作的?它們與map/set之間究竟存在哪些性能差異,又該如何根據場景精準選型?本文將從底層原理到上層用法,介紹哈希表與unordered容器的核心邏輯,擺脱O(log n)的束縛,實現效率的跨越式提升。
一.哈希表 Hash
我們可能遇這樣一種問題:分別統計一個字符串中每個小寫字母字母出現的次數;
一般而言,我們會採用“映射”的方式,將字母映射成數組下標,用數組內容統計字母出現次數;
這就是一種哈希結構!當我們想得到'z'出現次數,只需消耗O(1)時間,去訪問第'z'-'a'個數組元素:a['z'-'a'];
1.1哈希概念
哈希:通過哈希函數將數據的“鍵(Key)”映射為固定長度的“哈希值”,以此作為數據在哈希表中的存儲地址,實現“鍵-地址”的直接對應(儲存值和儲存位置的一一對應關係)。
核心價值:跳過遍歷,直接定位數據,奠定哈希表“平均O(1)”的高效性能基礎。
1.2哈希函數
但在實際情況中,我們的數據並不像'a'~'z'是連續的關係,可能是從1到1000甚至更多,難道我們要開闢“無限大”的數組空間?
所以,在多數情況下,我們的映射存在某種固定轉換關係,以確定“實際數據“與“映射編號”之間的關係:
像i=key%10這種在一次映射中固定的轉換關係,我們稱之為 哈希函數;i 稱之為哈希值;key 是數據的“鍵”;
①哈希函數的設置
hash(key) = key % capacity hash(key)就是下標i,capacity是我們開闢空間的大小;當然,我們這種轉換關係不一定非得取模;
常見類型:直接定址法、除留餘數法(上述用的)、平方取中法等,實際中需結合數據場景選擇。
②核心要求:
- 確定性:同一Key始終對應同一哈希值(無論經過多少次哈希計算,得到的哈希值始終相同 eg:999%10永遠是9,不會變);
- 高效性:計算過程快速,耗時可忽略;
- 均勻性:儘量讓不同Key的哈希值分散,減少衝突。
1.3哈希衝突/碰撞
如果我們在上述示例中在增加兩個數據:
哈希衝突:不同的“鍵(Key)”通過哈希函數計算後,得到了相同的“哈希值”,導致它們要爭奪同一個存儲地址。
這兩個數據的衝突直接導致哈希表無法正常存儲和查找,如何解決這個和問題?
有兩種核心方法
①閉散列 - 開放定址法
線性探測
衝突時,i順延向後,尋找下一個空閒地址存儲數據。
二次探測
二次探測:哈希地址衝突時,按“原始地址±i²(i=1,2,3…)的規則尋找空閒地址(解決線性探測的“聚集”問題,讓探測時的地址更加分散)
問題
對於開放定址法,提出兩個問題:刪除數據會導致什麼?沒有位置了怎麼辦?
先看刪除問題:
對於這個哈希表,通過開放定址法我們將44插入到8位置,當我們查找時需要從4不斷向後找直到找到44,;但當我們刪除6後,再次查找44會發生什麼?
走到空我們能否確定這個數據不存在?我們要遍歷整個數組去確定44是否存在嗎?這顯然與我們想要達到的O(1)複雜度相違背了
所以我們不得不對每個位置增加一個“狀態標記”;如果一個位置是DELETE(刪除)狀態,可能在插入44前這個位置是有數據的,不能停止向後查找;
enum STATE
{
EXIST,
EMPTY,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
STATE _state = EMPTY;//狀態
};
數據中包括狀態,在查找時通過狀態確認:
//查找部分
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
HashTable()
{
_table.resize(10);
}
HashData<const K, V>* Find(const K& key)//用K找V
{
//線性探測
//當數據類型不確定,無法用一種同一的方法進行計算時(如字符串無法%),寫成仿函數
HashFunc hf;
size_t hashi = hf(key) % _table.size();
while (_table[hashi]._state != EMPTY)//直到空才停止查找
{
if (_table[hashi]._state == EXIST
&& _table[hashi]._kv.first == key)
{
return (HashData<const K, V>*) & _table[hashi];
//HashData<const K, V>* 和 HashData<K, V>* 是兩個不同類型,用不同的模板參數去實例化(const K與K)
//需要強制類型轉換一下
//指針可以直接強轉
}
++hashi;
hashi %= _table.size();
}
return nullptr;
}
//按需編譯
bool Erase(const K& key)
{
HashData<const K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_n;
return true;
}
return false;
}
private:
vector<HashData<K, V>> _table;
size_t _n = 0;//存儲有效數據個數
//空間中數據不是連續存儲的,結構上是數組,邏輯上是哈希表
};
}
//仿函數部分
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//模版的特化
template<>
struct DefaultHashFunc<string>
{
size_t operator()(const string& str)
{
size_t hash = 0;
//BKDR
for (auto ch : str)
{
//降低重複概率
hash *= 131;
//溢出不重要,插入是溢出被截斷;
//查找時按相同規律,也會溢出截斷,不影響
hash += ch;
}
return hash;
}
};
哈希表擴展
對於第二個問題,空間不夠肯定要去擴容的,重點是什麼時候擴容?怎麼去擴容?
哈希載荷因子(又稱負載因子):哈希表中已存元素個數與表總容量的比值,用於衡量表的擁擠程度,核心是平衡哈希衝突概率與空間利用率。
以負載因子大等於0.7需要擴容為例:
但是不能直接用resize()擴容:hash(key) = key % capacity 當capacity改變,映射關係就變了,我們需要重新開闢一個哈希表並以新的映射方式插入所有數據;
bool Insert(const pair<K, V>& kv)
{
//擴容
if ((double)_n / _table.size() >= 0.7)
{
size_t newSize = _table.size() * 2;
//_table.resize(newSize); 直接resize映射關係變了,位置出現錯誤
HashTable<K, V> newHT;
//直接創建HashTable,比創建 vector<HashData<K, V>> 要好,能直接複用哈希表的Insert
newHT._table.resize(newSize);
//遍歷舊錶數據插入新表中
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i]._state == EXIST)
{
newHT.Insert(_table[i]._kv);
}
}
_table.swap(newHT._table);
}
//線性探測
HashFunc hf;仿函數
size_t hashi = hf(kv.first) % _table.size();//值%大小 = hashi
while (_table[hashi]._state == EXIST)
{
++hashi;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
return true;
}
②哈希桶
對於開放尋址法(線性和二次探測)而言,哈希衝突是互相影響的;所以,在大多數情況下,我們會採用另一個結構:哈希桶
概念
哈希桶:哈希表中承載數據的基本“容器”,每個桶對應一個哈希地址,元素經哈希函數計算後存入對應桶。
同樣採用哈希函數映射,但是面對哈希衝突,我們採用鏈式結構不斷在桶上“掛數據”:
當然為了提高效率,有時這種“鏈式”可以是其他數據結構:
struct HashNode
{
HashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
pair<K, V> _kv;
HashNode<K, V>* _next;//節點中有指向下一個節點的指針
};
template<class K,class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
private:
vector<Node*> _table;//指針數組 --
size_t _n = 0;//記錄存儲有效數據
};
擴展:
C++的標準哈希表(std::unordered_map/unordered_set):默認用哈希桶+鏈表處理衝突,即鏈地址法。 但是當單個桶中元素過多(超過閾值),會自動將鏈表轉為紅黑樹,以提升高衝突場景下的查找、插入效率(時間複雜度從O(n)降為O(log n))。
雖然哈希表最壞情況會退化(非絕對O1),但實際中,通過良好的哈希函數和動態擴容,可避免最壞情況,保證高效性。
插入
為了避免一條“鏈”太長,我們仍然需要去控制負載因子,對於哈希桶負載因子一般被控制在 1 :
bool Insert(const pair<K, V>& kv)
{
HashFunc hf;
if (Find(kv.first))
{
return false;
}
//負載因子到1擴容
if (_n == _table.size())
{
size_t newSize = _table.size() * 2;
vector<Node*> newTable;
newTable.resize(newSize,nullptr);
//遍歷舊錶,把節點拿過來——如果直接調insert,又會newNode出新節點,舊節點無法銷燬
for (size_t i = 0; i < _table.size(); ++i)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
//頭插到新表
size_t hashi = hf(cur->_kv.first) % newSize;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;//往後,直到一個桶為空
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
//插入數據 - 頭插
size_t hashi = hf(kv.first) % _table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
return true;
}
查找
所以,哈希桶的查找要根據映射關係,找到在哪個“桶”,然後遍歷鏈表尋找:
Node* Find(const K& key)
{
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;//返回節點
}
cur = cur->_next;
}
return nullptr;//沒找到
}
優勢
- 高效存儲:按哈希地址直接訪問,平均查找、插入、刪除效率接近O(1)。
- 衝突處理:同一桶可通過鏈表、紅黑樹等組織衝突元素,靈活解決哈希衝突。
- 空間適配:可動態擴容/縮容,平衡空間利用率與訪問效率。
二.unordered系列容器
STL unordered系列容器:基於哈希表實現的關聯式容器,核心是 std::unordered_map (鍵值對)、 std::unordered_set (單值),還有對應的多鍵版本( unordered_multimap/unordered_multiset(允許數據重複出現) )。
與基於紅黑樹(或AVL)實現的map/set不同(樹形map、set),unordered系列(hash map、set)是無序的,通過為鍵值提供的哈希函數進行關係映射,保證平均O(1)的時間複雜度。
接下來,為了更深入瞭解unordered系列容器以及哈希表的使用,我們需要了解 unordered_set 與 unordered_map 是如何用哈希表被封裝的;
2.1unordered的使用
正如上段文字所解釋:unordered系列本質就是對哈希表(默認使用哈希桶)的封裝,基於對哈希表的學習,實際我們已經能夠理解unordered_set 與 unordered_map 的核心原理了,但對於底層實現細節很多,所以先介紹使用;
首先unordered_ 系列是完全符合STL標準的容器,和STL的設計理念、接口風格高度統一:
2.2Hash封裝unordered_思路
模擬實現,是為了我們深入瞭解unordered_實現原理,加深對容器特性的理解。
封裝思路按照下面六個步驟展開,後面介紹主要以代碼為主,但想要徹底掌握,不能只看,我們一定要自己梳理思路去實現一遍:
- 實現哈希(採用哈希桶,完整代碼放在最後)
- 封裝map/set
- 普通迭代器的處理
- const迭代器的處理
- insert返回值 與 operator[]處理
- key不能修改問題
2.3以下是HashTable核心代碼:
代碼中有註釋,核心問題和重點放到最後,不再另做文字解釋。
#pragma once
#include<vector>
#include<string>
#include<iostream>
using namespace std;
//兩個仿函數 - 解決哈希函數問題(有些數據比如char不能直接取模)
//都能用 放全局 ——細節在類中
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//模版的特化
template<>
struct DefaultHashFunc<string>
{
size_t operator()(const string& str)
{
//return str[0];
size_t hash = 0;
for (auto ch : str)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
namespace hash_bucket
{
//泛型編程: 不是針對某種具體類型,而是針對廣泛的類型(兩種以上) —— 模板
template<class T>
struct HashNode
{
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
T _data;//不確定是key 還是 key-value的
HashNode<T>* _next;
};
//前置聲明
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable;
//template<class T>
template<class K, class T,class Ptr,class Ref, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
struct HTIterator
{
typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;
typedef HashNode<T> Node;
typedef HTIterator<K, T,Ptr,Ref, KeyOfT, HashFunc> Self;//
Node* _node;//只有node沒有_table,找不到下一個位置,需要把哈希表也傳過去
const HashTable<K, T, KeyOfT, HashFunc>* _pht;//創建哈希表的指針
//普通迭代器它是拷貝構造
//const迭代器是構造
HTIterator(const iterator& it)
:_node(it._node)
, _pht(it._pht)//傳參時this就是HashTable的指針,更方便
{}
HTIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht)
:_node(node)
,_pht(pht)//傳參時this就是HashTable的指針,更方便
{}
HTIterator(Node * node, const HashTable<K, T, KeyOfT, HashFunc>*pht)
:_node(node)
, _pht(pht)//傳參時this就是HashTable的指針,更方便
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
Self& operator++()
{
if (_node->_next)
{
//當前桶未結束
_node = _node->_next;
}
else
{
KeyOfT kot;//得到key
HashFunc hf;//得到key "數"
size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
//從下一個位置查找不為空的桶
++hashi;
while (hashi < _pht->_table.size())
{
if (_pht->_table[hashi])
{
_node = _pht->_table[hashi];//找到下一個桶
return *this;
}
else
{
++hashi;
}
}
//所有桶都走完了
_node = nullptr;//空充當最後一個位置
}
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
};
//set -> hash_bucket::HashTable<K, K> _t;
//map -> hash_bucket::HashTable<K,pair<K,V>> _t;
//第一個模板參數K有什麼用?
//在Find/Erase時,參數是K類型,如果沒有K,對於map,Find需要傳pair ×
template<class K,class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
typedef HashNode<T> Node;//第二個模板參數確定是key 還是 key-value
//友元聲明
template<class K, class T, class Ptr,class Ref,class KeyOfT, class HashFunc>
friend struct HTIterator;
public:
typedef HTIterator<K, T,T*,T&, KeyOfT, HashFunc> iterator;
typedef HTIterator<K, T,const T*,const T&, KeyOfT, HashFunc> const_iterator;
iterator begin()
{
//找第一個桶
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
if (cur)
{
return iterator(cur, this);
}
}
return iterator(nullptr, this);
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator begin()const
{
//找第一個桶
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
if (cur)
{
return const_iterator(cur, this);
}
}
return const_iterator(nullptr, this);
}
const_iterator end()const
{
return const_iterator(nullptr, this);
}
~HashTable()
{
for (size_t i = 0; i < _table.size(); ++i)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
HashTable()
{
_table.resize(10, nullptr);
}
pair<iterator,bool> Insert(const T& data)
{
HashFunc hf;
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end())
{
return make_pair(it, false);
}
//負載因子到1擴容
if (_n == _table.size())
{
size_t newSize = _table.size() * 2;
vector<Node*> newTable;
newTable.resize(newSize,nullptr);
//遍歷舊錶,把節點拿過來——如果直接調insert,又會newNode出新節點,舊節點無法銷燬
for (size_t i = 0; i < _table.size(); ++i)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
//頭插到新表
size_t hashi = hf(kot(cur->_data)) % newSize;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;//往後,直到一個桶為空
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
size_t hashi = hf(kot(data)) % _table.size();
//頭插
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
return make_pair(iterator(newnode,this),true);
}
iterator Find(const K& key)
{
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur,this);
}
cur = cur->_next;
}
return iterator(nullptr,this);
}
bool Erase(const K& key)
{
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (kot(cur->_data) == key)
{
//頭刪
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
--_n;
return false;
}
void Print()
{
KeyOfT kot;
for (size_t i = 0; i < _table.size(); ++i)
{
printf("[%d]->", i);
Node* cur = _table[i];
while (cur != nullptr)
{
cout << kot(cur->_data) << ":" << kot(cur->_data) <<"->";
cur = cur->_next;
}
cout << "NULL" << endl;
}
cout << endl;
}
private:
//編譯器默認析構:對內置類型不做處理,對自定義類型調默認析構 ——
// 存節點的“指針”是內置類型,不做處理,需要自己寫析構函數
//包括拷貝構造都需要自己寫
vector<Node*> _table;//指針數組
size_t _n = 0;//記錄存儲有效數據
};
}
2.4set核心代碼
代碼中有註釋,核心問題和重點放到最後,不再另做文字解釋。
template<class K>
class undered_set
{
//內部類,獲取 “鍵” Key
struct SetKeyOfT
{
K operator()(const K& key)
{
return key;
}
};
public:
//迭代器封裝 - 要保證set中值不允許在迭代器被修改
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;
const_iterator begin()const
{
return _ht.begin();
}
const_iterator end()const
{
return _ht.end();
}
返回值為了兼容
pair<iterator, bool> insert(const K& key)
{
// return _ht.Insert(key); //不嚴格,兩種不同類型轉換
pair<typename hash_bucket::HashTable<K, K, SetKeyOfT>::iterator, bool> ret = _ht.Insert(key);
return pair <const_iterator, bool>(ret.first,ret.second);
}
private:
hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
//底層機制以 HashTable 完成
};
2.5map核心代碼
template<class K, class V>
class undered_map
{
//內部類,獲取 “鍵” Key
struct MapKeyOfT
{
K operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
//對於map而言:value是允許被修改的,但 key不可以
typedef typename hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT>::iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT>::const_iterator const_iterator;
//所有操作幾乎都有 HashTable對應版本,傳遞調用就行
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator begin()const
{
return _ht.begin();
}
const_iterator end()const
{
return _ht.end();
}
//允許通過[key]去查找/添加/修改value
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
//插入要注意:不允洗鍵值重複
//insert返回值是為了方便[]操作
pair<iterator,bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
private:
hash_bucket::HashTable<K, pair<const K,V>,MapKeyOfT> _ht;
};
重點補充
第一個問題:set的模板參數
set只有一個模板參數,為什麼傳哈希表要傳兩個key?
為了兼容map,讓哈希表有K,V兩個模板參數;比如: 哈希表Find或者Erase操作需要我們用K值去查找,如果只有一個模板參數,我們只能傳pair;而兩個模板參數,我們只需要個set傳兩個K,會方便很多。
第二個問題:const迭代器
unordered_set是基於哈希表實現的,元素的位置由其哈希值決定,修改元素會改變其哈希值,導致哈希表結構混亂、查找失效。所以unordered_set的迭代器是隻讀的(常量迭代器),不允許通過迭代器修改元素;只需要在底層都使用哈希表的const迭代器。若需修改元素,需先刪除舊元素,再插入新元素。
而針對unordered_map而言,"value"是允許被修改的,不能像set一樣直接用哈希表的const迭代器封裝;非const迭代器返回pair<const K,T> 而const_iterator在此基礎上進制修改pair整體(包括second);
template<class K>
class undered_set
{
private:
struct SetKeyOfT
{
K operator()(const K& key)
{
return key;
}
};
public:
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;
const_iterator begin()const
{
return _ht.begin();
}
const_iterator end()const
{
return _ht.end();
}
pair<iterator, bool> insert(const K& key)
{
// return _ht.Insert(key); //不嚴格,兩種不同類型轉換
pair<typename hash_bucket::HashTable<K, K, SetKeyOfT>::iterator, bool> ret = _ht.Insert(key);
return pair <const_iterator, bool>(ret.first,ret.second);
}
private:
hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
};
第三個問題:內部類 KeyOfT
為什麼需要SetKeyOfT/MapKeyOfT:因為在哈希表中,插入統一得到的都是數據,而對於map這個數據數是pair,set不是;但我們底層用的是一個哈希表去封裝的,所以我們需要一個函數去得到數據。
第四個問題:DefaultHashFunc
在哈希表中,對string類型處理屬於模板特化:為模板特定類型/值寫專屬實現,優先匹配,解決哈希函數面對一些數據無法取模的操作。
//模板特化有兩種
template <typename T> struct A {}; // 通用
template <> struct A<int> {}; // 對int的全特化
第五個問題:素數擴容理論
有人説:unordered系列擴容時,將桶數設為素數,減少 hash(key)%桶數 的衝突,保障O(1)效率。(但並沒有明確論證)
三.unordered_系列總結
對比樹形 map/set,與哈希 map/set
|
|
map/set |
unordered_map/set |
|
底層結構 |
紅黑樹(有序平衡) |
哈希表(數組+鏈表/紅黑樹) |
|
有序性 |
按鍵自動排序 |
無序 |
|
複雜度 |
O(log n) |
平均O(1),最壞O(n) |
|
迭代器 |
雙向迭代器 |
單向迭代器 |
|
空間 |
較低(無冗餘) |
較高(哈希表擴容+桶開銷) |
綜上,unordered系列是“以空間換時間”的高效選擇——無需有序時,平均O(1)的增刪查足以碾壓樹形容器。避開最壞衝突場景,它就是性能優先場景的首選。