数据结构:Map和Set(1)

news2024/10/6 12:38:14

搜索树

概念

若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

这棵树的中序遍历结果是有序的

接下来我们来模拟一棵二叉搜索树,和二叉树一样,定义左右结点,结点值和根结点

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;
}

查找

拿目标值key与root进行比较,比root大的往右边搜索,比root小的往左边搜索;接着继续与左/右子树根结点比较,重复上面步骤

搜索代码

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

时间复杂度:

最好情况:完全二叉树,那时间复杂度为O(logN)

最坏情况:单分支二叉树,时间复杂度O(N)

插入

对于下方的二叉树,我们需要插入15

我们可以定义一个cur,负责遍历二叉树;定义一个parent记录子树的根结点位置

如果当前cur结点的值比目标插入的值小,就把parent定位到当前结点,把cur往右子树移动

如果cur值较大就向左子树移。cur负责帮助parent定位到目标值附近

定义一个node承载目标值,如果parent的值小于目标值就把node右插,反之则左插

    public boolean search(int key){
        TreeNode cur = root;
        while(cur != null){
            if(cur.val < key){
                cur = cur.right;
            } else if (cur.val>key) {
                cur = cur.left;
            }else{
                return true;
            }
        }
        return false;
    }
    public static boolean insert(TreeNode root, int val){
        if(root == null){
            root = new TreeNode(val);
            return true;
        }
        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 false;
            }
        }
        TreeNode node = new TreeNode(val);
        if(parent.val>val){
            parent.left = node;
        }else{
            parent.right = node;
        }
        return true;
    }

拿这么一个列表来测试一下

array = {5,12,3,2,11,15}

正常画出来的图是这样的

测试debug后画出来的图一模一样😊

 删除

设待删除的结点是cur,而待删除结点的双亲结点是parent

第一种情况:cur.left == null

1. cur是root时,让root = cur.right

2.cur不是root,cur是parent.left,则parent.left = cur.right

3.cur不是root,cur是parent.right,则parent.right = cur.right

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

第二种情况:cur.right == null

1.cur是root,则root = cur.left

2.cur不是root,cur是parent.left,则parent.left = cur.left

3.cur不是root,cur是parent.right,则parent.right = cur.left

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

第三种情况:cur.left != null && cur.right != null

我们逍遥删除cur位置这个40元素

首先确定的一点是,40的左子树比40都小,右子树比40都大

替换法:找一个数据来替换cur

1.确定cur这里将来要放的数据一定比左边都大,比右边都小

2.要么在左树里面找到最大的数值(左树最右边的数据);

要么就在右树里面找到最小的数值来替换(右树最左边的数据)

3.找到合适的数据之后,直接替换cur的值,然后删除那个合适的数据结点

        else{
            //找到合适的值(从右子树找最小值)
            //target负责找到合适值,targetParent负责记录target的双亲结点
            TreeNode targetParent = cur;
            TreeNode target = cur;
            while(target.left != null){
                targetParent = target;
                target = target.left;
            }
            cur.val = target.val;
            
            //删除target,因为已经到最左边了,所以直接让parent的左边 = target的右边
            //就算右边为空也没关系
            targetParent.left = target.right;
        }

其实这个代码还存在一个问题,如下图,我们找到50为最小值进行替换,替换完删除50的时候执行targetParent.left = target.right; 但是变成20改为55了,这明显不对劲

也就是说,上面的代码只适合targetParent.left = target的情况

遇到这种targetParent.right = target的情况,需要让targetParent的右边 = target的右边

代码修改(只修改删除target的部分)

            if(targetParent.left == target){
                targetParent.left = target.right;
            }else{
                targetParent.right = target.right;
            }

不想看分析可以直接看这

整个的代码:

    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);
                }
            }
        }

    private void removeNode(TreeNode cur, TreeNode parent) {
        if(cur.left == null){
            if(cur == root){
                root = parent.right;
            }else if(cur == parent.left){
                parent.right = cur.left;
            }else{
                parent.right = cur.right;
            }
        }else if(cur.right == null) {
            if(cur == root){
                root = parent.left;
            }else if(cur == parent.left){
                parent.left = cur.left;
            }else{
                parent.right = cur.left;
            }
        }else{
            //找到合适的值(从右子树找最小值)
            //target负责找到合适值,targetParent负责记录target的双亲结点
            TreeNode targetParent = cur;
            TreeNode target = cur;
            while(target.left != null){
                targetParent = target;
                target = target.left;
            }
            cur.val = target.val;

            //删除target
            if(targetParent.left == target){
                targetParent.left = target.right;
            }else{
                targetParent.right = target.right;
            }
        }
    }

Map的使用

搜索

我们学过的搜索

1.直接遍历: --> O(N),速度较慢

2.二分查找:--> O(logN),但要求搜索的序列有序

上面的搜索都属于静态搜索

而现实中我们需要在查找时进行一些插入和删除操作,就需要用到Map和Set这两个适合动态查找的容器了

Map属于Key-Value 模型 Key-Value 模型比如:
统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数: < 单词,单词出现的次数 >
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号

 Map有两种形式,二叉搜索树和哈希表

        Map<String,Integer> map = new TreeMap<>();//二叉搜索树, 查找复杂度O(logN)
        Map<String,Integer> map1 = new HashMap<>();//哈希表, 查找复杂度O(1) 哈希表-->数组+列表+红黑树

方法

put设置key和对应的value 

get通过key来找到对应的值 

如果找不到就返回null

getOrDefault方法和get差不多,只不过找不到就默认返回自己设置的返回值

keySet获取所有的key

entrySet返回key-value的所有关系

Set<Map.Entry<String,Integer>> entrySet = map.entrySet();

而Set是所有Entry的集合

注意:

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

2.Map里的键值对中key是唯一的,但value是可以重复的,以最后一个value为准

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


Set的使用

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

方法

iterator方法,输出每个key,每行1个 

注意:

1.Set是继承自Collection的一个接口类

2.TreeSet的底层是TreeMap

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


哈希表

概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,
必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数
理想搜索方法:可以不经过比较,一次直接从表中得到要搜索的元素,通过该元素的存储位置和它的关键码之间建立一一映射的关系

这种函数叫做哈希函数:hash(key) = key % capacity

capacity是存储元素底层空间的大小

 哈希冲突:不同的关键字key,通过相同的哈希函数,得到相同的值

哈希冲突无法解决,只能降低冲突率

哈希函数的设计

合理的哈希函数可以降低冲突率

原则:

1.哈希函数定义域包括需要存储的全部关键码,如果散列表允许有m个地址时,值域必须在0到m-1之间

2.哈希函数计算出来的地址能均匀分布在整个空间中

3.哈希函数比较简单

常见的哈希函数

直接订制法:取关键字的某个线性函数为散列地址

优点:简单、均匀
缺点:需要事先知道关 键字的分布情况
使用场景:适合查找比较小且连续的情况
  387. 字符串中的第一个唯一字符 - 力扣(LeetCode)
    public int firstUniqChar(String s) {
        int[] count = new int[26];
        for(int i = 0; i < s.length(); i++){
            char ch = s.charAt(i);
            count[ch-'a']++;
        }
        for(int i = 0; i< s.length();i++){
            char ch = s.charAt(i);
            if(count[ch-'a'] == 1){
                return i;
            }
        }
        return -1;
    }

除留余数法

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

负载因子调节 

由图片我们可以知道负载因子越高,冲突率逐渐增加

填入表里面的元素已经是固定的情况下,为了使负载因子降低,我们只能让散列表扩容


冲突避免方法

闭散列

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

那么怎么找到这个“下一个”位置呢?

1.线性探测法:

比如我们要在这个线性表中放入44,14,24,34,54

44与4冲突了,探测到4下一个的位置时空的,就把44放进去,后面的14,24,34挨个放进去空位置,到了54,后面没有位置可以放了,返回到前面继续探测,找到0位置 

但是线性探测有个缺点,产生冲突的数据会堆在一块 

2.二次探测

或者

H0时通过散列函数对关键码key计算出来的位置,m是表的大小,i = 0,1,2,3....代表的是要插入的数据排在第几位

 这样明显不会堆在一起,而是均匀散开来了


开散列/哈希桶

又叫链地址法(开链法),采用数组+链表(+红黑树,当数组长度>64 && 链表长度 >=8 之后才会把链表变成红黑树)的方法,把冲突的元素采用尾插法插入被冲突元素的后面(JDK 1.7之前采用头插法,JDK 1.8之后采用尾插法)

数组的每个元素是链表的头节点


手搓一个哈希桶

初始化链表

每个结点需要有三个域,key,value和next

    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 usedSize;//记录存放的有效数据

插入元素 

第一步:首先用cur进行遍历,遍历index下标的链表是否存在key,如果存在就更新value,遍历完还不存在就插入元素

    public void put(int key, int val){
        int index = key % array.length;
        //遍历index下标的链表是否存在key,如果存在就更新value,不存在就插入数据
        Node cur = array[index];
        while(cur!=null){
            if(cur.key == key){
                //更新value
                cur.val = val;
            }
            cur = cur.next;
        }
        //cur==null-->遍历完没有找到key
        // 头插法
        Node node = new Node(key,val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
    }

第二步:计算负载因子

如果负载因子仍然大于默认的最低负载因子,则散列表需要进行扩容

    public static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private float doLoadFactor(){
        return usedSize * 1.0f / array.length;
    }

面试题:可以这样进行扩容吗?

        if(doLoadFactor()>DEFAULT_LOAD_FACTOR){
            //扩容
            array = Arrays.copyOf(array, 2*array.length);
        }

都这么问了,那很明显是不行的😊

假设我们的11元素经过哈希之后放在插入到1下标这里

经过扩容之后,capacity发生了改变,变成了20

根据hash(key) = key % capacity,此时11%20 = 11

因为长度改变,原来冲突的元素放到了其他位置中去,所以这样扩容是不行滴

设置cur遍历原来的数组,每遍历到一个元素就纵向遍历链表的元素并进行头插法

代码: 

注意:这里为什么要用一个tmp保存cur.next呢?

所以我们用一个tmp来保存原来的纵向遍历时11元素的地址


获取元素

    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;
        }
        return -1;
    }

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

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

相关文章

串口通信(3)-接收一组固定长度的数据

本文为博主 日月同辉&#xff0c;与我共生&#xff0c;csdn原创首发。希望看完后能对你有所帮助&#xff0c;不足之处请指正&#xff01;一起交流学习&#xff0c;共同进步&#xff01; > 发布人&#xff1a;日月同辉,与我共生_单片机-CSDN博客 > 欢迎你为独创博主日月同…

Godot4实现游戏的多语言版本

要在Godot 4中实现多语言版本的游戏&#xff0c;您需要按照以下几个步骤来设置和管理游戏文本以及可能的其他资源&#xff0c;如图像或声音。以下是根据官方文档和详细教程整理的简明指南&#xff1a; 准备翻译文件&#xff1a; Godot支持使用.csv文件或.po文件进行国际化​​…

C语言习题整理①

一些C语言习题的整理。 目录 一、判断质数 二、判断回文数 三、判断水仙花数 四、输出乘法表 五、输出杨辉三角 一、判断质数 质数是指在大于1的自然数中&#xff0c;除了1和它本身以外不再有其他因数的自然数。质数又称素数。一个大于1的自然数&#xff0c;除了1和它自身…

数据结构与算法C语言版学习笔记(2)-线性表、顺序存储结构的线性表

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 数据结构部分的知识框架一、线性表的定义和特点1.定义2.特点 二、线性表的实际案例引入1.案例一&#xff1a;多项式的加减乘除2.案例二&#xff1a;当多项式是稀疏多…

AI 女友突然下线,大叔集体「崩溃」;谷歌聊天机器人称谷歌滥用垄断力量丨 RTE 开发者日报 Vol.78

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

微服务项目,请求从发出到后端处理器的历程

点击登录按钮,发出 http://localhost:8803/service_6001/admin/login/in请求,这是一个由nginx配置的前端项目 查看配置文件,该条请求会被映射形成对http://localhost:51603/admin/login/in的post请求 upstream heima-admin-gateway {server localhost:51603; } server {liste…

笔记50:正则表达式入门宝典

引自&#xff1a;正则表达式是什么? - 知乎 中“龙吟九野”所写的一个回答&#xff0c;个人感觉看完之后如同醍醐灌顶&#xff0c;查了很多资料都没有这篇文章写的基础和通透&#xff0c;感觉是正则表达式扫盲好文&#xff0c;所以搬运一下&#xff0c;侵权删&#xff0c;感谢…

吃透BGP,永远绕不开这些基础概述,看完再也不怕BGP了!

你们好&#xff0c;我的网工朋友。 总有人在私信里抱怨&#xff0c;BGP实在是太难了&#xff01; 一是这玩意儿本来就很复杂&#xff0c;需要处理大量的路由信息和复杂的算法&#xff1b;再一个是需要你有一定的实战经验才能深入理解运作。 虽然BGP确实有一定难度&#xff0c…

简简单单入门Makefile

笔记来源&#xff1a;于仕琪教授&#xff1a;Makefile 20分钟入门&#xff0c;简简单单&#xff0c;展示如何使用Makefile管理和编译C代码 操作环境 MacosVscode 前提准备 新建文件夹 mkdir learn_makefile新建三个cpp文件和一个头文件 // mian.cpp #include <iostrea…

fio数据整理之二

fio数据简单抓取 上文我们完成了一些fio output数据的简单抓取&#xff0c;本文将针对抓取的数据做进一步的处理&#xff0c;输出到表格之中&#xff0c;方便我们查看&#xff0c;统计结果。 本文先使用最简单的方法创建csv档案 我们现有个基本认知&#xff0c;在csv档案中&am…

通过you-get命令行工具下载B站等常见网站高清视频

参考&#xff1a; you-get&#xff1a;https://github.com/soimort/you-get EditThisCookie 源码&#xff1a;https://github.com/ETCExtensions/Edit-This-Cookie EditThisCookie 官网&#xff1a;https://www.editthiscookie.com/ you-get 是一个非常好用的命令行版视频下载工…

[wp]NewStarCTF 2023 WEEK5|WEB

前言:比赛是结束了&#xff0c;但我的学习还未结束&#xff0c;看看自己能复习几道题吧&#xff0c;第四周实在太难 Final 考点&#xff1a; ThinkPHP 5.0.23 RCE一句话木马上传SUID提权&#xff08;find&#xff09; 解题: 首先页面就给了ThinkPHP V5&#xff0c; 那无非考…

java制作游戏,如何使用libgdx,入门级别教学

第一步&#xff0c;进入libgdx的官网。点击get started 进入这个页面&#xff0c;点击setup a project 进入这个页面直接点击&#xff0c;Generate a project. 点击下载&#xff0c;下载创建工具 它会让你下载一个jar包&#xff0c;有java环境的人可以双击直接打开。 把android…

Linux学习之vim跳转到特定行数

参考的博客&#xff1a;《Vim跳到最后一行的方法》 《oeasy教您玩转vim - 14 - # 行头行尾》 《Linux&#xff1a;vim 中跳到首行和最后一行》 想要跳到特定行的话&#xff0c;可以在命令模式和正常模式进行跳转。要是对于vim的四种模式不太熟的话&#xff0c;可以到博客《Linu…

Arrays.asList() 和 List.of() 的列表之争

1. 概述 有时在Java中&#xff0c;为了方便&#xff0c;我们需要创建一个小列表或将数组转换为列表。Java 为此提供了一些辅助方法。 在本文中&#xff0c;我们将比较初始化小型临时数组的两种主要方法&#xff1a;List.of()和 Array.asList()。 2. Arrays.asList() Java 自…

跨境电商商城源码,助力商家全球布局(多语言切换\多货币转换\多商户入驻)

今天&#xff0c;我们要给大家介绍一款强大且多元化的跨境电商解决方案——WoShop跨境电商源码!这款源码拥有许多令人惊叹的功能&#xff0c;其中最引人注目的就是支持多语言切换、多货币转换以及多商户入驻! 设想一下&#xff0c;你是一个跨境电商的卖家&#xff0c;你的业务遍…

Cannot read properties of undefined (reading ‘prototype‘)

用vue注册的用import ElementUI from "element-ui"是不行的要用 import ElementUI from “element-plus”

【JAVA学习笔记】63 -坦克大战1.3-敌方发射子弹,击中坦克消失并爆炸,敌人坦克随机移动,规定范围限制移动

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter18/src/com/yinhai/tankgame1_3 〇、要求 增加功能 1.让敌人的坦克也能够发射子弹(可以有多颗子弹) 2.当我方坦克击中敌人坦克时&#xff0c;敌人的坦克就消失,如果能做出爆炸效果更好. …

Thread类的基本操作(JAVA多线程)

线程是操作系统中的概念&#xff0c;操作系统内核实现了线程这样的机制&#xff0c;并提供了一些API供外部使用。 JAVA中 Thread类 将系统提供的API又近一步进行了抽象和封装&#xff0c;所以如果想要使用多线程就离不开 Thread 这个类。 线程的创建(Thread类) 在JAVA中 创建…

SPSS多元方差分析

前言&#xff1a; 本专栏参考教材为《SPSS22.0从入门到精通》&#xff0c;由于软件版本原因&#xff0c;部分内容有所改变&#xff0c;为适应软件版本的变化&#xff0c;特此创作此专栏便于大家学习。本专栏使用软件为&#xff1a;SPSS25.0 本专栏所有的数据文件请点击此链接下…