Java数据结构第二十二期:Map与Set的高效应用之道(一)

news2025/3/12 16:32:07

专栏:Java数据结构秘籍

个人主页:手握风云

目录

一、Map和Set

1.1. 概念

二、搜索树

2.1. 概念

2.2. 查找操作

2.2. 插入操作

2.3. 删除操作

2.4. 性能分析

三、搜索

3.1. 概念及场景

3.2. 模型

四、Map

4.1. Map的说明

3.2. Map的使用

五、Set

5.1. Set的使用


一、Map和Set

1.1. 概念

        如果一些场景中需要设计到数据搜素相关的操作时,就需要用到上图中的Map和Set接口。Map用于存储键值对,每个键都映射到一个值,键必须是唯一的,但值可以重复,常见的实现类为HashMap、TreeMap。用于存储不重复的元素。它不允许存储重复的值,但不存储键值对,常见的实现类为HashSet、TreeSet。

二、搜索树

2.1. 概念

        TreeSet和TreeMap是与二叉搜索树相关的,二叉搜索树底层是用红黑树实现的。二叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树:

  • 若它的左⼦树不为空,则左⼦树上所有节点的值都⼩于根节点的值;
  • 若它的右⼦树不为空,则右⼦树上所有节点的值都⼤于根节点的值;
  • 它的左右⼦树也分别为⼆叉搜索树。

2.2. 查找操作

        查找的思路是,让需要查找的值key与结点的值域val进行比较。如果key大于val,就去遍历右子树;如果key小于val,就去遍历左子树;如果相等,直接返回这个结点。

    public TreeNode root = null;

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

        上面算法的时间复杂度最好情况下(一棵满二叉树)为logn,最坏情况下(只有单个分支)时间复杂度为n

2.2. 插入操作

        我们在完成插入操作之后,依然要保证这棵树是一棵二叉搜索树。我们把需要插入的数值val先与根结点的值root.val进行比较,大于在右树插入,小于在左树插入。如下图所示,我们需要将70插入进73的左边,70成为61的右子树,而此时的cur已经为空了。但问题来了,程序没有记录下30的位置,所以我们还需要一个p引用来记录cur走过的前一个位置。当cur为空时,如果val大于p.val,则插入p的右边;如果val小于p.val,则插入p的左边。如果这棵树为空,我们插入第一个结点时,直接让root等于插入的结点就可以。

    public void Insert(int val) {
        TreeNode newNode = new TreeNode(val);

        if (root == null) {
            root = newNode;
            return;
        }

        TreeNode cur = root;
        TreeNode parent = null;

        while (cur != null) {
            if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            } else if (cur.val > val) {
                parent = cur;
                cur = cur.left;
            } else {
                return;
            }
        }
        if (parent.val < val) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
    }

2.3. 删除操作

        删除操作相较于前两个来说比较复杂。如果这个节点没有左右结点,就直接置为空;如果这个结点有左孩子结点或者右孩子结点,就直接将孩子结点变为删除结点的父亲结点的子结点。但如果待删除的结点左右都不为空,该怎么办。

        我们要想删除val,就要先查询到该元素。如果要删除的结点为cur,它的父亲结点为parent。我们先假设cur.left为空,且cur是parent.left,那么parent.left=cur.right;再假设cur是parent.right,那么parent.right=cur.right。如下图所示,我们要删除的结点为61。

        同理,如果cur.right为空,且cur是parent.left,那么parent.left=cur.left;再假设cur是parent.right,那么parent.right=cur.left。

        完整代码实现:

    public void Remove(int val){
        TreeNode cur = root;
        TreeNode parent = null;
        while(cur != null){
            if(cur.val < val){
                parent = cur;
                cur = cur.right;
            }else if(cur.val > val){
                parent = cur;
                cur = cur.left;
            }else{
                RemoveNode(cur,parent);
                return;
            }
        }
    }

    private void RemoveNode(TreeNode cur, TreeNode parent) {
        if(cur.left == null){
            if(cur == root){
                root = root.right;
            }else if(cur == parent.left){
                parent.left = cur.right;
            }else{
                parent.right = cur.right;
            }
        }else if(cur.right == null){
            if(cur == root){
                root = root.left;
            }else if(cur == parent.left){
                parent.left = cur.left;
            }else{
                parent.right = cur.left;
            }
        }
    }

        下面是最难的一种情况,就是cur的左右结点都不为空,如果删除,该如何安排它的左右结点。利用替换法,找出一个“替罪羊”,让替换的值去替换我们要删除的结点。比如我们要删除73,在73的左子树找出最大值来替换73,这样就能保证要删除的结点左侧都是比它小的,然后我们去删70。如下图所示,这样就会出现要删除的结点一边为空,一边不为空。

TreeNode target = cur.right;
TreeNode targetParent = cur;
while(target.left != null){
    targetParent = target;
    target = target.left;
}
targetParent.left = target.right;

        代码写到这里,我们还有一种情况没有考虑到。我们看下图这种情况,81的左边为空,无法进入上面的while循环,那么我们就需要用81替换73。

    public void Remove(int val) {
        TreeNode cur = root;
        TreeNode parent = null;
        while (cur != null) {
            if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            } else if (cur.val > val) {
                parent = cur;
                cur = cur.left;
            } else {
                RemoveNode(cur, parent);
                return;
            }
        }
    }

    private void RemoveNode(TreeNode cur, TreeNode parent) {
        if (cur.left == null) {
            if (cur == root) {
                root = root.right;
            } else if (cur == parent.left) {
                parent.left = cur.right;
            } else {
                parent.right = cur.right;
            }
        } else if (cur.right == null) {
            if (cur == root) {
                root = root.left;
            } else if (cur == parent.left) {
                parent.left = cur.left;
            } else {
                parent.right = cur.left;
            }
        } else {
            TreeNode target = cur.right;
            TreeNode targetParent = cur;
            while(target.left != null){
                targetParent = target;
                target = target.left;
            }
            if(target == targetParent.left) {
                targetParent.left = target.right;
            }else{
                targetParent.left = target.right;
            }
        }
    }

2.4. 性能分析

        插⼊和删除操作都必须先查找,查找效率代表了⼆叉搜索树中各个操作的性能。 对有n个结点的⼆叉搜索树,若每个元素查找的概率相等,则⼆叉搜索树平均查找⻓度是结点在⼆叉搜 索树的深度的函数,即结点越深,则比较次数越多。

三、搜索

3.1. 概念及场景

        Map和set是⼀种专⻔⽤来进⾏搜索的容器或者数据结构,其搜索的效率与其具体的实例化⼦类有关。以前常见的搜索方式有:直接遍历或者二分查找。但这些只能在特定场景下才能使用,一般的情况下效率相对较低。这两种查找比较适合静态类型的查找,即⼀般不会对区间进⾏插⼊和删除操作了。而现实中的查找比如:根据姓名查询考试成绩或者通讯录中根据姓名查询联系方式。

3.2. 模型

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

  • 纯key模型

        有⼀个英⽂词典,快速查找⼀个单词是否在词典中。

  • . Key-Value 模型

        统计⽂件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数。

        Map中存储的就是key-value的键值对,Set中只存储了Key。

四、Map

4.1. Map的说明

        Map是⼀个接⼝类,该类没有继承⾃Collection,该类中存储的是结构的键值对,并且K⼀定是唯一的,不能重复。

public interface Map<K, V> 
    interface Entry<K, V>

        我们可以看下Structure里面,Map内部又实现了一个接口Entry,可以理解为二叉搜索树里的TreeNode。

3.2. Map的使用

方法说明
V get(Object key)返回key对应的value值
V getOrDefault(Object key,V defaultValue)返回key对应的value值,若key不存在,返回默认值
V put<key,value>设置key对应的value
V remove<Object key>删除key的映射关系
Set<K> keySet()返回所有key的不重复集合
Collection<V> values()返回所有value的可重复集合
Set<Map.Entry<K,V>> entrySet()返回key-value的所有映射关系
boolean containsKey(Object key)判断是否包含key
boolean containsKey(Object value)判断是否包含value
import java.util.Collection;
import java.util.Set;
import java.util.TreeMap;

public class Solution {
    public static void main(String[] args) {
        TreeMap<Character,Integer> tree1 = new TreeMap<>();
        //底层是二叉搜索树,要想实现插入与删除,进行的是k的比较
        //如果k是其他类,那么这个类必须是可比较的
        tree1.put('a',1);
        tree1.put('b',2);
        tree1.put('c',3);
        tree1.put('d',4);
        tree1.put('e',5);
        tree1.put('a',1);
        tree1.put('b',2);
        tree1.put('c',3);
        tree1.put('x',1);
        tree1.put('y',2);
        tree1.put('z',3);
        System.out.println(tree1.get('a'));//根据key获取value,打印1
        tree1.put('a',10);//设置key对应的value是唯一的

        System.out.println(tree1.get('a'));//打印10
        System.out.println(tree1.get('e'));//返回null

        System.out.println(tree1.getOrDefault('f',6));

        System.out.println(tree1.containsKey('c'));
        System.out.println(tree1.containsKey('f'));

        Set<Character> setTree = tree1.keySet();
        System.out.println(setTree);

        Collection<Integer> collection = tree1.values();
        System.out.println(collection);

        Integer in = tree1.remove('a');
        System.out.println(in);
    }
}

注意:

  • Map是⼀个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者 HashMap
  • 在TreeMap中插⼊键值对时,key不能为空,否则就会抛NullPointerException异常,value可以 为空。但是HashMap的key和value都可以为空。
  • Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然 后再来进行重新插入。

五、Set

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

5.1. Set的使用

方法说明
boolean add(E,e)添加元素,但重复元素不会被添加
void clear()清空集合
boolean contains(Object o)判断o是否在集合中
Iterator<E> Iterator()返回迭代器
boolean remove(Object o)删除集合中的o
int size()返回集合中的元素个数
boolean isEmpty()检查集合是否为空
Object[] toArray()将集合中的元素转化成数组
boolean containsAll(Collection<?> c)集合c中的元素是否全部存在于集合set中
boolean addAll(Collection<? extends E> c)将集合c中的元素添加进set中
import java.util.Iterator;
import java.util.TreeSet;

public class Solution {
    public static void main(String[] args) {
        TreeSet<String> tree = new TreeSet<>();
        System.out.println(tree.isEmpty());//集合是否为空

        tree.add("abc");//添加
        tree.add("def");
        tree.add("hello");
        tree.add("world");
        tree.add("hello");//只能添加一个
        System.out.println(tree);
        System.out.println(tree.isEmpty());

        System.out.println(tree.contains("abc"));//判断是否存在集合中
        System.out.println(tree.contains("bac"));

        System.out.println(tree.size());//获取长度

        String[] array = tree.toArray(new String[3]);//转化成数组
        for (String i: array) {
            System.out.print(i+" ");
        }

        System.out.println();
        tree.remove("world");//删除元素
        System.out.println(tree);

        Iterator<String> it = tree.iterator();
        while (it.hasNext()){
            System.out.print(it.next()+" ");
        }
    }
}

        Set中只存储了key,并且要求key⼀定要唯一,TreeSet的底层是使用Map来实现的,其使⽤key与Object的⼀个默认对象作为键值对插⼊到Map中的。我们来看一下TreeSet的构造方法的源码:

    public TreeSet() {
        this(new TreeMap<>());
    }
    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }
public interface NavigableMap<K,V> extends SortedMap<K,V>
public interface SortedMap<K,V> extends Map<K,V>
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }

        上面的TreeMap传给了m,m是NavigableMap类型的,而NavigableMap又继承了Map,我们再来看add方法,里面的e接收了key,PRESENT接收了value,而这个PRESENT又是一个Object类。

private transient NavigableMap<E,Object> m;

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

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

相关文章

兴达易控modbusTCP转profinet接防撞雷达测试

modbusTCP转profinet接防撞雷达测试 随着工业自动化程度的不断提高&#xff0c;现场设备之间的通信需求日益增长。ModbusTCP作为一种广泛应用的工业通信协议&#xff0c;因其简单、可靠的特点&#xff0c;被广泛应用于各种自动化设备中。而Profinet作为工业以太网的一种&#…

flutter实践:断点调试踩坑

问题&#xff1a;使用VSCode开发flutter,最近突然开始打断点不生效&#xff0c;程序可以attach,修改有日志输出&#xff0c;但是断点处怎么都停不了&#xff0c;程序异常断点会停。 分析&#xff1a;开始误以为是flutterSDK出了问题折腾了一天&#xff0c;后来又怀疑是lauch.j…

STM32——GPIO介绍

GPIO(General-Purpose IO ports,通用输入/输出接口)模块是STM32的外设接口的核心部分,用于感知外界信号(输入模式)和控制外部设备(输出模式),支持多种工作模式和配置选项。 1、GPIO 基本结构 STM32F407 的每个 GPIO 引脚均可独立配置,主要特性包括: 9 组 GPIO 端口…

Photo Works在线图片编辑器:一键修复老照片,轻松焕新记忆

★【概况介绍】 今天突然收到我的朋友电脑出故障了,截图给我,我一看就知道这个是缺少必要的组件引起的故障。结合这个问题,我来谈谈自己的解决思路和方法,希望能够帮助到大家。帮助大家是我最开心的事情。以前只是帮朋友解决问题,没有记录下来,刚刚接触到这个平台,刚好可…

SQLiteStudio:一款免费开源跨平台的SQLite管理工具

目录 1.简介 2.下载与安装 3.实现分析 4.总结 1.简介 SQLiteStudio 是一款专门用于管理 SQLite 数据库的图形化工具&#xff0c;由波兰开发者开发并维护。由于 SQLite 以其轻量级、零配置、嵌入式等特性被广泛应用于各种小型项目、移动应用和桌面应用中&#xff0c;而 SQLi…

Markdown 语法入门指南(VSCode 版)

此博客为一份详细的 Markdown 语法入门指南&#xff0c;专门针对在 VSCode 上使用 Markdown 的零基础用户。这份指南将包括 Markdown 的基础语法、在 VSCode 中的安装与使用方式、常见问题及注意事项。 Markdown 是一种轻量级标记语言&#xff0c;使用纯文本符号来标记格式&am…

PostgreSQL学习笔记:PostgreSQL vs MySQL

PostgreSQL 和 MySQL 都是广泛使用的关系型数据库管理系统&#xff0c;它们有以下一些对比&#xff1a; 一、功能特性 1. 数据类型支持 PostgreSQL&#xff1a;支持丰富的数据类型&#xff0c;包括数组、JSON、JSONB、范围类型、几何类型等。对于复杂数据结构的存储和处理非…

内存检测工具——Qt Creator

前言 检测内存错误的工具&#xff0c;有很多个&#xff0c;我今天粗浅的学了一下可在Qt上使用的工具们&#xff1a; Dr.Memory 工具之前我曾在关注的博主上看到相关的博客&#xff1a;C(Qt)软件调试---内存调试器Dr.Memory&#xff08;21&#xff09;_dr. memory-CSDN博客 今…

2.4 基于Vitest的单元测试基础设施搭建

文章目录 1. 现代单元测试体系解析测试金字塔演进Vitest核心定位2. 基础设施架构设计整体架构图3. 环境配置全流程3.1 基础环境搭建3.2 配置文件`vitest.config.ts`3.3 测试环境初始化4. 测试用例编写规范4.1 基础测试示例4.2 Vue组件测试4.3 异步逻辑测试5. Mock策略深度优化5…

⭐算法OJ⭐链表排序【归并排序】(C++/JavaScript 实现)

文章目录 148. Sort List解题思路归并排序的基本思想归并排序的步骤 实现实现步骤C 实现JavaScript 实现 复杂度总结 148. Sort List Given the head of a linked list, return the list after sorting it in ascending order. 解题思路 链表排序问题可以通过多种方法解决&am…

SegMAN模型详解及代码复现

SegMAN模型概述 模型背景 在深入探讨SegMAN模型之前&#xff0c;我们需要了解其研究背景。在SegMAN出现之前&#xff0c;计算机视觉领域的研究主要集中在以下几个方面&#xff1a; 手工制作方法&#xff0c;如SIFT基于卷积神经网络(CNN)的方法&#xff0c;如STN和PTN对平移、…

Manus AI:多语言手写识别的技术革命与未来图景

摘要&#xff1a;在全球化浪潮下&#xff0c;跨语言沟通的需求日益迫切&#xff0c;但手写文字的多样性却成为技术突破的难点。Manus AI凭借其多语言手写识别技术&#xff0c;将潦草笔迹转化为精准数字文本&#xff0c;覆盖全球超百种语言。本文从技术原理、应用场景、行业价值…

Stable Diffusion游戏底模推荐

一、基础通用型底模 SDXLbase &#x1f4da; 官方原版底模&#xff0c;支持1024x1024高清出图&#xff0c;适用于各类游戏场景和角色的基础生成&#xff0c;建议作为微调训练的基准模型。 来源: 相关搜索结果 写实风格搭配推荐 &#x1f3a8; 搭配 9realisticSDXL 或 麻袋real…

理解字符流和字节流,节点流和处理流、缓冲流、InputStreamReader、BufferInputStream、BufferReader...

DAY10.2 Java核心基础 IO流 字符流和字节流 字符流和字节流在每次处理数据的单位不同&#xff0c;一个是字符&#xff0c;一个是字节 如果复制文件类型是文本类型&#xff0c;字节流字符流都可以 如果复制的文件类型是非文本类型&#xff0c;则只能使用字节流&#xff0c;使…

DBeaver安装教程+连接TDengine数据库

为TDengine安装的DBeaver教程 安装 23.1.1 版本以上的DBeaver 因为官方文档说这个版本之上的DBeaver才支持TDengine内嵌前往DBeaver 官方文档进行版本下载滑到链接最下面点击进入 点击download&#xff0c;进入选择下载版本 等待下载成功即可双击自行安装 打开数据库连接TDen…

【三维重建】Proc-GS:使用3DGS的程序性城市建筑生成

标题&#xff1a;《Proc-GS: Procedural Building Generation for City Assembly with 3D Gaussians》 项目&#xff1a;https://city-super.github.io/procgs/ 来源&#xff1a;香港中文大学&#xff1b;上海人工智能实验室 等 文章目录 摘要一、 程序代码定义 (Procedural Co…

商业智能BI的未来,如何看待AI+BI这种模式?

昨天在和一位朋友线上聊天的时候&#xff0c;提了一个问题&#xff0c;你是如何看待AI&#xff08;人工智能&#xff09;BI&#xff08;商业智能&#xff09;这种模式和方向的&#xff0c;我大概来说一下我个人的看法。 以我在商业智能BI项目中接触到的行业和企业&#xff0c;…

25.3.12.Linux内核如何和设备树协同工作的?

1.编写设备树 cd arch/riscv/boot/dts/ 再cd到厂商,例如下述内容。 2.编译设备树(dts->dtb)通过dtc命令来转换 3.解析设备树 例如上述内容,都是对设备树的解析。 这里重点说一下内核对设备树的处理吧,因为这个内容是设备树的重点了。 从源代码文件 dts 文件开始

Flutter 基础组件 Text 详解

目录 1. 引言 2. 基本使用 3. 自定义样式 4. 文本对齐与溢出控制 5. 外边距 5.1 使用 Container 包裹 5.2 使用 Padding 组件 5.3 在 Row/Column 中使用 5.4 动态边距调整 5.5 关键区别说明 5.6 设置 margin 无效 6. 结论 相关推荐 1. 引言 Text 组件是 Flutter 中…

Torch 模型 model => .onnx => .trt 及利用 TensorTR 在 C++ 下的模型部署教程

一、模型训练环境搭建和模型训练 模型训练环境搭建主要牵扯 Nvidia driver、Cuda、Cudnn、Anaconda、Torch 的安装&#xff0c;相关安装教程可以参考【StarCoder 微调《个人编程助手: 训练你自己的编码助手》】中 5.1 之前的章节。 模型训练的相关知识可以参考 Torch的编程方…