您的位置:首页 > 理论基础 > 数据结构算法

散列表

2014-10-16 13:53 232 查看
在数组中根据数组的下标查找一个元素只需要O(1)的时间,散列表是类似于数组的动态集合的数据结构,可以根据元素的关键字在一个表中快速地操作元素。

当散列表的关键字比较小,可以取自 {0, 1, ..., m-1} 一个有限的小范围内时,可以使用一个大小为 m 的数组 T 表示这个动态集合,这个数组称为直接寻址表,动态集合中的元素位于 T[key]中。

当这个动态集合变得很大,使用数组保存这些数据将变得不可能。我们可以使用一个比较小的数组 T —— 散列表,使用一个散列函数将关键字 k 转换为数组 T 的一个位置——槽,即将关键字映射到槽中,散列函数缩小了数组下标的范围,即减小了数组的大小,同时对这个大的动态集合的操作也会象普通数组一个变得快捷方便。

由于将一个数量比数组 T 大得多的动态集合来说,要将全部元素保存到数组中,散列函数计算出的下标值一定会有重复的值,即多个关键字可能会映射到同一个槽中,这种情形称为冲突(collision)。解决冲突的方法有多种,一种称为链接法,另一种称为开放寻址法。

链接法中,把散列到同一槽中的所有元素都放在一个链表中,每个槽中有一个指针,指向存储所有散列到这个单元的链表的表头,如果不存在这样的元素,则相应槽的指针为 null。要查找一个元素,根据元素的关键字 k,使用散列函数 h(k) 计算出槽的位置,然后遍历这个槽指向的链表。向散列表中插入元素和删除元素的操作也很简单,首先计算出散列位置,剩下的就是对链表进行操作。

对于一个能存放 n 个元素、具有 m 个槽位的散列表 T,一个链表的平均元素数为 n/m,这个值也叫做 T 的装载因子α。最坏情况下,所有 n 个元素都放散列到同一槽中,那么查找时间就是 O(n),即长度为 n 的链表的查找时间,所以链接法的平均性能依赖于散列函数。我们可以假定 n 个元素散列到 m 个槽位上是平均的,即每个槽位上大约有 n/m 个元素,称这个假设为简单均匀散列。简单均匀散列的查找平均时间为 O(1+α)。

#ifndef _CHAINING_HASH_H_
#define _CHAINING_HASH_H_

/**********************************************************************
算法导论 链接法散列表

散列表的每个槽中保存的是散列元素链表的头结点的指针,如果这个指针为空,表示相应槽中没有
散列元素
***********************************************************************/

template <class T>
class ChainingHash{
public:
// 槽位链表的结点类型
class Node{
friend class ChainingHash < T > ;
T value()const { return _value; }
private:
// 只简单地使用整型数值为键
int _key;
// 元素值
T _value;
// 前驱结点的指针,链表的头结点为 null
Node* _prev;
// 指向后继结点,链表的最后一个结点为 null
Node* _next;

Node() :_prev(nullptr), _next(nullptr){}
Node(int key, T v) : _prev(nullptr), _next(nullptr), _key(key), _value(v){}
};

ChainingHash(){ for (size_t i = 0; i < _slots_size; ++i)_slots[i] = nullptr; };
~ChainingHash();

// 查找包含关键字 key 的元素,如果找到返回这个元素的指针,否则返回 null
Node* search(int key);
// 向散列中插入一个元素,元素的关键字为 key,元素值为 value,返回散列元素的指针
// 如果插入的 key 在散列表中已存在,则替换相应的值
Node* insert(int key, const T& value);
// 从散列表中删除具有关键字 key 的元素
void remove(int key);

private:
// 槽位的长度
static const size_t _slots_size = 10;
// 槽位数组,数组中的每个元素是一个指向 Node 类型的指针
Node* _slots[_slots_size];

private:
// 散列函数,返回一个不大于 _slots_size 的无符号整数
// 这里只简单地返回一个余数
size_t hash(int key){ return key % _slots_size; }
// 将一个结点从双向链表中断开
void disconnect(Node*);
// 向一个双向链表中插入一个结点
void insertNode(Node* head, Node* node);
};

template <class T>
typename ChainingHash<T>::Node* ChainingHash<T>::search(int key){
// 取得槽位
auto hashcode = hash(key);
// 取得槽位的链表头指针
Node *node = _slots[hashcode];
// 遍历槽链表
while (node && node->_key != key)
node = node->_next;

return node;
}

template <class T>
typename ChainingHash<T>::Node * ChainingHash<T>::insert(int key, const T& value){
// 先在散列表中查找具有相同关键的元素,如果找到,直接替换其值。
Node *node = search(key);
if (node){
node->_value = value;
return node;
}

auto hashcode = hash(key);
// 取得槽位的链表头指针
Node *head = _slots[hashcode];
if (!head)
head = _slots[hashcode] = new Node();

node = new Node(key, value);
insertNode(head, node);

return node;
}

template <class T>
void ChainingHash<T>::remove(int key){
auto node = search(key);
if (node){
disconnect(node);
delete node;
}
}

template <class T>
void ChainingHash<T>::insertNode(Node *head, Node* node){
node->_next = head->_next;
node->_prev = head;
if (head->_next)
head->_next->_prev = node;
head->_next = node;
}

template <class T>
void ChainingHash<T>::disconnect(Node* node){
if (node->_next)
node->_next->_prev = node->_prev;
node->_prev->_next = node->_next;
node->_next = nullptr;
node->_prev = nullptr;
}

template <class T>
ChainingHash<T>::~ChainingHash(){
// 将每个槽及槽指向的链表元素释放空间
for (size_t i = 0; i < _slots_size; ++i){
// 有散列元素占用,释放槽链表的其它元素空间
Node *node = _slots[i];
while (node && node->_next){
auto n = node->_next;
delete node;
node = n;
}
}
}

#endif

散列函数

好的散列函数应(近似地)满足简均匀散列假设:每个关键字都被等可能地散列到 m 个槽位中的任何一个,并与其他关键字已散列到哪个槽位无关。一种好的方法导出的散列值,在某种程度上应独立于数据可能存在的任何模式。

三种散列方法:

1. 除法散列法:通过取 k 除以 m 的余数,将关键字 k 映射到 m 个槽位中的某一个上,散列函数为:h(k) = k mod m

应用除法散列法时,要避免选择 m 的某些值,如 m 不应为 2 的幂。一个不太接近 2 的整数幂的素数,常常是 m 的一个较好的选择。

2. 乘法散列法:一般包含两个步骤,第一步,用关键字 k 乘上常数 A (0<A<1),并提取 kA 的小数部分;第二步,用 m 乘以这个值,再向下取整,散列函数为 h(k) = m(kA mod 1)

乘法散列法的一个优点是对 m 的选择不是特别关键,一般选择它为 2 的某个幂次,A 取 0.6180339887... 是一个比较好的值。

3.全域散列法:对于以上两种散列法来说,如果针对某个特定的散列函数选择要散列的关键字,可能会将 n 个关键字全部散列到同一个槽中,散列表将退化为链表,使得平均检索时间为 O(n)。全域散列法是随机地选择散列函数,使之独立于要存储的关键字。

设计一个散列函数:hab(k) = ((ak + b) mod p)mod m 。其中 p 为一个足够大的素数, a 属于集合 {0, 1, 2, ..., p - 1},b 属于集合 {1, 2, ..., p-1}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息