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

news2024/11/18 17:50:04

前言

我们将学习 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 的使用。

在讲解之前,我们先讨论一个问题,二叉搜索树的搜索的时间复杂度有好有坏,我们该如何将二叉搜索树的性能变成最好的?当二叉搜索树是一颗完全二叉树的时候,性能是最佳的,那如何保持为完全二叉树,这时候就需要将二叉搜索树先固定成 AVL 树(平衡树,高度差 小于等于 1),如何固定?答案就是使用红黑树这个数据结构维持,它能让二叉树每时每刻都是一个平衡树,也就是无论是插入还是删除,只要而二叉搜索树不为平衡树,红黑树会自动将其调整为平衡树。

在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 1st
                            treeifyBin(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 1st
            treeifyBin(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.coloradmin.cn/o/1955519.html

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

相关文章

ctfshow-web入门-php特性(web142-web146)

目录 1、web142 2、web143 3、web144 4、web145 5、web146 1、web142 要求 v1 是数字&#xff0c;之后将 v1乘以 0x36d&#xff08;即16进制的869&#xff09;五次&#xff0c;然后将结果转换为整数并赋值给变量 $d&#xff0c;使用 sleep 函数使程序休眠 $d 秒&#xff0c…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 项目排期安排(200分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题目在线…

CentOS7 yum报错Cannot find a valid baseurl for repo

问题 Loaded plugins: fastestmirror Determining fastest mirrors Could not retrieve mirrorlist http://mirrorlist.centos.org/?release7&archx86_64&repoos&infravag error was 14: curl#6 - "Could not resolve host: mirrorlist.centos.org; Unknown…

基于多种机器学习算法的短信垃圾分类模型

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主导入第三方库读取数据数据预处理数据分析与可视化机器学习建模贝叶斯逻辑回归支持向量机随机森林XGBoost总结每文一语 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私…

阅读笔记2:董超底层视觉之美|底层视觉是什么?

原文链接&#xff1a;https://mp.weixin.qq.com/s/9EQNwXqCM6odwe5n9dOrmw 本文针对底层视觉给出了一个大致的定义&#xff0c;然后通过底层视觉与人工智能、计算机视觉、图像处理等相关概念的对比来对底层视觉做了进一步的界定。 1. 底层视觉是什么 以像素级图像为输入、处…

利用小爱同学与点灯科技+esp8266+舵机,制作智能关灯神器:小白也可制作,米家同步设备可实现多部手机进行控制。(亲测有用)

利用小爱同学与点灯科技&#xff0c;制作智能关灯神器&#xff1a;小白也可制作&#xff0c;米家同步设备可实现多部手机进行控制。 文章目录 利用小爱同学与点灯科技&#xff0c;制作智能关灯神器&#xff1a;小白也可制作&#xff0c;米家同步设备可实现多部手机进行控制。1.…

vue3 快速入门 (六) : vue中调用HTTP请求

1. 本文环境 Vue版本 : 3.4.29Node.js版本 : v20.15.0系统 : Windows11 64位IDE : VsCode 1.91.0 2. 访问HTTP 在Vue中&#xff0c;访问HTTP&#xff0c;可以使用axios第三方库。 axios 是一个基于 promise 的网络请求库&#xff0c;可以用于浏览器和 node.js。 axios使用简…

一步步理清开源路径规划导航引擎GraphHopper

一步步理清开源路径规划导航引擎GraphHopper&#xff0c;学习应用 GraphHopper简介 源码用 Java 编写&#xff0c;开发人员可以通过 Maven 轻松上手。可以用作** Java 库**来计算两个或多个点之间路线的距离、时间、逐向指示和许多道路属性。可以用作独立的 Web 服务器来计算…

redis的学习

! 快速入门 安装 1.使用docker安装redis docker pull redisdocker run -d --name redis -p 6379:6379 --restart unless-stopped -v /etc/docker/Redis/data:/data -v /etc/docker/Redis/conf/redis.conf:/usr/local/etc/redis/redis.conf redis redis-server /usr/local/e…

Python 百题(实战快速掌握语法)_2(链表)

目录 实现链表类 挑战介绍 挑战内容 挑战要求 示例代码 版权说明 参考答案 代码分析&#xff1a; Node 类 LinkedList 类 方法分析 总结 删除链表的重复项 挑战介绍 挑战内容 挑战要求 示例代码 版权说明 参考答案 代码分析&#xff1a; 寻找链表倒数第 k…

算法笔记--知识点记录2

enumerate函数 enumerate 是 Python 中的一个内置函数&#xff0c;它用于将一个可遍历的数据对象&#xff08;如列表、元组或字符串&#xff09;组合为一个索引序列&#xff0c;同时列出数据和数据下标&#xff0c;通常用在 for 循环当中。 使用 enumerate 可以同时获得每个元…

什么是C++活锁?

听过太多的死锁&#xff08;Deadlock&#xff09;&#xff0c;今天来聊聊什么是C活锁&#xff08;Livelock&#xff09;。 C中的活锁&#xff08;Livelock&#xff09;并非特指C语言本身的一个概念&#xff0c;而是并发编程和操作系统中可能遇到的一种现象。活锁指的是任务或者…

数据库 执行sql添加删除字段

添加字段&#xff1a; ALTER TABLE 表明 ADD COLUMN 字段名 类型 DEFAULT NULL COMMENT 注释 AFTER 哪个字段后面; 效果&#xff1a; 删除字段&#xff1a; ALTER TABLE 表明 DROP COLUMN 字段;

【C++】C++入门知识(上)

好久不见&#xff0c;本篇介绍一些C的基础&#xff0c;没有特别的主题&#xff0c;话不多说&#xff0c;直接开始。 1.C的第一个程序 C中需要把定义文件代码后缀改为 .cpp 我们在 test.cpp 中来看下面程序 #include <stdio.h> int main() {printf("hello world\n…

Linux文件与相关函数的知识点3

main函数参数 int main(int argc,char *argv[]) { return 0; } C语言规定了main函数的参数只能有两个&#xff0c;一个是argc,一个是argv并且&#xff0c;argc只能是整数&#xff0c;第二个必须是指向字符 串的指针数组。 argc: 参数表示命令行中参数的个数&#xff0…

【建议收藏】CTF网络安全夺旗赛刷题指南(非常详细)零基础入门到精通,收藏这一篇就够了

在数字化浪潮汹涌澎湃的今天&#xff0c;网络安全已成为国家、企业和个人无法忽视的重要议题。为了挖掘和培养网络安全人才&#xff0c;一场场紧张刺激、充满智慧的CTF&#xff08;Capture The Flag&#xff09;安全竞赛应运而生。 一、CTF安全竞赛简介 CTF安全竞赛&#xff0c…

鸿蒙仓颉语言之【安全密码库crypto4cj】功能示例

功能示例 MD5使用样例 from crypto4cj import md5cj.*main() { var md: Array<UInt8> Array<UInt8>(16, item: 0)var result: String String(Array<Char>(33, item: 0))var str: String "helloworld"var ret md5(str.toUtf8Array(), md)r…

lua 游戏架构 之 游戏 AI (六)ai_auto_skill

定义一个为ai_auto_skill的类&#xff0c;继承自ai_base类。ai_auto_skill类的目的是在AI自动战斗模式下&#xff0c;根据配置和条件自动选择并使用技能。 lua 游戏架构 之 游戏 AI &#xff08;一&#xff09;ai_base-CSDN博客文章浏览阅读379次。定义了一套接口和属性&#…

ASCII码图片

在许多计算机语言中&#xff0c;比较字母、字符串大小时&#xff0c;都会运用到ASCII码。而这是很多创客容易遗忘的&#xff0c;今天发给大家ASCII码大全图片&#xff0c;记得收藏

Redis是多线程还是单线程?

文章目录 1、用户态和内核态2、阻塞IO3、非阻塞IO4、IO多路复用4.1 select4.2 poll4.3 epoll4.4 epoll中的ET和LT4.5 epoll的服务端流程 5、信号驱动6、异步IO7、对比8、Redis是单线程的吗&#xff1f;9、单线程多线程网络模型变更 1、用户态和内核态 1、ubuntu和Centos 都是Li…