hash_map的簡潔實現
hash_map是經常被使用的一種數據結構,而其實現方式也是多種多樣。如果要求我們使用盡可能簡單的方式實現hash_map,具體該如何做呢?
我們知道hash_map最重要兩個概念是hash函數和沖突解決算法。hash_map鍵-值之間的映射關系,hash函數將鍵映射為內存地址,沖突解決算法用於解決不同的鍵映射為相同地址時候的情況。
數據結構和算法導論中介紹了大量的hash函數和沖突解決算法,如果選擇實現精簡的hash_map,那么可以選擇“除留取余法”作為hash函數,選擇“開散列地址鏈”作為沖入解決算法。這樣的選擇不僅使得hash_map實現簡單,而且有很高的查詢效率。其設計結構如下:
基於地址鏈的開散列哈希表實現
如圖所示,鍵347通過除留取余法得到地址鏈的索引,然后將347對應的元素插入到該地址鏈的尾端。
為了實現地址鏈,我們需要定義鏈表節點類型Node。為了簡化問題,我們將鍵值都定義為int類型。
{
int key; // 鍵
int value; // 值
Node*next; // 鏈接指針
Node( int k, int v):key(k),value(v),next(NULL){}
};
value用於記錄節點數組,key在檢索節點時會被使用,next保存鏈表關系。
基於此,定義hash_map類型SimHash。
class SimHash
{
Node**map; // 地址鏈數組
size_t hash( int key) // hash函數,除留取余法
{
return key%SIZE;
}
public:
SimHash()
{
map= new Node*[SIZE];
for(size_t i= 0;i<SIZE;i++)map[i]=NULL; // 初始化數組為空
}
~SimHash()
{
for(size_t i= 0;i<SIZE;i++) // 清除所有節點
{
Node*p;
while(p=map[i])
{
map[i]=p->next;
delete p;
}
}
delete[] map; // 清除數組
}
void insert( int key, int value)
{
Node*f=find(key); // 插入前查詢
if(f)
{
f->value=value; // 存在鍵則覆蓋
return;
}
Node*p=map[hash(key)]; // 確定地址鏈索引
Node*q= new Node(key,value); // 創建節點
while(p&&p->next)p=p->next; // 索引到地址鏈末端
if(p)p->next=q; // 添加節點
else map[hash(key)]=q; // 地址鏈的第一個節點
}
void remove( int key)
{
Node*p=map[hash(key)]; // 確定地址鏈索引
Node*q=NULL; // 前驅指針
while(p)
{
if(p->key==key) // 找到鍵
{
if(q)q->next=p->next; // 刪除節點
else map[hash(key)]=p->next; // 刪除地址鏈的最后一個節點
delete p;
break;
}
q=p;
p=p->next;
}
}
Node* find( int key)
{
Node*p=map[hash(key)]; // 確定地址鏈索引
while(p)
{
if(p->key==key) break; // 查詢成功
p=p->next;
}
return p;
}
};
首先,我們需要一個Node**類型的指針map記錄地址鏈數組的地址,至於數組大小,我們給一個較大的值(這里設置為100),為了簡化問題,不需要考慮插入時表滿等情況。在構造函數和析構函數內對map內存分配和撤銷。另外,節點的內存是動態增加的,因此析構時需要單獨處理每個地址鏈表。
對於數據結構,最關鍵的便是插入、刪除、檢索操作,為此定義操作insert、remove、find。
檢索操作實現時,首先通過hash函數定位地址鏈表的索引,然后在地址鏈表上檢索鍵是否存在即可。
插入操作實現時,首先會檢索鍵是否存在,如果存在則僅僅更新對應節點的數據即可,否則創建新的節點,插入到鏈表的結尾。對於空鏈表需要做特殊處理。
刪除操作實現時,需要使用兩個指針記錄節點的位置信息,當遇到滿足鍵的節點時,就將該節點從鏈表內刪除即可。如果刪除鏈表的第一個節點,需要做特殊處理。
以上,便是一個較簡單的hash_map的實現了,代碼行約80行左右,當然這肯定不是最簡單的,如果你有興趣可以再做進一步的簡化。
如果考慮哈希表的鍵值類型、特殊鍵類型的hash映射(字符串類型鍵如何映射為數值)、特殊鍵類型的比較處理(怎么比較兩個自定義類型鍵是否相等)、索引運算重載這些問題的話,hash_map的實現就變得復雜了。不過這些在STL內實現的比較完整,若你感興趣可以多做了解。這里我給出自己的一個考慮以上問題的簡單hash_map實現。
{
size_t operator()( const char* str) const
{
size_t ret= 0;
while(*str)ret=(ret<< 5)+*str++;
return ret;
}
};
struct str_cmp
{
bool operator()( const char* str1, const char* str2) const
{
while(*str1&&*str2)
{
if(*str1++!=*str2++) return false;
}
return !*str1&&!*str2;
}
};
template< class K, class T>
class Hashnode
{
public:
Hashnode(K k,T d):key(k),data(d),next(NULL)
{}
K key;
T data;
Hashnode*next;
};
template< class K, class T, class H, class C>
class Hashmap
{
Hashnode<K,T>**map; // hash鏈表數組
size_t div; // 數組大小
size_t hash(K key) // hash函數,獲取桶號——除留余數法
{
return h(key)%div;
}
H h;
C c;
Hashnode<K,T>* _find(K key)
{
size_t pos=hash(key); // 獲取桶號
for(Hashnode<K,T>*p=map[pos];p;p=p->next)
{
if(c(p->key,key)) // 找到了key
{
return p;
}
}
return NULL;
}
public:
size_t size; // hash表容量
size_t count; // 元素個數
Hashmap(size_t sz= 6):size( 6),div( 2),count( 0)
{
if(sz> 6)
{
size=sz;
div=size/ 3;
}
map= new Hashnode<K,T>*[div];
for(size_t i= 0;i<div;i++)map[i]=NULL;
}
~Hashmap()
{
for(size_t i= 0;i<div;i++)
{
while( true)
{
Hashnode<K,T>*p=map[i];
if(p)
{
map[i]=p->next;
delete p;
}
else break;
}
}
delete[] map;
}
void insert(K key,T data)
{
if(count>=size) // 表滿,增加空間
{
Hashnode<K,T>**oldmap=map;
size_t olddiv=div;
size*= 2;
div=size/ 3;
map= new Hashnode<K,T>*[div];
for(size_t i= 0;i<div;i++)map[i]=NULL;
for(size_t i= 0;i<olddiv;i++)
{
for(Hashnode<K,T>*p=oldmap[i],*t;p;p=t)
{
t=p->next;
p->next=NULL; // 消除后繼信息
size_t pos=hash(p->key); // 重新映射
Hashnode<K,T>*q;
for(q=map[pos];q&&q->next;q=q->next);
if(!q)map[pos]=p;
else q->next=p;
}
}
delete oldmap;
}
Hashnode<K,T>*p=_find(key); // 已經存在,替換數據
if(p)p->data=data;
else
{
size_t pos=hash(key); // 獲取桶號
Hashnode<K,T>*p;
for(p=map[pos];p&&p->next;p=p->next); // 索引到最后位置
Hashnode<K,T>*q= new Hashnode<K,T>(key,data);
if(!p)map[pos]=q; // 插入數據節點
else p->next=q;
count++;
}
}
void remove(K key)
{
if(count<=size/ 2) // 元素少於一半
{
Hashnode<K,T>**oldmap=map;
size_t olddiv=div;
size/= 2;
div=size/ 3;
map= new Hashnode<K,T>*[div];
for(size_t i= 0;i<div;i++)map[i]=NULL;
for(size_t i= 0;i<olddiv;i++)
{
for(Hashnode<K,T>*p=oldmap[i],*t;p;p=t)
{
t=p->next;
p->next=NULL; // 消除后繼信息
size_t pos=hash(p->key); // 重新映射
Hashnode<K,T>*q;
for(q=map[pos];q&&q->next;q=q->next);
if(!q)map[pos]=p;
else q->next=p;
}
}
delete oldmap;
}
size_t pos=hash(key); // 獲取桶號
for(Hashnode<K,T>*p=map[pos],*q=NULL;p;p=p->next)
{
if(c(p->key,key)) // 找到了key
{
if(q)q->next=p->next;
else map[pos]=p->next;
delete p;
count--;
break;
}
q=p;
}
}
T& get(K key)
{
Hashnode<K,T>*p=_find(key);
if(p) return p->data;
// 沒有key,插入map[key]=0
insert(key, 0);
return get(key);
}
T& operator[](K key)
{
return get(key);
}
bool find(K key)
{
return !!_find(key);
}
};
int main()
{
Hashmap< char*, int,str_hash,str_cmp> hashmap;
hashmap[ " A "]= 1;
hashmap[ " B "]= 2;
hashmap[ " C "]= 3;
hashmap[ " D "]= 4;
hashmap[ " E "]= 5;
hashmap[ " F "]= 6;
hashmap[ " G "]= 7;
hashmap[ " H "]= 8;
cout<<hashmap[ " A "]<< " "<<hashmap[ " B "]<<endl;
cout<<hashmap[ " C "]<< " "<<hashmap[ " D "]<<endl;
cout<<hashmap[ " E "]<< " "<<hashmap[ " F "]<<endl;
cout<<hashmap[ " G "]<< " "<<hashmap[ " H "]<<endl;
cout<<hashmap.size<<endl;
hashmap.remove( " A ");
hashmap.remove( " B ");
hashmap.remove( " C ");
hashmap.remove( " D ");
hashmap.remove( " E ");
cout<<hashmap.size<<endl;
cout<<hashmap[ " A "]<< " "<<hashmap[ " B "]<<endl;
cout<<hashmap[ " C "]<< " "<<hashmap[ " D "]<<endl;
cout<<hashmap[ " E "]<< " "<<hashmap[ " F "]<<endl;
cout<<hashmap[ " G "]<< " "<<hashmap[ " H "]<<endl;
cout<<hashmap.size<<endl;
return 0;
}