0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

哈希表是什么?哈希表数据结构详细资料分析

算法与数据结构 来源:未知 作者:易水寒 2018-09-24 10:25 次阅读

我所写的这些数据结构,都是比较经典的,也是面试中经常会出现的,这篇文章我就不闲扯了,全是干货,如果你能读完,希望对你有所帮助~

哈希表也称为散列表,是根据关键字值(key value)而直接进行访问的数据结构。也就是说,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度。这个映射函数称为哈希函数(也称为散列函数),映射过程称为哈希化,存放记录的数组叫做散列表。比如我们可以用下面的方法将关键字映射成数组的下标:

arrayIndex=hugeNumber%arraySize

那么问题来了,这种方式对不同的关键字,可能得到同一个散列地址,即同一个数组下标,这种现象称为冲突,那么我们该如何去处理冲突呢?一种方法是开放地址法,即通过系统的方法找到数组的另一个空位,把数据填入,而不再用哈希函数得到的数组下标,因为该位置已经有数据了;另一种方法是创建一个存放链表的数组,数组内不直接存储数据,这样当发生冲突时,新的数据项直接接到这个数组下标所指的链表中,这种方法叫做链地址法。下面针对这两种方法进行讨论。

1.开放地址法

1.1 线性探测法

所谓线性探测,即线性地查找空白单元。我举个例子,如果21是要插入数据的位置,但是它已经被占用了,那么就是用22,然后23,以此类推。数组下标一直递增,直到找到空白位。下面是基于线性探测法的哈希表实现代码:

publicclassHashTable {

privateDataItem[] hashArray; //DateItem类是数据项,封装数据信息

privateint arraySize;

privateint itemNum; //数组中目前存储了多少项

privateDataItem nonItem; //用于删除项的

publicHashTable() {

arraySize = 13;

hashArray = newDataItem[arraySize];

nonItem = newDataItem(-1); //deleted item key is -1

}

publicboolean isFull() {

return (itemNum == arraySize);

}

publicboolean isEmpty() {

return (itemNum == 0);

}

publicvoid displayTable() {

System.out.print("Table:");

for(int j = 0; j < arraySize; j++) {

if(hashArray[j] != null) {

System.out.print(hashArray[j].getKey() + " ");

}

else {

System.out.print("** ");

}

}

System.out.println("");

}

publicint hashFunction(int key) {

return key % arraySize; //hash function

}

publicvoid insert(DataItem item) {

if(isFull()) {

//扩展哈希表

System.out.println("哈希表已满,重新哈希化..");

extendHashTable();

}

int key = item.getKey();

int hashVal = hashFunction(key);

while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {

++hashVal;

hashVal %= arraySize;

}

hashArray[hashVal] = item;

itemNum++;

}

/*

* 数组有固定的大小,而且不能扩展,所以扩展哈希表只能另外创建一个更大的数组,然后把旧数组中的数据插到新的数组中。但是哈希表是根据数组大小计算给定数据的位置的,所以这些数据项不能再放在新数组中和老数组相同的位置上,因此不能直接拷贝,需要按顺序遍历老数组,并使用insert方法向新数组中插入每个数据项。这叫重新哈希化。这是一个耗时的过程,但如果数组要进行扩展,这个过程是必须的。

*/

publicvoid extendHashTable() { //扩展哈希表

int num = arraySize;

itemNum = 0; //重新记数,因为下面要把原来的数据转移到新的扩张的数组中

arraySize *= 2; //数组大小翻倍

DataItem[] oldHashArray = hashArray;

hashArray = newDataItem[arraySize];

for(int i = 0; i < num; i++) {

insert(oldHashArray[i]);

}

}

publicDataItemdelete(int key) {

if(isEmpty()) {

System.out.println("Hash table is empty!");

returnnull;

}

int hashVal = hashFunction(key);

while(hashArray[hashVal] != null) {

if(hashArray[hashVal].getKey() == key) {

DataItem temp = hashArray[hashVal];

hashArray[hashVal] = nonItem; //nonItem表示空Item,其key为-1

itemNum--;

return temp;

}

++hashVal;

hashVal %= arraySize;

}

returnnull;

}

publicDataItem find(int key) {

int hashVal = hashFunction(key);

while(hashArray[hashVal] != null) {

if(hashArray[hashVal].getKey() == key) {

return hashArray[hashVal];

}

++hashVal;

hashVal %= arraySize;

}

returnnull;

}

}

classDataItem {

privateint iData;

publicDataItem (int data) {

iData = data;

}

publicint getKey() {

return iData;

}

}

线性探测有个弊端,即数据可能会发生聚集。一旦聚集形成,它会变得越来越大,那些哈希化后落在聚集范围内的数据项,都要一步步的移动,并且插在聚集的最后,因此使聚集变得更大。聚集越大,它增长的也越快。这就导致了哈希表的某个部分包含大量的聚集,而另一部分很稀疏。

为了解决这个问题,我们可以使用二次探测:二次探测是防止聚集产生的一种方式,思想是探测相隔较远的单元,而不是和原始位置相邻的单元。线性探测中,如果哈希函数计算的原始下标是x, 线性探测就是x+1, x+2, x+3, 以此类推;而在二次探测中,探测的过程是x+1, x+4, x+9, x+16,以此类推,到原始位置的距离是步数的平方。二次探测虽然消除了原始的聚集问题,但是产生了另一种更细的聚集问题,叫二次聚集:比如讲184,302,420和544依次插入表中,它们的映射都是7,那么302需要以1为步长探测,420需要以4为步长探测, 544需要以9为步长探测。只要有一项其关键字映射到7,就需要更长步长的探测,这个现象叫做二次聚集。

二次聚集不是一个严重的问题,但是二次探测不会经常使用,因为还有好的解决方法,比如再哈希法。

1.2 再哈希法

为了消除原始聚集和二次聚集,现在需要的一种方法是产生一种依赖关键字的探测序列,而不是每个关键字都一样。即:不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。再哈希法就是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长,对于指定的关键字,步长在整个探测中是不变的,不同关键字使用不同的步长、经验说明,第二个哈希函数必须具备如下特点:

和第一个哈希函数不同;

不能输出0(否则没有步长,每次探索都是原地踏步,算法将进入死循环)。

专家们已经发现下面形式的哈希函数工作的非常好:

stepSize=constant-key%constant

其中 constant 是质数,且小于数组容量。

再哈希法要求表的容量是一个质数,假如表长度为15(0-14),非质数,有一个特定关键字映射到0,步长为5,则探测序列是 0,5,10,0,5,10,以此类推一直循环下去。算法只尝试这三个单元,所以不可能找到某些空白单元,最终算法导致崩溃。如果数组容量为13, 质数,探测序列最终会访问所有单元。即 0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一个空位,就可以探测到它。下面看看再哈希法的代码:

publicclassHashDouble {

privateDataItem[] hashArray;

privateint arraySize;

privateint itemNum;

privateDataItem nonItem;

publicHashDouble() {

arraySize = 13;

hashArray = newDataItem[arraySize];

nonItem = newDataItem(-1);

}

publicvoid displayTable() {

System.out.print("Table:");

for(int i = 0; i < arraySize; i++) {

if(hashArray[i] != null) {

System.out.print(hashArray[i].getKey() + " ");

}

else {

System.out.print("** ");

}

}

System.out.println("");

}

publicint hashFunction1(int key) { //first hash function

return key % arraySize;

}

publicint hashFunction2(int key) { //second hash function

return5 - key % 5;

}

publicboolean isFull() {

return (itemNum == arraySize);

}

publicboolean isEmpty() {

return (itemNum == 0);

}

publicvoid insert(DataItem item) {

if(isFull()) {

System.out.println("哈希表已满,重新哈希化..");

extendHashTable();

}

int key = item.getKey();

int hashVal = hashFunction1(key);

int stepSize = hashFunction2(key); //用hashFunction2计算探测步数

while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {

hashVal += stepSize;

hashVal %= arraySize; //以指定的步数向后探测

}

hashArray[hashVal] = item;

itemNum++;

}

publicvoid extendHashTable() {

int num = arraySize;

itemNum = 0; //重新记数,因为下面要把原来的数据转移到新的扩张的数组中

arraySize *= 2; //数组大小翻倍

DataItem[] oldHashArray = hashArray;

hashArray = newDataItem[arraySize];

for(int i = 0; i < num; i++) {

insert(oldHashArray[i]);

}

}

publicDataItemdelete(int key) {

if(isEmpty()) {

System.out.println("Hash table is empty!");

returnnull;

}

int hashVal = hashFunction1(key);

int stepSize = hashFunction2(key);

while(hashArray[hashVal] != null) {

if(hashArray[hashVal].getKey() == key) {

DataItem temp = hashArray[hashVal];

hashArray[hashVal] = nonItem;

itemNum--;

return temp;

}

hashVal += stepSize;

hashVal %= arraySize;

}

returnnull;

}

publicDataItem find(int key) {

int hashVal = hashFunction1(key);

int stepSize = hashFunction2(key);

while(hashArray[hashVal] != null) {

if(hashArray[hashVal].getKey() == key) {

return hashArray[hashVal];

}

hashVal += stepSize;

hashVal %= arraySize;

}

returnnull;

}

}

2. 链地址法

在开放地址法中,通过再哈希法寻找一个空位解决冲突问题,另一个方法是在哈希表每个单元中设置链表(即链地址法),某个数据项的关键字值还是像通常一样映射到哈希表的单元,而数据项本身插入到这个单元的链表中。其他同样映射到这个位置的数据项只需要加到链表中,不需要在原始的数组中寻找空位。下面看看链地址法的代码:

publicclassHashChain {

privateSortedList[] hashArray; //数组中存放链表

privateint arraySize;

publicHashChain(int size) {

arraySize = size;

hashArray = newSortedList[arraySize];

//new出每个空链表初始化数组

for(int i = 0; i < arraySize; i++) {

hashArray[i] = newSortedList();

}

}

publicvoid displayTable() {

for(int i = 0; i < arraySize; i++) {

System.out.print(i + ": ");

hashArray[i].displayList();

}

}

publicint hashFunction(int key) {

return key % arraySize;

}

publicvoid insert(LinkNode node) {

int key = node.getKey();

int hashVal = hashFunction(key);

hashArray[hashVal].insert(node); //直接往链表中添加即可

}

publicLinkNodedelete(int key) {

int hashVal = hashFunction(key);

LinkNode temp = find(key);

hashArray[hashVal].delete(key);//从链表中找到要删除的数据项,直接删除

return temp;

}

publicLinkNode find(int key) {

int hashVal = hashFunction(key);

LinkNode node = hashArray[hashVal].find(key);

return node;

}

}

下面是链表类的代码,用的是有序链表:

publicclassSortedList {

privateLinkNode first;

publicSortedList() {

first = null;

}

publicboolean isEmpty() {

return (first == null);

}

publicvoid insert(LinkNode node) {

int key = node.getKey();

LinkNode previous = null;

LinkNode current = first;

while(current != null && current.getKey() < key) {

previous = current;

current = current.next;

}

if(previous == null) {

first = node;

}

else {

node.next = current;

previous.next = node;

}

}

publicvoiddelete(int key) {

LinkNode previous = null;

LinkNode current = first;

if(isEmpty()) {

System.out.println("chain is empty!");

return;

}

while(current != null && current.getKey() != key) {

previous = current;

current = current.next;

}

if(previous == null) {

first = first.next;

}

else {

previous.next = current.next;

}

}

publicLinkNode find(int key) {

LinkNode current = first;

while(current != null && current.getKey() <= key) {

if(current.getKey() == key) {

return current;

}

current = current.next;

}

returnnull;

}

publicvoid displayList() {

System.out.print("List(First->Last):");

LinkNode current = first;

while(current != null) {

current.displayLink();

current = current.next;

}

System.out.println("");

}

}

classLinkNode {

privateint iData;

publicLinkNode next;

publicLinkNode(int data) {

iData = data;

}

publicint getKey() {

return iData;

}

publicvoid displayLink() {

System.out.print(iData + " ");

}

}

在没有冲突的情况下,哈希表中执行插入和删除操作可以达到O(1)的时间级,这是相当快的,如果发生冲突了,存取时间就依赖后来的长度,查找或删除时也得挨个判断,但是最差也就O(N)级别。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 函数
    +关注

    关注

    3

    文章

    3859

    浏览量

    61297
  • 数据结构
    +关注

    关注

    3

    文章

    562

    浏览量

    39898
  • 哈希表
    +关注

    关注

    0

    文章

    9

    浏览量

    4704

原文标题:读完这篇,希望你能真正理解什么是哈希表

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    数据结构概述及线性

    第一讲 数据结构概述及线性 1 数据结构概述1.1 概述    60年代初期,还没有独立的“数据结构”课程,有关内容散见于操作系统、编译
    发表于 12-05 21:20

    序列化哈希到文件

    ,s.Number,s.Classes,s.Love})});BinarySerialize.Serialize(strFile, ht); }}}//获取列表数据,并把列表数据预装到哈希
    发表于 06-18 18:28

    收藏 | 程序员面试,你必须知道的8大数据结构

    数据结构首先列出一些最常见的数据结构,我们将逐一说明:数组栈队列链表树图字典树(这是一种高效的树形结构,但值得单独说明)散列表(哈希)数
    发表于 09-30 09:35

    算法与数据结构——哈希

    周立功教授数年之心血之作《程序设计与数据结构》以及《面向第三章为算法与数据结构,本文为3.5 哈希表。
    的头像 发表于 09-25 11:37 5236次阅读
    算法与<b class='flag-5'>数据结构</b>——<b class='flag-5'>哈希</b>表

    基于分段哈希码的倒排索引树结构

    哈希技术被视为最有潜力的相似性搜索方法,其可以用于大规模多媒体数据搜索场合。为了解决在大规模图像情况下,数据检索效率低下的问题,提出了一种基于分段哈希码的倒排索引树
    发表于 11-28 17:40 0次下载
    基于分段<b class='flag-5'>哈希</b>码的倒排索引树<b class='flag-5'>结构</b>

    哈希哈希算法的介绍

    聊到区块链的时候也少不了会听到“哈希”、“哈希函数”、“哈希算法”,是不是听得一头雾水?别急,这一讲我们来讲讲什么是哈希算法。
    的头像 发表于 05-22 14:11 5959次阅读
    <b class='flag-5'>哈希</b>及<b class='flag-5'>哈希</b>算法的介绍

    数据结构与算法分析的C语言描述的电子教材详细资料免费下载

    本文档的主要内容详细介绍的是数据结构与算法分析的C语言描述的电子教材详细资料免费下载
    发表于 08-09 17:36 0次下载

    为什么要学习数据结构数据结构的应用详细资料概述免费下载

    本文档的主要内容详细介绍的是为什么要学习数据结构数据结构的应用详细资料概述免费下载包括了:数据结构在串口通信当中的应用,
    发表于 09-11 17:15 13次下载
    为什么要学习<b class='flag-5'>数据结构</b>?<b class='flag-5'>数据结构</b>的应用<b class='flag-5'>详细资料</b>概述免费下载

    数据结构教程之线性表的详细资料说明

    本文档的主要内容详细介绍的是数据结构教程之线性表的详细资料说明包括了:线性的操作和线性表的链式表示和实现。
    发表于 04-30 08:00 0次下载
    <b class='flag-5'>数据结构</b>教程之线性表的<b class='flag-5'>详细资料</b>说明

    哈希表是什么?为什么需要使用哈希

    我们在这篇文章将要学习最有用的数据结构之一—哈希表,哈希表的英文叫 Hash Table,也可以称为散列表或者 Hash 表。
    的头像 发表于 04-06 13:50 1.1w次阅读
    <b class='flag-5'>哈希</b>表是什么?为什么需要使用<b class='flag-5'>哈希</b>表

    基于Xilinx Virtex-II FPGA的硬件哈希算法的研究分析

    在计算关键词在文档里出现次数的过程中,需要一种存储结构来存储相关信息,这种存储结构必须易于执行查找、插入及删除操作。哈希是一种以常数平均时间执行查找、插入和删除操作的算法。在计算关键词在文档里的出现次数时应用
    发表于 07-28 17:13 1769次阅读
    基于Xilinx Virtex-II FPGA的硬件<b class='flag-5'>哈希</b>算法的研究<b class='flag-5'>分析</b>

    计算机系统中哈希表的优化

    应⽤能⼤幅度提升哈希表对哈希冲突的容忍能⼒,进⽽提升查询的速度,并且能帮助哈希表进⾏极致的存储空间压缩。 1 背景 哈希表是⼀种查找性能⾮常优异的
    的头像 发表于 03-02 14:10 1877次阅读

    哈希表是什么,它是如何根据键来得到值的

    多种哈希算法代码,用于文件校验、简单加密等场合。 哈希表也称作散列表,叫法不同,是一个意思。这种数据结构提供了键值对的映射关系,给出键就可以快速得到对应的值,比如上面提到的"50号"就是键,游戏机
    发表于 06-06 10:10 903次阅读
    <b class='flag-5'>哈希</b>表是什么,它是如何根据键来得到值的

    哈希是什么,常见的哈希算法有哪些

    什么是哈希哈希又称作“散列”,是一种数学计算机程序,它接收任何一组任意长度的输入信息,通过哈希算法变换成固定长度的数据指纹输出形式,如字母和数字的组合,该输出就是“
    的头像 发表于 06-23 14:57 6441次阅读

    Linux内核分析 端口哈希

    是用来封装各种协议的绑定哈希表,具体定义如下所示,这个结构体在[Linux内核角度分析服务器Listen细节中介绍过,具体地,struct inet_bind_hashbcket是bind相关的
    的头像 发表于 07-31 11:03 420次阅读
    Linux内核<b class='flag-5'>分析</b> 端口<b class='flag-5'>哈希</b>桶