哈希表與STL中的unordered_map(hash_map)、unordered_set(hash_set)

 

1.什麼是哈希表,爲什麼要有哈希表?

總的來講,哈希表是一種查找和存儲結合一體的技術。

想一想我們在順序表(如數組)中查找是怎麼查找的,我們遍歷數組,將每一個元素都與給定關鍵字進行比較,直到查找成功或遍歷結束,查找時間複雜度爲O(n); 但O(n)實在是太慢了,尤其是在海量數據中查找某一個關鍵字,所以我們提出了改進查找算法,先將順序表中元素進行排序(排序時間複雜度不計入查找時間複雜度),然後進行二分查找(Binary Search),二分查找的時間複雜度爲O(logn),就是說如果要從100萬份數據中找到我們的關鍵數據,那麼最多進行20次比較就可以找到,200萬份的話也只需用21次比較即可,相比順序查找效率提升了很多,但是20次查找在我們需要頻繁查找的時候也會顯得有點力不從心,我們不可能按下查找鍵之後等程序跑一會兒,所以能不能進一步降低查找時間複雜度,提升查找速度呢?答案是肯定的,大牛是厲害的。

我們先看看在有序數組a[ N ]的二分查找中查找到關鍵字後會發生什麼,查找到相應關鍵字後會成功返回數組的下標i,我們的目的就是找到這個i,然後通過順序存儲位置計算方法,Loc(ai) = Loc(a1) + i*c(c爲每個數組元素所佔的內存大小);得到最後的內存地址,將目標元素取出來,所以我們發現我們查找過程中的比較的最終目的都是爲了找到目標數據的存儲位置,那麼爲什麼不直接將關鍵字與目標數據的存儲位置對應起來呢?就像我問你你家在哪,你根本不需要查找,你和你的家是對應的,而這就是哈希表的核心思想,它的時間複雜度幾乎是O(1)。

也就是說我們只需要通過某個函數將關鍵字和其存儲位置相對應起來即可:

                                           存儲位置 = f(關鍵字)

我們通過查找關鍵字不需要比較就可以獲得需要記錄的存儲位置,這是一種新的存儲技術----散列存儲技術(將記錄存儲到其關鍵字對應的f(key)內存位置)

這種對應 關係 f稱爲哈希函數(Hash),然後以這種散列存儲方式將記錄存儲到一塊連續的存儲空間中,這塊連續存儲空間稱爲散列表或者哈希表(Hash Table).

整個散列過程其實只需要兩步:

1.將記錄存儲起來,就是構建哈希表

那我們就需要用明確關鍵字,然後通過 哈希函數 計算其應該存儲的位置,將其存儲起來

2.通過關鍵字進行查找,用相同的哈希函數計算關鍵字的內存地址,看該地址下的內存空間中是否與該關鍵字相匹配,匹配則查找成功,否則失敗

關於具體採用的哈希函數,這裏簡單提一下,之後具體分析一下如何調用 unordered_map與 unordered_set

哈希函數構造方法有:

1.直接定址法 f(key) = a*key + b  (內存地址分佈簡單均勻,不會產生衝突,即不會有兩個不同的關鍵字有相同的存儲地址)

2.平方取中法、數字分析法、除留餘數法等等,還有解決衝突的方法開放定址法、鏈地址法等等

 

2.unordered_set 與 unordered_map

2.1unordered_set 與 unordered_map簡單介紹

unorder_set 於 unordered_map均是基於哈希表實現的,

兩者的存儲查找過程基本是類似的:

存儲:

1.先分配一大塊連續內存存儲空間

2.明確鍵key

3.哈希函數f(key)找到其對應的存儲位置

4.這裏兩者會有不同,unordered_set會將 key值保存到該存儲空間,而 unordered_map會將 key-value值保存到該存儲空間

查找取值:

1.明確待查找的關鍵字key

2.通過相同的哈希函數計算該關鍵字對應的存儲地址

3.然後比較存儲位置處的元素的key值 是否與待查找的key值相等(比較函數),相等則查找成功,不等則查找失敗

4.查找成功後兩者又有區別,unordered_set返回的key值,而unordered_map返回的是key對應的value值

從上面我們發現,我們需要注重關注的是 有關存儲位置的哈希函數 和 有關是否找到的比較函數

而這兩者正是我們在 實例化 unordered_set 或 unordered_map對象時需要指定的參數

2.1 unordered_map的使用

在unordered_map中鍵key和 value相關聯,但順序無關緊要,若關注順序問題,則用map比較合適

unordered_map<Key,T,HashFun,EqualKey>

Key表示存儲的鍵的類型(可以是用戶自定義數據類型,但必須提供相應的HashFun和EqualKey)

T表示Key的相關聯的值的類型

HashFun爲哈希函數,缺省值爲 hash<Key>(系統提供的哈希函數,如果你的Key值是下面的一種類型,則可以缺省,但在自定義Key類型時必須自己定義哈希函數,下面會討論如何定義哈希函數)

系統提供的hash<float>簡化源碼,目的是爲了說明hash是一個函數對象,返回值爲size_t類型(size_t類型簡介)

	struct hash<float>
	{	 size_t operator()(const float _Keyval) const noexcept
		{	// hash _Keyval to size_t value by pseudorandomizing transform
		return (_Hash_representation(_Keyval == 0.0F ? 0.0F : _Keyval));	// map -0 to 0
		}
	};
struct hash<int>
struct hash<char*>
struct hash<const char*>
struct hash<char> 
struct hash<unsigned char> 
struct hash<signed char>
struct hash<short>
struct hash<unsigned short> 
struct hash<int> 
struct hash<unsigned int>
struct hash<long> 
struct hash<unsigned long>

EqualKey表示一個返回bool類型的函數對象,其實是一個二元謂詞,缺省值爲 equal_to<Key>

//equal_to源碼
template<class _Ty = void>
	struct equal_to
	{	// functor for operator==
	_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type;
	_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type;
	_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type;

	constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const
		{	// apply operator== to operands
		return (_Left == _Right);
		}
	};

可以看出它的返回值爲bool類型,有兩個參數,符合二元謂詞的特徵

 

知道unordered_map的實例化所需參數後,爲了靈活運用就需要構造自己的哈希函數:

已知哈希函數實際上是一個函數對象,用構造函數對象的方法配合映射函數f(key)構造即可

構造流程需要注意以下幾點:

  1. 使用struct,然後重載operator().
  2. 返回是size_t
  3. 參數是你要hash的Key的類型。
  4. 函數是const類型的

同時爲了靈活運用(主要是爲了滿足根據實際情況定義的數據類型),也需要構建比較構造函數

已知比較構造函數是一個二元謂詞,構造流程注意以下幾點:

  1. 使用struct,然後重載operator().
  2. 返回 bool
  3. 兩個參數是待比較的鍵Key
  4. 定義自己的相等規則

多說不練假把戲,下面以一個例子進行說明:(在ordered_map中添加已經有的數據類型比較簡單,Hash_Fun和Equal_to比較函數都可以缺省,所以不再多說,下面向unordered_map中添加自己定義的數據類型)

簡單實例:有一個演講比賽,參數選手有姓名,年紀,參數ID屬性,我們需要將選手的ID和其最後取得的名次對應起來。

1.先構建選手類

//演講比賽,一個選手對應一個比賽結果
class player
{
public:
    player()//之所以要有空的無參構造函數是因爲在填入player進行實例化的時候,系統可能會實例化 
    //player類,調用無參,如果我們不提供無參,可能會出現bug
    {}
	player(int ID,string name,int age)
	{
		this->ID = ID;
		this->age = age;
		this->name = name;
	}
public:
	int ID;
	string name;
	int age;
};

2.填入自定義的數據類型,我們需要爲其提供 哈希函數對象,找到其應該存儲的位置,因爲關鍵詞爲選手ID,進行ID與存儲位置的映射

//兩種自己定義的數據類型,需要寫出自己的哈希函數和查找時需要用到的比較函數
//哈希函數在這裏是一個函數對象,我們想要用ID來進行查找
//爲方便,這裏選取的哈希函數比較簡單 存儲位置 = Key
struct IDhash
{
	size_t operator()(const player& p)const
	{
		return size_t(p.ID);
	}
};

3.填入自定義數據類型,我們查找時需要用到比較函數,因爲調用的是equal_to<Key>,Key爲自定義數據類型,所以需要定義自己的比較函數,比較關鍵字選手ID是否 相等,相等返回true,否則返回false

//比較函數是一個二元謂詞
struct equal_ID
{
	bool operator()(const player & p1,const player & p2) const
	{
		return p1.ID == p2.ID;
	}
};

4.準備工作完畢,開始向unordered_map中添加元素

//先實例化選手類
player xiaoming(101, "xiaoming", 23);
player xiaoyi(102, "xiaoyi", 25);
player xiaozheng(103, "xiaozheng", 13);
player xiaoyang(104, "xiaoyang", 23);
player xiaoshu(105, "xiaoshu", 12);
player xiaojia(106, "xiaojia", 14);

//實例化unordered_map
unordered_map<player,int,IDhash, equal_ID> mymap;

//添加元素
mymap[xiaoming] = 1;//表示xiaoming是第一名
mymap[xiaoyi] = 6;
mymap[xiaozheng] = 3;
mymap[xiaoyang] = 5;
mymap[xiaoshu] = 2;
mymap[xiaojia] = 4;

5.開始查找,我們要知道xaioyi是第幾名

cout << mymap[xiaoyi] << endl; //輸出結果爲6,查找成功

2.2unordered_map的類方法

unordered_map的方法與map的方法基本類似,下面主要介紹幾個常用的方法:

更多細節請點擊鏈接獲取《泛型編程與STL》查看,u7jb

查找

iterator unordered_map::find(const key_type & key)
//在ordered_map中查找key,如果找到則返回找到位置的迭代器,沒有找到則返回 unordered_map::end()
//迭代器,通常通過判斷返回值是否是unordered_map::end()來判斷元素是否找到

刪除

size_type unordered_map::erase(const key_type& key)
//刪除unordered_map中的關鍵字爲key的元素,返回刪除的個數,非0即1,成功返回1,不成功返回0

void unordered_map::erase(iterator f,iterator l)
//刪除unordered_map中的迭代器 [f,l)的元素

插入

mapped_type& unordered_map::operator[](const key_type&  key)

pair<iterator,bool> unordered_map::insert(const value_type & x)
//將x插入到unordered_map中,如果x確實被插入了,返回值pair的第二部分爲true,否則,如果x沒有被插入,則pair的第二部分爲false

這裏先簡單談一下 mapped_type 和value_type類型,再說兩個插入函數的區別與用法

mapped_type指的是與key_type相關聯的object型別,在這裏指的就是 鍵對應的值T(unordered_map中的T),它是一個元素,我們通常說的值value

value_type是存儲在unordered_map中的object型別,即上文中我們提到的找到key對應的存儲空間,然後將鍵key與值value一起放到存儲空間中,而鍵key與value就組成了這種value_type類型,它實際上是以對組pair形式出現的,所以我們用insert形式插入時,需要先將鍵key與value轉換爲對組形式,然後插入

比如接着上文,我們再初始化一個選手,名叫 "ergouzi",它是第7名

player ergouzi(107, "ergouzi", 15);

//插入
mymap.insert(pair<player,int>(ergouzi,7));

將 pair<player,int>(ergouzi,7) 插入到unordered_map中,如果確實被插入了,返回值pair<iterator,bool>的第二部分爲true,否則,如果沒有被插入,即pair<iterator,bool>已經存在ergouzi的ID,則的第二部分爲false

而對於operator[ ],它實際上就是一次重載,一種爲了方便而產生的簡寫,不同之處在於如果我們寫如下代碼:

player ergouzi(107, "ergouzi", 15);

//插入
mymap[ergouzi] = 8;

即使之前我們已經插入了二狗子,排名第7,但是operator[ ]會將我們的ergouzi改爲第8名,而不會因爲unordered_map中已經存在了ergouzi而報錯。

實際上 mymap[ ] 等價於 mymap.insert(value_type(key,mapped_type())).first->second

解釋:insert返回的是pair<iterator,bool>,first表明取出unordered_map中鍵爲key的iterator,

iterator->first指向鍵key,iterator->second指向鍵關聯的value

判斷是否爲空已經判斷unordered_map中的元素個數

bool unordered_map::empty()
//空返回true,否則返回false

size_type unordered_map::size()
//返回unordered_map中元素的個數

2.3 unordered_set

unorder_set<Key,HashFun,EqualKey>

與unordered_map基本類似

 

參考文獻:《大話數據結構》

                  《泛型編程與STL》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章