首先我们需要明确一点,什么是HashMap,有图有真相:这就是HashMap
- 开个玩笑………O(∩_∩)O哈哈~………
哈希表:
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出现在各类的面试题中,重要性可见一斑。
在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能
数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。
我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。
比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。
存储位置 = f(关键字)
其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,比如我们要在哈希表中执行插入操作:
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
哈希冲突
然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式,
其存储和取值的流程大概为(敲黑板!):
- 例如要将A键值对存入HashMap,首先通过将A的Key带入哈希函数,得到一个实际的存储地址,将键值对存储在此地址
- 当有第二个键值对B插入时,同样将B的Key带入哈希函数,为其寻找一个存储地址,如果当前地址之前没有存入过信息则直接存入。
- 如果第二次存储的B的key通过计算,获得的值和A的Key计算的结果相同,也就是说他们两个要同时存在同一个地址下,这就产生了“哈希冲突”。
- 为了解决这种冲突,这里添加了一个“链表”的概念,即当两个Key通过计算得到相同的值,则在该地址上添加链表,在第一个位置插入B的键值对和A键值对的地址。
- 当需要查找的时候,通过将要查找的键值对的Key带入哈希函数得到相应的地址,
- 如果该地址只有一个键值对(没有链表),则直接取到,如果存在多个键值对(有链表),则使用 .equse()方法对链表上所有的键进行比较,直到找到相应的键值对。
- 但是就算是这样,也无法避免很多键值对的键通过hash算法计算后得到的结果相同,也就是在一个链表中存在很多数据,如果这时候需要从头开始遍历链表,也不是一个好的选择,所以JDK1.8给出了一种优雅的做法——红黑树。
在HashMap中Key 不允许重复出现(相信你已经明白为什么了),Value 随意,jdk 8 之前,其内部是由数组+链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑树。
什么是红黑树?
要了解红黑树,先来看看二叉树
二叉树:
- 左子树上所有的节点的值都小于或等于他的根节点上的值
- 右子树上所有节点的值均大于或等于他的根节点的值
- 左、右子树也分别为平衡二叉树
例如我们查找 10
1、因为10大于9,所以从右侧开始查找,找到13;
2、 因为10 小于13 所以从13 的左侧继续找,找到11;
3、而10又小于11 所以从11 的左侧找,至此找到10;
4、Game Over !!!
不过二叉查找树有一些问题,可能会出现不平横的情况,即如下图所示的情况
从这种情况可以看出,明显存在左子树和右子树深度相差过多,在使用平衡情况下的二叉查找树是时间复杂度为logn,而出现这种极端情况的话,想要查9的位置就需要每一次都遍历下一个右子树,很有可能时间复杂度变为n(与数组普通查询的时间复杂度相同)
基于上述情况,引入了平衡二叉树,红黑树即为平衡二叉树的一种
红黑树:
- 节点是红色或黑色
- 根节点一定是黑色
- 每个叶节点都是黑色的空节点(NIL节点)
- 每个红节点的两个子节点都是黑色的(从每个叶子到跟的所有路径上不能有两个连续的红节点)(即对于层来说除了NIL节点,红黑节点是交替的,第一层是黑节点那么其下一层肯定都是红节点,反之一样)
- 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点
有图有真相,上图:
那么红黑树为何如此优秀呢?
假设我们向树中插入20
可以看到,插入以后树已经不是一个平衡的二叉树,而且并不满足红黑树的要求,因为20和21均为红色,这种情况下就需要对红黑树进行变色,21需要变为黑色,22就会变成红色,如果22变成红色,则需要17和25都变成黑色
而17变成黑色显然是不成立的,因为如果17变为黑色,那么13就会变为红色,不满足二叉树的规则,因此此处需要进行另一个操作---------左旋操作
左旋:下图就是一个左旋的例子,一般情况下,如果左子树深度过深,那么便需要进行左旋操作以保证左右子树深度差变小
对于上图由于右子树中17变为黑色以后需要把13变成红色,因此进行一次左旋,将17放在根节点,这样既可保证13为红色,左旋后结果
而后根据红黑树的要求进行颜色的修改
进行左旋后,发现从根节点17,到1左子树的叶子节点经过了两个黑节点,而到6的左叶子节点或者右叶子节点要经历3个黑节点,很显然也不满足红黑树,因此还需要进行下一步操作,需要进行右旋操作
右旋:与左旋正好相反
由于是从13节点出现的不平衡,因此对13节点进行右旋,得到结果
而后再对其节点进行变色,得到结果
参考链接:
https://www.cnblogs.com/chengxiao/p/6059914.html