在 紅黑樹詳解 文章中,二叉搜索樹具有對數平均時間的表現是構造在這樣的假設下的:輸入數據有足夠的隨機性。
本篇介紹的hashtable(散列表)的數據結構,在插入、刪除、搜尋等操作上也具有“常數平均時間”的表現,而且這種表現是以統計數據為基礎,不需仰賴輸入元素的隨機性。
1. hashtable
概述 hashtable 可提供對任何有名項的存取和刪除操作。由於操作對象是有名項,所以hashtable也可被視為一種字典結構。這種結構嘗試提供常數時間之基本操作,如: 要存取所有的16-bits且不帶正負號的整數,我們可以擁有65536個元素的array A,初值全部為0,每個元素值代表相應元素的出現次數。於是不論插入、刪除、搜尋,每個操作都在嘗試時間內完成。
這個解法存在兩個問題。第一,如果元素是32-bits而非16-bits,我們所准備的array A 的大小就必須是2^32 = 4GB, 大得不切實際;第二,如果元素的型態是字符串(或其他)而非整數,將無法被拿來作為array的索引。
如何避免使用一個大得荒謬的array呢?辦法之一就是使用某種映射函數,將大數映射為小數。負責將某一元素映射為一個“大小可接受之索引”,這樣的函數稱為hash function(散列函數)。例如,假設x是任意整數,TableSize 是 array 大小,則 x%TableSize 會得到一個整數,范圍在0 ~ TableSize - 1 之間,恰可作為表格的索引。
使用hash function 會帶來一個問題:可能有不同的元素被映射到相同的位置(亦即有相同的索引)。這無法避免,因為元素個數大於array容量。這便是所謂的“碰撞(collision)”問題。解決碰撞問題的辦法有許多種,包括線性探測(linear probing)、二次探測(quadratic probing)、開鏈(separate chaining).... 等做法。線性探測和二次探測在《STL源碼剖析》中有介紹,請參見5.7相關章節。SGI STL 采用的是開鏈。
開鏈法,是在每一個表格元素中維護一個list;hash function 為我們分配某一個list,然后我們在那個list身上執行元素的插入、刪除、搜尋等操作。使用開鏈法,表格的負載系數將大於1。SGI STL 的hash table 便是采用這種做法。
2. hashtable 的桶子(buckets)與節點(nodes)
//hashtable node節點定義; template <class Value> struct __hashtable_node { __hashtable_node* next; Value val; };
注意,buckets所維護的linked list ,並不采用STL的list 或 slist,而是自行維護上述的hash table node。至於buckets 聚合體,則以vector完成,以便有動態擴充能力。
3. hashtable 的迭代器
參見相關源碼;
注意:
(1)hashtable 迭代器必須永遠維系着與整個“buckets vector”的關系,並記錄目前所指的節點(因為可能需要從bucket跳到bucket)。
(2)hashtable 迭代器沒有后退操作(operator--),hashtable也沒有定義所謂的逆向迭代器。
4. hashtable的數據結構
參見相關源碼;
hashtable 的模板參數相當多,包括:
(1)Value:節點的實值型別;
(2)Key:節點的鍵值型別;
(3)HashFcn:hash function的函數型別;
(4)ExtractKey:從節點中取出鍵值的方法(函數或仿函數);
(5)EqualKey:判斷鍵值相同與否的方法(函數或仿函數);
(6)Alloc:空間配置器,缺省使用std::alloc。
雖然開鏈法並不要求表格大小必須為質數,但SGI STL 仍然以質數來設計表格大小,並且先將28個質數(逐漸呈現大約兩倍的關系)計算好,已備隨時訪問,同時提供一個函數__stl_next_prime(unsigned long n),用來查詢在這28個質數之中,“最接近某數並大於某數”的質數。
5. hashtable 的構造與內存管理
構造函數參見相關源碼;
(1)hashtable 的插入操作與表格重整 插入操作會判斷是否需要重整表格,hashtable 提供了 resize 函數執行此重整過程。其對於“表格重建與否”的判斷,是拿元素個數(把新增元素計入后)和bucket vector 的大小來比較。如果前者大於后者,就重建表格。由此可判知,每個bucket(list)的最大容量和buckets vector的大小相同(解釋:理想情況下,元素均勻插入到bucket vector的每一個bucket(也即list)中;在極端情況下,有可能所有元素都插入到了同一個bucket下面,那么元素總個數就是該bucket下面的元素,此時,拿元素個數和bucket vector 的大小比較來判斷重建與否,由此得出,極端情況下(所有元素插入到同一個bucket),bucket的元素個數不可能大於bucket vector的大小(因為如果大於,就會引發重建過程),也即,每個bucket 的最大容量 和 bucket vector 的大小相同。>o< 好像有點羅嗦。)。
參見相關源碼;
(2)判知元素的落腳處 有時候我們的程序需要知道某個元素值位於哪一個bucket之內。SGI 提供了bkt_num() 函數來執行此操作,再由bkt_num()函數調用hash function,取得一個可以執行modulus(取模)運算的數值。之所以加了一層包裝,是因為有些元素型別無法直接拿來對hashtable 的大小進行模運算,如字符串const char*, 這時候我們需要做一些轉換。
(3)復制和整體刪除 由於整個hash table 由vector 和 linked-list 組合而成,因此,復制和整體刪除,都需要特別注意內存的釋放問題。hashtable提供了兩個相關函數:clear()和copy_from()。
參見相關源碼;
6. hash functions
<stl_hash_fun.h>定義有數個現成的hash functions,全都是仿函數。 有關hash的STL_TEMPLATE_NULL ,在<stl_config.h> 中皆被定義為template<>。其中針對 char*, const char*, char, unsigned char, signed char, short, unsigned short, int, unsigned int, long, unsigned long 都提供了特化版本,由此觀之,SGI hashtable 無法處理上述所列各項型別以外的元素,例如 string,double,float 。欲處理這些型別,用戶必須自行為它們定義hash function。