散列表
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+α)。
散列函数
好的散列函数应(近似地)满足简均匀散列假设:每个关键字都被等可能地散列到 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}
当散列表的关键字比较小,可以取自 {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}
相关文章推荐
- Java中利用散列表实现股票行情的查询
- 软件开发者面试百问-----在散列表和排序后的列表中找一个元素,哪个查找速度最快?
- 白话算法(6) 散列表(Hash Table) 从理论到实用(下)
- 散列表(哈希表)
- 《算法导论》第11章 散列表 (2)散列表
- 算法导论(十一)--散列表
- 用java实现的哈希表(散列表)
- Linux进程PID散列表
- 哈希表/散列表 指针版模版
- 《算法导论》第十一章----散列表(直接寻址、链接法解决碰撞)
- 散列表查找的代码实现 - 数据结构和算法86
- 查找 之 散列表查找(哈希表)
- java专题——散列表查找
- 算法导论11(散列表)
- 算法导论---------------散列表(hash table)
- 散列表描述
- 大话数据结构—散列表查找(哈希表)
- 哈希表(散列表)
- 第十一章 散列表
- 散列表