STL原始碼剖析(五)hashtable
阿新 • • 發佈:2018-12-03
文章目錄
- 1. hashtable概述
- 2. hashtable的桶與節點
- 3. hashtable迭代器
- 4. hashtable資料結構
- 5. hashtable構造與記憶體管理
- 6. 元素操作
- 7. hash functions
1. hashtable概述
hashtable
,即我們在資料結構中所說的散列表,也叫雜湊表,在插入、刪除、搜尋等操作上具有“常數平均時間”的表現,而且這種表現是以統計為基礎,不需仰賴輸入元素的隨機性- 使用hash function將某一元素對映為一個“大小可接受的索引”,通過該索引找到在array中的位置,從而構成一個hashtable;使用hash function可能帶來一個問題:即不同的元素經過hash function的作用被對映到相同的位置,這樣就產生了碰撞,我們應該採取什麼方法來解決碰撞呢?
- 解決碰撞的方法有許多,我們在這主要分析三種:
1.線性探測 2.二次探測3.開鏈法
1.1 線性探測
- 1.
利用hash function計算出某個元素的插入位置
- 2.
若該位置上的空間不可用,則循序往下尋找,直到找到一個可用空間為止
分析線性探測的表現:
首先需做兩個假設:1)表格足夠大 2)每個元素能夠獨立
在此假設情況下,最壞的情況是線性巡訪整個表格,平均情況則是巡訪一半表格,可這也與我們期望的常數時間相差甚遠
線性探測還可能帶來主集團問題,主集團即平均插入成本的成長幅度遠高於負載係數的成長幅度
1.2 二次探測
- 1.
利用hash function計算出某個元素的插入位置H
- 2.
若該位置實際上已被使用,則依序嘗試H + 1 ^2、H + 2 ^2...H + i ^2,直到發現新空間
分析二次探測:
二次探測是為了消除主集團,但卻可能造成次集團:兩個元素經hash function計算出來的位置若相同,則插入時所探測的位置也相同,形成某種浪費
1.3 開鏈法
- 1.
在每一個表格元素種維護一個list
- 2.
hash function為我們分配某一個list,然後在list上執行元素的插入、搜尋、刪除操作
SGI STL的hashtable正是採用這種做法實現的
2. hashtable的桶與節點
-
稱hashtable表格中的元素為“桶”,是因為表格內的每個單元,涵蓋的不只是個節點,甚至可能是
一“桶”節點
-
hashtable的節點定義:
template <class Value>
struct __hashrable_node {
__hashtable_node* next;
Vlalue val;
};
STL並不以list或slist維護hashtable node,而是以vector製造的bucket來維護,至於為什麼用vector,是因為需要動態擴充能力
3. hashtable迭代器
- hashtable迭代器的定義(僅部分原始碼):
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
//...
typedef __hashtable_node <Value> node;
typedef forward_iterator_tag iterator_category; //hashtable沒有後退操作
//...
node* cur; //迭代器所指節點
hashtable* ht; //連線容器
//建構函式
__hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}
__hashtable_iterator() {}
//取值
reference operator*() const { return cur->val; }
pointer operator->() const { return &(operator*()); }
//迭代器遞增操作
iterator& operator++();
iterator operator++(int);
//判斷迭代器是否相等
bool operator==(const iterator& it) const { return cur == it.cur; }
bool operator!=(const iterator& it) const { return cur!= it.cur; }
};
//遞增操作實現
template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++()
{
const node* old = cur;
cur = cur->next; //如果存在節點則就是該cur,不存在節點則進入if流程,找到下一個bucket
if (!cur) {
size_type bucket = ht->bkt_num(old->val); //找到當前值所對應的桶
while (!cur && ++ bucket < ht->buckets.size())
cur = ht->buckets[bucket]; //令cur指向下一個bucket
}
return *this;
}
template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++(int)
{
iterator tmp = *this;
++*this;
return tmp;
{
4. hashtable資料結構
- 在下面定義中,可以看到
buckets聚合體
以vector完成:
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey, class Alloc>
class hashtable {
public:
typedef HashFcn hasher;
typedef EqualKey key_equal;
typedef size_t size_type;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
typedef simple_allloc<node, Alloc> node_allocator;
vector<node*, Allloc> buckets; //以buckets維護節點
size_type num_elements; //節點個數
public:
size_type bucket_count() const { return buckets.size(); }
...
};
value:節點的實值型別
Key:節點的鍵值型別
HashFcn:hashfunction的函式型別
ExtractKey:從節點中取出鍵值的方法
E權力Key: 判斷鍵值相同與否的方法
Alloc:空間配置器
SGI STL以質數來設計表格大小,並且先將28個質數計算好,已備隨時訪問,同時提供一個函式,用來查詢在這28個函式中“最接近並大於或等於n的那個質數”
:
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
16110612741, 3221225473ul, 4294967291ul
};
//在28個質數中找出最接近並大於或等於n的質數
inline unsigned long __stl_next_prime(unsigned long n)
{
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list + __stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last-1): *pos;
}
//最大buckets數
size_type max_bucket_count() const
{
return __stl_prime_list[__stl_num_primes - 1]; }
5. hashtable構造與記憶體管理
- 節點配置與節點釋放函式:
node* new_node(const value_type& obj)
{
node* n = node_allocator::allocate();
n->next = 0;
__STL_TRY {
construct(&n->val, obj);
return n;
}
__STL_UNWIND(node_allocator::deallocate(n));
}
void delete_node(node* n)
{
destroy(&n->val);
node_alllocaor::deallocate(n);
}
- hashtable構造:
hashtable(size_type n, const HashFcn& hf, const EqualKey& eql)
: hash(hf), equals(eql), get_key(ExtractKey()), num_elements(0)
{
initialize_buckets(n);
}
void initialize_buckets(size_type n)
{
buckets.reserve(n_bckets);
buckets.insert(buckets.end(), n_buckets, (node*) 0);
num_elements = 0;
}
- 插入元素:分為兩種,insert_unique與insert_equal
// 插入操作,不允許重複
pair<iterator, bool> insert_unique(const value_type& obj)
{
resize(num_elements + 1);
return insert_unique_noresize(obj);
}
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
pair<typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator, bool>
hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
::insert_unique_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj);
node* first = buckets[n];
// 如果已經存在則直接返回
for (node* cur = first; cur; cur = cur->_next)
if (equals(get_key(cur->val), get_key(obj)))
return pair<iterator, bool>(iterator(cur, this), false);
// 插入新節點
node* tmp = new_node(obj); //產生新節點
tmp->next = first; //將新節點插入連結串列頭部
buckets[n] = tmp;
++num_elements; //節點個數累計加1
return pair<iterator, bool>(iterator(tmp, this), true); ///返回一個迭代器,指向新增節點
}
//插入操作,允許重複
iterator insert_equal(const value_type& obj)
{
resize(num_elements + 1);
return insert_equal_noresize(obj);
}
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
pair<typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator, bool>
hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
::insert_equal_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj);
node* first = buckets[n];
// 如果已經存在則直接插入並返回
for (node* cur = first; cur; cur = cur->_next)
if (equals(get_key(cur->val), get_key(obj))) {
node* tmp = new_node(obj); //產生新節點
tmp->next = first; //將新節點插入連結串列頭部
buckets[n] = tmp;
++num_elements; //節點個數累計加1
return pair<iterator, bool>(iterator(tmp, this), true); ///返回一個迭代器,指向新增節點
}
// 表示沒有發現新節點
node* tmp = new_node(obj); //產生新節點
tmp->next = first; //將新節點插入連結串列頭部
buckets[n] = tmp;
++num_elements; //節點個數累計加1
return pair<iterator, bool>(iterator(tmp, this), true); ///返回一個迭代器,指向新增節點
}
//判斷是否需要重建表格
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
::resize(size_type num_elements_hint)
{
const size_type old_n = buckets.size();
// 超過原來表格的大小時才進行調整
if (__num_elements_hint > old_n) {
// 新的表格大小
const size_type n = next_size(num_elements_hint);
// 在邊界情況下可能無法調整(沒有更大的素數了)
if (n > old_n) {
vector<node*, All> tmp(n, (node*)(0),
__STL_TRY {
// 填充新的表格
for (size_type bucket = 0;bucket < old_n; ++bucket) {
node* first = buckets[bucket]; //指向節點所對應的起始節點
while (first) {
//以下找出節點落在哪一個新bucket內
size_type __new_bucket = bkt_num(first->val, n);
//令舊節點指向對應序列的下一個節點
buckets[bucket] = first->next;
//將當前節點插入到新bucket內
first->next = tmp[new_bucket];
tmp[new_bucket] = first;
//準備處理下一個節點
first = buckets[bucket];
}
}
// 通過swap交換
buckets.swap(tmp);
}
}
}
}
- 判知元素的落腳處:
size_type bkt_num(const value_type& obj, size_t n) const {
return bkt_num_key(get_key(obj), n);
}
size_type bkt_num(const value_type& obj) const
{
return bkt_num_key(get_key(obj));
}
size_type bkt_num_key(const value_type& key) const {
return bkt_num_key(key, buckets.size());
}
size_type bkt_num_key(const value_type& key,szie_t n) const {
return hash(key) % n;
}
6. 元素操作
操作 | 功能 |
---|---|
copy_from | 複製整個hashtable |
clear | hashtable整體刪除 |
find | 搜尋鍵值為key的元素 |
count | 計算鍵值為key的元素個數 |
- 一個例項:
#include <iostream>
#include <hash_set>
#include <algorithm>
using namespace std;
int main()
{
//指定保留50個buckets
hashtable<int, int, hash<int>,
identify<int>, equal_to<int>, alloc> iht(50, hash<int>(), equal_to<iny>());
cout << iht.size() << endl; //0
cout << iht.bucket_count() << endl; //53,滿足要求的最小質數
cout << iht.max_bucket_count() << endl; //4294967291
iht.insert_unique(59);
iht.insert_unique(63);
iht.insert_unique(108);
iht.insert_unique(2);
iht.insert_unique(53);
iht.insert_unique(55);
cout << iht.size() << endl; //6
//宣告迭代器
hashtable<int, int, hash<int>,
identify<int>, equal_to<int>, alloc>::iterator ite = iht.begin();
for (int i = 0; i < iht.size(); ++i, ++ite)
cout << *ite << ' '; //53 53 2 108 59 63
cout << endl;
for (int i = ); i < iht.bucket_count() ; ++i) {
int n = iht.elems_in_bucket(i);
if (n != 0)
cout << "bucket[" << i << "] has " << n << " elems." << endl;
}
//bucket[0] has 1 elems
//bucket[2] has 3 elems
//bucket[6] has 1 elems
//bucket[10] has 1 elems
//插入48個元素
for (int i = 0; i <= 47; ++i)
{
iht.insert_equal(i);
}
cout << iht.size() << endl; //54
cout << iht.bucket_count() << endl; //大於53,更改buckets個數為大於53的質數,97
for (int i = 0; i < iht.bucket_count(); ++i){
int n = iht.elems_in_bucket(i);
if (n != 0)
cout << "bucket[" << i << "] has " << n << " elems." << endl;
}
//列印結果:bucket[2]和bucket[11]的節點個數為2
//其餘的bucket[0]~bucket[47]的節點個數為1
//此外,bucket[53],[55],[59],[63]的節點個數為1
ite = ite.begin();
for (int i = 0; i < iht