1. 程式人生 > >讀完這篇,你一定能真正理解什麼是雜湊表

讀完這篇,你一定能真正理解什麼是雜湊表

本文全是乾貨,讀完希望對你有所幫助~ 

雜湊表也稱為散列表,是根據關鍵字值(key value)而直接進行訪問的資料結構。也就是說,它通過把關鍵字值對映到一個位置來訪問記錄,以加快查詢的速度。這個對映函式稱為雜湊函式(也稱為雜湊函式),對映過程稱為雜湊化,存放記錄的陣列叫做散列表。比如我們可以用下面的方法將關鍵字對映成陣列的下標:

arrayIndex=hugeNumber%arraySize

在這裡我還是要推薦下我自己建的大資料學習交流裙:805127855, 裙 裡都是學大資料開發的,如果你正在學習大資料 ,小編關的),包括我自己整理的一份2018最新的大資料進階資料和高階開歡迎你加入,大家都是軟體開發黨,不定期分享乾貨(只有大資料開發相發教程,歡迎進階中和進想深入大資料的小夥伴。
 


列地址,即同一個陣列下標,這種現象稱為衝突,那麼我們該如何去處理衝突呢?一種方法是開放地址法,即通過系統的方法找到陣列的另一個空位,把資料填入,而不再用雜湊函式得到的陣列下標,因為該位置已經有資料了;另一種方法是建立一個存放連結串列的陣列,陣列內不直接儲存資料,這樣當發生衝突時,新的資料項直接接到這個陣列下標所指的連結串列中,這種方法叫做鏈地址法。下面針對這兩種方法進行討論。

1.開放地址法

1.1 線性探測法

所謂線性探測,即線性地查詢空白單元。我舉個例子,如果21是要插入資料的位置,但是它已經被佔用了,那麼就是用22,然後23,以此類推。陣列下標一直遞增,直到找到空白位。下面是基於線性探測法的雜湊表實現程式碼:

 
  1. public class HashTable {

  2.    private DataItem[] hashArray; //DateItem類是資料項,封裝資料資訊

  3.    private int arraySize;

  4.    private int itemNum; //陣列中目前儲存了多少項

  5.    private DataItem nonItem; //用於刪除項的

  6.    public HashTable() {

  7.        arraySize = 13;

  8.        hashArray = new DataItem[arraySize];

  9.        nonItem = new DataItem(-1); //deleted item key is -1

  10.    }

  11.    public boolean isFull() {

  12.        return (itemNum == arraySize);

  13.    }

  14.    public boolean isEmpty() {

  15.        return (itemNum == 0);

  16.    }

  17.    public void displayTable() {

  18.        System.out.print("Table:");

  19.        for(int j = 0; j < arraySize; j++) {

  20.            if(hashArray[j] != null) {

  21.                System.out.print(hashArray[j].getKey() + " ");

  22.            }

  23.            else {

  24.                System.out.print("** ");

  25.            }

  26.        }

  27.        System.out.println("");

  28.    }

  29.    public int hashFunction(int key) {

  30.        return key % arraySize;     //hash function

  31.    }

  32.  

  33.    public void insert(DataItem item) {

  34.        if(isFull()) {          

  35.            //擴充套件雜湊表

  36.            System.out.println("雜湊表已滿,重新雜湊化..");

  37.            extendHashTable();

  38.        }

  39.        int key = item.getKey();

  40.        int hashVal = hashFunction(key);

  41.        while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {

  42.            ++hashVal;

  43.            hashVal %= arraySize;

  44.        }

  45.        hashArray[hashVal] = item;

  46.        itemNum++;

  47.    }

  48.    /*

  49.     * 陣列有固定的大小,而且不能擴充套件,所以擴充套件雜湊表只能另外建立一個更大的陣列,然後把舊陣列中的資料插到新的陣列中。但是雜湊表是根據陣列大小計算給定資料的位置的,所以這些資料項不能再放在新陣列中和老陣列相同的位置上,因此不能直接拷貝,需要按順序遍歷老陣列,並使用insert方法向新陣列中插入每個資料項。這叫重新雜湊化。這是一個耗時的過程,但如果陣列要進行擴充套件,這個過程是必須的。

  50.     */

  51.    public void extendHashTable() { //擴充套件雜湊表

  52.        int num = arraySize;

  53.        itemNum = 0; //重新記數,因為下面要把原來的資料轉移到新的擴張的陣列中

  54.        arraySize *= 2; //陣列大小翻倍

  55.        DataItem[] oldHashArray = hashArray;

  56.        hashArray = new DataItem[arraySize];

  57.        for(int i = 0; i < num; i++) {

  58.            insert(oldHashArray[i]);

  59.        }

  60.    }

  61.    public DataItem delete(int key) {

  62.        if(isEmpty()) {

  63.            System.out.println("Hash table is empty!");

  64.            return null;

  65.        }

  66.        int hashVal = hashFunction(key);

  67.        while(hashArray[hashVal] != null) {

  68.            if(hashArray[hashVal].getKey() == key) {

  69.                DataItem temp = hashArray[hashVal];

  70.                hashArray[hashVal] = nonItem; //nonItem表示空Item,其key為-1

  71.                itemNum--;

  72.                return temp;

  73.            }

  74.            ++hashVal;

  75.            hashVal %= arraySize;

  76.        }

  77.        return null;

  78.    }

  79.  

  80.    public DataItem find(int key) {

  81.        int hashVal = hashFunction(key);

  82.        while(hashArray[hashVal] != null) {

  83.            if(hashArray[hashVal].getKey() == key) {

  84.                return hashArray[hashVal];

  85.            }

  86.            ++hashVal;

  87.            hashVal %= arraySize;

  88.        }

  89.        return null;

  90.    }

  91. }

  92. class DataItem {

  93.    private int iData;

  94.    public DataItem (int data) {

  95.        iData = data;

  96.    }

  97.    public int getKey() {

  98.        return iData;

  99.    }

  100. }

 

線性探測有個弊端,即資料可能會發生聚集。一旦聚集形成,它會變得越來越大,那些雜湊化後落在聚集範圍內的資料項,都要一步步的移動,並且插在聚集的最後,因此使聚集變得更大。聚集越大,它增長的也越快。這就導致了雜湊表的某個部分包含大量的聚集,而另一部分很稀疏。

為了解決這個問題,我們可以使用二次探測:二次探測是防止聚集產生的一種方式,思想是探測相隔較遠的單元,而不是和原始位置相鄰的單元。線性探測中,如果雜湊函式計算的原始下標是x, 線性探測就是x+1, x+2, x+3, 以此類推;而在二次探測中,探測的過程是x+1, x+4, x+9, x+16,以此類推,到原始位置的距離是步數的平方。二次探測雖然消除了原始的聚集問題,但是產生了另一種更細的聚集問題,叫二次聚集:比如講184,302,420和544依次插入表中,它們的對映都是7,那麼302需要以1為步長探測,420需要以4為步長探測, 544需要以9為步長探測。只要有一項其關鍵字對映到7,就需要更長步長的探測,這個現象叫做二次聚集。

二次聚集不是一個嚴重的問題,但是二次探測不會經常使用,因為還有好的解決方法,比如再雜湊法。

1.2 再雜湊法

為了消除原始聚集和二次聚集,現在需要的一種方法是產生一種依賴關鍵字的探測序列,而不是每個關鍵字都一樣。即:不同的關鍵字即使對映到相同的陣列下標,也可以使用不同的探測序列。再雜湊法就是把關鍵字用不同的雜湊函式再做一遍雜湊化,用這個結果作為步長,對於指定的關鍵字,步長在整個探測中是不變的,不同關鍵字使用不同的步長、經驗說明,第二個雜湊函式必須具備如下特點:

  • 和第一個雜湊函式不同;

  • 不能輸出0(否則沒有步長,每次探索都是原地踏步,演算法將進入死迴圈)。

 

專家們已經發現下面形式的雜湊函式工作的非常好:

stepSize=constant-key%constant

 其中 constant 是質數,且小於陣列容量。

再雜湊法要求表的容量是一個質數,假如表長度為15(0-14),非質數,有一個特定關鍵字對映到0,步長為5,則探測序列是 0,5,10,0,5,10,以此類推一直迴圈下去。演算法只嘗試這三個單元,所以不可能找到某些空白單元,最終演算法導致崩潰。如果陣列容量為13, 質數,探測序列最終會訪問所有單元。即 0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一個空位,就可以探測到它。下面看看再雜湊法的程式碼:

 
  1. public class HashDouble {

  2.    private DataItem[] hashArray;

  3.    private int arraySize;

  4.    private int itemNum;

  5.    private DataItem nonItem;

  6.    public HashDouble() {

  7.        arraySize = 13;

  8.        hashArray = new DataItem[arraySize];

  9.        nonItem = new DataItem(-1);

  10.    }

  11.    public void displayTable() {

  12.        System.out.print("Table:");

  13.        for(int i = 0; i < arraySize; i++) {

  14.            if(hashArray[i] != null) {

  15.                System.out.print(hashArray[i].getKey() + " ");

  16.            }

  17.            else {

  18.                System.out.print("** ");

  19.            }

  20.        }

  21.        System.out.println("");

  22.    }

  23.    public int hashFunction1(int key) { //first hash function

  24.        return key % arraySize;

  25.    }

  26.  

  27.    public int hashFunction2(int key) { //second hash function

  28.        return 5 - key % 5;

  29.    }

  30.  

  31.    public boolean isFull() {

  32.        return (itemNum == arraySize);

  33.    }

  34.    public boolean isEmpty() {

  35.        return (itemNum == 0);

  36.    }

  37.    public void insert(DataItem item) {

  38.        if(isFull()) {

  39.            System.out.println("雜湊表已滿,重新雜湊化..");

  40.            extendHashTable();

  41.        }

  42.        int key = item.getKey();

  43.        int hashVal = hashFunction1(key);

  44.        int stepSize = hashFunction2(key); //用hashFunction2計算探測步數

  45.        while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {

  46.            hashVal += stepSize;

  47.            hashVal %= arraySize; //以指定的步數向後探測

  48.        }

  49.        hashArray[hashVal] = item;

  50.        itemNum++;

  51.    }

  52.    public void extendHashTable() {

  53.        int num = arraySize;

  54.        itemNum = 0; //重新記數,因為下面要把原來的資料轉移到新的擴張的陣列中

  55.        arraySize *= 2; //陣列大小翻倍

  56.        DataItem[] oldHashArray = hashArray;

  57.        hashArray = new DataItem[arraySize];

  58.        for(int i = 0; i < num; i++) {

  59.            insert(oldHashArray[i]);

  60.        }

  61.    }

  62.    public DataItem delete(int key) {

  63.        if(isEmpty()) {

  64.            System.out.println("Hash table is empty!");

  65.            return null;

  66.        }

  67.        int hashVal = hashFunction1(key);

  68.        int stepSize = hashFunction2(key);

  69.        while(hashArray[hashVal] != null) {

  70.            if(hashArray[hashVal].getKey() == key) {

  71.                DataItem temp = hashArray[hashVal];

  72.                hashArray[hashVal] = nonItem;

  73.                itemNum--;

  74.                return temp;

  75.            }

  76. hashVal += stepSize;

  77.            hashVal %= arraySize;

  78.        }

  79.        return null;

  80.    }

  81.    public DataItem find(int key) {

  82.        int hashVal = hashFunction1(key);

  83.        int stepSize = hashFunction2(key);

  84.        while(hashArray[hashVal] != null) {

  85.            if(hashArray[hashVal].getKey() == key) {

  86.                return hashArray[hashVal];

  87.            }

  88.            hashVal += stepSize;

  89.            hashVal %= arraySize;

  90.        }

  91.        return null;

  92.    }

  93. }

2. 鏈地址法

在開放地址法中,通過再雜湊法尋找一個空位解決衝突問題,另一個方法是在雜湊表每個單元中設定連結串列(即鏈地址法),某個資料項的關鍵字值還是像通常一樣對映到雜湊表的單元,而資料項本身插入到這個單元的連結串列中。其他同樣對映到這個位置的資料項只需要加到連結串列中,不需要在原始的陣列中尋找空位。下面看看鏈地址法的程式碼:

在這裡我還是要推薦下我自己建的大資料學習交流裙:805127855, 裙 裡都是學大資料開發的,如果你正在學習大資料 ,小編歡迎你加入,大家都是軟體開發黨,不定期分享乾貨(只有大資料開發相關的),包括我自己整理的一份2018最新的大資料進階資料和高階開發教程,歡迎進階中和進想深入大資料的小夥伴。
  1. public class HashChain {

  2.    private SortedList[] hashArray; //陣列中存放連結串列

  3.    private int arraySize;

  4.    public HashChain(int size) {

  5.        arraySize = size;

  6.        hashArray = new SortedList[arraySize];

  7.        //new出每個空連結串列初始化陣列

  8.        for(int i = 0; i < arraySize; i++) {

  9.            hashArray[i] = new SortedList();

  10.        }

  11.    }

  12.    public void displayTable() {

  13.        for(int i = 0; i < arraySize; i++) {

  14.            System.out.print(i + ": ");

  15.            hashArray[i].displayList();

  16.        }

  17.    }

  18.    public int hashFunction(int key) {

  19.        return key % arraySize;

  20.    }

  21.    public void insert(LinkNode node) {

  22.        int key = node.getKey();

  23.        int hashVal = hashFunction(key);

  24.        hashArray[hashVal].insert(node); //直接往連結串列中新增即可

  25.    }

  26.    public LinkNode delete(int key) {

  27.        int hashVal = hashFunction(key);

  28.        LinkNode temp = find(key);

  29.        hashArray[hashVal].delete(key);//從連結串列中找到要刪除的資料項,直接刪除

  30.        return temp;

  31.    }

  32.  

  33.    public LinkNode find(int key) {

  34.        int hashVal = hashFunction(key);

  35.        LinkNode node = hashArray[hashVal].find(key);

  36.        return node;

  37.    }

  38. }

 

下面是連結串列類的程式碼,用的是有序連結串列:

 
  1. public class SortedList {

  2.    private LinkNode first;

  3.    public SortedList() {

  4.        first = null;

  5.    }

  6.    public boolean isEmpty() {

  7.        return (first == null);

  8.    }

  9.    public void insert(LinkNode node) {

  10.        int key = node.getKey();

  11.        LinkNode previous = null;

  12.        LinkNode current = first;

  13.        while(current != null && current.getKey() < key) {

  14.            previous = current;

  15.            current = current.next;

  16.        }

  17.        if(previous == null) {

  18.            first = node;

  19.        }

  20.        else {

  21.            node.next = current;

  22.            previous.next = node;

  23.        }

  24.    }

  25.    public void delete(int key) {

  26.        LinkNode previous = null;

  27.        LinkNode current = first;

  28.        if(isEmpty()) {

  29.            System.out.println("chain is empty!");

  30.            return;

  31.        }

  32.        while(current != null && current.getKey() != key) {

  33.            previous = current;

  34.            current = current.next;

  35.        }

  36.        if(previous == null) {

  37.            first = first.next;

  38.        }

  39.        else {

  40.            previous.next = current.next;

  41.        }

  42.    }

  43.    public LinkNode find(int key) {

  44.        LinkNode current = first;

  45.        while(current != null && current.getKey() <= key) {

  46.            if(current.getKey() == key) {

  47.                return current;

  48.            }

  49.            current = current.next;

  50.        }

  51.        return null;

  52.    }

  53.    public void displayList() {

  54.        System.out.print("List(First->Last):");

  55.        LinkNode current = first;

  56.        while(current != null) {

  57.            current.displayLink();

  58.            current = current.next;

  59.        }

  60.        System.out.println("");

  61.    }

  62. }

  63. class LinkNode {

  64.    private int iData;

  65.    public LinkNode next;

  66.    public LinkNode(int data) {

  67.        iData = data;

  68.    }

  69.    public int getKey() {

  70.        return iData;

  71.    }

  72.    public void displayLink() {

  73.        System.out.print(iData + " ");

  74.    }

  75. }

 

在沒有衝突的情況下,雜湊表中執行插入和刪除操作可以達到O(1)的時間級,這是相當快的,如果發生衝突了,存取時間就依賴後來的長度,查詢或刪除時也得挨個判斷,但是最差也就O(N)級別。

雜湊表就分享這麼多,本文建議收藏,在等班車的時候、吃飯排隊的時候可以拿出來看看。利用碎片化時間來學習!

關注微信公眾號:程式設計師交流互動平臺!獲取資料學習!