在数组中依据数组的下标查找一个元素仅仅须要O(1)的时间,散列表是类似于数组的动态集合的数据结构,能够依据元素的keyword在一个表中高速地操作元素。
当散列表的keyword比較小。能够取自 {0, 1, ..., m-1} 一个有限的小范围内时。能够使用一个大小为 m 的数组 T 表示这个动态集合。这个数组称为直接寻址表。动态集合中的元素位于 T[key]中。
当这个动态集合变得非常大。使用数组保存这些数据将变得不可能。我们能够使用一个比較小的数组 T —— 散列表。使用一个散列函数将keyword k 转换为数组 T 的一个位置——槽,即将keyword映射到槽中,散列函数缩小了数组下标的范围,即减小了数组的大小。同一时候对这个大的动态集合的操作也会象普通数组一个变得快捷方便。
因为将一个数量比数组 T 大得多的动态集合来说,要将所有元素保存到数组中,散列函数计算出的下标值一定会有反复的值。即多个keyword可能会映射到同一个槽中。这样的情形称为冲突(collision)。解决冲突的方法有多种,一种称为链接法,还有一种称为开放寻址法。
链接法中,把散列到同一槽中的全部元素都放在一个链表中,每一个槽中有一个指针,指向存储全部散列到这个单元的链表的表头,假设不存在这种元素。则对应槽的指针为 null。
要查找一个元素。依据元素的keyword k。使用散列函数 h(k) 计算出槽的位置。然后遍历这个槽指向的链表。
向散列表中插入元素和删除元素的操作也非常easy。首先计算出散列位置,剩下的就是对链表进行操作。
对于一个能存放 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(); // 查找包括keyword key 的元素,假设找到返回这个元素的指针,否则返回 null Node* search(int key); // 向散列中插入一个元素。元素的keyword为 key,元素值为 value,返回散列元素的指针 // 假设插入的 key 在散列表中已存在,则替换对应的值 Node* insert(int key, const T& value); // 从散列表中删除具有keyword 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
散列函数
好的散列函数应(近似地)满足简均匀散列如果:每一个keyword都被等可能地散列到 m 个槽位中的不论什么一个,并与其它keyword已散列到哪个槽位无关。一种好的方法导出的散列值,在某种程度上应独立于数据可能存在的不论什么模式。
三种散列方法:
1. 除法散列法:通过取 k 除以 m 的余数,将keyword k 映射到 m 个槽位中的某一个上,散列函数为:h(k) = k mod m
应用除法散列法时,要避免选择 m 的某些值。如 m 不应为 2 的幂。一个不太接近 2 的整数幂的素数,经常是 m 的一个较好的选择。
2. 乘法散列法:一般包括两个步骤,第一步。用keyword k 乘上常数 A (0<A<1),并提取 kA 的小数部分;第二步。用 m 乘以这个值,再向下取整,散列函数为 h(k) = m(kA mod 1)
乘法散列法的一个长处是对 m 的选择不是特别关键。一般选择它为 2 的某个幂次,A 取 0.6180339887... 是一个比較好的值。
3.全域散列法:对于以上两种散列法来说。假设针对某个特定的散列函数选择要散列的keyword。可能会将 n 个keyword所有散列到同一个槽中。散列表将退化为链表,使得平均检索时间为 O(n)。全域散列法是随机地选择散列函数,使之独立于要存储的keyword。
设计一个散列函数:hab(k) = ((ak + b) mod p)mod m 。当中 p 为一个足够大的素数。 a 属于集合 {0, 1, 2, ..., p - 1},b 属于集合 {1, 2, ..., p-1}