JavaDS —— 二叉搜索树、哈希表、Map 与 Set

前言

我们将学习 Map 与 Set 这两个接口下的 TreeMap 与 TreeSet ,HashMap 与 HashSet ,在学习这四个类使用之前,我们需要先学习 二叉搜索树与 哈希表的知识。

二叉搜索树

在学习二叉树的时候,我们就已经了解过二叉搜索树的概念与性质,性质我们来回顾以下:

二叉搜索树(Binary Search Tree),也称为二叉查找树或二叉排序树,是一种特殊的二叉树。它的定义基于以下性质:

若它的左子树不空,则左子树上所有节点的值都小于根节点的值。
若它的右子树不空,则右子树上所有节点的值都大于根节点的值。
它的左、右子树也分别为二叉搜索树。
此外,二叉搜索树的一个重要特性是它的中序遍历结果一定是有序的。这意味着在二叉搜索树中,如果按照中序遍历的方式访问所有节点,将得到一个有序的节点值序列。‌

在这里插入图片描述

二叉搜索树的这些性质使得它在数据检索、排序等算法中具有高效性,尤其是在需要频繁查找、插入或删除数据的场景中,二叉搜索树的操作效率通常优于其他数据结构

现在我们来模拟实现二叉搜索树,先创建好类:

public class BinarySearchTree {static class TreeNode {int val;TreeNode left;TreeNode right;public TreeNode(int val) {this.val = val;}}public TreeNode root;
}

搜索

搜索可以类比成二分查找,当根节点数值大于要查找的数值,就去左子树继续查找,当根节点的数值小于要查找的数值,就去右子树继续查找。

    public TreeNode search(int key) {TreeNode cur = root;while(cur != null) {if(cur.val == key) {return cur;} else if(cur.val > key) {cur = cur.left;} else {cur = cur.right;}}return null;}

当二叉搜索树是一颗均匀分布的完全二叉树时,搜索的时间复杂度最佳,为O(logN)
当二叉搜索树为一颗单分支的树,此时搜索效率最差,为O(N)

在这里插入图片描述

插入

插入和搜索类似。

这里要注意的是,如果插入的数值已经存在于二叉树中,就不能继续插入,也就是说二叉搜索树只能保存一份数据,无法保存两份及以上的相同的数据。

    public void insert(int key) {TreeNode node = new TreeNode(key);if(root == null) {root = node;return;}TreeNode cur = root;TreeNode prev = null;while(cur != null) {if(cur.val == key) {return;} else if(cur.val > key) {prev = cur;cur = cur.left;} else {prev = cur;cur = cur.right;}}if(key > prev.val) {prev.right = node;} else {prev.left = node;}}

删除

先找到要删除的结点:我们既要得到删除的结点还要直到删除的结点的双亲结点,这样才可以进行下一步的删除。

    public void remove(int key) {TreeNode prev = null;TreeNode cur = root;while(cur != null) {if(cur.val > key) {prev= cur;cur = cur.left;} else if(cur.val < key) {prev= cur;cur = cur.right;} else {removeNode(prev,cur);return;}}}

分情况讨论:del 左子树为空,del 右子树为空,del 左右子树均不为空。
在这里插入图片描述

当del 左子树为空:del 有两种情况,一个是在 parent 的左边,另一种是在 parent 的右边,根据不同的情况我们直接将del 的右子树直接连接到 parent 左/右 上
这里要注意如果 del 是根节点的话,我们的 root 就要改变

        if(del.left == null) {if(del == root) {root = del.right;} else if(parent.left == del) {parent.left = del.right;} else {parent.right = del.right;}}

当del 右子树为空:del 有两种情况,一个是在 parent 的左边,另一种是在 parent 的右边,根据不同的情况我们直接将del 的左子树直接连接到 parent 左/右 上
这里要注意还有第三种情况如果 del 是根节点的话,我们的 root 就要改变。

        else if(del.right == null) {if(del == root) {root = del.left;} else if(parent.left == del) {parent.left = del.left;} else {parent.right = del.left;}}

第三种情况: del 的左右子树均不为空

在这种情况下,直接去删除这个结点是一件很麻烦的事情,如果采用直接删除,我们需要重新在其子树找到合适的结点,然后将其变为新的双亲结点 ,并且要连接好两颗子树。

因此为了方便,我们采用替换删除法,替换删除法是找到一个合适的结点,将这个结点的数值直接覆盖到你要删除的结点的数值域里,这样就变相删除了这个结点,然后再将这个合适的结点删除掉。

什么是合适的结点?
由于这是一颗二叉搜索树,所以该树的根节点大于左子树,小于右子树,所以合适的结点就要满足这两个条件,比左子树的大,比右子树的小
这个合适的结点是从要删除的结点的左子树或者右子树出发寻找,根据二叉搜索树的性质,根节点的左子树任意结点都比根节点的右子树任意结点小,那么我们只要找到左子树最大的结点的数值将其覆盖到要删除结点的数值。而左子树的最大结点为左子树最右边的结点
如果是从右子树出发,那我们就要寻找到右子树的最小结点,也就是右子树的最左边的结点

在这里插入图片描述

这里我以找寻右子树最左边的结点为例子
这里要注意如果 while 循环没有进去,也就是 prev = del,cur = del.right;那么 prev 的右边是否为 cur
如果循环进去了,那结果就是 prev 的左边为 cur
所以最后删除的时候要分类讨论一下。
在这里插入图片描述

		else {TreeNode prev = del;TreeNode cur = del.right;while(cur.left != null) {prev = cur;cur = cur.left;}del.val = cur.val;if(prev.right == cur) {prev.right = cur.right;} else {prev.left = cur.right;}}

removeNode 最终代码:

    private void removeNode(TreeNode parent,TreeNode del) {if(del.left == null) {if(del == root) {root = del.right;} else if(parent.left == del) {parent.left = del.right;} else {parent.right = del.right;}} else if(del.right == null) {if(del == root) {root = del.left;} else if(parent.left == del) {parent.left = del.left;} else {parent.right = del.left;}} else {TreeNode prev = del;TreeNode cur = del.right;while(cur.left != null) {prev = cur;cur = cur.left;}del.val = cur.val;if(prev.right == cur) {prev.right = cur.right;} else {prev.left = cur.right;}}}

Map 与 Set 的简单介绍

Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。

回顾之前我们学习到的搜索,假设有一组数据,需要查找某一个数据的时候,我们会考虑到两种方法,一种是直接遍历,时间复杂度为O(N) ,另一种是二分查找,时间复杂度为O(logN),二分查找的前提是数据必须是有序的。

上面两种查找方式比较适合静态类型的查找即一般不会对数据有插入和删除的操作

但是在现实中,我们的查找有:
1.根据姓名查找个人信息
2.找到不重复的集合,即筛选掉重复的数据,只保留一份数据。

这些查找可能会伴随一些插入和删除的操作,这就是动态查找,那么我们就不适合使用上面两种静态查找的方式了,这时候Java 给我们提供了两个容器 Map 和 Set 两个适合动态查找的容器。

两个模型

一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:

1.纯 key 模型
比如:有一个英文词典,快速查找一个单词是否在词典中
快速查找某个名字在不在通讯录中

2.Key-Value 模型
比如:统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
Map中存储的就是key-value的键值对,Set中只存储了Key

哈希表

概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:
插入元素:
根据待插入元素的关键码以此函数计算出该元素的存储位置并按此位置进行存放搜索元素

搜索元素:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置在结构中按此位置取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)

举个例子:现在有一组数据 { 1,5,9,2,4,7}
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
存储空间大小为 10

在这里插入图片描述
在存储的时候,我们按照哈希函数来确定数据的具体位置,在搜索的时候,将要搜索的元素通过哈希函数计算出具体的下标,然后直接去该处拿去元素。

可见哈希表的存储和搜索的时间复杂度为 O(1)

冲突

概念

对于两个数据元素的关键字 a 和 b ,在进行插入的时候,通过哈希函数的计算,发现Hash(a) 等于Hash(b) ,即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率

避免冲突 —— 哈希函数设计

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则:
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单

常见哈希函数设方法:
1.直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

当我们事先直到数据的分布情况,我们可以朝着线性分布的方向继续设计哈希函数,也就是一次函数Hash(Key)= A*Key + B,让数据在哈希表中分布均匀。

2.除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

3.平方取中法
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4.折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5.随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。

通常应用于关键字长度不等时采用此法

6.数学分析法
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

负载因子调节(重点)

哈希表的负载因子定义为 a = 填入表中的元素个数 / 散列表的长度

现在我们来看一下负载因子和冲突率的关系图:

在这里插入图片描述

可以得出一个结论:当负载因子越大,冲突率就会越高,而负载因子的大小又与填入表中元素的个数成正比。

对于开放定址法,负载因子是一个很关键的因素,应该严格限制在 0.7 - 0.8以下。超过0.8,查表是的CPU 缓存不命中(cache missing) 按照指数曲线上升。
在 Java 的 hash 库中,负载因子被定义在 0.75,一旦超过这个数值,就会对哈希表进行扩容

解决冲突

解决哈希冲突两种常见的方法是:闭散列和开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

  1. 线性探测
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
    通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

举个例子:在上面我们已经插入了一组数据 { 1,5,9,2,4,7}
在这里插入图片描述
现在又要插入一个数据,这个数据是 17,由于 hash(17) = 17 & 10 = 7,而 7 下标已经插入了数据,这时候会发生哈希冲突,使用线性探测,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止,发现 8 下标的位置为空,那么我们直接将 17 插入到 8 小标里面。

在这里插入图片描述
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他
元素的搜索。比如删除元素7,如果直接删除掉,17查找起来可能会受影响,因为 17 的哈希值为 7 ,但是 7 下标没有数据,那计算机到底是认为存在17 还是不存在 17 呢?因此线性探测采用标记的伪删除法来删除一个元素。

线性探测的缺陷是产生冲突的数据堆积在一块,这与其 找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找

  1. 二次探测
    因此二次探测为了避免线性探测的问题,找下一个空位置的方法为: H = ( H0 + i ^2 ) % m 或者 H = ( H0 - i ^2 ) % m,其中 i = 1、2、3…,H0 是通过哈希函数 Hash(x) 对元素的关键码 key 进行计算得到的位置,m 为表的长度。

举个例子,还是插入17 这个元素,使用二次探测法插入:

在这里插入图片描述

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。

因此,闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

开散列/哈希桶(重点)

开散列法又叫链地址法(开链法),也就是数组加链表的形式,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。

那么在上面讨论 17 应该插入在哪个位置,如果使用链地址法,应该放在 7 的后面,如下图:

在这里插入图片描述

冲突严重时的解决办法

刚才我们提到了,哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:

1.每个桶的背后是另一个哈希表
2.每个桶的背后是一棵搜索树

模拟实现哈希表

由于Java 自己的hash 库使用的是 链地址法,所以这里我使用数组加单链表的方法来模拟哈希表,并且负载因子和 Java 的 0.75 设置一致,哈希函数我设置为 hash(x) = key % 数组长度
并且插入结点采用头插法
模拟 key - val 模型

public class Hash {static class Node {int key;int val;Node next;public Node(int key, int val) {this.key = key;this.val = val;}}public Node[] elem = new Node[10];private int useSize;private static final double DEFAULT_LOAD_FACTOR = 0.75;
}

扩容

当负载量超过负载因子的时候,我们就要扩容,我们先写一个方法用于检测目前的负载量:

    //检测当前的负载量private double checkCurrentLoad() {return useSize * 1.0 / elem.length;}

然后我们来写扩容方法,注意哈希表不是简单的扩容,扩容意味着原本哈希表存放的数据就要进行移动,使之与新表匹配,所以我们先创建一个新数组,然后遍历旧哈希表,将所有结点移动到新哈希表上,最后改变 elem 的引用,就是实现了哈希表的扩容。

    private void resize() {Node[] newArray = new Node[elem.length * 2];for (int i = 0; i < elem.length; i++) {Node cur = elem[i];while(cur != null) {Node curN = cur.next;int index = cur.key % newArray.length;cur.next = newArray[index];newArray[index] = cur;cur = curN;}}elem = newArray;}

插入

我们先插入数据,这里要注意哈希表不能存放两份及以上的相同的 key ,如果有,则会更新 val 值
这里采用头插法
等到插入完成后,要判断此时是否超出负载因子的设置范围,如果超过了,则要进行扩容。

    //插入数据public void put(int key,int val) {//是否已经存在 key ,存在则更新 val 值int index = key % elem.length;Node cur = elem[index];while(cur != null) {if(cur.key == key) {cur.val = val;return;}cur = cur.next;}//头插法Node node = new Node(key, val);node.next = elem[index];elem[index] = node;useSize++;//检测是否超过负载因子if(checkCurrentLoad() >= DEFAULT_LOAD_FACTOR) {resize();}}

获取 val

直接通过哈希函数获取 key 所在下标,然后遍历这个下标的链表,获取 val 值

    //获取 val 值public int get(int key) {int index = key % elem.length;Node cur = elem[index];while(cur != null) {if(cur.key == key) {return cur.val;}cur = cur.next;}return -1;}

泛型类哈希表模拟

在获取key 位置的时候,我们不能直接直接通过 key % 数组长度 来获取,因为 key 此时是 泛型,但是我们可以通过 hashCode() 方法来得知它的哈希值,哈希值是整型,所以我们可以利用哈希值来算 key 的下标

在比较两个泛型结点的 key 是否相同的时候,我们不能直接使用 == ,而是要使用 equals 来比较两个泛型的内容相不相同。

所以我们在使用 HashMap 或者 HashSet 的时候,如果插入的是自定义类型,那这个自定义类型就啊哟重写好 hashCode() 和 equals 两个方法!!!

public class hash2<K,V> {static class Node<K,V> {K key;V val;Node<K,V> next;public Node(K key, V val) {this.key = key;this.val = val;}}private Node<K,V>[] elem = new Node[10];private int useSize;private static final double DEFAULT_LOAD_FACTOR = 0.75;//插入数据public void put(K key,V val) {//是否已经存在 key ,存在则更新 val 值int index = key.hashCode() % elem.length;Node<K,V> cur = elem[index];while(cur != null) {if(cur.key.equals(key)) {cur.val = val;return;}cur = cur.next;}//头插法Node<K,V> node = new Node<>(key,val);node.next = elem[index];elem[index] = node;useSize++;//检测是否超过负载因子if(checkCurrentLoad() >= DEFAULT_LOAD_FACTOR) {resize();}}private void resize() {Node<K,V>[] newArray = new Node[elem.length * 2];for (int i = 0; i < elem.length; i++) {Node<K,V> cur = elem[i];while(cur != null) {Node<K,V> curN = cur.next;int index = cur.key.hashCode() % newArray.length;cur.next = newArray[index];newArray[index] = cur;cur = curN;}}elem = newArray;}//检测当前的负载量private double checkCurrentLoad() {return useSize * 1.0 / elem.length;}//获取 val 值public V get(K key) {int index = key.hashCode() % elem.length;Node<K,V> cur = elem[index];while(cur != null) {if(cur.key.equals(key)) {return cur.val;}cur = cur.next;}return null;}
}

虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1) 。

Map 与 Set 的使用

在上面,大家已经了解到 二叉搜索树和哈希表,现在我们来学习 Map 与 Set 的使用。

在讲解之前,我们先讨论一个问题,二叉搜索树的搜索的时间复杂度有好有坏,我们该如何将二叉搜索树的性能变成最好的?当二叉搜索树是一颗完全二叉树的时候,性能是最佳的,那如何保持为完全二叉树,这时候就需要将二叉搜索树调整为平衡树,如何调整?答案就是使用红黑树这个数据结构维持,它能让二叉树保持为一个平衡树,也就是无论是插入还是删除,红黑树始终能让二叉树保持为一个平衡树。

在Java 中提供的哈希表是使用链地址法,也就是数组加链表,还有加红黑树,即哈希表等于 数组 + 链表 + 红黑树Java 的哈希表一旦数组长度超过 64 并且链表长度超过 8 ,就会通过红黑树将链表进行树化调整为红黑树,保持哈希表优秀的性能

平衡树与红黑树会在后序文章中讲解,大家尽情期待~~

下面我们来看一下 Map 与 Set 两个接口的情况:
在这里插入图片描述

Map 的使用

Map是一个接口类该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复


注意事项:

1.Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap

2.Map中存放键值对的Key是唯一的,value是可以重复的

3.在TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常,并且key 必须是可比较的,value可以为空。但是HashMap的key和value都可以为空

4.Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。

5.Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。

6.Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。


TreeMap 与 HashMap

Map底层结构TreeMapHashMap
底层结构红黑树哈希桶
插入/删除/查找的时间复杂度O(logN)O(1)
存储的元素是否有序关于key 有序无序
线程安全不安全不安全
插入/删除/查找的区别需要进行元素的比较通过哈希函数计算哈希地址
比较与覆写key 必须能够比较,否则就会抛NullPointerException异常自定义类型需要重写 equals 和 hashCode 方法
应用场景要求key有序不关心key 有无序,重点关注的是时间性能

Map 常用方法

方法解释
V get(Object key)返回 key 对应的 value
V getOrDefault(Object key, V defaultValue)返回 key 对应的 value,如果 key 不存在,则返回默认值
V put(K key, V value)设置 key 对应的 value
V remove(Object key)删除 key 对应的映射关系
Set< K> keySet()返回 key 的不重复集合
Colletion< V> values()返回所有 value 的可重复集合
Set<Map.Entry<K,V>> entrySet()返回所有的 key-value 映射关系
boolean containsKey(Object key)判断是否包含 key
boolean containsValue(Object value)判断是否包含 value

代码演示:

    public static void main(String[] args) {HashMap<String,Integer> map = new HashMap<>();map.put("abcd",123);map.put("hello",64);map.put("world",256);map.put("error",1024);System.out.println("size: " + map.size());System.out.println("hello: " + map.get("hello"));System.out.println(map.containsKey("ab"));System.out.println(map.containsValue(256));}

在这里插入图片描述


    public static void main(String[] args) {HashMap<String,Integer> map = new HashMap<>();map.put("abcd",123);map.put("hello",64);map.put("world",256);map.put("error",1024);map.put("null",1024);System.out.println(map.entrySet());map.remove("abcd");System.out.println("======================");System.out.println(map.entrySet());}

在这里插入图片描述


    public static void main(String[] args) {HashMap<String,Integer> map = new HashMap<>();map.put("abcd",123);map.put("hello",64);map.put("world",256);map.put("error",1024);map.put("null",1024);Set<Map.Entry<String,Integer>> set = map.entrySet();for(Map.Entry<String,Integer> x: set) {System.out.println("key: " + x.getKey() + "val: " + x.getValue());}}

keySet 方法可以返回所有 key 的不重复合集,可以使用 Set 来接收
如果想接收所有的 values 包括重复的 values 的合集,可以使用 Collection 来接收

在这里插入图片描述


Map.Entry<K,V>

Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式。

方法解释
K getKey()返回 entry 中的 key
V getValue()返回 entry 中的 value
V setValue(V value)将键值对的 value替换为指定value
    public static void main(String[] args) {HashMap<String,Integer> map = new HashMap<>();map.put("abcd",123);map.put("hello",64);map.put("world",256);map.put("error",1024);map.put("null",1024);Set<Map.Entry<String,Integer>> set = map.entrySet();for(Map.Entry<String,Integer> x: set) {x.setValue(10256);}System.out.println("======================");System.out.println("检测将每个键值对的 value 设置为 10256是否成功");for(Map.Entry<String,Integer> x: set) {System.out.println("key: " + x.getKey() + "   val: " + x.getValue());}}

在这里插入图片描述

Set 的使用

Set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了Key

注意事项:
1.Set是继承自Collection的一个接口类

2.Set中只存储了key,并且要求key一定要唯一

3.TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的

4.Set最大的功能就是对集合中的元素进行去重

5.实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。

6.Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入

7.TreeSet中不能插入null的key,HashSet可以


TreeSet 与 HashSet

Set底层结构TreeSetHashSet
底层结构红黑树哈希桶
插入/删除/查找的时间复杂度O(logN)O(1)
存储的元素是否有序关于key 有序无序
线程安全不安全不安全
插入/删除/查找的区别需要进行元素的比较通过哈希函数计算哈希地址
比较与覆写key 必须能够比较,否则就会抛NullPointerException异常自定义类型需要重写 equals 和 hashCode 方法
应用场景要求key有序不关心key 有无序,重点关注的是时间性能

Set 常用方法

方法解释
boolean add(E e)添加元素,如果添加的是重复元素就不会添加成功
void clear()清空集合
boolean contains(Object o)判断 o 是否在集合中
Iterator< E> iterator()返回迭代器
boolean remove(Object o)删除集合中的 o
int size()返回 set 中的元素个数
boolean isEmpty()检测 set 是否为空,空则返回true,否则返回false
Object[] toArray()将 set 中的元素转化为数组返回
boolean containsAll(Collection< ?> c)集合 c 中的元素是否在 set 中全部存在,是则返回true, 否则返回false
boolean addAll(Collection<? extends E> c)将集合 c 中的元素添加到 set 中,可以达到去重的效果。

代码演示:

    public static void main(String[] args) {TreeSet<Integer> treeSet = new TreeSet<>();treeSet.add(10);treeSet.add(100);treeSet.add(85);treeSet.add(32);System.out.println(treeSet.contains(100));Iterator<Integer> it = treeSet.iterator();while(it.hasNext()){System.out.print(it.next() + " ");}System.out.println();System.out.println("======演示删除======");treeSet.remove(100);it = treeSet.iterator();while(it.hasNext()){System.out.print(it.next() + " ");}System.out.println();System.out.println(treeSet.size());System.out.println(treeSet.isEmpty());Integer[] arr = (Integer[])treeSet.toArray(new Integer[0]);for (int x: arr) {System.out.print(x+"  ");}}

在这里插入图片描述

Java 的 HashMap 部分源码解析

在这里插入图片描述

构造方法

在这里插入图片描述

将默认 初始容量为 16,将负载因子设置为 默认值 0.75


在这里插入图片描述

调用了另一个构造方法来设置初始容量,但是负载因子还是设置为默认值 0.75


在这里插入图片描述
可以设置初始容量以及负载因子。

        if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;

当默认容量小于 0 会抛异常,当初始容量 大于 最大的默认容量也会抛异常。

        if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);

当负载因子小于 0 或者负载因子不是数字 就会抛异常

下面是 isNaN 的源码:

在这里插入图片描述

现在我们来看一下 this.threshold = tableSizeFor(initialCapacity);是如何设置初始容量的:

在这里插入图片描述

返回给定目标容量的2次幂。
举个例子,如果你设置的初始容量为 10,经过一顿操作,实际设置的初始容量为 2 ^ 4 = 16


put

下面是 哈希表 的数组:

在这里插入图片描述

但是在此之前我们都没有看到一个数组的创建,实际上 数组的创建会在 put 方法里体现

在这里插入图片描述
put 方法传入了 hash(key):

在这里插入图片描述

通过 (h = key.hashCode()) ^ (h >>> 16) 会使 Key 分布的更均匀


put 方法 调用了 putVal 方法:

在这里插入图片描述

if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;

当数组为 null 或者 数组的长度为 0 就会扩容。

        if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);

当 key 的哈希值 所在的存储位置为空时,直接插入结点。


走到这里的时候,说明这个哈希值对应的数组的位置已经被插入了

Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;

当哈希值是相同 并且 一个已经被插入的结点的 key 和待插入的结点的 key 的地址是相同,又或者 key 不是空并且 两个的 key 的内容是相同的话,就将待插入的结点赋值给 e 。


如果不满足上面的条件,就说明这个结点是一个要插入到 链表或者红黑树的结点,因此下面中的p instanceof TreeNode 会判断此时数组存储的是 链表还是红黑树,如果是红黑树, e 就是一个红黑树的结点,并通过 putTreeVal() 方法来将 结点插入到红黑树中。

else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

走到这里,说明需要需要在链表里插入新结点。

下面就是链表的插入代码:

else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}
}

要注意其中这一行代码:

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);

这行代码是判断是否需要将链表调整为红黑树。

put 源码分析到这里,感兴趣可以继续往下分析

get

在这里插入图片描述
调用了 getNode() 方法:

在这里插入图片描述

if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & (hash = hash(key))]) != null) 

先判断这个 key 是否存在,从数组不为空并且数组长度不为 0 并且 key 的哈希值所在的 数组位置不为空,才会继续寻找,否则直接返回 null


if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;

检查是不是第一个结点,是的话直接返回,不是的话,说明在第一个结点后面的结点,需要继续寻找。


if ((e = first.next) != null) {if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}

当第一个结点后面存在下一个结点才继续寻找,first instanceof TreeNode 是 true 说明第一个结点是树结点,说明数组连接的是红黑树,那么就会通过红黑树的搜索方法getTreeNode 来进行搜索,如果不是则是以链表的形式进行搜索。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/876822.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

酒店智能门锁接口pro[0922]D801 对接收银-SAAS本地化-未来之窗行业应用跨平台架构

proUSB接口函数[0922中性版]-D801 调用函数库&#xff1a; 提供Windows下的32位动态连接库proRFL.DLL&#xff0c;函数使用详细说明 //-----------------------------------------------------------------------------------// 功能&#xff1a;读DLL版本&#xff0c;不涉…

【Linux C | 网络编程】进程池退出的实现详解(五)

上一篇中讲解了在进程池文件传输的过程如何实现零拷贝&#xff0c;具体的方法包括使用mmap&#xff0c;sendfile&#xff0c;splice等等。 【Linux C | 网络编程】进程池零拷贝传输的实现详解&#xff08;四&#xff09; 这篇内容主要讲解进程池如何退出。 1.进程池的简单退…

Java并发编程(上)

并发&#xff1a;多个线程&#xff08;进程&#xff09;竞争一个资源 并行&#xff1a;多个线程&#xff08;进程&#xff09;同时运行不同资源 线程和进程的关系简单地说&#xff0c;进程是一个容器&#xff0c;一个进程中可以容纳若干个线程&#xff0c;一个进程里面&#…

微信小程序入门

创建一个入门程序 这是index.vxml代码 <!--index.wxml--> <navigation-bar title"Weixin" back"{{false}}" color"black" background"#FFF"></navigation-bar> <view class"container" ><view&…

苹果CMS:资源采集站如何设置定时采集详细教程讲解

我们搭建好站点之后&#xff0c;会自定义一些采集&#xff0c;但是需要每天去手动执行&#xff0c;有时候甚至会忘记&#xff0c;那我们如何处理呢&#xff1f;今天我们就来介绍一下如何设置定时器。 如果按照官方例子来设置定时器会遇到一个问题就是采集的资源未绑定类型&…

WAF+API安全代表厂商|瑞数信息入选IDC报告《生成式AI推动下的中国网络安全硬件市场现状及技术发展趋势》

近日&#xff0c;全球领先的权威资讯机构IDC正式发布《IDC Market Presentation&#xff1a;生成式AI推动下的中国网络安全硬件市场现状及技术发展趋势&#xff0c;2024》报告。报告中IDC 评估了众多厂商的安全硬件产品能力&#xff0c;并给出了产品对应的推荐厂商供最终用户参…

04 | 深入浅出索引(上)

此系列文章为极客时间课程《MySQL 实战 45 讲》的学习笔记&#xff01; 索引的常见模型 可以提供查询效率的数据结构有很多&#xff0c;常见的有三种&#xff1a;哈希表、有序数组、搜索数。 哈希表是一种以 key-value 形式存储的数据结构。输入一个 key&#xff0c;通过固定…

强烈推荐java人,2024年大厂面试背这份(八股文+场景题结合)!很管用!

2024 年的行情&#xff0c;和 3~4 年前不同&#xff0c;通过海量简历投递和海量面试找工作的时代已经过去了。 在如今面试机会较少&#xff0c;并且面试难度较大的情况下。 充分做好面试的准备才是快速通过面试最有效的方法&#xff01; 切忌把真实面试当靶场&#xff0c;最…

信息学奥赛初赛天天练-48-CSP-J2020完善程序2-变量交换、冒泡排序、贪心算法、最小区间覆盖

PDF文档公众号回复关键字:20240728 2020 CSP-J 完善程序2 1 完善程序 (单选题 &#xff0c;每小题3分&#xff0c;共30分) 最小区间覆盖 给出 n 个区间&#xff0c;第 i 个区间的左右端点是 [ai,bi]。现在要在这些区间中选出若干个&#xff0c;使得区间 [0, m] 被所选区间的…

前端框架 element-plus 发布 2.7.8

更新日志 功能 组件 [级联选择器 (cascader)] 添加持久化属性以提升性能 (#17526 by 0song)[日期选择器 (date-picker)] 类型添加月份参数 (#17342 by Panzer-Jack)[级联选择器 (cascader)] 添加标签效果属性 (#17443 by ntnyq)[加载 (loading)] 补充加载属性 (#17174 by zhixi…

第九讲 后端1

后端&#xff08;Backend&#xff09; 用带噪声的数据估计内在状态&#xff08;Estimated the inner state from noisy data&#xff09;——状态估计问题渐进式&#xff08;Incremental&#xff09;&#xff1a;保持当前状态的估计&#xff0c;在假如新信息时&#xff0c;更新…

【算法专题】双指针算法之18. 四数之和(力扣)

欢迎来到 CILMY23的博客 &#x1f3c6;本篇主题为&#xff1a;双指针算法之18. 四数之和&#xff08;力扣&#xff09; &#x1f3c6;个人主页&#xff1a;CILMY23-CSDN博客 &#x1f3c6;系列专栏&#xff1a;Python | C | C语言 | 数据结构与算法 | 贪心算法 | Linux | 算…

ProxmoxPVE虚拟化平台--U盘挂载、硬盘直通

界面说明 ### 网络设置 ISO镜像文件 虚拟机中使用到的磁盘 挂载USB设备 这个操作比较简单&#xff0c;不涉及命令 选中需要到的虚拟机&#xff0c;然后选择&#xff1a; 添加->USB设置选择使用USB端口&#xff1a;选择对应的U盘即可 硬盘直通 通常情况下我们需要将原有…

【Linux 16】进程间通信的方式 - 共享内存

文章目录 &#x1f308; 一、共享内存概述⭐ 1. 什么是共享内存⭐ 2. 如何实现共享内存⭐ 3. 操作系统允许存在多个共享内存⭐ 4. 操作系统如何管理共享内存⭐ 5. 获取共享内存的唯一标识符 key⭐ 6. 为什么要由用户提供 key &#x1f308; 二、查看共享内存⭐ 1. 使用 ipcs -m…

TCP 协议的 time_wait 超时时间

优质博文&#xff1a;IT-BLOG-CN 灵感来源 Time_Wait 产生的时机 TCP四次挥手的流程 如上所知&#xff1a;客户端在收到服务端第三次FIN挥手后&#xff0c;就会进入TIME_WAIT状态&#xff0c;开启时长为2MSL的定时器。 【1】MSL是Maximum Segment Lifetime报文最大生存时间…

root 用户和权限

目录 1. 超级管理员 root 2. 切换用户 Switch User 2.1 普通用户切换到 root 用户 2.2 root 用户切换到普通用户 3. sudo 命令 3.1 配置认证 无论是 Windows&#xff0c;MacOS&#xff0c;Linux 均采用多用户的管理模式管理权限&#xff1b; 1. 超级管理员 root 在 Li…

2年社招冲击字节,一天三面斩获offer

在工作满两年的时间选择了求变&#xff0c;带着运气和实力以社招身份重新看今天的互联网环境&#xff0c;从结果看还是复合预期的。 整个面试的流程还挺快的。周中让招聘专员给投递了简历。问什么时候面试&#xff0c;申请了一个周日&#xff0c;直接安排三面。下周周中就开启…

C#中的wpf基础

在WPF中&#xff0c;Grid 是一种非常强大的布局控件&#xff0c;用于创建网格布局。它允许你将界面划分为行和列&#xff0c;并将控件放置在这些行和列中。 以下是一些关键点和示例&#xff0c;帮助你理解 WPF 中的 Grid&#xff1a; 基本属性 RowDefinitions&#xff1a;定义…

[MIT6.5840]MapReduce

MapReduce Lab 地址 https://pdos.csail.mit.edu/6.824/labs/lab-mr.html 论文地址 https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/mapreduce-osdi04.pdf 工作原理 简单来讲&#xff0c;MapReduce是一种分布式框架&#xff0c;可以用来处理…

windows 安装docker桌面版

下载 下载两个&#xff1a; git桌面版 docker desktop 启动docker 执行安装文件&#xff0c;启动 更新wsl2 假如报错&#xff0c;会提示失败原因。 win10会提示跳转到&#xff1a; https://learn.microsoft.com/zh-cn/windows/wsl/install-manual#step-4—download-the-l…