字典的另一种表示是散列。它用一个散列函数(也称哈希函数)把字典的数对映射到一个散列表(也称哈希表)的具体位置。与跳表相比,它把操作的时间提高到渐近等于1,但最坏情况下依然是渐近等于N。但是如果经常输出所有元素或按照顺序查找元素(如查找第10个最小元素之类),跳表的执行效率将优于散列。相较于线性表,一个良好的散列函数得到的散列表,其查找、插入、删除的平均时间复杂度渐近等于1,即使是最坏情况下(所有元素都哈希到同一个桶),它的性能也和线性表相同。
基于跳表的字典实现
散列函数和散列表
1.桶和起始桶
当关键字范围太大,不能用理想方法表示时,可以采用并不理想的散列表和散列函数:散列表位置的数量比关键字的个数少,散列函数把若干个不同的关键字映射到散列表的同一个位置,也叫哈希冲突,将带来哈希溢出。散列表的每个位置叫作一个桶(bucket);对关键字k的数对,f(k)是起始桶;桶的数量等于散列表的长度或大小。本文考虑两种极端情况:每个桶只能存储一个数对和每个桶都是可以容纳全部数对的线性表(哈希链表)。
2.除法散列函数
在多种散列函数中,最常用的是散列函数:f(k)=k%D 其中k是关键字,D是散列的长度(桶的数量)。
3.冲突和溢出
当两个不同的关键字通过散列函数后,对应的起始桶相同时,就发生了冲突。当该桶没有空间多存储一个元素时,将带来溢出。当映射到散列表中任何一个桶里的关键字数量大致相等时,冲突和溢出的平均数最少。均匀散列函数就是这样的函数。我们需要选择一个良好的散列函数,那么就需要选择一个好的散列函数的除数D。这也就是为什么一般哈希桶数(散列函数除数)一般都是良好的素数,如果是合数,当我们的数据满足一定特点时(如偶数居多、元素多为2的幂等待情况),这时的D不是一个好的散列函数除数,得到的也是一个不良的散列函数。
4.解决冲突和溢出
1.线性探查
最简单的方法就是找到下一个可用的桶,本文第一种情况就选择这种方法。因此,这种方法的搜索过程如下:首先搜索起始桶f(k),然后把散列表当作环表继续搜索下一个桶,直到以下情况之一发生:1.存在关键字k的桶已找到;2.到达要给空桶;3.又回到起始桶。后两种情况说明关键字k不存在。因此这种情况下,删除操作除了需要删除指定的元素,还要从该元素的下一个桶开始,逐个检查每个桶,以确定要移动的元素,直到到达一个空桶或回到删除位置为止,然后依次将这些桶的元素向前移动。
2.使用链式哈希表
如果散列表的每一个桶都可以容纳无限多个记录,那么溢出问题就不存在了。实现这个目标的一个方法就是给散列表的每一个位置配置一个线性表。本文的第二种情况就是使用这种方法,为每个桶配置一个有序链表。
3.其他解决方法
如再哈希、设置公共溢出区等方法。
具体的C++实现
字典的ADT描述
#pragma once
//字典结构的抽象描述
template <typename K,typename V>
class dictionary {
public:
virtual ~dictionary(){
}
virtual bool empty() const = 0;
//返回字典中数对的数目
virtual size_t size() const = 0;
//返回匹配数对的指针
virtual std::pair<const K, V>* find(const K& _key) const = 0;
virtual void erase(const K& _key) = 0;
virtual void insert(const std::pair<const K, V>& _pair) = 0;
};
1.线性探查的哈希表
哈希表满异常
#pragma once
#include <string>
using std::string;
#include <iostream>
using std::cin; using std::cout; using std::ends; using std::endl;
class fullHashTable
{
public:
fullHashTable(const std::string& _msg="The hash table is full!"):msg(_msg){
}
std::string what()const {
return msg; }
void output()const {
cout << msg << endl; }
private:
std::string msg;
};
线性探查的哈希表实现
#pragma once
#ifndef hashTable_H
#define hashTable_H
#include "dictionaryADT.h"
#include "fullHashTable.h"
//使用先行探查来解决移出 每个桶只装一个元素
//散列表的平均性能远优于线性表(如查找、插入、删除) 即使在最坏情况(也就是所有元素hash到所有同一个起始桶)也和线性表相同
template <typename K, typename V>
class hashTable {
public:
hashTable(int _divisor=11);
~hashTable();
bool empty()const {
return dict_size == 0; }
size_t size()const {
return dict_size; }
std::pair<const K, V>* find(const K& _Key)const;
void insert(const std::pair<const K, V>& _pair);
void erase(const K& _Key);
void output(std::ostream& os)const;
private:
std::pair<const K, V>** table; //保存指向堆中pair对象的数组
size_t dict_size; //字典大小
int divisor; //散列函数除数
int search(const K& _Key)const; //查找给定的key所在的位置
};
template <typename K, typename V>
hashTable<K, V>::hashTable(int _divisor){
divisor = _divisor;
dict_size = 0;
//分配并初始化table
table = new std::pair<const K, V>* [divisor]();
}
template <typename K