數據結構的選型中,“高效查找與操作”始終是核心需求。當面對海量數據的插入、查詢場景時,基於紅黑樹實現的map/set雖能保證有序性,卻受限於O(log n)的時間複雜度,難以突破性能瓶頸。而哈希表及其衍生的unordered_map/unordered_set,憑藉“平均O(1)”的極致效率,成為解決這類問題的最優解之一。 

為什麼哈希表能實現遠超紅黑樹的操作速度?unordered系列容器的底層是如何依託哈希表實現高效運作的?它們與map/set之間究竟存在哪些性能差異,又該如何根據場景精準選型?本文將從底層原理到上層用法,介紹哈希表與unordered容器的核心邏輯,擺脱O(log n)的束縛,實現效率的跨越式提升。

一.哈希表 Hash

我們可能遇這樣一種問題:分別統計一個字符串中每個小寫字母字母出現的次數;

一般而言,我們會採用“映射”的方式,將字母映射成數組下標,用數組內容統計字母出現次數;

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希表

這就是一種哈希結構!當我們想得到'z'出現次數,只需消耗O(1)時間,去訪問第'z'-'a'個數組元素:a['z'-'a']

1.1哈希概念

哈希通過哈希函數將數據的“鍵(Key)”映射為固定長度的“哈希值”,以此作為數據在哈希表中的存儲地址,實現“鍵-地址”的直接對應(儲存值和儲存位置的一一對應關係)

核心價值跳過遍歷,直接定位數據,奠定哈希表“平均O(1)”的高效性能基礎。

1.2哈希函數

但在實際情況中,我們的數據並不像'a'~'z'是連續的關係,可能是從1到1000甚至更多,難道我們要開闢“無限大”的數組空間?

所以,在多數情況下,我們的映射存在某種固定轉換關係,以確定“實際數據“與“映射編號”之間的關係:

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希衝突_02

像i=key%10這種在一次映射中固定的轉換關係,我們稱之為 哈希函數;i 稱之為哈希值;key 是數據的“鍵”;

①哈希函數的設置

hash(key) = key % capacity hash(key)就是下標i,capacity是我們開闢空間的大小;當然,我們這種轉換關係不一定非得取模;

常見類型:直接定址法、除留餘數法(上述用的)、平方取中法等,實際中需結合數據場景選擇。

②核心要求:
  1. 確定性:同一Key始終對應同一哈希值(無論經過多少次哈希計算,得到的哈希值始終相同 eg:999%10永遠是9,不會變);
  2. 高效性:計算過程快速,耗時可忽略;
  3. 均勻性:儘量讓不同Key的哈希值分散,減少衝突。

1.3哈希衝突/碰撞

如果我們在上述示例中在增加兩個數據:

[STL]拒絕O(log N)!哈希表與unordered系列指南_unordered_系列容器_03

哈希衝突:不同的“鍵(Key)”通過哈希函數計算後,得到了相同的“哈希值”,導致它們要爭奪同一個存儲地址。


這兩個數據的衝突直接導致哈希表無法正常存儲和查找,如何解決這個和問題?

有兩種核心方法

①閉散列 - 開放定址法

線性探測

衝突時,i順延向後,尋找下一個空閒地址存儲數據。

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希衝突_04

二次探測

二次探測:哈希地址衝突時,按“原始地址±i²(i=1,2,3…)的規則尋找空閒地址(解決線性探測的“聚集”問題,讓探測時的地址更加分散)

[STL]拒絕O(log N)!哈希表與unordered系列指南_unordered_系列容器_05

問題

對於開放定址法,提出兩個問題:刪除數據會導致什麼?沒有位置了怎麼辦?

先看刪除問題:

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希衝突_06

對於這個哈希表,通過開放定址法我們將44插入到8位置,當我們查找時需要從4不斷向後找直到找到44,;但當我們刪除6後,再次查找44會發生什麼?

[STL]拒絕O(log N)!哈希表與unordered系列指南_STL_07

走到空我們能否確定這個數據不存在?我們要遍歷整個數組去確定44是否存在嗎?這顯然與我們想要達到的O(1)複雜度相違背了

[STL]拒絕O(log N)!哈希表與unordered系列指南_STL_08

所以我們不得不對每個位置增加一個“狀態標記”;如果一個位置是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;
		}
	};
哈希表擴展

對於第二個問題,空間不夠肯定要去擴容的,重點是什麼時候擴容?怎麼去擴容?

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希表_09

哈希載荷因子(又稱負載因子:哈希表中已存元素個數與表總容量的比值,用於衡量表的擁擠程度,核心是平衡哈希衝突概率與空間利用率。

以負載因子大等於0.7需要擴容為例:

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希衝突_10

但是不能直接用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;
}

②哈希桶

對於開放尋址法(線性和二次探測)而言,哈希衝突是互相影響的;所以,在大多數情況下,我們會採用另一個結構:哈希桶

概念

哈希桶:哈希表中承載數據的基本“容器”,每個桶對應一個哈希地址,元素經哈希函數計算後存入對應桶。

同樣採用哈希函數映射,但是面對哈希衝突,我們採用鏈式結構不斷在桶上“掛數據”:

[STL]拒絕O(log N)!哈希表與unordered系列指南_STL_11

當然為了提高效率,有時這種“鏈式”可以是其他數據結構:

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希表_12

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),但實際中,通過良好的哈希函數和動態擴容,可避免最壞情況,保證高效性。

插入

[STL]拒絕O(log N)!哈希表與unordered系列指南_unordered_系列容器_13

為了避免一條“鏈”太長,我們仍然需要去控制負載因子,對於哈希桶負載因子一般被控制在 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的使用

[STL]拒絕O(log N)!哈希表與unordered系列指南_哈希表_14

正如上段文字所解釋:unordered系列本質就是對哈希表(默認使用哈希桶)的封裝,基於對哈希表的學習,實際我們已經能夠理解unordered_set 與 unordered_map 的核心原理了,但對於底層實現細節很多,所以先介紹使用;

首先unordered_ 系列是完全符合STL標準的容器,和STL的設計理念、接口風格高度統一:

[STL]拒絕O(log N)!哈希表與unordered系列指南_unordered_系列容器_15

2.2Hash封裝unordered_思路

模擬實現,是為了我們深入瞭解unordered_實現原理,加深對容器特性的理解。

封裝思路按照下面六個步驟展開,後面介紹主要以代碼為主,但想要徹底掌握,不能只看,我們一定要自己梳理思路去實現一遍:

  1. 實現哈希(採用哈希桶,完整代碼放在最後)
  2. 封裝map/set
  3. 普通迭代器的處理
  4. const迭代器的處理
  5. insert返回值 與 operator[]處理
  6. 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)的增刪查足以碾壓樹形容器。避開最壞衝突場景,它就是性能優先場景的首選。