談一談哈希表以及哈希算法

一、先談談數(shù)組與鏈表

?經(jīng)常寫代碼的小伙伴應該不陌生,在編程過程中常常面臨著兩個問題:存儲和查找,存儲和查找的效率往往決定了整個程序的效率。
?實際上,數(shù)據(jù)結構的存儲方式只有兩種:數(shù)組(順序存儲)和鏈表(鏈式存儲)【一種是連續(xù)的存儲結構,一種是不連續(xù)的存儲結構】。
?數(shù)組的隨機訪問性強,查找速度快(連續(xù)內存空間導致的);但是插入和刪除效率低【每次必須搬移后面的所有數(shù)據(jù)以保持連續(xù),時間復雜度 O(N)】。但正因為連續(xù)存儲,內存空間必須一次性分配夠,可能浪費內存。數(shù)組大小固定,不能動態(tài)拓展。如果要擴容,需要重新分配一塊更大的空間,再把數(shù)據(jù)全部復制過去,時間復雜度 O(N);按序號查找時,數(shù)組可以隨機訪問,時間復雜度為O(1)。按值查找時,若數(shù)組無序,時間復雜度為O(n);但是當數(shù)組有序時,可以采用折半查找將時間復雜度降為O(logn); 
?鏈表插入刪除速度快,內存利用率高,不會浪費內存。大小沒有固定,拓展很靈活【每一個數(shù)據(jù)存儲了下一個數(shù)據(jù)的地址,增刪效率高,時間復雜度O(1)】。但是正因為存儲空間不連續(xù),不能隨機查找,必須從第一個開始遍歷,查找效率低,平均需要O(n)。

二、什么是哈希表

?數(shù)據(jù)結構種類很多,甚至你也可以發(fā)明自己的數(shù)據(jù)結構,但是底層存儲無非數(shù)組或者鏈表。像堆棧、隊列、樹、圖等數(shù)據(jù)結構,數(shù)組和鏈表才是「結構基礎」。
?但是,上述這些數(shù)據(jù)結構,并不能折衷一下數(shù)組和鏈表的優(yōu)缺點。為了滿足數(shù)據(jù)的查找和修改很容易,同時又不占用很多空間,于是就有了所謂的哈希表【Hash table,散列表】,一種不同于堆棧、隊列、樹、圖的數(shù)據(jù)結構,可以說是數(shù)組和鏈表的結合體吧【結合了數(shù)組的快速查詢的優(yōu)點又能融合鏈表方便快捷的增加刪除元素的優(yōu)勢,不是指一定要同時使用到數(shù)組和鏈表兩種存儲結構】。
?那究竟什么是哈希表呢?
?其實所謂的哈希表,是一種根據(jù)<key—value>【也就是關鍵碼值】進行數(shù)據(jù)訪問的數(shù)據(jù)結構。乍一聽挺難理解,但并不復雜。
?核心思想:
?1、內部是一個數(shù)組
?2、關鍵碼key經(jīng)過變換(hash函數(shù))得到int類型的值
?3、int類型的值變成一個合法的下標
?4、把值value放到數(shù)組這個合法的下標的位置
?所以有:
?記錄的存儲位置=hashfunction(關鍵字)
?當向該結構中:
?1、插入元素
?根據(jù)待插入元素的關鍵碼,以此函數(shù)計算出該元素的存儲位置并按此位置進行存放。
?2、搜索元素
?對元素的關鍵碼進行同樣的計算,把求得的函數(shù)值當做元素的存儲位置,在結構中按此位置取元素比較,若關鍵碼相等,則搜索成功。
?該方式即為哈希(散列)方法,哈希方法中使用的轉換函數(shù)稱為哈希(散列)函數(shù),構造出來的結構稱為哈希表(HashTable)(或者稱散列表)。
?所以說,散列技術既是一種存儲方法,也是一種查找方法。然而它與線性表、 樹、圖等結構不同的是,前面幾種結構,數(shù)據(jù)元素之間都存在某種邏輯關系,可以用連線圖示表示出來,而散列技術的記錄之間不存在什么邏輯關系,它只與關鍵字有關聯(lián)。因此,散列主要是面向査找的存儲結構。

三、哈希沖突以及負載系數(shù)

?在理想的情況下,每一個關鍵字key,通過散列函數(shù)計算出來的地址都是不一樣的,可現(xiàn)實中,這只是一個理想。
?我們時常會碰到兩個關鍵字key1 ≠ key2,但卻有f (key1) = f (key2),這種現(xiàn)象稱為沖突(collision),并把key1和 key2稱為這個散列函數(shù)的同義詞(synonym)。
?出現(xiàn)了沖突當然非常糟糕,那將造成數(shù)據(jù)査找錯誤。盡管我們可以通過精心設計的散列函數(shù)讓沖突盡可能的少,但是不能完全避免。于是如何處理沖突就成了一個很重要的課題,這在我們后面也需要詳細講解。
?處理哈希沖突是個大坑……需要深入了解的東西很多。當然,小編文章后半部分會來詳細解釋下。
?那什么又是哈希表的負載系數(shù)(Load Factor)呢?
?其實,負載系數(shù)的公式是 \alpha =\frac{n}{N}
?其中N是哈希表的尺寸,n是哈希表中已存在元素個數(shù)。
?(1)α<1即表中有常有可用的空間,允許我們向當前哈希表插入其他元素
?(2)α=1即表中空間已經(jīng)被填滿,此時需要擴容
?(3)α>1即n>N已經(jīng)溢出,這種情況在具體的代碼實現(xiàn)中是要避免的。
?α我們也稱之為裝填因子。
?后面兩種情況,就要求我們對現(xiàn)在的哈希表的向操作系統(tǒng)申請更多的內容空間(更多存儲桶),哈希表的這種操作就叫重散列(Rehashing)。
?至于什么是重散列,后續(xù)會講到。

四、哈希表的常用構造方法
1、直接定址法

?直接定址法使用下面的公式:f(key)=a*key+b(b為常數(shù))
?比如統(tǒng)計出生年份,那么就可以使用f(key)=key-1990來計算存儲的位置,也就是散列地址。

出生年份人數(shù)
?代碼如下所示:

#include <iostream>
#include <vector>
 
using namespace std;
 
 //哈希表插入的每個數(shù)據(jù)節(jié)點 
template <typename K, typename V>
class HashNode
{
    public:
    HashNode(const K& key = K(), const V& value = V())//加上K()和V(),可缺省構造
    {
        key_ = key;
        value_ = value;
    }
 
    K key_;
    V value_;
    bool initialized_ = false;
};
 
template <typename K, typename V>
class HashTable
{
    public:
    HashTable(int size)
    {
        count=0; 
        node_num_ = size;// 存儲容量 
        nodes_.resize(size);//分配哈希表空間 
    }
    //直接定址法 
    int hashFunction(const K& key)
    {
        return hash(key)-1990;
    }
 
    //根據(jù)關鍵碼查找鍵值 
    V find(const K& key)
    {
        int index = hashFunction(key);
        if(nodes_[index].key_ == key)
        {
            return nodes_[index].value_;
        }
   
    }
    //哈希表數(shù)據(jù)的插入 
    void insert(const K& key, const V& value)
    {
        if(whetherfull()){
            int index = hashFunction(key);
            count++;
            nodes_[index].key_ = key;
            nodes_[index].value_ = value;
            nodes_[index].initialized_ = true;
        }
        
    }
   //del()方法是刪除哈希表中某個數(shù)據(jù)。該方法接收一個參數(shù) key
    void del(const K& key)
    {
        int index = hashFunction(key);
        if(nodes_[index].initialized_)
        {
            count--;
            nodes_[index].initialized_ = false;
            cout<<"成功刪除"<<endl;
        }else{
            cout<<"哈希表中無該數(shù)據(jù)"<<endl;
        }
    }
    
    int hash(const K& key)
    {
        return key;
    }
 
    bool whetherfull()
    {
        if(count<=node_num_){
            return true;
         }else{
            cout<<"表已經(jīng)滿了,無法繼續(xù)插入數(shù)據(jù)"<<endl;
            return false;
         }
    }
private:
    vector<HashNode<K, V> > nodes_;
    int node_num_;//存儲容量 
    int count;//當前存儲數(shù)據(jù)數(shù) 
};
 
int main(int argc, char** argv)
{
    HashTable<int, int> table(20);
    table.insert(1991, 1281);
    table.insert(1992, 1283);
    table.insert(1993, 1834);
    cout<<table.find(1993)<<endl;
    table.del(1993);
    return 0;
}

?這樣的散列函數(shù)優(yōu)點就是簡單、均勻,也不會產(chǎn)生沖突【不存在f (key1) = f (key2)的情況】,但問題是這需要事先知道關鍵字的分布情況,適合査找表較小且連續(xù)的情況。由于這樣的限制,在現(xiàn)實應用中,直接定址法雖然簡單,但卻并不常用。

2、除留余數(shù)法

?除留余數(shù)法此方法為最常用的構造散列函數(shù)方法。對于散列表長為m的散列函數(shù)公式為:f( key ) = key{\,}{\,}{\,}mod{\,}{\,} {\,}p ( p ≤ m )
?mod是取模(求余數(shù))的意思。
?很顯然,本方法的關鍵就在于選擇合適的p, p如果選得不好,就可能會容易產(chǎn)生同義詞。下面我們來舉個例子看看:

例子
?可以看到總共有12個記錄,現(xiàn)在我們要針對它設計一個散列表。如果采用除留余數(shù)法,那么可以先嘗試將散列函數(shù)設計為f(key) = key mod 12【tablesize即表的尺寸】的方法。比如29 mod 12 = 5,所以它存儲在下標為5的位置。
?最終得到的存儲結果如下:
存儲結果
?代碼其實也只是改動了下上述代碼的哈希函數(shù),如下所示:

#include <iostream>
#include <vector>
 
using namespace std;
 
 //哈希表插入的每個數(shù)據(jù)節(jié)點 
template <typename K, typename V>
class HashNode
{
    public:
    HashNode(const K& key = K(), const V& value = V())//加上K()和V(),可缺省構造
    {
        key_ = key;
        value_ = value;
    }
 
    K key_;
    V value_;
    bool initialized_ = false;
};
 
template <typename K, typename V>
class HashTable
{
    public:
    HashTable(int size)
    {
        count=0; 
        node_num_ = size;// 存儲容量 
        nodes_.resize(size);//分配哈希表空間 
    }
    //除留余數(shù)法 
    int hashFunction(const K& key)
    {
        return hash(key)%node_num_;
    }
 
    //根據(jù)關鍵碼查找鍵值 
    V find(const K& key)
    {
        int index = hashFunction(key);
        if(nodes_[index].key_ == key)
        {
            return nodes_[index].value_;
        }
   
    }
    //哈希表數(shù)據(jù)的插入 
    void insert(const K& key, const V& value)
    {
        if(whetherfull()){
            int index = hashFunction(key);
            count++;
            nodes_[index].key_ = key;
            nodes_[index].value_ = value;
            nodes_[index].initialized_ = true;
        }
    }
   //del()方法是刪除哈希表中某個數(shù)據(jù)。該方法接收一個參數(shù) key
    void del(const K& key)
    {
        int index = hashFunction(key);
        if(nodes_[index].initialized_)
        {
            count--;
            nodes_[index].initialized_ = false;
            cout<<"成功刪除"<<endl;
        }else{
            cout<<"哈希表中無該數(shù)據(jù)"<<endl;
        }
    }
    
    int hash(const K& key)
    {
        return key;
    }
 
    bool whetherfull()
    {
        if(count<=node_num_){
            return true;
         }else{
            cout<<"表已經(jīng)滿了,無法繼續(xù)插入數(shù)據(jù)"<<endl;
            return false;
         }
    }
private:
    vector<HashNode<K, V> > nodes_;
    int node_num_;//存儲容量 
    int count;//當前存儲數(shù)據(jù)數(shù) 
};
 
int main(int argc, char** argv)
{
    HashTable<int, string> table(12);
    table.insert(12, "喬丹");
    table.insert(25, "魔術師");
    //table.insert(1993, 1834);
    cout<<table.find(12)<<endl;
    //table.del(1993);
    return 0;
}

?上述給的例子過于簡單且沒有出現(xiàn)沖突的情況,一旦出現(xiàn)沖突,小編上面的代碼可就不對勁了。因為這種求余數(shù)作為下標的出現(xiàn)沖突的機率還是常有的,我舉個極端的例子,如下所示:

極端例子
?可以發(fā)現(xiàn),按照f(key) = key mod 12去計算,最終的f(key)都相等且為0。
?但是我們如果不選用p=12來做除留余數(shù)法,而選用p=11,大家計算下可以發(fā)現(xiàn)沖突減少了,只有兩個沖突。所以合理選取p值也能一定程度減少沖突。
?至于解決沖突有哪些方法,代碼該怎么補充修改,小編接下來也會講到的。

3、數(shù)字分析法

?數(shù)字分析法通常適合處理關鍵字位數(shù)比較大的情況,例如我們現(xiàn)在要存儲某家公司員工登記表,如用手機號作為關鍵字,那么我們發(fā)現(xiàn)抽樣后面的四位數(shù)字作為散列地址是不錯的選擇。
?我們來看個例子,看看使用哈希表怎么來進行存儲,如下圖所示:

聯(lián)系薄
?散列函數(shù)我們可以這樣子去設計:
f(key)=atoi(key+7)(這里的key為字符串)?假設key="15625063012",那么f(key)=atoi(key+7)=3012。
?atoi這個函數(shù)這里小編就不詳細解釋了。
?具體代碼這里不再展示,只是更改下上述代碼的哈希函數(shù)而已。
?除了上述介紹的三種哈希表的構造方法之外,還有:

4、平方取中法

?取關鍵字平方之后的中間極為作為哈希地址,一個數(shù)平方之后中間幾位數(shù)字與數(shù)的每一位都相關,取得位數(shù)由表長決定。比如:表長為512,=2^9,可以取平方之后中間9位二進制數(shù)作為哈希地址。一個簡單的小例子:
平方取中法
5、折疊法

?關鍵字位數(shù)很多,而且關鍵字中每一位上的數(shù)字分布大致均勻的時候,把關鍵詞分割成位數(shù)相同的幾個部分,然后疊加。
折疊法
五、哈希表的沖突解決算法

?hash沖突是不可能完全避免的,那么我們要考慮的還有就是當產(chǎn)生哈希沖突的時候,我們如何來解決。比較常見的hash沖突的解決方法有:開放定址法、鏈地址法。
?接下來,小編就來聊一聊這兩種沖突的解決辦法。

1、開放定址法

?其實,開放地址法很好理解。它的實現(xiàn)是在插入一個元素的時候,先通過哈希函數(shù)進行判斷,若是發(fā)生哈希沖突,就以當前地址為基準,根據(jù)再尋址的方法(探查序列),去尋找下一個地址,若發(fā)生沖突再去尋找,直至找到一個為空的地址為止,再將數(shù)值填充進去。所以這種方法又稱為再散列法。
?公式為:fi(key) = (f(key)+di){\,}{\,}{\,} mod {\,}{\,}{\,}tablesize(1<=di<tablesize-1)??其實,開放地址法包括三小種分別是 線性探測、平方探測、雙散列。下面依次介紹各種方法。

1.1線性探測法

?線性探測法的地址增量di = 1, 2, ... , tablesize-1,其中,i為探測次數(shù)。該方法一次探測下一個地址,知道有空的地址后插入,若整個空間都找不到空余的地址,則產(chǎn)生溢出。
?舉個例子,現(xiàn)在我們的關鍵字集合如下:

關鍵字集合
?可以發(fā)現(xiàn)表長為12。 我們用散列函數(shù)f(key) = key mod 12。
?當計算前5個數(shù){12,67,56,16,25}時,都是沒有沖突的散列地址,直接存入,得到:
無沖突情況出現(xiàn)前
?計算key = 37時,發(fā)現(xiàn)f(37) = 1,此時就與25所在的位置沖突。
?于是我們應用上面的公式f1(37) = (f(37)+1) mod 12 = 2。于是將37存入下標為2的位置。這其實就是房子被人買了于是買下一間的作法:
存入37
?接下來22,29,15,47都沒有沖突,正常的存入:
image.png
?到了 key=48,我們計算得到f(48) = 0,與12所在的0位置沖突了,不要緊,我們f1(48) = (f(48)+1) mod 12 = 1,此時又與25所在的位置沖突。于是f2(48) = (f(48)+2) mod 12=2,還是沖突……一直到 f6(48) = (f(48)+6) mod 12 = 6時,才有空位,機不可失,趕快存入:
多次探測
?到了 key=34,我們計算得到f(34) = 10,與22所在的10位置沖突了,不要緊,我們f1(34) = (f(34)+1) mod 12 = 11,此時又與47所在的位置沖突。于是f2(34) = (f(34)+2) mod 12=0,還是沖突……一直到 f11(34) = (f(34)+11) mod 12 = 9時,才有空位,存入:
最終哈希結果

?我們把這種解決沖突的開放定址法稱為線性探測。
?可以發(fā)現(xiàn),我們探測了多少次,di的最終值就為多少,是一個公差為1的遞增序列。
?所以呢,在插入、查找、刪除數(shù)據(jù)的時候,代碼要進一步的修改,代碼設計如下:

#include <iostream>
#include <vector>
#include <stdlib.h>
using namespace std;
 
 //哈希表插入的每個數(shù)據(jù)節(jié)點 
template <typename K, typename V>
class HashNode
{
    public:
    HashNode(const K& key = K(), const V& value = V())//加上K()和V(),可缺省構造
    {
        key_ = key;
        value_ = value;
    }
 
    K key_;
    V value_;
    int collision=0; //沖突次數(shù) 
    bool initialized_ = false;
};
 
template <typename K, typename V>
class HashTable
{
    public:
    HashTable(int size)
    {
        count=0; 
        node_num_ = size;// 存儲容量 
        nodes_.resize(size);//分配哈希表空間 
    }
    //除留余數(shù)法 
    int hashFunction(const K& key)
    { 
        return hash(key)%node_num_;
    }
 
    //根據(jù)關鍵碼查找鍵值 
    V find(const K& key)
    {
        int index = hashFunction(key);
        int countnum=0;
        while(nodes_[index].initialized_)//判斷該位置是否有值 
        {
            if(countnum>=node_num_){
                string s="哈希表中無該數(shù)據(jù)";
                return s;
            }
            if(nodes_[index].key_==key)//判斷該位置的值的關鍵字是否相等 
            {
                return nodes_[index].value_;
            }
            index++;
            countnum++;
            index=index%node_num_;
        }
    
   
    }
    //哈希表數(shù)據(jù)的插入 
    void insert(const K& key, const V& value)
    {
        if(!whetherfull()){
            return;
        }
        int index = hashFunction(key);
        int collision=0;
        count++;
        //線性探測 
        //發(fā)生沖突,尋找下一個位置存放 
        while(nodes_[index].initialized_)
        {
            collision++;
            index++;
            if(index == node_num_)
            {
                index = 0;
            }
        }
        nodes_[index].key_ = key;
        nodes_[index].value_ = value;
        nodes_[index].initialized_ = true;
        nodes_[index].collision=collision;
    }
   //del()方法是刪除哈希表中某個數(shù)據(jù)。該方法接收一個參數(shù) key
    void del(const K& key)
    {
        int index = hashFunction(key);
        int countnum=0;
        while(nodes_[index].initialized_ )
        {
            if(countnum-node_num_>=12)
            {
                cout<<"哈希表中無該數(shù)據(jù)"<<endl;
                return ;
            }
            if(nodes_[index].key==key)
            {
                count--;
                nodes_[index].initialized_ = false;
                cout<<"成功刪除"<<endl;
                return ;
            }
            index++;
            index=index%node_num_;
        }
        
    }
    
    int hash(const K& key)
    {
        return key;
    }
    
   void PrintHashTable()
    {
        for (int i = 0; i<node_num_; i++) 
        {
            if(nodes_[i].initialized_){
                cout<<"key: "<<nodes_[i].key_<<"--value: " <<nodes_[i].value_<<endl;    
            }else{
                cout<<"key: [ ]--value: [ ]" <<endl;
            }
            
        }
    }
    bool whetherfull()
    {
        if(count<=node_num_){
            return true;
         }else{
            cout<<"表已經(jīng)滿了,無法繼續(xù)插入數(shù)據(jù)"<<endl;
            return false;
         }
    }
    
private:
    vector<HashNode<K, V> > nodes_;
    int node_num_;//存儲容量 
    int count;//當前存儲數(shù)據(jù)數(shù) 
};
 
int main(int argc, char** argv)
{
    HashTable<int, string> table(12);
    table.insert(12, "喬丹");
    table.insert(67, "科比");
    table.insert(56, "魔術師");
    table.insert(16, "庫里");
    table.insert(25, "詹姆斯");
    table.insert(37, "鄧肯");
    table.insert(22, "格林");
    table.insert(29, "杜蘭特");
    table.insert(15, "奧尼爾");
    table.insert(47, "姚明");
    table.insert(48, "卡特");
    table.insert(34, "歐文");
    //table.insert(1993, 1834);
    //cout<<table.find(12)<<endl;
    cout<<table.find(64)<<endl;
    table.PrintHashTable();
    return 0;
}

?我們還是看剛才那個例子,可以發(fā)現(xiàn)我們在解決沖突的時候,為了處理沖突就會占用下一個位置,而如果沖突較多時,就會出現(xiàn)數(shù)據(jù)都聚集在一塊區(qū)域。例如48和37這種本來都不是同義詞卻需要爭奪一個地址的情況,我們稱這種現(xiàn)象為堆積【如果沖突較多時,就會出現(xiàn)數(shù)據(jù)都聚集在一塊區(qū)域】。很顯然,堆積的出現(xiàn),使得我們需要不斷處理沖突,無論是存入還是査找效率都會大大降低。

1.2平方探測法
看例子

?我們看我們上述的例子,可以發(fā)現(xiàn)當最后一個key=34,f(key)=10,與22所在的位置沖突,可是22后面沒有空位置了,反而它的前面有一個空位置,盡管可以不斷地求余數(shù)后得到結果,但效率很差【從后到前】。
?因此我們可以改進di = 12, -12, 22, -22,……, q2, -q2 (q <= tablesize/2),這樣就等于是可以雙向尋找到可能的空位置。
?我們還是用上述的例子來進行闡述。關鍵字集合如下:

關鍵字集合
?可以發(fā)現(xiàn)表長為12。 我們用散列函數(shù)f(key) = key mod 12。
?當計算前5個數(shù){12,67,56,16,25}時,都是沒有沖突的散列地址,直接存入,得到:
無沖突情況出現(xiàn)前
?計算key = 37時,發(fā)現(xiàn)f(37) = 1,此時就與25所在的位置沖突。于是我們重新尋找位置,有f1(37)=(f(37)+12)mod 12=2【第一次沖突i=1,di=12】,于是將37存入下標為2的位置。
存入37
?接下來22,29,15,47都沒有沖突,正常的存入:
image.png
?到了 key=48,我們計算得到f(48) = 0,與12所在的0位置沖突了。計算,有f1(48)=(f(48)+12)mod 12=1,該位置已有數(shù)據(jù),繼續(xù)尋找下一個位置;f2(48)=(f(48)-12)mod 12=11,該位置已經(jīng)有數(shù)據(jù),繼續(xù)尋找下一個位置;f3(48)=(f(48)+22)mod 12=4,該位置已經(jīng)有數(shù)據(jù),繼續(xù)尋找下一個位置;f4(48)=(f(48)-22)mod 12=8,該位置已經(jīng)有數(shù)據(jù),繼續(xù)尋找下一個位置;f5(48)=(f(48)+32)mod 12=9,該位置無數(shù)據(jù),插入。
插入key=48
?到了 key=34,我們計算得到f(34) = 10,與22所在的10位置沖突了,不要緊,繼續(xù)尋找空位置,有f1(34)=(f(34)+12) mod 12=11,該位置已有數(shù)據(jù),繼續(xù)尋找空位置;f2(34)=(f(34)-12) mod 12=9,該位置已有數(shù)據(jù),繼續(xù)尋找空位置;有f3(34)=(f(34)+22) mod 12=2,該位置已有數(shù)據(jù),繼續(xù)尋找空位置;有f4(34)=(f(34)-22) mod 12=6,該位置無數(shù)據(jù),插入。
最終結果
?其實,增加平方運算的目的是為了不讓關鍵字都聚集在某一塊區(qū)域,形成堆積,我們稱這種方法為平方探測法(也叫二次探測法)。

代碼設計關鍵點之一

?下面,我們來分析下代碼設計中比較關鍵的地方。舉個例子,(-1) mod 12的余數(shù)正常為11,但是如果在C++編譯器中,這么去計算負數(shù)除以某個正整數(shù)的余數(shù),如下代碼所示:

#include<iostream>
using namespace std;

int main()
{
    int a=-1;
    int c=a%12; 
    cout<<c<<endl;
    return 0;
 } 

?程序運行后,發(fā)現(xiàn)輸出的結果為-1。這明顯不太對,因此我們需要做點小改進,有兩種方法。

#include<iostream>
using namespace std;

int main()
{
    int a;//負整數(shù) 
    int b; //正整數(shù) 
    int c;
    cin>>a>>b; 
    //第一種寫法 
    if(a%b<0){
        c=a%b+b;
    }else{
        c=a%b;
    } 
    //第二種 寫法 
    //c=(a%b+b)%b
    cout<<c<<endl;
    return 0;
 } 
上述解決方法真的對嗎

?上述問題是非常關鍵的,如果沒采取這種分類求余數(shù)方法,得到的結果還發(fā)生錯誤,導致地址探測出現(xiàn)問題。
?但是,如果我們用上述表長為12的哈希表去存儲其他數(shù)據(jù),有可能會出現(xiàn)一些問題【一般情況下我們是把表長設置為質數(shù)/素數(shù)的】,接著往下看。
?對于線性探測,讓散列表近乎填滿元素是個壞主意,因為此時表的性能會降低。對于平方探測,情況更糟:一旦表被填滿超過一半,若表的大小不是素數(shù),那么甚至在表被填滿一半之前,就不能保證找到空的單元了。這是因為最多只有表的一半可以用做解決沖突的備選位置。
?這又是為什么,小編簡單做個證明如下:
?證明:
?令表的大小tablesize是一個大于3的素數(shù)(質數(shù)),我們證明,前[tablesize / 2]個備選位置是互異的。
?可以得知(f(x)+i2) mod tablesize 和(f(x)+j2) mod tablesize 是這些位置中的兩個,其中0 < i, j <= [tablesize / 2]。為推出矛盾,假設這兩個位置相同,但i != j,于是:
?1) f(x) + i^2 ^= f(x) + j2 (mod tablesize)
?2) i2 - j2 = 0
?3) (i + j)(i - j) = 0
?所以i = -j或者i = j,因為i != j,且i,j都大于0,所以前[tablesize / 2]個備選位置是互異的。
?由于要被插入的元素,若無任何沖突發(fā)生,也可以放到經(jīng)散列得到的單元,因此任何元素都有[tablesize / 2]個可能被放到的位置,如果最多有[tablesize / 2]個位置可以使用,那么空單元總能夠找到。哪怕表有比一半多一個的位置被填滿,那么插入都有可能失敗。表的大小是素數(shù)也非常重要,如果表的大小不是素數(shù),則備選單元的個數(shù)也可能銳減。
?開放定址散列表中,標準的刪除操作不能實行。因為相應的單元可能已經(jīng)引起過沖突,元素繞過了它存在了別處。因此,開放定址散列表需要懶惰刪除。
?定理1:如果使用平方探測,且表的大小是素數(shù),那么當表至少有一半是空的時候,總能夠插入一個新的元素。
?接下來,我們來舉一個簡單的例子。
?將4個關鍵字分別為10,6,4,15的元素插入到大小為5的散列表中。
?hash函數(shù)f(x)=key mod 5。
?插入key=5時,f(10)=10 mod 5=0,插入,有:

插入key=10
?插入key=6時,f(6)=6 mod 5=1,插入,有:
插入key=6
?插入key=4時,f(4)=4 mod 5=4,插入,有:
插入key=4

?插入key=15時,f(15)=15 mod 5=0,發(fā)生沖突,探測下一個地址,有:f1(15)=(f(15)+12) mod 5=1,該位置已有數(shù)據(jù)繼續(xù)沖突,繼續(xù)探尋下一個地址;f2(15)=(f(15)-12) mod 5=4,該位置已有數(shù)據(jù)發(fā)生沖突,繼續(xù)探測下一個可用地址,有f3(x)=(f(15)+22) mod 5 =4,該位置已有數(shù)據(jù)發(fā)生沖突,繼續(xù)探測下一個可用地址,有f4(x)=(f(15)-22) mod 5 =1,該位置已有數(shù)據(jù)發(fā)生沖突。進行下一次探測時,q=3>tablesize/2【5/2】,因此探測結束。
?可以發(fā)現(xiàn)此時key=15這個元素使用平方探測法無法插入哈希表,但此時表中還有空位置。
?定理2:哈希表的長度為4k+3(k為整數(shù))形式的素數(shù),平方探測法可以探測到整個哈希表空間。
?了解完對應的知識后,我們可以來對代碼做下改進,如下:

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <math.h>
#include <cmath> 
using namespace std;
 
 //哈希表插入的每個數(shù)據(jù)節(jié)點 
template <typename K, typename V>
class HashNode
{
    public:
    HashNode(const K& key = K(), const V& value = V())//加上K()和V(),可缺省構造
    {
        key_ = key;
        value_ = value;
    }
 
    K key_;
    V value_;
    int collision=0; //沖突次數(shù) 
    bool initialized_ = false;
};
 
template <typename K, typename V>
class HashTable
{
    public:
    HashTable(int size)
    { 
        count=0; 
        collision=0; 
        tablesize = nextPrime(size);// 存儲容量 
        nodes_.resize(tablesize);//分配哈希表空間 
    }
    //找到下一個質數(shù) 
    int nextPrime(int n)
    {
        while(!isprime(n) ){
            n++;
        }
        return n;

    }
    //判斷是否質數(shù) 
    bool isprime(int x)
    {
        //判斷質數(shù)
        if(x==0||x==1){
            return false;
        }
        for(int i=2;i<=(int)sqrt(x)+1;i++)
        {
            if(x%i==0){
                return false;
            }
        }
        return true;
    }

    //除留余數(shù)法 
    int hashFunction(const K& key)
    { 
        return key%tablesize;
    }
    
     //找到空位置
    int findpos(const K& key) 
    {
        int index = hashFunction(key);
        int indextemp=index;
        int i=0,j=1; 
        collision=0;
        //發(fā)生沖突,尋找下一個位置存放 【平方探測】 
        while(nodes_[index].initialized_ )//判斷該位置是否有值 
        {
            if(nodes_[index].key_==key){
                return index; 
            }
            if(j>tablesize/2){
                return tablesize;
            }
            collision++;
            index=indextemp+pow(-1,i)*pow(j,2);//按照增量序列進行設計 
            if(hashFunction(index)<0){
                index=hashFunction(index)+tablesize;
            }else{
                index=hashFunction(index);
            }
            i++;
            if(i%2){
                j++;
            }
        }
        return index;
    }
    
    //根據(jù)關鍵碼查找鍵值 
    V find(const K& key)
    {
       int index=findpos(key);
       if(nodes_[index].initialized_){
            return nodes_[index].value_;
       }else{
            cout<<"查找不到"<<endl;
       }
    
    }
    
    //哈希表數(shù)據(jù)的插入 
    void insert(const K& key, const V& value)
    {
        int index = findpos(key);
        if(nodes_[index].initialized_){
            cout<<"鍵值重復,無法插入"<<endl;
            return;
        }
        if(index==tablesize){
            cout<<"增量已經(jīng)大于tablesize/2,無法插入"<<endl;
            return;
        }
        nodes_[index].key_ = key;
        nodes_[index].value_ = value;
        nodes_[index].initialized_ = true;
        nodes_[index].collision=collision;
        count++; 
    }
   //del()方法是刪除哈希表中某個數(shù)據(jù)。該方法接收一個參數(shù) key
    void del(const K& key)
    {
        int index = findpos(key);
        if(nodes_[index].initialized_){
            nodes_[index].initialized_ = false;
        }else{
            cout<<"表中沒有該數(shù)據(jù)"<<endl;
            return ;
        }
        
    }
    
    void PrintHashTable()
    {
        for (int i = 0; i<tablesize; i++) 
        {
            if(nodes_[i].initialized_){
                cout<<"key: "<<nodes_[i].key_<<"--value: " <<nodes_[i].value_<<endl;    
            }else{
                cout<<"key: [ ]--value: [ ]" <<endl;
            }
            
        }
    }
    
private:
    vector<HashNode<K, V> > nodes_;
    int tablesize;//存儲容量 
    int count;//當前存儲數(shù)據(jù)數(shù) 
    int collision;//臨時存儲沖突次數(shù) 
};
 
int main(int argc, char** argv)
{
    HashTable<int, string> table(4);
    table.insert(10, "喬丹");
    table.insert(6, "科比");
    table.insert(4, "魔術師");
    table.insert(15, "庫里");
    /*table.insert(25, "詹姆斯");
    table.insert(37, "鄧肯");
    table.insert(22, "格林");
    table.insert(29, "杜蘭特");
    table.insert(15, "奧尼爾");
    table.insert(47, "姚明");
    table.insert(48, "卡特");
    table.insert(34, "歐文");
    //table.insert(1993, 1834);*/
    //table.del(10);
    //cout<<table.find(15)<<endl;
    //table.rehash();
    table.PrintHashTable();
    return 0;
}

?上述代碼后續(xù)小編會進行適當優(yōu)化。

表空間不夠怎么辦

?對于使用平方探測法的閉散列里,如果元素填的太滿的話后續(xù)插入將耗費過長的時間,甚至可能insert失敗,因為這里面會有太多的移動和插入混合操作。怎么辦呢?一種解決方法是建立另外一個大約兩倍大的表,再用一個新的散列函數(shù),掃描整個原始表然后按照新的映射插入到新的表里。上述有說過,這種操作叫做重散列【再散列】。
?我們舉一個最簡單的例子,有一個長度為3的哈希表,現(xiàn)在需要插入的鍵值對是(6,"Mosa"),(7,"Kally"),(8"Berry"),且哈系表的散列函數(shù)是 hash(k)=k mod N,當前N=3。
?這三個鍵值對插入表后的狀態(tài),如下圖,當前N=3,n=3,那么α=1,此時,就需要執(zhí)行重散列的操作,以滿足后續(xù)插入元素有可用的存儲桶可用。

哈希表已滿

?那問題來了,究竟需要擴容至多大的尺寸才適合呢?首先實現(xiàn)哈希表的底層基本數(shù)據(jù)結構就是順序表,更貼切地說是數(shù)組,按照慣例就是新的內存空間是原來內存空間的2倍。
?這樣的話,擴容后的尺寸不就是N=2x3=6嗎?但是,我們前面剛分析過,若N是非素數(shù)的話,那么多個不同的鍵值對出現(xiàn)散列沖突的概率會比使用素數(shù)高出許多,也就是說新哈希表的尺寸N不可能是2的倍數(shù)。
?因此更準確地說,重散列的最終效果就是為哈希列表擴容提供更多的可用的存儲位置,我們擴容后的哈希表尺寸可以靠近2N附近的素數(shù)。在本例來說,因為2N=2x3=6,那么6附近的素數(shù)可以是5和7,或者遠點11,13都可以,當然按照我們這里的約定,當然7是完美的選擇。
擴容
?但這里的事情還沒完,重散列的第二個步驟就是將舊表的元素遷移到新表中,遷移過程中就需要對就舊表的每個元素的鍵傳入hash(key)重新散列一遍,此時散列函數(shù)中的N是新表的尺寸即N=7,這也是重散列的關鍵操作,下圖已經(jīng)展示的很清楚。
舊表遷移
?上述的例子是在表滿了【插入失敗時再進行重散列】,其實,具體實現(xiàn)可以用平方探測以很多種方式實現(xiàn),包括:
?(1)只要表有一半滿了就做
?(2)只有當插入失敗時才做(這種比較極端)
?(3)途中策略:當表到達某個裝填因子時再做
?隨著裝填因子的增加,表的性能會有所下降,所以第三個方法或許是最好的。
?接下來貼上改進的代碼,如下:

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <math.h>
#include <cmath> 
using namespace std;
 
 //哈希表插入的每個數(shù)據(jù)節(jié)點 
template <typename K, typename V>
class HashNode
{
    public:
    HashNode(const K& key = K(), const V& value = V())//加上K()和V(),可缺省構造
    {
        key_ = key;
        value_ = value;
    }
 
    K key_;
    V value_;
    int collision=0; //沖突次數(shù) 
    bool initialized_ = false;
};
 
template <typename K, typename V>
class HashTable
{
    public:
    HashTable(int size)
    { 
        count=0; 
        collision=0; 
        tablesize = nextPrime(size);// 存儲容量 
        nodes_.resize(tablesize);//分配哈希表空間 
    }
    //找到下一個質數(shù) 
    int nextPrime(int n)
    {
        while(!isprime(n) ){
            n++;
        }
        return n;

    }
    //判斷是否質數(shù) 
    bool isprime(int x)
    {
        //判斷質數(shù)
        if(x==0||x==1){
            return false;
        }
        for(int i=2;i<=(int)sqrt(x)+1;i++)
        {
            if(x%i==0){
                return false;
            }
        }
        return true;
    }

    //除留余數(shù)法 
    int hashFunction(const K& key)
    { 
        return key%tablesize;
    }
    
     //找到空位置
    int findpos(const K& key) 
    {
        int index = hashFunction(key);
        int indextemp=index;
        int i=0,j=1; 
        collision=0;
        //發(fā)生沖突,尋找下一個位置存放 【平方探測】 
        while(nodes_[index].initialized_ )//判斷該位置是否有值 
        {
            if(nodes_[index].key_==key){
                return index; 
            }
            collision++;
            index=indextemp+pow(-1,i)*pow(j,2);//按照增量序列進行設計 
            if(hashFunction(index)<0){
                index=hashFunction(index)+tablesize;
            }else{
                index=hashFunction(index);
            }
            i++;
            if(i%2){
                j++;
            }
        }
        return index;
    }
    
    //根據(jù)關鍵碼查找鍵值 
    V find(const K& key)
    {
       int index=findpos(key);
       if(nodes_[index].initialized_){
            return nodes_[index].value_;
       }else{
            cout<<"查找不到"<<endl;
       }
    
    }
    
    //哈希表數(shù)據(jù)的插入 
    void insert(const K& key, const V& value)
    {
        int index = findpos(key);
        if(nodes_[index].initialized_){
            cout<<"鍵值重復,無法插入"<<endl;
            return;
        }
        nodes_[index].key_ = key;
        nodes_[index].value_ = value;
        nodes_[index].initialized_ = true;
        nodes_[index].collision=collision;
        count++; 
        if(count>tablesize/2)
        {
            rehash();
        }
    }
   //del()方法是刪除哈希表中某個數(shù)據(jù)。該方法接收一個參數(shù) key
    void del(const K& key)
    {
        int index = findpos(key);
        if(nodes_[index].initialized_){
            nodes_[index].initialized_ = false;
        }else{
            cout<<"表中沒有該數(shù)據(jù)"<<endl;
            return ;
        }
        
    }
    
    //重散列 
    void rehash()
    {
        int oldTableSize = tablesize;
        count=0;
        vector<HashNode<K, V> > oldnodes_;
        oldnodes_.assign(nodes_.begin(), nodes_.end());
        tablesize=nextPrime(2*oldTableSize);
        nodes_.clear();
        nodes_.resize(tablesize);//分配哈希表空間 
        for( int i=0; i<oldTableSize; i++ )
        {
            if(oldnodes_[i].initialized_)
            {
                insert(oldnodes_[i].key_,oldnodes_[i].value_);
            }       
        }
        oldnodes_.clear();
    }
    
    void PrintHashTable()
    {
        for (int i = 0; i<tablesize; i++) 
        {
            if(nodes_[i].initialized_){
                cout<<"key: "<<nodes_[i].key_<<"--value: " <<nodes_[i].value_<<endl;    
            }else{
                cout<<"key: [ ]--value: [ ]" <<endl;
            }
            
        }
    }
    
private:
    vector<HashNode<K, V> > nodes_;
    int tablesize;//存儲容量 
    int count;//當前存儲數(shù)據(jù)數(shù) 
    int collision;//臨時存儲沖突次數(shù) 
};
 
int main(int argc, char** argv)
{
    HashTable<int, string> table(4);
    table.insert(10, "喬丹");
    table.insert(6, "科比");
    table.insert(4, "魔術師");
    table.insert(15, "庫里");
    /*table.insert(25, "詹姆斯");
    table.insert(37, "鄧肯");
    table.insert(22, "格林");
    table.insert(29, "杜蘭特");
    table.insert(15, "奧尼爾");
    table.insert(47, "姚明");
    table.insert(48, "卡特");
    table.insert(34, "歐文");
    //table.insert(1993, 1834);*/
    //table.del(10);
    cout<<table.find(15)<<endl;
    //table.rehash();
    table.PrintHashTable();
    return 0;
}

?重散列是一種非常昂貴的操作;其運行時間為O(N),因為有N個元素要再散列而且表的大小約為2N,不過這并不經(jīng)常發(fā)生,所以實際效果并沒有這么差。本程序中只要表滿到一半就再散列。

1.3 雙散列

?雙重散列是線性開型尋址散列(開放尋址法)中的沖突解決技術。雙重散列使用在發(fā)生沖突時將第二個散列函數(shù)應用于鍵。
?此算法使用:(hash1(key) + i * hash2(key)) % tablesize
?hash1() 和 hash2() 是哈希函數(shù),而 tablesize 是哈希表的大小。當發(fā)生碰撞時,我們通過重復增加 步長i 來尋找鍵。
?第一個hash函數(shù):hash1(key) = key % tablesize。
?第二個hash函數(shù)是:hash2(key) = PRIME – (key % PRIME),其中 PRIME 是小于tablesize的質數(shù)。
?當然,第二個Hash函數(shù)表現(xiàn)好的特征包括:
?(1)不會產(chǎn)生 0 索引
?(2)能在整個HashTable 循環(huán)探測
?(3)計算會快點
?(4)與第一個Hash函數(shù)互相獨立
?(5)PRIME 與 tablesize 是相對質數(shù)
?hash2( key )作為關鍵,必須要合理選取,否則會引起災難性的后果——各種撞車。這個策略暫時不做過多分析了。所以小編也不做解釋了。

2、鏈地址法

?前面我們談到了散列沖突處理的開放定址法,它的思路就是一旦發(fā)生了沖突,就去尋找下一個空的散列地址,這其實是一種治標不治本的沖突解決方法。那么,我們其實可以利用鏈表這一數(shù)據(jù)結構,把同一位置的沖突對象組織在一起,不就解決問題了嗎?哈哈,這種沖突解決方法就叫做鏈地址法。
?將所有關鍵字為同義詞的記錄存儲在一個單鏈表中,我們稱這種表為同義詞子表,在散列表中只存儲所有同義詞子表的頭指針。

舉個簡單的例子:

?對于關鍵字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我們用前面同樣的12為除數(shù),進行除留余數(shù)法。得到的最終結果如下:


哈希結果

?此時,已經(jīng)不存在什么沖突換址的問題,無論有多少個沖突,都只是在當前位置給單鏈表增加結點的問題,很不錯的解決思路吧!

鏈地址法概述

?(1)一開始開辟一個tablesize的空間的數(shù)組;
?(2)將要存儲的對象轉換成整數(shù),用tablesize取模,結果會對應到數(shù)組中的一個索引;如果發(fā)生哈希沖突,取模的結果會指向數(shù)組中同一個索引;
?(3)將產(chǎn)生哈希沖突的對象鏈入其對應索引掛接的對象的后面,形成一條鏈表;
?(4)數(shù)組中的每個索引后都掛有一條鏈表,這就是所謂的鏈地址法(Separate Chain)
?注意下,在拉鏈法中,裝填因子α可以大于 1,但一般均取α≤1。

鏈地址法的優(yōu)勢與缺點

?與開放定址法相比,拉鏈法有如下幾個優(yōu)點:
?(1)鏈地址法處理沖突簡單,且無堆積現(xiàn)象,即非同義詞決不會發(fā)生沖突,因此平均查找長度較短;
?(2)由于鏈地址法中各鏈表上的結點空間是動態(tài)申請的,故它更適合于造表前無法確定表長的情況;
?(3)開放定址法為減少沖突,要求裝填因子α較小,故當結點規(guī)模較大時會浪費很多空間。而鏈地址法中可取α≥1,且結點較大時,拉鏈法中增加的指針域可忽略不計,因此節(jié)省空間;
?(4)在用鏈地址法構造的散列表中,刪除結點的操作易于實現(xiàn)。只要簡單地刪去鏈表上相應的結點即可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結 點的空間置為空,否則將截斷在它之后填人散列表的同義詞結點的查找路徑。這是因為各種開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。因此在用開放地址法處理沖突的散列表上執(zhí)行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點。
?鏈地址法的缺點:指針需要額外的空間,故當結點規(guī)模較小時,開放定址法較為節(jié)省空間,而若將節(jié)省的指針空間用來擴大散列表的規(guī)模,可使裝填因子變小,這又減少了開放定址法中的沖突,從而提高平均查找速度。
?至于什么是平均查找長度,后續(xù)做個簡單補充。

貼上代碼
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <math.h>
#include <cmath> 
using namespace std;
 
 //哈希表插入的每個數(shù)據(jù)節(jié)點 
template <typename K, typename V>
class HashNode
{
    public:
    HashNode(const K& key = K(), const V& value = V())//加上K()和V(),可缺省構造
    {
        key_ = key;
        value_ = value;
    }
    K key_;
    V value_;
    int collision=0; //沖突次數(shù) 
    bool initialized_ = false;
    HashNode<K,V>* NextNode=NULL;
};
 
template <typename K, typename V>
class HashTable
{
    public:
    typedef HashNode<K, V> hashnode;
    HashTable(int size)
    { 
        count=0; 
        collision=0; 
        tablesize = nextPrime(size);// 存儲容量 
        nodes_.resize(tablesize);//分配哈希表空間 
    }
    //找到下一個質數(shù) 
    int nextPrime(int n)
    {
        while(!isprime(n) ){
            n++;
        }
        return n;

    }
    //判斷是否質數(shù) 
    bool isprime(int x)
    {
        //判斷質數(shù)
        if(x==0||x==1){
            return false;
        }
        for(int i=2;i<=(int)sqrt(x)+1;i++)
        {
            if(x%i==0){
                return false;
            }
        }
        return true;
    }

    //除留余數(shù)法 
    int hashFunction(const K& key)
    { 
        return key%tablesize;
    }
    
    
    //根據(jù)關鍵碼查找鍵值 
    hashnode* find(const K& key)
    {
        int index =  hashFunction(key);
        hashnode* p=nodes_[index].NextNode;
        collision=0;
        temp=NULL;
        while (p && key != p->key_)
        {
            collision++;
            temp=p;
            p = p->NextNode;
        }
        return p;
    }
    
    //哈希表數(shù)據(jù)的插入 
    bool insert(const K& key, const V& value)
    {
        hashnode* p = find(key);
        if (!p)
        {
            //關鍵詞未找到,可以插入
            hashnode* new_cell = new hashnode();
            new_cell->key_ = key;
            new_cell->value_ = value;
            new_cell->collision=collision;
            int pos = hashFunction(key);
            new_cell->NextNode = nodes_[pos].NextNode;
            nodes_[pos].NextNode = new_cell;
            return true;
        }
        else
        {
            cout << "鍵值已存在!" << endl;
            return false;
        }
                
    }
   //del()方法是刪除哈希表中某個數(shù)據(jù)。該方法接收一個參數(shù) key
    void del(const K& key)
    {
        hashnode* p = find(key);
        if(!p)
        {
            cout<<"無該數(shù)據(jù)"<<endl;
            return ;
        }
        int pos=hashFunction(key);
        if(!temp)
        {
            nodes_[pos].NextNode=p->NextNode;
        }else{
            temp->NextNode=p->NextNode;
        }
        cout<<"成功刪除"<<endl;
        delete p;
        return;
    }
    
    void PrintHashTable()
    {
        hashnode* p;
        for (int i = 0; i <tablesize; i++)
        {
            p = nodes_[i].NextNode;
            if(!p)
            {
                cout<<"key: [ ]--value: [ ]" <<endl;
            }else{
                while (p)
                {
                    cout<<"key: "<<p->key_<<"--value: " <<p->value_<<"沖突次數(shù):"<<p->collision<<"  ";
                    p = p->NextNode;
                }
                cout<<endl;
            }
        }
    }
    
private:
    vector<HashNode<K, V> > nodes_;
    hashnode* temp;//用于存儲查找關鍵字在鏈表里的前一個數(shù)據(jù)地址 
    int tablesize;//存儲容量 
    int count;//當前存儲數(shù)據(jù)數(shù) 
    int collision;//臨時存儲沖突次數(shù) 
};
 
int main(int argc, char** argv)
{
    HashTable<int, string> table(4);
    table.insert(10, "喬丹");
    table.insert(6, "科比");
    table.insert(4, "魔術師");
    table.insert(15, "庫里");
    /*table.insert(25, "詹姆斯");
    table.insert(37, "鄧肯");
    table.insert(22, "格林");
    table.insert(29, "杜蘭特");
    table.insert(15, "奧尼爾");
    table.insert(47, "姚明");
    table.insert(48, "卡特");
    table.insert(34, "歐文");
    //table.insert(1993, 1834);*/
   // table.del(10);
    //cout<<table.find(15)<<endl;
    //table.rehash();
    table.PrintHashTable();
    return 0;
}

?PS:代碼雖然跑的起來,但小編水平有限可能也有需要改正的地方哈!

六、哈希表的查找性能

?我們使用平均查找長度(ASL)來度量查找表的查找效率:成功、不成功。
?這里,我們通過一個簡單例子了解下。
?關鍵字序列:{7、8、30、11、18、9、14}
?散列函數(shù): H(Key) = (key x 3) MOD 7
?裝載因子: 0.7
?處理沖突:線性探測再散列法

1、查找成功的ASL計算方法:

?因為現(xiàn)在的數(shù)據(jù)是7個,填充因子是0.7。所以數(shù)組大小=7/0.7=10,即寫出來的散列表大小為10,下標從0~9。
?第一個元素7,帶入散列函數(shù),計算得0。
?第二個元素8,帶入散列函數(shù),計算得3。
?第三個元素30,帶入散列函數(shù),計算得6。
?第四個元素11,帶入散列函數(shù),計算得5。
?第五個元素18,帶入散列函數(shù),計算得5;和11沖突,使用線性探測法,得7。
?第六個元素9,帶入散列函數(shù),計算得6;和30沖突,使用線性探測法,得8。
?第七個元素14,帶入散列函數(shù),計算得0;和7沖突,使用線性探測法,得1。
?所以散列表如下:


哈希結果

?這里的沖突次數(shù)計算不再重復,所謂的查找次數(shù)就是沖突次數(shù)加上1。
?成功平均查找長度(ASLs)=(1+2+1+1+1+3+3)/ 7=12/ 7

2、查找不成功的ASL計算方法:

?(1) 定義什么叫查找不成功
?查找不成功時呢?什么是查找不成功呢?查找不成功就是從查找位置開始直到一個位置為空需要比較的次數(shù)。
?舉個例子來說吧。在已知上面散列表的基礎上,如果要查找key為4的關鍵字。根據(jù)散列函數(shù)可以計算Hash(key)=Hash(4)=5。此時在地址為5的地方取出那個數(shù)字,發(fā)現(xiàn)key=11,不等于4。這就說明在裝填的時候會發(fā)生沖突。根據(jù)沖突處理方法,會繼續(xù)檢測地址為6的值,發(fā)現(xiàn)key=30,依然不等。這個時候到了地址為9,但是依然沒有找到。那么就說明根本就沒有key=4這個關鍵字,說明本次查找不成功,總共查找了5次。
?再舉一個例子。查找key為0的關鍵字,根據(jù)散列函數(shù)Hash(key)=Hash(0)=0。此時在地址為0的地方取出那個數(shù)字,發(fā)現(xiàn)key=7,不等于0。這就說明在裝填的時候會發(fā)生沖突。根據(jù)沖突處理方法,會繼續(xù)檢測地址為1的值,發(fā)現(xiàn)key=14,依然不等。這個時候到了地址為2,發(fā)現(xiàn)為空,依然沒有找到。所以停止查找,本次查找不成功。因為如果key=0這個關鍵字存在的話,依照沖突處理函數(shù),就一定能找到它??偛荒軄G了吧。故其查找次數(shù)為3。
?這里要注意下,當前哈希表線性探測地址時,最多會將整個表遍歷一遍,查找失敗則是遇到了空的地址。
?第二個難點,位置7、8、9要不要考慮呢?答案是不需要。一個解釋方式是通過哈希函數(shù)來解釋,哪個數(shù)字經(jīng)過哈希函數(shù)的映射之后能映射到7、8或者9?即哪個數(shù)字對7取模之后能得到7、8或者9?這個數(shù)字是不存在的,也就是說根本就不存在能夠通過哈希函數(shù)映射到7、8或者9的元素,也就不會有查找失敗了。這也是為什么ASL的分母是7的原因:要查找的所有元素的哈希函數(shù)映射都在0~6內。那為什么7、8或者9中存在元素呢?被前面的數(shù)字“推過來”的唄。所以,7、8或者9計入從其他位置開始的比較次數(shù)(分子),但是不從自身開始計算比較次數(shù),也不計入分母。分母是模長。
?(2)根據(jù)第一點定義的不成功,依次推下去:
?查找地址為0的值所需要的次數(shù)為3;
?查找地址為1的值所需要的次數(shù)為2;
?查找地址為2的值所需要的次數(shù)為1;
?查找地址為3的值所需要的次數(shù)為2;
?查找地址為4的值所需要的次數(shù)為1;
?查找地址為5的值所需要的次數(shù)為5;
?查找地址為6的值所需要的次數(shù)為4
?(3)計算
?查找不成功ASLu=(3+2+1+2+1+5+4)/ 7=18/ 7
?對于其他沖突解決辦法,大抵上也是這么來計算的,比如鏈地址法就是下一個位置為null就代表查不到,其他的小編就不講了。

七、寫在最后

?啰里啰唆寫了一大堆,也算是總結了下知識吧。當然,也存在或多或少的錯誤,還得請小伙伴們多多指正。
?寫完這篇,算是真真正正在簡書寫了10萬以上的雜記了。想想為什么能夠堅持有空寫寫點東西呢,想來也是一種興趣。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容