Java【二叉搜索树和哈希表】模拟实现 + 【Map和Set】介绍

news2024/11/27 19:39:17

文章目录

  • 前言
  • 一、二叉搜索树
    • 1、什么是二叉搜索树
    • 2、模拟实现二叉搜索树
      • 2.1, 查找
      • 2.2, 插入
      • 2.3, 删除
    • 3、性能分析
  • 二、模型
  • 三、哈希表
    • 1、什么是哈希表
      • 1.1, 什么是哈希冲突
      • 1.2, 避免, 解决哈希冲突
        • 1.2.1, 避免: 调节负载因子
        • 1.2.2, 解决1: 闭散列(了解)
        • 1.2.3, 解决2: 开散列/哈希桶(重点)
    • 2、模拟实现哈希表
      • 2.1, put --- 插入 Key-Value 键值对
        • 2.1.1, resize --- 扩容, 调节负载因子
      • 2.2, get ---获取 Key 的 Value 值
    • 3、性能分析
  • 四、Java 提供的 TreeMap 和 HashMap
    • 1, Map 常用方法
      • 1.1, 关于 Entry 的说明:
      • 1.2, Entry 的常用方法:
    • 2, Map 注意事项
    • 3, TreeMap 和 HashMap 的区别
  • 五、Java 提供的 TreeSet 和 HashSet
    • 1, Set 常用方法
    • 2, Set 注意事项
    • 3, TreeSet和HashSet的区别
  • 总结

前言

在 Java 的集合框架中, 除了 List 接口和 Queue 接口, 还有两个接口十分重要, 十分常用 : MapSet

📌实现 Map 接口的有两个类: TreeMap 和 HashMap
📌实现 Set 接口的有两个类: TreeSet 和 HashSet

👉TreeMap 和 TreeSet 底层都是红黑树, 红黑树是在近似平衡的二叉搜索树 (VAL树) 上, 加之颜色变化及一些特殊性质的数据结构
👉HashMap 和 HashSet 底层都是哈希表

VAL树, 红黑树, B树,B+树的相关内容以后会在 [数据结构进阶] 专栏中分享

1️⃣先学习二叉搜索树, 并对其模拟实现
2️⃣再认识两种模型: 纯Key模型 和 Key-Value模型
3️⃣再学习哈希表, 并对其模拟实现

4️⃣5️⃣再学习Java中的 Map 和 Set


提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎评论区指点~ 废话不多说,直接发车~

一、二叉搜索树

1、什么是二叉搜索树

二叉搜索树(又称二叉排序树): 它或者是一棵空树,或者是具有以下性质的二叉树:
1️⃣若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2️⃣若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3️⃣它的左右子树也分别为二叉搜索树

说人话:
对每一颗树(子树)来说:
1️⃣根节点一定比左孩子结点大, 一定比右孩子结点小
2️⃣左树的所有结点都小于根节点, 右树所有结点都大于根节点

如图:
在这里插入图片描述

❗️❗️二叉搜索树还有一个特质: 二叉搜索树中序遍历序列一定是有序的

也可以利用这一点判断一棵树是否为二叉搜索树

二叉搜索树, 顾名思义就是一种主要用于查找的结构, 下面对二叉搜索树进行模拟实现, 主要实现 查找, 插入, 删除 操作


2、模拟实现二叉搜索树

首先写出二叉搜索树的成员属性和构造方法(和二叉树一致)

public class BinarySearchTree {
    static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val) {
            this.val = val;
        }
    }

    public TreeNode root;

为了方便理解 + 代码易懂, 暂时不使用泛型以及封装


2.1, 查找

📌按照左小右大的规律, 依次进行结点之间的比较

📌因为目标数据一旦比当前结点的 val 大/小, 只可能出现在当前节点的某一侧, 另一侧的结点不需要比较, 所以不需要用递归遍历所有的结点, 利用循环, 沿着某一条路径走到 null 即可

📌循环中找到了返回该结点, 循环结束还没找到返回null

过程图解:
在这里插入图片描述

代码实现:

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

2.2, 插入

经过每个结点的比较, 找到合适的位置插入即可
在这里插入图片描述
⚠️⚠️⚠️cur 遍历每一个结点时, 找到合适位置后, 此时 cur 已经是 null, 那么 cur 的父节点还能找到吗? 连父节点都找不到, 怎么插入(链接两个结点的引用)进去呢, 所以要记录上一个结点

退出循环找到合适位置后, 和父节点比较大小, 判断应该插入在父节点的左还是右

代码实现:

	public void insert(int val) {
        if (root == null) {
            root = new TreeNode(val);
        }
        TreeNode cur = root;
        TreeNode parent = null;
        while (cur != null) {
            parent = cur;
            if (val < cur.val) {
                cur = cur.left;
            } else if (val == cur.val) {
                return;
            } else {
                cur = cur.right;
            }
        }
        // 比较 val 和记录下来的 parent.val
        if (val < parent.val) {
            parent.left = new TreeNode(val);
        } else {
            parent.right = new TreeNode(val);
        }
    }

2.3, 删除

删除之前要先查找到这个结点, 首先写出查找的代码

public void remove(int val) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode parent = null;
        while (cur != null) {
            parent = cur;
            if (val == cur.val) {
                // 找到了, 进行删除操作
                removeNode(parent, cur);
                System.out.println(val + "找到了,已删");
                return;
            } else if (val < cur.val) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }
        System.out.println(val +"没找到");
    }

❗️❗️❗️
删除二叉搜索树中的数据比较复杂, 因为需要考虑删除某个数据后, 整棵树依然是二叉搜索树, 要判断待删除结点的左右孩子如何处置

分为三种情况讨论:
1️⃣cur.left == null 时又分为三种情况
2️⃣ cur.right == null 时又分为三种情况
3️⃣cur.left != null && cur.right != null 时需要"替换删除"

三种情况图解分析如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

⚠️⚠️⚠️上图中最后一步实际上不是让 107 和 100 交换, 因为 100 还有左右子树的引用, 是让 107 这个结点的 val 值覆盖掉了 cur 这个结点的 val 值

然后问题就转化成了如何删掉 107 这个结点, 具体如何找到 107 ,以及如何删掉 107 的过程如下:
在这里插入图片描述
在这里插入图片描述

代码实现:

    /**
     * 三种情况:
     * 1,左为空
     * 2,右为空
     * 3,左右都不空
     * @param root
     * @param cur
     */
    private void removeNode(TreeNode root, TreeNode cur) {
        if (cur.left == null) {
            /**
             * 三种情况
             * 1,cur == root
             * 2,cur == root.left
             * 3,cur == root.right
             */
            if (cur == root) {
                cur = cur.right;
            }else if(cur == root.left) {
                root.left = cur.right;
            }else{
                root.right = cur.right;
            }
        } else if (cur.right == null) {
            /**
             * 三种情况
             * 1,cur == root
             * 2,cur == root.left
             * 3,cur == root.right
             */
            if(cur == root) {
                cur = cur.left;
            }else if(cur == root.left) {
                root.left = cur.left;
            }else {
                root.right = cur.left;
            }
        } else {
            TreeNode targetParent = cur;
            TreeNode target = cur.right;
            // 右树找最小值
            while (target.left != null) {
                targetParent = target;
                target = target.left;
            }
            cur.val = target.val;
            // 判断右树结点是否为为1
            if (target == targetParent.left) {
                targetParent.left = target.right;
            }
            if (target == targetParent.right) {
                targetParent.right = target.right;
            }
        }
    }

3、性能分析

二叉搜索树的搜索性能取决于结点之间的比较次数, 而这又取决于树的状态

👉最好情况下: 二叉搜索树是一棵近似平衡的完全二叉树状态, 时间复杂度达到 O(log2N)
👉最坏情况下: 二叉搜索树是一棵单分支的斜树, 时间复杂度达到 O(N)

插入和删除的性能同理

为了解决二叉搜索树最坏情况下是单分支结构时导致的性能性下降问题, 引出了VAL树, 它通过旋转子树处理, 使二叉搜索树接近于平衡, 以后详细介绍


二、模型

在学习二叉搜索树之前,接触过的搜索方式可能只有暴力遍历和二分查找,
暴力遍历在数据量大时效率很慢,而二分查找必须在使用前先保证数据集合有序,并且这两种方式都只适用于静态查找

现实生活中查找时很有可能同时伴随着插入和删除,这就是动态查找,本篇介绍的 Map 和 Set 就是适合于动态查找的集合容器

一般把搜索的数据称作关键字(Key), 和关键字对应的称作值(Value)

1️⃣纯Key模型: 只存储Key
一般可以看用于搜索某个关键字是否存在
2️⃣Key-Value模型: 存储 Key 和每个 Key 对应的Value
一般可以用于搜索某个关键字出现的次数(单词在句子中出现的次数等等)

👉Map 这种结构中存储的就是 Key-Value(称作键值对) , Set 这种结构中只存储 Key

关于 Map 和 Set 的具体使用方式, 哈希表之后再一起介绍

三、哈希表

顺序结构的顺序表, 堆, 链式结构的链表, 二叉搜索树等, 关键字和存储位置之间都没有对应关系, 所以在查找关键字时, 必须经过关键字之间的比较

顺序表, 链表查找的时间复杂度达到O(N), 二叉搜索树查找的时间复杂度达到O(log₂N) 或 O(N)

🚗🚗🚗
那有没有一种搜索方式能够不经过关键字之间的比较, 直接得到要搜索的关键字? 这样, 时间复杂度就能够达到O(1) ------ 正是哈希表, 恐怖如斯 ❗️


1、什么是哈希表

哈希表(散列表) 这种数据结构可以通过哈希(散列)函数, 使元素的存储位置和关键码(hashCode)之间有一一映射的关系, 在插入和查找数据时, 根据关键字的hashCode, 计算出存储位置

例如这样一组数据: 1, 7, 6, 4, 5, 9
如果令哈希函数为: f (key) = key % capacity, 则数据的存储方式如图所示:
在这里插入图片描述

这样, 在搜索关键字时, 能够直接得到它的存储位置, 进行随机访问, 效率很高

但问题来了❗️❗️❗️ 依照如上哈希函数, 假如要再插入 14, 24, 34, 44 呢? 这些数据都会被存储到4 下标这个位置, 可是4 下标已经存储了关键字 4 了

这种情况叫做: 哈希冲突


1.1, 什么是哈希冲突

👉不同的关键字通过相同的哈希函数, 计算出来了相同的哈希地址, 这种现象就是哈希冲突(哈希碰撞)

如上述例子: 4, 14, 24, 34, 44 这几个不同的关键字都通过相同的哈希函数 : f (key) = key % capacity , 得到了相同的哈希地址: 4下标

👉把具有不同关键码但是哈希地址相同的数据称作" 同义词 ", 那么 4, 14, 24, 34, 44 就是同义词


1.2, 避免, 解决哈希冲突

要明确: 由于哈希表底层的数组容量往往小于实际存储的关键字数量, 这就导致一定会有多个关键字存储在同一个哈希地址上, 所以哈希冲突是无法消除的

但是, 还是有一些解决哈希冲突的方案的

一定要清楚!!
这里说的无法消除哈希冲突是指: 一定会有几个数据被分配到同一个哈希地址
而解决哈希冲突是指: 如果发生冲突, 不让他们同时存储在一个空间上

在解决哈希冲突之前, 首先要保证哈希函数的合理性, 有以下三点原则:
1️⃣哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间
2️⃣哈希函数计算出来的地址能均匀分布在整个空间中
3️⃣哈希函数应该比较简单

哈希函数设计的越合理, 越精妙, 哈希冲突的概率越低, 但仍然无法消除
一般来说, 都是使用库里面的哈希函数, 不会让我们自己设计

下面介绍如何尽量避免和解决哈希冲突


1.2.1, 避免: 调节负载因子

负载因子(∝) : ∝ = 表中已有数据 / 表长度

假如规定负载因子为 0.75, 当哈希表长度为 10, 已经填入了 8 个数据时, 就超过了负载因子, 表中负载因子越大, 发生哈希冲突的概率就会越大, 此时需要调节负载因子, 让负载因子变小

由于无法控制表中要插入多少个数据(分子), 所以我们只能对数组扩容(分母)

⚠️⚠️⚠️注意:
填入第 8 个数据时, 发现负载因子已经大于0.75, 此时需要对数组扩容
如果哈希函数是: 关键字的关键码 % 数组长度, 那么当数组扩容之后, 数组长度改变,哈希函数改变, 哈希地址也要重新分配, 需要对表中的所有数据重新计算哈希地址, 重新分配存储空间


1.2.2, 解决1: 闭散列(了解)

由于闭散列的弊端较大, 所以一般不使用, 仅作了解

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

分为两种: 线性探测二次探测

📌📌📌线性探测 : 冲突的数据, 依次往后面找空位置存放, 效果如图所示:
在这里插入图片描述

❌弊端:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

📌📌📌二次探测 : 针对线性探测会把冲突的数据堆积到一起这个缺陷, 二次探测是通过某种函数, 重新计算了哈希地址, 使得冲突的数据能够相对分散一些 , 效果如图所示::

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

综合来看, 闭散列的最大弊端就是空间利用率太低, 而第二种解决方案对空间利用率较高


1.2.3, 解决2: 开散列/哈希桶(重点)

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

什么意思?🤷‍♂️
例如, HashMap 存储 <Key,Value> 模型的数据, Key表示数字, Value表示这个数字在某处出现的次数
插入 <1,0>, <4,3>, <7,2>, <14,5>, <44,8> , 哈希桶存储形式 如图所示:
在这里插入图片描述

可以看到, 由于哈希表中每个下标存储的是一个链表的头结点, 因此可以 “同时” 存储多个哈希冲突的数据, 而不会像闭散列那种方案, 占用数组中其它空间, 所以开散列对表中空间利用率比较高


2、模拟实现哈希表

我们对哈希表(开散列/哈希桶的方式)进行模拟实现, 刚才了解过, 每个桶都是一个单链表(先不考虑红黑树), 链表是由结点(作为内部类)组织起来的

如果采用Key-Value 模型, 结点中就需要三个域:
两个值域: Key 和 Value, 以及一个指针域: next

先写出 HashBuck 的成员属性

public class HashBuck {
    static class Node{
        public int key;
        public int val;
        public Node next;

        public Node(int key,int val) {
            this.key = key;
            this.val = val;
        }
    }
    public Node[] array;
    public int uesdSize;
    public static  final double LOAD_FACTOR = 0.75;// 负载因子

    public HashBuck(int capacity){
        array = new Node[10];
        // 没有初始化数组,里面每一个结点默认是null
    }

Java集合里的哈希桶中设定负载因子就是0.75
为了方便理解 + 代码易懂, 暂时不使用泛型以及封装

主要模拟实现插入Key-Value键值对获取Key的Value值这两种操作


2.1, put — 插入 Key-Value 键值对

插入是对在哈希桶(链表)中进行插入删除操作, jdk1.8 之前是头插, 之后修改为尾插, 头插难度稍大, 在这里以头插的方式展现

设定哈希函数为: index(哈希地址) = key % array.length;

	public void put(int key,int val) {
        int index = key % array.length;
        Node cur = array[index];
        // 遍历这个哈希值对应的链表中的结点,如果有相同的就替换掉
        // 如果没有就头插
        while(cur != null){
            if (cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
            uesdSize++;
        }
        Node node = new Node(key, val);
        // newArray[newIndex] 中存储的是链表的头结点
        node.next = array[index];
        // 存储新的头结点
        array[index] = node;
        uesdSize++;

        // 判断此时是否已经超过LOAD_FACTOR负载因子的值
        if (uesdSize * 1.0 / array.length > LOAD_FACTOR) {
            // 扩容
            grow();
        }
    }

⚠️⚠️⚠️每次插入完后, 都需要计算负载因子是否超过0.75, 如果超过需要对数组扩容

2.1.1, resize — 扩容, 调节负载因子

    private void grow() {
        Node[] newArray = Arrays.copyOf(array, array.length * 2);
        for (Node node : array) {
            // 重新分布每一个结点的新的哈希地址
            Node cur = node;
            while (cur != null) {
                Node curNext = cur.next;
                int newIndex = cur.key % newArray.length;

                // newArray[newIndex] 中存储的是链表的头结点
                cur.next = newArray[newIndex];
                newArray[newIndex] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }

2.2, get —获取 Key 的 Value 值

    public int get(int key) {
        int index = key % array.length;
        Node cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                return cur.val;
            }
            cur = cur.next;
        }
        // 也可以抛异常
        throw new TheKeyDoesNotExistException("没有这个key值");
    }

3、性能分析

尽管哈希表一定发生会冲突, 但Java集合中采用 哈希桶 这种方式解决冲突, 发生冲突的数据以链表的方式存储, 加之 调节负载因子 , 所以链表的长度 = 冲突数据的个数, 会远小于表中数据, 所以链表的长度是一个常数

因此通常意义下,我们认为哈希表的 插入 / 删除 / 查找 时间复杂度是O(1)


四、Java 提供的 TreeMap 和 HashMap

Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复, TreeMap 是用红黑树实现的, HashMap 是用哈希表实现的

实例化方式:

    public static void main(String[] args) {
    	// 第一个参数代表Key, 第二个参数代表Value
        Map<Character, Integer> treeMap = new TreeMap<>();
        Map<Character, Integer> hashMap = new HashMap<>();
    }

1, Map 常用方法

下面介绍集合中 TreeMap 和 HashMap 常用的方法都是一样的, 只是底层数据结构不一样, 以 TreeMap 为例

    public static void main89(String[] args) {
        Map<Character, Integer> treeMap = new TreeMap<>();
        // 1,插入数据
        treeMap.put('a',3); // 字符'a'代表Key, 整型3代表Value
        treeMap.put('d',42);
        treeMap.put('c',16);
        treeMap.put('e',42);
        treeMap.put('b',28);
        treeMap.put('f',99);
        treeMap.put('f',0);
        System.out.println("1, 插入后的 map 集合: " + treeMap);

        // 2,删除数据
        int removeValue = treeMap.remove('a');
        System.out.println("2, 删除数据的 Value 值: " + removeValue);
        System.out.println("   删除一次后的 map 集合: " + treeMap);

        // 3,获取Key对应的Value值
        int getValue = treeMap.get('f');
        System.out.println("3, 获取 'f' 的 Value 值: " + getValue);

        // 4,如果Key不存在, 获取默认Value值
        int getDefaultValue = treeMap.getOrDefault('x',-1);// 'x' 不存在, 设置Value值默认为-1
        System.out.println("4, 获取 'x' 的 Value 值: " + getDefaultValue);

        // 5,判断是否包含Key
        boolean containsKey = treeMap.containsKey('b');
        System.out.println("5, 是否包含 Key 为 'b' 的数据: " + containsKey);
        System.out.println("   得到这个 Key 的 Value 值: " + treeMap.get('b'));

        // 6,判断是否包含Value
        boolean containsValue = treeMap.containsValue(0);
        System.out.println("6, 是否包含 Value 为 0 的数据: " + containsValue);

        // 7,获取map的长度(键值对个数)
        int size = treeMap.size();
        System.out.println("7, map的长度(键值对个数)为: " + size);

        // 8,获取不重复的Key集合
        Set<Character> retSet = treeMap.keySet();
        System.out.println("8, 获取不重复的 Key 集合: " + retSet);

        // 9,获取可重复的Value集合
        Collection<Integer> retValue = treeMap.values();
        System.out.println("9, 获取可重复的 Value 集合: " + retValue);

        // 10,获取所有Key-Value的映射关系集合
        Set<Map.Entry<Character, Integer>> enteySet = treeMap.entrySet();
        System.out.println("10, 所有Key-Value的映射关系集合: " + enteySet);
    }

输出结果:
在这里插入图片描述


1.1, 关于 Entry 的说明:

第十条中,Map.Entry<Character, Integer> 是啥?
Entry<K, V>是 Map 内部实现的用来存放 <key, value> 键值对映射关系的内部接口, 所以通过 " . " 点号可以访问这个内部接口

Set 中的数据是 纯Key 模型, treeMap.entrySet() 这个方法就是把所有的 Key-Value 键值对的映射关系存放在 Set 中, Set 的对象中每一个数据的类型是 Entry
如图:
在这里插入图片描述

对这个 set 使用 fro-each 遍历, 即可得到其中的每一个 entry

        for(Map.Entry<Character, Integer> entry : enteySet) {
            
        }

1.2, Entry 的常用方法:

Entry 这个内部接口中主要提供了 <key, value> 的获取,value 的设置以及 Key 的比较方式。

使用方式如下:

        for(Map.Entry<Character, Integer> entry : enteySet) {
            // 获取entry (键值对)中的Key
            char keyRet = entry.getKey();
            // 获取entry (键值对)中的value
            int valueRet = entry.getValue();
            System.out.println("key = " + keyRet + " , value = " + valueRet);

            if(keyRet == 'b') {
                // 修改value值
                int updateRet = entry.setValue(-valueRet);
                System.out.println("把 key 为 'b' 的键值对的 value 值改为负数: " + updateRet);
            }
        }

Value值可以修改, 但是Key值是不可以, 不支持修改的


2, Map 注意事项

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

2️⃣Map 中存放键值对的Key 是唯一的(不可重复),value 是可以重复的

在上述代码示例中, 插入了< ’ f ’ , 99> , 又插入了< ’ f ’ , 0>
第二次插入时已经存在一个 Key 值为 ’ f ’ , 由于 Key 不可重复, 所以 < ’ f ’ , 0> 会直接覆盖掉 < ’ f ’ , 99>
因此, 输出的第三行显示: ’ f ’ 对应的 Value 值为 0

3️⃣在 TreeMap 中插入键值对时,key 不能为空,否则就会抛 NullPointerException 异常,value 可以为空。但是 HashMap 的 key 和 value 都可以为空

因为 TreeMap 是用红黑树实现的, 上面介绍二叉搜索树中说过, 树结构的增删查改需要经过关键字之间的比较, 如果关键字为 null 就不可以比较了, 所以会抛异常

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

因为 Set 中的数据不可以重复, 可以达到去重的目的, 下面会介绍到

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

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


3, TreeMap 和 HashMap 的区别

如图
在这里插入图片描述

那什么时候使用 TreeMap,什么时候使用 HashMap 呢

如果要求Key在集合中保持有序,就使用 TreeMap,如果不要求Key有序,或追求性能高,时间快,则使用HashMap


五、Java 提供的 TreeSet 和 HashSet

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

Set 中 K一定是唯一的,不能重复, 所以 Set 有去重的功能, TreeSet 是用红黑树实现的, HashSet 是用哈希表实现的

实例化方式:

    public static void main(String[] args) {
        Set<Integer> treeSet = new TreeSet<>();
        Set<Integer> hashSet = new HashSet<>();
    }

1, Set 常用方法

下面介绍集合中 TreeSet和 HashSet 常用的方法都是一样的, 只是底层数据结构不一样, 这次以 HashSet 为例

    public static void main(String[] args) {
        Set<Integer> hashSet = new HashSet<>();
        // 1,插入数据
        hashSet.add(1);
        hashSet.add(2);
        hashSet.add(3);
        hashSet.add(4);
        hashSet.add(5);
        boolean firstAdd = hashSet.add(6);
        boolean secondAdd = hashSet.add(6);
        System.out.println("1, 插入后 hashSet 中数据集合: " + hashSet);
        System.out.println("   第一次插入 Key = 6 是否成功: " + firstAdd);
        System.out.println("   第二次插入 Key = 6 是否成功: " + secondAdd);// 重复的Key不能再插入

        // 2,删除数据
        hashSet.remove(6);
        boolean removeRet = hashSet.remove(7);// 不存在的数据不能删除
        System.out.println("2, 删除 key = 6 后 set 中数据集合: " + hashSet);
        System.out.println("   删除 key = 7 是否成功: " + removeRet);

        // 3,判断是否包含某个key
        boolean containsRet = hashSet.contains(5);
        System.out.println("3, hashSet 中是否包含 key = 5: " + containsRet);

        // 4,判断是否为空
        boolean emptyRet = hashSet.isEmpty();
        System.out.println("4, hashSet 是否为空: " + emptyRet);

        // 5,获取hashSet的长度(数据个数)
        int size = hashSet.size();
        System.out.println("5, hashSet 的长度(数据个数): " + size);

        // 6,清空hashSet
        hashSet.clear();
        System.out.println("6, 清空 hashSet 后数据集合: " + hashSet);
    }

输出结果:
在这里插入图片描述


2, Set 注意事项

1️⃣Set 是继承自 Collection 的一个接口类
2️⃣Set 中只存储了 key,并且要求 key 一定要唯一

Set 可以达到去重的目的

3️⃣TreeSet 的底层是使用 Map 来实现的,其使用 key 与 Object 的一个默认对象作为键值对插入到 Map 中的
4️⃣Set 最大的功能就是对集合中的元素进行去重
5️⃣实现 Set 接口的常用类有 TreeSet 和 HashSet ,还有一个 LinkedHashSet,LinkedHashSet 是在HashSet的基础上维护了一个双向链表来记录元素的插入次序
6️⃣Set 中的 Key 不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7️⃣TreeSet 中不能插入 null 的 key,HashSe t可以

因为 HashSe 底层是由哈希表实现的, 哈希表的操作不需要关键字之间的比较


3, TreeSet和HashSet的区别

TreeMap 和 HashMap 的区别就是 TreeSet 和 HashSet 的区别

如图:
在这里插入图片描述

什么时候使用 TreeSet ,什么时候使用 HashSet ? 参考 TreeMap 和 HashMap


总结

以上就是本篇的所有内容了, 本篇主要介绍了
1️⃣Java 集合中 Map 和 Set 接口底层的数据结构: 二叉搜索树 和 哈希表
2️⃣介绍了 Map 和 Set 中存储的两种数据模型
3️⃣介绍了 Map 和 Set 的常用方法的使用方式以及一些细节要点

如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~


上山总比下山辛苦
下篇文章见

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

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

相关文章

基于卷积神经网络CNN的水果分类预测,卷积神经网络水果等级识别

目录 背影 卷积神经网络CNN的原理 卷积神经网络CNN的定义 卷积神经网络CNN的神经元 卷积神经网络CNN的激活函数 卷积神经网络CNN的传递函数 卷积神经网络CNN水果分类预测 基本结构 主要参数 MATALB代码 结果图 展望 背影 现在生活&#xff0c;为节能减排&#xff0c;减少电能…

推荐系统 FM因式分解

reference&#xff1a;知乎 FM算法解析 LR算法没有二阶交叉 如果是id类特征&#xff0c;这里的x是0/1&#xff0c;raw的特征输入就是float&#xff0c;当然&#xff0c;在我的理解里&#xff0c;一般会把raw的特征进行分桶&#xff0c;还是映射到0/1特征&#xff0c;不然这个w…

树莓派测试wifi与eth速率

测试网速方法&#xff1a; 1.安装插件&#xff1a; 首先在树莓派端安装iperf3 sudo apt install iperf3PC端也需要安装iperf3&#xff0c;单击下面网址即可 下载网址 压缩包解压到桌面&#xff0c;文件内容如下图所示&#xff1a; 2.开始测速服务&#xff1a; 树莓派端在…

周报终结者 GitLab 个人工作记录查询器

序言 每周都要写周报&#xff0c;烦死人。为了解救自己&#xff0c;把自己从无聊的工作中抽离出来。 特别写了一个工具。可以查询GitLab中自己一段时间内的所有提交记录。 按照项目和分支进行排序 效果 还可以查询原始的json数据方便自己进行筛选和扩展 使用方式 1.获取个人…

从 1 秒到 10 毫秒!在 APISIX 中减少 Prometheus 请求阻塞

本文介绍了 Prometheus 插件造成长尾请求现象的原因&#xff0c;以及如何解决这个问题。 作者屠正松&#xff0c;Apache APISIX PMC Member。 原文链接 现象 在 APISIX 社区中&#xff0c;曾有部分用户陆续反馈一种神秘现象&#xff1a;部分请求延迟较长。具体表现为&#xf…

Android电视盒子最强看电视app-tvbox配置(视频源)教程

今天给大家分享一下安卓tv上最强的看视频神器-tvbox的配置方法 tvbox是一款影视观看类的软件&#xff0c;各种影视资源都是为你免费提供的&#xff0c;还有海量热门影视为你提供电视直播&#xff0c;让你可以实时在线进行观看以及体验一样&#xff0c;超多影视剧内容你感兴趣的…

实景建模整合了什么优势?有哪些领域应用?

近年来&#xff0c;无接触经济、线上营销模式成为了热门&#xff0c;伴随着国家十四五规划的出台&#xff0c;对数字经济的扶持是巨大的。VR实景迎来了发展新利好&#xff0c;实景建模—专业的倾斜摄影测量三维实景建模平台&#xff0c;为你真实还原现实世界&#xff01; 实景建…

Homekit智能家居系列一智能触摸面板开关

触摸开关&#xff0c;即通过触摸方式控制的墙壁开关&#xff0c;其感官场景如同我们的触屏手机&#xff0c;只需手指轻轻一点即可达到控制电器的目的&#xff0c;随着人们生活品质的提高&#xff0c;触摸开关将逐渐将换代传统机械按键开关。 触摸开关控制原理 触摸开关我们把…

【Adobe】GenP3.0的使用教程

1、Google一下GenP 2、或者直接点击&#xff1a;https://www.reddit.com/r/GenP/ 3、选择GenP 3.0 - NEW 4、点击下载 注&#xff1a;这个地址可以不用科学上网即可下载&#xff1a;https://www.mediafire.com/file/jr0jqeynr4h21f9/Adobe_GenP_3.0.zip/file 5、点击运行 RunM…

【并发基础】操作系统中线程/进程的生命周期与状态流转以及Java线程的状态流转详解

目录 一、操作系统中进程和线程的状态 1.1 进程 1.1.1 进程的概念 1.1.2 进程的状态 1.1.3 进程调度流程图&#xff08;五状态&#xff09; 1.1.4 挂起状态 1.1.4 进程调度流程图&#xff08;六状态和七状态&#xff09; 1.1.5 睡眠状态 1.1.6 进程的诞生与消亡 1.2 线程 1.2.1…

运营新人必看 eBay申诉的最全资料

eBay作为一个电商交易平台&#xff0c;限制还是不少的。前几篇eBay系列文章发布后&#xff0c;也有些兄弟来向我反应eBay平台的严格程度&#xff0c;很容易因为这样那样的原因就被封掉了&#xff0c;自己也莫名其妙的。龙哥今天就总结一下eBay封号中比较常见的原因和万一被封要…

关于js数据类型的理解

目录标题一、js数据类型分为 基本数据类型和引用数据类型二、区别&#xff1a;传值和传址三、深浅拷贝传值四、数据类型的判断一、js数据类型分为 基本数据类型和引用数据类型 1、基本数据类型 Number、String、Boolean、Null、undefined、BigInt、Symbol 2、引用数据类型 像对…

详解信道估计的发展与最新研究进展(MIMO)

目录 一. MIMO信道估计的重要性 二. 最经典的两种信道估计方法 2.1 最小二乘信道估计(LS) 2.2 最小均方误差信道估计(MMSE) 三. 优化传统的MIMO信道估计技术 四. 介绍压缩感知技术 五. 基于压缩感知的MIMO信道估计 5.1 压缩感知怎么用在MIMO信道估计 5.2 改进压缩感知…

BIO,NIO,AIO

IO模型 用什么样的通道进行数据传输和接收&#xff0c;java支持3种io网络编程模式 BIO NIO AIO BIO 同步阻塞 一个客户端连接对应一个处理线程 BIO示例代码&#xff08;客户端和服务端&#xff09; package com.tuling.bio;import java.io.IOException; import java.net.So…

WIFI P2P架构

WI-FI P2P定义架构3个组件组织结构技术标准P2P DiscoveryDevice Discovery&#xff08;扫描&#xff09;流程p2p probe 管理帧Group Formation&#xff08;组网&#xff09;GO Negotiation&#xff08;GON&#xff09;流程P2P Public Action管理帧Provision Discovery&#xff…

Rust Web 进阶(一):Rust异步编程(Tokio)

这一篇将讲解什么是和为什么进行异步编程&#xff0c;Rust 怎么样进行异步编程以及其机制&#xff0c;并且讲解目前 rust 常用的异步编程的函数库。本章的内容来自杨旭老师的教程&#xff1a; https://www.bilibili.com/video/BV16r4y187P4/?spm_id_from333.999.0.0&vd_s…

ADSP21489之CCES开发笔记(七)

一、SPORT简介&#xff1a;ADI21489具有八个独立的同步串行端口(SPORT)&#xff0c;可为各种外围设备提供I/O接口。他们是称为SPORT0至SPORT7&#xff0c;每个SPORT都有自己的一组控制寄存器和数据缓冲器&#xff0c;具有一定范围的时钟和帧同步选项。 二、特性&#xff1a;支持…

【云原生】Apisix接入Nacos、K8s服务注册中心自动获取服务

背景我在K8s中部署了两个使用SpringCLoudK8s框架的微服务&#xff0c;每个服务既暴露了HTTP接口方便测试&#xff0c;也暴露了gRpc接口更接近生产&#xff0c;他们的端口如下所示&#xff1a;常规功能上游我们可以在这里配置我们的服务HTTP配置一个HTTP服务&#xff0c;输入地址…

设计模式 - 行为型 - 模板模式学习

现象&#xff1a; 设计模式 - 行为型 - 模板模式学习 介绍&#xff1a; 模板模式的设计思路&#xff0c;在抽象类中定义抽象方法的执行顺序&#xff0c; 并将抽象方法设定为只有子类实现&#xff0c;但不提供独立访问的方法 只能通过已经被安排好的定义方法去执行 可以控制整…

如何快速生成数据字典SQL语句

如何快速生成数据字典SQL语句 一、首先我们找到需要生成的数据字典的国家标准 以民族数据字典为例 打开浏览器搜索民族字典代码表得到如下数据&#xff0c;并把得到的数据存入Excel表格中 国标民族数据字典 国标民族字典 第七次全国人口普查民族代码表与民族国标代码 第七次…