作者 | 倪升武

责编 | 胡巍巍

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

哈希表也称为散列表,是根据关键字值(key value)而直接进行访问的数据结构。也就是说,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度。

这个映射函数称为哈希函数(也称为散列函数),映射过程称为哈希化,存放记录的数组叫做散列表。比如我们可以用下面的方法将关键字映射成数组的下标:

  • arrayIndex=hugeNumber%arraySize

那么问题来了,这种方式对不同的关键字,可能得到同一个散列地址,即同一个数组下标,这种现象称为冲突,那么我们该如何去处理冲突呢?

一种方法是开放地址法,即通过系统的方法找到数组的另一个空位,把数据填入,而不再用哈希函数得到的数组下标,因为该位置已经有数据了;

另一种方法是创建一个存放链表的数组,数组内不直接存储数据,这样当发生冲突时,新的数据项直接接到这个数组下标所指的链表中,这种方法叫做链地址法。下面针对这两种方法进行讨论。

开放地址法

线性探测法

所谓线性探测,即线性地查找空白单元。我举个例子,如果21是要插入数据的位置,但是它已经被占用了,那么就是用22,然后23,以此类推。

数组下标一直递增,直到找到空白位。下面是基于线性探测法的哈希表实现代码:

public class HashTable {
   private DataItem[] hashArray; //DateItem类是数据项,封装数据信息
   private int arraySize;
   private int itemNum; //数组中目前存储了多少项
   private DataItem nonItem; //用于删除项的
   public HashTable() {
       arraySize = 13;
       hashArray = new DataItem[arraySize];
       nonItem = new DataItem(-1); //deleted item key is -1
   }
   public boolean isFull() {
       return (itemNum == arraySize);
   }
   public boolean isEmpty() {
       return (itemNum == 0);
   }
   public void 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("");
   }
   public int hashFunction(int key) {
       return key % arraySize;     //hash function
   }
   public void 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方法向新数组中插入每个数据项。这叫重新哈希化。这是一个耗时的过程,但如果数组要进行扩展,这个过程是必须的。
    */
   public void extendHashTable() { //扩展哈希表
       int num = arraySize;
       itemNum = 0; //重新记数,因为下面要把原来的数据转移到新的扩张的数组中
       arraySize *= 2; //数组大小翻倍
       DataItem[] oldHashArray = hashArray;
       hashArray = new DataItem[arraySize];
       for(int i = 0; i < num; i++) {
           insert(oldHashArray[i]);
       }
   }
   public DataItem delete(int key) {
       if(isEmpty()) {
           System.out.println("Hash table is empty!");
           return null;
       }

线性探测有个弊端,即数据可能会发生聚集。一旦聚集形成,它会变得越来越大,那些哈希化后落在聚集范围内的数据项,都要一步步的移动,并且插在聚集的最后,因此使聚集变得更大。

聚集越大,它增长得也越快。这就导致了哈希表的某个部分包含大量的聚集,而另一部分很稀疏。

为了解决这个问题,我们可以使用二次探测:二次探测是防止聚集产生的一种方式,思想是探测相隔较远的单元,而不是和原始位置相邻的单元。

线性探测中,如果哈希函数计算的原始下标是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,就需要更长步长的探测,这个现象叫做二次聚集。

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

再哈希法

为了消除原始聚集和二次聚集,现在需要的一种方法是产生一种依赖关键字的探测序列,而不是每个关键字都一样。

即:不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。再哈希法就是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长,对于指定的关键字,步长在整个探测中是不变的,不同关键字使用不同的步长、经验说明,第二个哈希函数必须具备如下特点:

  • 和第一个哈希函数不同;

  • 不能输出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,一直下去,只要表中有一个空位,就可以探测到它。下面看看再哈希法的代码:

public class HashDouble {
   private DataItem[] hashArray;
   private int arraySize;
   private int itemNum;
   private DataItem nonItem;
   public HashDouble() {
       arraySize = 13;
       hashArray = new DataItem[arraySize];
       nonItem = new DataItem(-1);
   }
   public void 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("");
   }
   public int hashFunction1(int key) { //first hash function
       return key % arraySize;
   }
   public int hashFunction2(int key) { //second hash function
       return 5 - key % 5;
   }
   public boolean isFull() {
       return (itemNum == arraySize);
   }
   public boolean isEmpty() {
       return (itemNum == 0);
   }
   public void 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++;
   }
   public void extendHashTable() {
       int num = arraySize;
       itemNum = 0; //重新记数,因为下面要把原来的数据转移到新的扩张的数组中
       arraySize *= 2; //数组大小翻倍
       DataItem[] oldHashArray = hashArray;
       hashArray = new DataItem[arraySize];
       for(int i = 0; i < num; i++) {
           insert(oldHashArray[i]);
       }
   }
   public DataItem delete(int key) {
       if(isEmpty()) {
           System.out.println("Hash table is empty!");
           return null;
       }
       int hashVal = hashFunction1(key);
       int stepSize = hashFunction2(key);
       while(hashArray[hashVal] != null) {

链地址法

在开放地址法中,通过再哈希法寻找一个空位解决冲突问题,另一个方法是在哈希表每个单元中设置链表(即链地址法),某个数据项的关键字值还是像通常一样映射到哈希表的单元,而数据项本身插入到这个单元的链表中。

其他同样映射到这个位置的数据项只需要加到链表中,不需要在原始的数组中寻找空位。下面看看链地址法的代码:

public class HashChain {
   private SortedList[] hashArray; //数组中存放链表
   private int arraySize;
   public HashChain(int size) {
       arraySize = size;
       hashArray = new SortedList[arraySize];
       //new出每个空链表初始化数组
       for(int i = 0; i < arraySize; i++) {
           hashArray[i] = new SortedList();
       }
   }
   public void displayTable() {
       for(int i = 0; i < arraySize; i++) {
           System.out.print(i + ": ");
           hashArray[i].displayList();
       }
   }
   public int hashFunction(int key) {
       return key % arraySize;
   }
   public void insert(LinkNode node) {
       int key = node.getKey();
       int hashVal = hashFunction(key);
       hashArray[hashVal].insert(node); //直接往链表中添加即可
   }
   public LinkNode delete(int key) {
       int hashVal = hashFunction(key);
       LinkNode temp = find(key);
       hashArray[hashVal].delete(key);//从链表中找到要删除的数据项,直接删除
       return temp;
   }
   public LinkNode find(int key) {
       int hashVal = hashFunction(key);
       LinkNode node = hashArray[hashVal].find(key);
       return node;
   }
}

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

public class SortedList {
   private LinkNode first;
   public SortedList() {
       first = null;
   }
   public boolean isEmpty() {
       return (first == null);
   }
   public void 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;
       }
   }
   public void delete(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;
       }
   }
   public LinkNode find(int key) {
       LinkNode current = first;
       while(current != null && current.getKey() <= key) {
           if(current.getKey() == key) {
               return current;
           }
           current = current.next;
       }
       return null;
   }
   public void displayList() {
       System.out.print("List(First->Last):");
       LinkNode current = first;
       while(current != null) {
           current.displayLink();
           current = current.next;
       }
       System.out.println("");
   }
}
class LinkNode {
   private int iData;
   public LinkNode next;
   public LinkNode(int data) {
       iData = data;
   }
   public int getKey() {
       return iData;
   }
   public void displayLink() {
       System.out.print(iData + " ");
   }
}

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

哈希表就分享这么多,本文建议收藏,在等班车的时候、吃饭排队的时候可以拿出来看看。利用碎片化时间来学习!

作者简介:倪升武,CSDN 博客专家,CSDN达人课作者。硕士毕业于同济大学,曾先后就职于 eBay、爱奇艺、华为。目前在科大讯飞从事Java领域的软件开发,他的世界不仅只有coding。

声明:本文为作者投稿,版权归其个人所有。

推荐阅读:

  • 社畜码农

  • 程序员如何 Get 分布式锁的正确姿势?| 技术头条

  • 中国云计算的十年江湖

  • 20k~80k,华为、京东、火币、比特大陆招人啦!想赶上这波人才荒,你要掌握这些...

  • Python告诉你:这类程序员最赚钱!

  • 胜过iPhone XS?Google Pixel的“夜视功能”是怎样炼成的

  • 当程序员不再care用户感受......

程序员面试中常见的哈希表,到底是什么?相关推荐

  1. java程序员面试中的5个杀手锏问题

    java程序员面试中的5个杀手锏问题,不管你去面试的频率如何,下面这五个问题是每个软件工程师都应该问的--将有助于你确定自己在这家公司长期工作是否会合作愉快. 你们的企业文化是什么? 你每天将会有10 ...

  2. 程序员面试中最常见的几个问题,拿走不谢

    程序员面试不同于一般岗位的面试.当你在羡慕程序员十几万美金年薪的同时,你却不知道程序员面试时,要经历的重重磨难和压力.今天,我们就来总结一下程序员面试时最常见的几道问题. Phone Screen,一 ...

  3. 程序员面试失败常见的5大原因,别让自己输在这些上面!

    https://www.toutiao.com/a6528668025413960205/?tt_from=mobile_qq&utm_campaign=client_share&ti ...

  4. python程序员面试题精选100题_在Python程序员面试中被问的最多的10道题

    我们在为大家整Python程序员面试试题中,发现了一些被面试官问到的最多的一些问题,以下就是本篇内容: Python是个非常受欢迎的编程语言,随着近些年机器学习.云计算等技术的发展,Python的职位 ...

  5. java面试时候算法题多吗_java程序员面试中最容易被问到的18个算法题(附答案!)...

    作者:cpp软件架构狮 链接:https://www.toutiao.com/i6618515311836529156/ (点击阅读原文前去围观) 算法是比较复杂又基础的学科,每个学编程的人都会学习大 ...

  6. 程序员面试中遇到问题

    面试时候经常会问的一些问题(不断补充中) 面试必备基础题目(虽然不一定常用, 仅用于面试, 面试就是把人搞的都不会然后砍价, 当然您可以讲我可以查资料完成, 但是面试的时候就是没道理的, 起码我是经常 ...

  7. mfc链表中的数据如何排序输出_java程序员面试中最容易被问到的18个算法题(附答案!)...

    算法是比较复杂又基础的学科,每个学编程的人都会学习大量的算法.而根据统计,以下这18个问题是面试中最容易遇到的,本文给出了一些基本答案,供算法方向工程师或对此感兴趣的程序员参考. 1)请简单解释算法是 ...

  8. 程序员面试中,有些问题要这样答

    2019独角兽企业重金招聘Python工程师标准>>> 对程序员而言,面试时让他写一段超难的代码他觉得很容易,让他和人面对面聊天反而会觉得很难.程序员觉得说不如做,自己的实力在这儿, ...

  9. 程序员常说的「哈希表」是个什么鬼?

    今天聊聊「哈希表」,「哈希表」主要作用在于高效查找. 在编程实现中,常常面临着两个问题:存储和查找,存储和查找的效率往往决定了整个程序的效率. 脑补下,你在家里忘记了指甲刀放在哪里,通常要在你家所有抽 ...

最新文章

  1. python pdb 基础调试
  2. 切糕[HNOI2013]
  3. JSON与JAVA数据的转换
  4. 10-异步爬虫(线程池/asyncio协程)实战案例
  5. 图论--最短路-- Dijkstra模板(目前见到的最好用的)
  6. AngularJS快速入门指南03:表达式
  7. Python的Pexpect库
  8. 二阶振荡环节的谐振频率_自动控制系统时域分析十三:对数频率特性
  9. 十大经典排序算法(动态演示+代码)
  10. 大数据学习笔记19:MR案例——汇总三科成绩表文件
  11. KDD2021|小红书在推荐多样化的实践——SSD
  12. NSUserDefaults 的用法
  13. Beginning Python chapter 3: Working with strings
  14. 小甲鱼python课后题和答案简书_小甲鱼Python入门 10讲 答案
  15. 根据RGB配色改变图片颜色
  16. 地理学中的经典统计分析方法
  17. 解决绿C(C与C++程序设计学习与实验系统 2015)软件程序中文出现乱码问题
  18. 树莓派pxe网络启动官方文档(译文)
  19. 网上报名考试管理系统-功能齐全安全稳定-简单易用-系统试用免费
  20. 如何选择适合你的兴趣爱好(五十九),芭蕾舞

热门文章

  1. allt什么意思_Allt是什么意思
  2. Flutter进阶—布局方法演示
  3. php magento 开发,magento 2模块开发实例helloworld模块 hello world 程序 c语言hello world代码 c语言hello worl...
  4. delphi webservice 如何 共享 变量_医疗质量|如何实现非药物医嘱闭环管理?
  5. npm 报错 : npm ERR! Maximum call stack size exceeded
  6. 2021大二实训part01
  7. 说说代码质量、代码安全和软件测试那些事
  8. 从年末生产故障解锁RocketMQ集群部署的最佳实践
  9. 中台“不火”了,企业数智转型如何破圈?
  10. 以“智变”应万变,揭秘新华三六大数字化解决方案!