HashTable雜湊表/散列表(線性探測和二次探測)
HashTable的簡單介紹
HashTable是根據關鍵字直接訪問在記憶體儲存的資料結構。
HashTable叫雜湊表或者散列表。
它通過一個關鍵值的函式將所需的資料直接對映到表中的位置來訪問資料,這個對映函式叫雜湊函式(雜湊函式),存放記錄的陣列叫散列表(雜湊表)。
比如:
給定一字串“abckhdgubhkacdggk”找出第一次只出現一次的字元。
對於這個問題,用指標解決,用兩個指標,一個指標cur指向當前字元,另一個指標next向後遍歷,如果找到cur與next相同,cur指向下一個字元,next再進行遍歷,直到next遍歷完成所有字元,都與cur不同,則此時的cur指向的字元就是第一個只出現一次的字元。時間複雜度O(n^2)。
這種方法可以解決但是時間複雜度比較高,優化一下,在查詢時紅黑樹和堆排序的查詢時間複雜度會低一點,建紅黑樹然後再根據關鍵字進行查詢。時間複雜度O(nlogn)。
能不能再優化一下?
那就是運用到雜湊表,所有字元總共256個,我們可以利用字元對應的ASCII值為下標建立陣列。遍歷一遍字串,將每個字元對應的下標ASCII處加1,遍歷完成後就從頭往後進行查詢,查詢時直接能找到對應的位置。所以這個時間複雜度就為O(n)。
這就是雜湊表的直接定址法。
當對一組10以內的數進行排序,也可以用雜湊表。比如:1、3、5、2、5、3、5、8,建立一個數組a[10],將對應的數存放在對應的下標處,出現一次加一下,列印時如果下標對應的是0就不列印,對應1就列印1次,對應的下標裡存幾就列印幾。如下:
但是並不是所有的數都適合這種排序,如:1000、1002、1000、1005、1000、1008。最大數是1008,開1008大小的陣列前1000個就浪費了,我們只需開(1008 - 1000)的大小就好,對應的下標就是(1008-1000)/1000。
這就是雜湊表的除留餘數法。
除此外,構造雜湊表的方法還有:平方取中法、摺疊法、隨機數法、數學分析法
雜湊衝突(雜湊碰撞)
不同的關鍵字通過相同的雜湊函式處理後可能產生相同的值雜湊地址。我們稱這種情況為雜湊衝突或雜湊碰撞。(任意的雜湊函式都不能避免產生衝突)
散列表的載荷因子定義:α = 填入表中的元素 / 表的長度
解決雜湊衝突的方法:
1. 閉雜湊方法-開放定址法
2. 開鏈法/拉鍊法
線性探測法和二次探測法
閉雜湊方法-開方地址法有線性探測法和二次探測法
線性探測法:
Hash(key) = key % 10(表長);
89放入9的位置;18放入8的位置;49與89衝突,往後加到尾了就再回到頭,0的位置為空放入;58與18衝突,往後加有89,再加有49,再加就放入1的位置;9與89衝突一直加到2的位置放入。
Hash(key) + 0,Hash(key)+1,……
線性探測的增刪查:
#pragma once
#include <vector>
enum State
{
EXIST,
EMPTY,
DELETE,
};
template <class K,class V>
struct HashNode
{
pair<K, V> _kv;
State _s;//狀態
};
template <class K,class V,class HushFunc>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
:_size(0)
{}
HashTable(size_t n)
:_size(0)
{
_tables.resize(n);
}
Node* Find(const K& key)//查詢
{
size_t index = _HashFunc(key);
while (_tables[index]._s != EMPTY)
{
if (_tables[index]._kv.first == key)
{
if (_tables[index]._s == EXIST)
return &_tables[index];
else
return NULL;
}
index += 1;
if (index == _tables.size())
{
index = 0;
}
}
return NULL;
}
bool Insert(const pair<K, V>& kv)//插入
{
_Check();//增容
size_t index = _HashFunc(kv.first);
while (_tables[index]._s == EXIST)
{
if (_tables[index]._kv.first == kv.first)//有key,直接返回
return false;
index += 1;
if (index == _tables.size())//到尾了,就從開頭找
index = 0;
}
_tables[index]._kv = kv;
_tables[index]._s = EXIST;
++_size;
return true;
}
bool Remove(const K& key)//刪除
{
Node* cur = Find(key);//找到了key
if (cur)
{
cur->_s = DELETE;
--_size;
return true;
}
else
return false;
}
protected:
void _Check()//增容
{
if (_tables.size() == 0)
_tables.resize(10);
if ((_size * 10) / (_tables.size()) == 7)
{
size_t newSize = _tables.size() * 2;
HashTable<K, V, HushFunc> newHashTable(newSize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if ((_tables[i]._s) == EXIST)
{
newHashTable.Insert(_tables[i]._kv);
}
}
Swap(newHashTable);
}
}
void Swap(HashTable<K, V,HushFunc> &ht)//交換
{
swap(_size, ht._size);
_tables.swap(ht._tables);
}
size_t _HashFunc(const K& key)//
{
return key % _tables.size();
}
protected:
vector<Node> _tables;
size_t _size;//雜湊表實際存放的個數
};
void TestHashTable()
{
int a[] = { 89, 18, 49, 58, 9 };
HashTable<int, int,int> ht1;
for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
{
ht1.Insert(make_pair(a[i], i));
}
ht1.Insert(make_pair(10, 1));
ht1.Insert(make_pair(11, 1));
ht1.Insert(make_pair(12, 1));
ht1.Insert(make_pair(13, 1));
ht1.Insert(make_pair(14, 1));
ht1.Find(10);
ht1.Remove(10);
ht1.Find(10);
ht1.Remove(10);
}
二次探測
Hash(key) = key % 10(表長);
89放入9的位置;18放入8的位置;49與89衝突,加1^2,到0的位置;58與18衝突,加1^2,到了9的位置,有數繼續加2^2到2的位置;9與89衝突,加1^2,在0的位置,繼續加2^2,到3的位置放入。
Hash(key)+ i^2(i= 1、2、3……不算第一放入的數)
線性探測和二次探測必須考慮載荷因子,超過0.7-0.8就增容,增大效率,(以空間換時間)
其中刪除是惰性刪除,也就是隻標記刪除記號,實際操作則待表格重新整理時再進行,因為HashTable中的每一個元素不僅代表自己,也關係到其他元素。