散列表之鏈接法

注意:
本文中的所有代碼你可以在這裏
https://github.com/qeesung/algorithm/tree/master/chapter11/11-2/hashTable(這裏會及時更新)
或者這裏
http://download.csdn.net/detail/ii1245712564/8804203
找到

散列表之鏈接法

在之前的文章《散列表之直接尋址表》中介紹了散列表中一種最基本的類型,直接尋址表。直接尋址表是通過鍵值直接來尋找在在數組裏面的映射位置,但是直接尋址表存在一個缺陷,在鍵值範圍較大且元素個數不多的時候,空間利用率不高,比如現在我們只需要映射兩個元素(key:1,data1)(key:10000,data2) ,此時我們需要分配一個大小爲10001 的數組,將第一個元素放到數組的1 位置,第二個元素放到數組的最後一個位置,剩下的所有數組位置都沒有被用到,造成空間的浪費。

於是,針對上面的情況,我們想到了下面的解決方案

hash(key)={01,key=1,key=10000

通過散列函數hash ,來將key 映射到合適的位置上,即通過hash(key) 來找到在數組上的位置,最後我們只需要一個大小爲2的數組就可以了。

散列表的定義

在直接尋址的方式下,具有關鍵字k 的元素被放到數組位置k 中。在散列方式下,該元素放在數組的hash(k) 位置中,即通過散列函數hash(hash function)傳入鍵值k 來計算元素在數組中合適的位置。

假設我們有一集合U={d1,d2,d3,...dn} ,集合中過的每一個元素di 都有一個鍵值keyi 和一個數組值datai ;新建一個數組T=[0...m] ,遍歷一遍集合U ,將集合中的每一個元素都放到數組的hash(key) 位置上。

這裏的散列表大小m 一般要比集合的大小U 小的多,可以說是具有關鍵字k 的元素被散列到hash(k) ,也就是說hash(k) 是鍵值k 的散列值。

Alt text

散列表的基本操作

散列表的基本操作有:

  • INSERT:插入一個元素
  • DELETE:刪除一個元素
  • SEARCH:搜索一個元素

在我們進行編碼實現之前,先來考慮一下下面幾個問題

問題1:散列函數hash 該怎麼設計?
回答1:散列函數的設計關乎到整個散列表的運行效率,所以不可等閒待之,需要針對輸入的元素集合來設計合適的散列函數,這也是散列表中最大的難點,比如在之前的例子中,我們設計的散列函數很簡單,如果輸入的key是1,就返回0,如果輸入的key是1000,那麼就返回1.如果把這個集合運用到其他集合上,那麼就是一個很差勁的散列函數


問題2:因爲集合U 的大小|U| 遠大於散列表的大小m ,所以經常會發生不同鍵值卻有相同散列值的情況,於是散列表的同一個位置就會發生衝突,這時應該怎麼辦?
回答2:我們可以採用鏈表的形式,將散列值相同的元素放入同一鏈表裏面,再將列表掛在散列表的對應位置上。
Alt text

散列表的編碼實現

下面是用到的文件,我們只列出部分的文件的內容,你可以在這裏獲得全部內容:
https://github.com/qeesung/algorithm/tree/master/chapter11/11-2/hashTable

.
├── hash_table.cc 散列表源文件
├── hash_table.h 散列表頭文件
├── hash_table_test.cc 散列表測試源文件
├── hash_table_test.h 散列表測試頭文件
├── link_list.cc 鏈式鏈表源文件
├── link_list.h 鏈式鏈表頭文件
├── list.h 鏈表接口文件
├── main.cc 主測試文件
├── Makefile
└── README.md

0 directories, 10 files

散列表的設計

hash_table.h

#ifndef  HASHTABLE_INC
#define  HASHTABLE_INC

#include "link_list.h"
#include <vector>

/**
 *        Class:  LinkHashTable
 *  Description:  鏈接法散列表
 */
template < class T >
class LinkHashTable
{
    public:
        /** ====================  LIFECYCLE     ======================================= */
        LinkHashTable (int table_size=0 , int(*_HASH)(const T &)=0);                             /** constructor */
        /** ====================  MUTATORS      ======================================= */
        bool hash_insert(const T &); /*插入操作*/ 
        bool hash_delete(const T &);  /*刪除操作*/ 
        std::vector<T> hash_search(int) const; /*查找操作*/ 
        void clear();
        void printToVec(std::vector<std::vector<T> > & );
    private:
        /** ====================  DATA MEMBERS  ======================================= */
        int (*HASH)(const T &);// 定義一個散列函數指針
        LinkList<T> * array; //鏈表的數組 
        const size_t table_size;// 散列表的大小
}; /** ----------  end of template class LinkHashTable  ---------- */

#include "hash_table.cc"
#endif   /* ----- #ifndef HASHTABLE_INC  ----- */

散列表的成員中有一個散列函數的指針HASH,用戶在構造散列表的時候需要制定對應的散列函數,還有一個array指針,是用來指向散列表的起始地址的。

hash_table.cc

#include "link_list.h"
/**
 *       Class:  LinkHashTable
 *      Method:  LinkHashTable
 * Description:  
 */
template < class T >
LinkHashTable < T >::LinkHashTable (int _table_size , int(*_HASH)(const T &)) :\
        table_size(_table_size),HASH(_HASH)
{
    array = new LinkList<T>[table_size]();
}  /** ----------  end of constructor of template class LinkHashTable  ---------- */


template < class T >
bool LinkHashTable<T>::hash_insert (const T & ele)
{
    int index = HASH(ele);
    if(index >= table_size || index <0)
        return false;
    return array[index].insert(ele);
}       /** -----  end of method LinkHashTable<T>::hash_insert  ----- */


template < class T >
bool LinkHashTable<T>::hash_delete (const T & ele)
{
    int index = HASH(ele);
    if(index >= table_size || index <0)
        return false;
    return array[index].remove(ele);
}       /** -----  end of method LinkHashTable<T>::hash_delete  ----- */


template < class T >
std::vector<T> LinkHashTable<T>::hash_search (int k) const
{
    if(k < 0 || k >= table_size)
        return std::vector<T>(); 
    std::vector<int> vec;
    array[k].printToVec(vec);
    return vec;
}       /** -----  end of method LinkHashTable<T>::hash_search  ----- */



template < class T >
void LinkHashTable<T>::clear ()
{
    for(int i =0 ; i < table_size ; ++i)
    {
        array[i].clear();
    }
    return ;
}       /** -----  end of method LinkHashTable<T>::clear  ----- */


template < class T >
void LinkHashTable<T>::printToVec (std::vector<std::vector<T> > & vec)
{
    for(int i =0 ; i < table_size ; ++i)
    {
        std::vector<int> tempVec;
        array[i].printToVec(tempVec);
        vec.push_back(tempVec);
    }
    return ;
}       /** -----  end of method LinkHashTable<T>::printToVec  ----- */

主測試文件

main.cc

#include <stdlib.h>
#include "hash_table_test.h"
#include <cppunit/ui/text/TestRunner.h>
#include <cppunit/TestCaller.h>
#include <cppunit/TestSuite.h>



/** 
 * +++  FUNCTION  +++
 *         Name:  main
 *  Description:  測試的主函數
 */
int main ( int argc, char *argv[] )
{
    CppUnit::TextUi::TestRunner runner; 
    CppUnit::TestSuite * suite = new CppUnit::TestSuite();

    suite->addTest(new CppUnit::TestCaller<HashTableTest>("Test insert by qeesung", &HashTableTest::test_insert));
    suite->addTest(new CppUnit::TestCaller<HashTableTest>("Test delete by qeesung", &HashTableTest::test_delete));
    suite->addTest(new CppUnit::TestCaller<HashTableTest>("Test search by qeesung", &HashTableTest::test_search));
    runner.addTest(suite);
    runner.run("",true);
    return EXIT_SUCCESS;

    return EXIT_SUCCESS;
}               /** ----------  end of function main  ---------- */

編譯運行

編譯方法

  • 如果你安裝了cppunit
make
  • 如果沒有安裝cppunit,你需要這樣編譯
g++ hash_table_test.cc youSourceFile.cc -o target

運行方法

./hashTest(or your target)

結論

散列表最重要的是設計一個性能良好的散列函數(hash function),這也是散列表的難點,如果散列函數設計差勁的話,有可能造成數據分佈的不平衡,刪除和查找操作性能變差。我們考慮最差勁的散列函數,就是將所有的鍵值都映射爲同一個散列值,那麼對散列表的查找和刪除操作的運行時間將和操作一個鏈式鏈表一樣,都爲O(n) 。我們將在後面的文章中詳細講解散列函數的設計,敬請關注(づ ̄3 ̄)づ╭❤~

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