【Java底层原理】-> HashMap & HashTable & TreeMap 详细分析

Ⅰ 从面试出发

HashMap 现在也算是面试官非常爱考的一个东西了,针对 HashMap 可以考量的东西很多,比如牵扯到的几种数据结构(散列表,链表,红黑树),典型的应用场景,以及技术实现等等。尤其是在 Java 8 中,HashMap 发生了很大的变化,都是可以被考察的点。

这篇文章还是延续我上一篇的模式 <链接>,先从一个简单的面试题出发,然后从数据结构与算法以及力扣题的总结予以补充,最后再从源码进行分析。

Q: 请你说说 HashMap, HashTable, TreeMap 有什么不同?

下面的答案源引自极客时间,杨晓峰《Java核心技术面试精讲》。

Hashtable、HashMap、TreeMap 都是最常见的一些 Map 实现,是以键值对的形式存储和操作数据的容器类型。


Hashtable 是早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。


HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户 ID 和用户信息对应的运行时存储结构。


TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。

本篇文章着重在相关的数据结构和算法的补充以及 HashMap 的源码实现上,因为 HashMap 的源码可说的地方比较多,面试常考的也是这里,像 HashMap 的扩容,树化以及散列冲突的处理,都是很值得学习的。

Ⅱ Map 整体结构

在分析 HashMap 之前,我们先来看一看 Map 家族的整体结构。在上一篇 ArrayList & LinkedList 分析 中,我也提到了 Map 虽然通常被包括在 Java 集合框架里,但是它并不属于集合(Collection)。

在这里插入图片描述

Map 接口通常有四个常用的实现类,HashMapHashtableLinkedHashMapTreeMap,我们着重来看一下这四个类的特点。
在这里插入图片描述

  • HashMap
    1. HashMap 根据键值对(Key, Value)来存储数据,因此大部分时间都可以直接根据 Key 访问到对应的 Value,访问的时间复杂度是 O(1) 的。但是HashMap 的遍历访问顺序是不确定的。
    2. HashMap 允许键为 null(仅可以一个键为null),允许多条值为null
    3. HashMap 是线程不安全的,如果多个线程同时对它进行操作,可能会有数据不一致的问题。在需要线程安全的场景下,可以使用Collections类的synchronizedMap() 方法使得HashMap 具有线程安全能力,或者直接使用JUC的ConcurrentHashMap
  • LinkedHashMap
    1. LinkedHashMapHashMap的一个子类,是用双向链表 + 散列表实现的,因此保留了插入顺序,默认是按照插入的顺序进行遍历。
    2. LinkedHashMap可以通过改变参数实现 LRU cache 的效果,可以按照访问顺序(包括put,remove,get)排序。
  • TreeMap
    1. TreeMap的本质是BST,也就是平衡二叉树,是用红黑树实现的,因此也可以将键值有序排列。默认是按照键值升序排列的,可以通过对Comparator接口的实现自行排序。
    2. 在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
  • HashTable
    1. HashTable 是一个遗留类,继承自Dictionary类,所以是线程安全的。
    2. HashTable是同步的,由于ConcurrentHashMap引入了分段锁,所以并发性并不如ConcurrentHashMap
    3. 基本功能和HashMap类似。

HashMap 的性能表现非常依赖于哈希值的有效性,因此必须要遵循 hashcode 和 equals 的一些约定

  • 重写了 hashcode 必须重写 equals。
  • equals 相等, hashcode 必须相等。
  • hashcode 需要保持一致性,类的状态改变不能影响哈希值的一致。
  • equals 的自反性,传递性,对称性。

关于最后一点其实就是高中里集合的几个定义,大家如果不清楚可以去看这篇文章👉 equals 自反,对称,传递

Ⅲ 相关数据结构与算法

一、 数据结构

① 散列表

这里我们还是着重看一下 HashMap 就好了。

在这里插入图片描述
HashMap的结构大概如上图所示,这里用到了一个数据结构叫 散列表(hash table),也叫哈希表。这里不再赘述概念,对散列表不熟悉的同学请跳转去看下面的文章。

【数据结构与算法】->数据结构->散列表(上)->散列表的思想&散列冲突的解决

【数据结构与算法】->数据结构->散列表(中)->工业级散列表的设计

【数据结构与算法】->数据结构->散列表(下)->散列表和它的好朋友链表

要熟悉相关的概念,比如散列冲突、散列函数,只看第一篇就够了。在接下来讲解 HashMap 的时候我会讲到 HashMap 解决散列冲突(也叫哈希碰撞)的方法,如果你对这里也不熟悉,可以再把第二篇文章也看了。

② 链表

这个非常基础,我不再赘述,在上一篇文章中我分析到了 LinkedList,在其中对链表的相关知识做了详尽的讲解和链接,可以直接跳转过去 👉 <链表相关>

③ 红黑树

红黑树应该属于数据结构里最难的一种了,所以放心,一般不会有面试官会变态到让你手写红黑树出来,如果让你写了,可能就是变相的劝退吧。🐕

如果想对红黑树有个详细的了解的话,大家可以先去看看2-3树,红黑树其实就是对2-3树的一个再优化。这里我只完成一个概述,因为确实即使看了红黑树的源码(大概率看不懂),对面试的帮助也不会很大,毕竟面试官也不会。

首先要明确红黑树是平衡二叉树,而平衡二叉树是从 BST(二叉查找树)来的,关于二叉树以及二叉查找树不明确的同学可以去看我下面的文章👇

【数据结构与算法】->数据结构->树与二叉树

【数据结构与算法】->数据结构->二叉查找树

我们知道二叉查找树最根本的特性就是:在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。 这样我们就可以对链表进行 <二分查找> 了。

但是数据的插入是很不规律的,有可能会发生很多数据都插入到同一边的情况,那这时候 BST 的查找时间复杂度就不再是 O(logn) 了,而是退化成了一根链表,查找的时间复杂度变成了 O(n)。

这样平衡二叉树就诞生了,比如最经典的 AVL 树,就是为了防止一边数据过多,退化成链表,所以严格定义了平衡因子,当左右子树的高度相差大于 1 的时候,就要进行一个旋转操作,使得其平衡,也就是达到左右子树高度最多相差 1 的程度。

这个旋转操作就是平衡二叉树中最重要的操作之一,一共有四种旋转方式(左旋右旋左右旋右左旋)。这里我给出几个简单的图示。

在做旋转操作的时候,大家一定要想到BST的性质,左子树的节点都是小于它的根的,而右子树的结点都是大于它的。在旋转后这个性质也是绝对也是不能变的。

  1. 左旋

左旋发生在右右子树的情况。注意哦,这时候A的值一定是小于B的值,而B一定小于C的,这样才满足右子树的所有结点都大于根的性质(这种情况A是根,B自然要比A大,C要比B大)。

在这里插入图片描述
左旋后:
在这里插入图片描述
这样左旋完成之后,是不是还是满足左子树的结点(A)小于根(B),而根又小于右子树的结点(C)。整棵树还平衡了,左右子树的高度一致了。是不是很神奇?

  1. 右旋

右旋发生在左左子树的情况,
在这里插入图片描述
右旋后
在这里插入图片描述
是不是很简单?

现在我们看稍微复杂一点的情况。

  1. 左右旋

左右旋发生在左右子树的情况。

在这里插入图片描述
这种左右子树的情况,C是大于B的,而B是小于A的,我们要做的就是先进行一次左旋。
在这里插入图片描述
大家想想是不是没毛病,C是A的左子树里的一个结点,所以必然是小于A的,而它又是B的右子树,所以这样左旋以后还是满足BST的性质的。可以看到,这又变成了前面的左左子树的情况,那我们再进行一次右旋就好了。

在这里插入图片描述
4. 右左旋

同理可以知道,右左旋发生在右左子树的情况。
在这里插入图片描述
我们先进行右旋。

在这里插入图片描述
然后再进行左旋。
在这里插入图片描述

这时候就会有人想到,那如果要旋转的结点还带有子树怎么办呢?不怕,我们只要心里默念左小右大,对BST释放出足够的尊重,就可以想明白这个旋转方法。

  1. 带有子树的右旋

在这里插入图片描述
右旋需要将B的右子树挂在A的左子树上面。看看三角形里我标记的注释,应该可以想来吧?

右旋后
在这里插入图片描述
6. 带有子树的左旋

左旋就是右旋的逆操作嘛,我们直接把上面右旋后的结果拿下来。

在这里插入图片描述
刚才右旋是把中间的结点换了个父节点,接在了挪下来的A的左边,那我们要左旋,就还是把B结点拉下来,把刚才的中间这坨还是接到B的右边就好了呀。

左旋后:
在这里插入图片描述

有没有大呼精妙?这就是AVL树的平衡操作。

你不需要特别懂得AVL到底是怎么实现的,只要把这四个旋转搞清楚,就足以应付面试了。

说了这么多AVL,我们接着来看红黑树。

在看了上面的旋转操作,你有没有发现AVL的弊端?对,就是旋转的操作很消耗时间,一次还好,但是AVL的平衡因子要求如此之严格,几乎每次挪动一个结点,都要触发旋转操作。这样树的维护成本就会很高。

近似平衡二叉树应运而生,这就是红黑树。红黑树能够确保任何一个结点的左右子树的高度相差小于两倍。 这样维护二叉树的时间更少,总体上性能就会更好一点。

红黑树具体的原理是很麻烦的,同样如果大家要具体了解的话,建议先看看2-3树是什么。我这里只列出几条硬性质,在面试的时候了解上面AVL的旋转以及红黑树的这几条性质就已经足够了。

  1. 每个结点要么是红色,要么是黑色。(废话)
  2. 根结点是黑色。
  3. 每个叶子结点(都为null)是黑色。
  4. 不能有相邻的红色结点。
  5. 从任一结点到其每个叶子节点的所有路径都包含相同数量的黑色结点。

这五条性质使得红黑树可以保证

关键性质: 从根到叶子结点的最长路径不多于最短的可能路径的两倍长

因此红黑树也是可以近乎稳定地达到 O(logn) 的时间复杂度,并且比AVL树的维护成本要小。

二、算法

关于HashMap的题其实都不难,这里我列几个比较经典的,大家可以多去练练。

首先就是力扣的第一题 #1 两数之和

在这里插入图片描述

第一题自然是很简单的,就是求一个数组中哪两个数字之和等于给定的 target。

当然,用两个 for 循环就可以搞定,不过时间复杂度是 O(n2) 的。那怎么用 O(n) 解决这道题呢?答案就是 HashMap。在上面我们说过了它的性质就是数据是通过键值对来存储的,并且键是唯一的,那我们就可以把遍历过的数字都放到 HashMap中去,在访问前先看 HashMap中有 target-curNum,如果有,那就已经找到了,取出来就可以,那个就是第一个数。

在这里插入图片描述
第二道题就是 #146 LRU cache

这道题我在讲解链表时讲过,要用一个双向链表 + Map 实现。第二种方法就是用我们前面提到的 LinkedHashMap,因为它就是一个双向链表 + 散列表的组合,正好对应了这道题的要求,并且我们也说了通过传入的参数可以让 LinkedHashMap 实现 LRU Cache。我直接将代码贴出。

class LRUCache extends LinkedHashMap<Integer, Integer> {
   
    private int capacity;

    public LRUCache(int capacity) {
   
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
   
        return super.getOrDefault(key, -1)
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值