数据结构之Map与Set(上)

news2024/12/30 2:43:38

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:数据结构(Java版) 

目录

二叉搜索树

Map和Set的介绍与使用 

Map的常用方法及其示例

Set的常用方法及其示例

哈希表 

冲突-概念

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

冲突-避免-负载因子调节


在正式学习Map和Set之前,我们先来学习一下:二叉搜索树。

二叉搜索树

二叉搜索树又称为二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树,也有该性质。如下图所示:

下面我们就来模拟实现二叉搜索树。

准备:

    // 二叉搜索树的节点
    static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;
        public TreeNode(int val) {
            this.val = val;
        }
    }
    // 根节点
    public TreeNode root;

插入元素:

思路:如果根节点为空,则插入的元素为根结点。如果根结点不为空,就要遍历二叉搜索树找到可以合适存放该元素的节点。在找的过程中,要注意:当前根结点的值小于要插入的元素,就遍历右子树继续找, 当前根结点的值大于要插入的元素,就遍历左子树继续找,直至根结点为空,开始插入操作。(二叉搜索树中没有相同的元素)

代码实现:

    // 插入(只有类似头插和尾插两种情况)
    public boolean insert(int key) {
        if (root == null) {
            root = new TreeNode(key);
            return true;
        }
        TreeNode prev = null; // 记录要插入的元素的根结点
        TreeNode cur = root;
        while (cur != null) {
            prev = cur;
            if (cur.val < key) {
                cur = cur.right;
            } else if (cur.val > key) {
                cur = cur.left;
            } else {
                // 不能插入相同的元素
                return false;
            }
        }
        if (prev.val > key) {
            prev.left = new TreeNode(key);
        } else {
            prev.right = new TreeNode(key);
        }
        return true;
    }

 查找元素:

思路:和插入的思路大体相同。

代码实现:

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

删除元素:

思路:要删除的元素有四种情况:

1、其左右子树都为空;

2、其左右子树都不为空;

3、其左子树为空,右子树不为空;

4、其左子树不为空,右子树为空。

我们只需要分类讨论进行删除操作即可。首先,还是得找到要删除的节点;其次就是开始分情况讨论处理要删除的节点:如果其左右子树都为空,那么就只需要其前一个节点指向它的指针置为null即可。如果其左右子树都不为空,那么就只能找到合适的节点来代替该节点,实现伪删除。这个合适的节点就是左子树最大值(左子树最右边的节点),以及右子树的最小值(右子树最左边的节点)任选其一即可。剩下的两种情况就只需要把要删除的节点的前一个节点的指向它的指针指向这个节点的下一个节点即可。

代码实现:

    // 删除
    public boolean remove(int key) {
        if (root == null) {
            // 抛异常
            throw new BinarySearchTreeIsEmptyException("搜索树为空异常");
        }
        // 开始寻找要删除的节点
        TreeNode cur = root;
        TreeNode prev = null;
        while (cur != null) {
            if (cur.val < key) {
                prev = cur;
                cur = cur.right;
            } else if (cur.val > key) {
                prev = cur;
                cur = cur.left;
            } else {
                // 找到了,开始删除
                removeOfKey(prev, cur);
                return true;
            }
        }
        // 走到这里就说明没有找到要删除的节点
        return false;
    }

    private void removeOfKey(TreeNode prev, TreeNode cur) {
        // 删除的节点有四种情况

        // 1、要删除的节点左右子树都为空
        if (cur.left == null && cur.right == null) {
            // 还得判断这个节点是在树的哪边
            if (cur == root) {
                root = null;
                return;
            } else if (prev.left == cur){
                prev.left = null;
                return;
            } else {
                prev.right = null;
                return;
            }
        }
        // 2、要删除的节点左右子树都不为空
        // 根结点和普通的节点都是一个情况,因此不用分类讨论
        if (cur.left != null && cur.right != null) {
            // 1、要么从左子树中找到最大值(左子树中最右边)代替根节点
            // 2、要么从右子树中找到最小值(右子树中最左边)代替根节点
            TreeNode tmp = root.left;
            while (tmp.right != null) {
                prev = tmp;
                tmp = tmp.right;
            }
            // 下面就是一个伪删除
            cur.val = tmp.val;
            prev.right = null;
            return;
            /*// 或者下面这种方式
            TreeNode tmp = root.right;
            while (tmp.left != null) {
                prev = tmp;
                tmp = tmp.left;
            }
            // 下面就是一个伪删除
            root.val = tmp.val;
            prev.left = null;
            return;*/
        }
        // 3、要删除的节点左子树为空,右子树不为空
        if (cur.left == null && cur.right != null) {
            if (cur == root) {
                root = root.right;
            } else if (prev.left == cur){
                prev.left = cur.right;
            } else {
                prev.right = cur.right;
            }
        }
        // 4、要删除的节点左子树不为空,右子树为空
        if (cur.left != null && cur.right == null) {
            if (cur == root) {
                root = root.left;
            } else if (prev.left == cur){
                prev.left = cur.left;
            } else {
                prev.right = cur.left;
            }
        }
    }

异常部分的代码:

public class BinarySearchTreeIsEmptyException extends RuntimeException {
    public BinarySearchTreeIsEmptyException(String msg) {
        super(msg);
    }

    public BinarySearchTreeIsEmptyException() {
        super();
    }
}

注意:插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:可能是一棵完全二叉树,也有可能是一棵单分支的树。 如下图所示:

而二叉搜索树就是Map和Set的不同子类的底层实现方式。

Map和Set的介绍与使用 

我们在学习数据结构之前的搜索大概就是这两种:

1、直接搜索:遍历这组数据看看有没有我们需要的数据;

2、二分查找:每次对半查找,虽然效率高,但是数据必须是有序的情况才可以。

难道除了上面两种查找,就没有其它的方法了吗?我们今天学习的Map和Set就是一种全新的搜索方法。

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

1、纯key 模型,比如:
有一个英文词典,快速查找一个单词是否在词典中;
快速查找某个名字在不在通讯录中;
2、Key-Value 模型,比如:
统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>;
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号;
而Map中存储的就是key-value的键值对,Set中只存储了Key。

通过上图可知:Map是一个单独的接口,而Set是继承了Collection的接口,那么Set也就有了一些add、remove、contains这种Collection之中的方法了。 

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 对应的映射关系(key 和 value 都被删除了)
Set<K> keySet()返回所有 key 的不重复集合(也就是说这个集合中不存在重复的元素)
Collection<V> values()返回所有 value 的可重复集合
 Set<Map.Entry<K, V>> entrySet()返回所有的 key-value 映射关系
boolean containsKey(Object key)判断是否包含 key
boolean containsValue(Object value)判断是否包含value

注意:

1、Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap;
2、Map中存放键值对的Key是唯一的,value是可以重复的;
3、在TreeMap中插入键值对时,key不能为空,否则就会抛NulliPointerException异常,value可以为空。但是HashMap的key和value都可以为空。因为 TreeMap底层就是一棵二叉搜索树(也叫作红黑树),既然是一棵二叉搜索树,那么存放的元素肯定是要可以比较的(自定义类要么实现比较器,要么继承Comparable接口实现compareTo方法)
4、Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
5、Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
6、Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入(这其实不能叫做修改了)。

TreeMap的使用

public class Test {
    public static void main(String[] args) {
        Map<String, Integer> map = new TreeMap<>();

        map.put("a", 1);
        map.put("b", 2);
        map.put("c", 3);
        map.put("d", 4);
        map.put("f", 5);

        // 注意:如果没找到对应的key就会返回null
        System.out.println(map.get("a")); // 输出1
        System.out.println(map.get("e")); // 输出null

        System.out.println(map.getOrDefault("a", 100)); // 输出1
        System.out.println(map.getOrDefault("e", 100)); // 输出100

        // 返回key对应的Set集合(key-String)
        Set<String> set = map.keySet();

        // 返回value对应的Collection集合(value-Integer)
        Collection<Integer> collection = map.values();

        // 把Map中的元素全部存放到Set中
        Set<Map.Entry<String, Integer>> mapSet = map.entrySet();

        // 判断Map中是否有对应的key
        System.out.println(map.containsKey("a")); // 输出true
        System.out.println(map.containsKey("e")); // 输出false

        // 判断Map中是否有对应的value
        System.out.println(map.containsValue(1)); // 输出true
        System.out.println(map.containsValue(6)); // 输出false
    }
}

注意:如果Map中已经存在了key,再去put的话,就只会更改其value值。 

HashMap的使用与TreeMap的使用是类似的。

有以下几点要注意:

1、HashMap在存放元素时,不需要这个元素是否可以比较。因为HashMap的底层不是一个二叉搜索树。

Set的常用方法及其示例

常用方法的介绍
方法解释
boolean add(E e)添加元素,但重复元素不会被添加成功(天然的去重功能)
void clear()清空集合
boolean contains(Object o)判断 o 是否在集合中
Iterator<E> iterator返回迭代器(继承了 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中,可以达到去重的效果

 注意:
1、Set中只存储了key,并且要求key一定要唯一;
2、TreeSet的底层是使用Map来实现的,其使用key与Object作为一个默认对象作为键值对插入到Map中的;

3、Set最大的功能就是对集合中的元素进行去重;
4、实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序;
5、Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入;
7. TreeSet中不能插入 null 的key(一定要是可比较的对象),HashSet可以。

TreeSet的使用

public class Test {
    public static void main(String[] args) {
        Set<String> set =new TreeSet<>();

        set.add("abc");
        set.add("def");
        set.add("ghi");

        System.out.println(set.contains("abc")); // 输出true
        System.out.println(set.contains("iii")); // 输出false

        // 获取迭代器
        Iterator<String> stringIterator = set.iterator();
        // 利用迭代器来遍历Set
        while (stringIterator.hasNext()) {
            System.out.print(stringIterator.next()+" ");
        }
        System.out.println();

        // 向下转型会有风险,而且也不一定会强转成功(这里就会失败)
        //String[] strs = (String[]) set.toArray();
        Object[] str =  set.toArray();

        for (int i = 0; i < set.size(); i++) {
            System.out.print(str[i]+" ");
        }
    }
}

哈希表 

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。

顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2 N),搜索的效率取决于搜索过程中
元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素(就和我们在数组中查找元素一样,利用下标O(1)查找)。

当向该结构中: 

插入元素:
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key)= key % capacity;capacity为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。

但是又会出现另外一个问题:如果往集合中插入44,那么就会出现冲突的现象。

冲突-概念

对于两个数据元素的关键字 i 和 j (i != j),有 i != j ,但有:Hash(i) == Hash(j),即:不同关键字通过相同的哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词” 。

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

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

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。哈希函数设计原则:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;
2、哈希函数计算出来的地址能均匀分布在整个空间中;
3、哈希函数应该比较简单。
常见哈希函数
1. 直接定制法
取关键字的某个线性函数为散列地址:Hash(Key)=A*Key+B。

优点:简单、均匀;缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况。
2.除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key)= key% p(p<=m),将关键码转换成哈希地址。

冲突-避免-负载因子调节

由上面两张图片可知:

1、负载因子随着插入的元素越来越多,其值也是越来越大,并且冲突率也是越来越高;

2、降低负载因子,就可以有效的降低冲突率,而在插入元素个数不变的情况下,只能增大散列表的长度,也就是扩容。

总结:哈希表是一种查找元素非常高的数据结构,但是也有缺点,会产生哈希冲突,虽然可以通过设计哈希函数和调节负载因子来避免发生冲突,但是还是不可避免的,因此当冲突发生时,我们就要想着来解决了,下一篇博客,我们再来一起学习吧!

好啦!本期 数据结构之Map与Set(上)的学习之旅就到此结束啦!我们下一期再一起学习吧!

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

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

相关文章

客户管理系统平台(CRM系统)是什么?它的核心主要解决哪些问题?

客户管理系统平台CRM是什么&#xff1f;客户关系管理系统CRM的核心主要解决哪些问题&#xff1f; CRM系统不仅仅是一套软件&#xff0c;更是一种策略&#xff0c;一种管理理念和一种企业发展方向。它通过整合客户数据、优化业务流程、提升客户体验&#xff0c;帮助企业在激烈的…

K8s第三节:k8s1.23.1升级为k8s1.30.0

上回书说到我们使用了kubeadm安装了k8s1.23.1,但是在k8s1.24之前还是使用docker作为容器运行时&#xff0c;所以这一节我打算将我安装的k8s集群升级为1.30.0版本&#xff1b; 1、修改containerd 配置 因为我们安装的docker自带containerd&#xff0c;所以我们不需要重新安装con…

蓝凌EKP二次开发资料大全 完整蓝凌二次开发资料 蓝凌 EKP开发实战教程 蓝凌OA二次开发资料大全 蓝凌OA java开发快速入门

蓝凌EKP二次开发资料大全 完整蓝凌二次开发资料 蓝凌 EKP开发实战教程 蓝凌OA二次开发资料大全 记得两年前花了非常贵的费用去现场学习的资料&#xff0c;把这些开发技术文档分享出来&#xff0c;希望通过这个资料&#xff0c; 为大家学习开发大大减少时间。期待大家能快速上…

《UE5_C++多人TPS完整教程》学习笔记32 ——《P33 动画蓝图(Animation Blueprint)》

本文为B站系列教学视频 《UE5_C多人TPS完整教程》 —— 《P33 动画蓝图&#xff08;Animation Blueprint&#xff09;》 的学习笔记&#xff0c;该系列教学视频为 Udemy 课程 《Unreal Engine 5 C Multiplayer Shooter》 的中文字幕翻译版&#xff0c;UP主&#xff08;也是译者…

Python实战:类

一、圆的面积、周长 class Circle:# 初始化一个类参数&#xff1a;rdef __init__(self,r):self.r r# 计算面积的方法def get_area(self):return 3.14*pow(self.r,2)# 计算周长的方法def get_perimeter(self):return 2*3.14*self.r#创建对象 r eval(input(请输入圆的半径&…

Vue 2 和 Vue 3 生命周期钩子

Vue 2 和 Vue 3 生命周期钩子 在 Vue.js 开发中&#xff0c;了解生命周期钩子对于编写有效的组件至关重要。Vue 2 和 Vue 3 在生命周期钩子上大致相同&#xff0c;但 Vue 3 的 Composition API 引入了一种新的方式来处理它们。这里我会概述两者的生命周期钩子&#xff0c;并指…

2024年8月7日(mysql主从 )

回顾 主服务器 [rootmaster_mysql ~]# yum -y install rsync [rootmaster_mysql ~]# tar -xf mysql-8.0.33-linux-glibc2.12-x86_64.tar [rootmaster_mysql ~]# tar -xf mysql-8.0.33-linux-glibc2.12-x86_64.tar.xz [rootmaster_mysql ~]# cp -r mysql-8.0.33-linux-glibc2.…

QT找不到编辑框

问题展示&#xff1a; 解决办法&#xff1a;ALT0 然后我的变成了这种&#xff1a; 解决办法&#xff1a;文件系统改变成项目&#xff1a;

DNTR——F

文章目录 AbstractIntroductionContribution Related WorkAdvancements in Feature Pyramid Networks (FPNs)Coarse-to-Fine Image Partitioning in Drone Imagery DetectionDevelopments in Loss Function Approaches for Tiny Object DetectionR-CNN for Small Object Detect…

大炼模型进入尾声,“失眠”的欧洲和日本能否扳回一局?

大数据产业创新服务媒体 ——聚焦数据 改变商业 2022年末&#xff0c;ChatGPT-3.5的惊艳亮相&#xff0c;瞬间引爆了全球范围内的生成式AI&#xff08;GenAI&#xff09;热潮。 这场现代版的"淘金热"迅速在科技领域蔓延&#xff0c;尤其是在全球两大科技强国——中国…

简单分享下python打包手机app的apk

Python 把python程序打包成apk的完整步骤 1. 引言 在移动应用市场蓬勃发展的今天&#xff0c;开发人员常常需要将自己的Python程序打包成APK文件&#xff0c;以便在Android设备上运行。本文将详细介绍将Python程序打包成APK的完整步骤。 2. 准备工作 在开始打包前&#xff0c…

全网最详解LVS(Linux virual server)

目录 一、LVS&#xff08;Linux virual server&#xff09;是什么&#xff1f; 二、集群和分布式简介 2.1、集群Cluster 2.2、分布式 2.3、集群和分布式 三、LVS运行原理 3.1、LVS基本概念 3.2、LVS集群的类型 3.2.1 nat模式 3.2.2 DR模式 3.2.3、LVS工作模式总结 …

RSYSLOG收到华为防火墙日志差8小时的解决方法

RSYSLOG收到华为防火墙日志差8小时 这个问题其实不关Rsyslog配置的事&#xff0c;只要修改华为墙的配置就好 处理方法&#xff1a; info-center loghost 172.18.6.91 language Chinese local-time 在华为web界面添加ip是不会添加local-time这个参数的&#xff0c; 需要在命令…

sqli-labs第二关详解

首先让id1&#xff0c;正常显示&#xff0c;接着尝试and 11和and 12 and 11正常&#xff0c;and 12不正常 所以可以判断是数字型注入&#xff0c;使用order by 判断列数&#xff0c;发现有三个字段 使用union语句&#xff0c;找出能显示信息的地方 接下来就是找出数据库名称和版…

Leetcode75-7 除自身以外数组的乘积

没做出来 本来的思路是遍历一遍得到所有乘积和然后除就行 但是题目不能用除法 答案的思路 for(int i0;i<n;i) //最终每个元素其左右乘积进行相乘得出结果{res[i]*left; //乘以其左边的乘积left*nums[i];res[n-1-i]*right; //乘以其右边的乘积right*nums[n-1-i]…

搭建 Web 群集Haproxy

案例概述 Haproxy 是目前比较流行的一种群集调度工具&#xff0c;同类群集调度工具有很多&#xff0c;如 LVS 和Nginx。相比较而言&#xff0c;LVS 性能最好&#xff0c;但是搭建相对复杂;Nginx 的upstream模块支持群集功能&#xff0c;但是对群集节点健康检查功能不强&#xf…

海量数据处理商用短链接生成器平台 - 10

第二十一章 短链服务冗余双写-链路测试和异常消息处理实战 第1集 冗余双写MQ架构-消费者配置自动创建队列和集群测试 简介&#xff1a; 冗余双写MQ架构-MQ消费者配置自动创建队列 controller-service层开发配置文件配置MQ ##----------rabbit配置-------------- spring.rab…

古彝文——唯一存活的世界六大古文字

关注我们 - 数字罗塞塔计划 - 早在五千年前&#xff0c;彝族的先祖就发明了十月太阳历&#xff0c;成为中华文明的重要创造者之一&#xff1b;同时&#xff0c;彝族的先祖也创制了古彝文&#xff0c;开创了独具特色的彝族文化。古彝文也被称为古夷文、传统彝文&#xff0c;是相…

Unity补完计划 之 动态控制TileMap

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 1.TileMap &TileBase Unity - Scripting API: Tilemap &#xff0c;看手册内容太多了故介绍几个常用的公共方法 首…

一个IT能拖垮整个公司?你若不信,看完此文再来评论

看到文章的标题&#xff0c;你也许会心生疑惑&#xff1a;一个IT真的能拖垮整个公司吗&#xff1f;也可能会觉得我根本就是在哗众取宠、博人眼球。 而我要说&#xff0c;在特定的条件下&#xff0c;这真的不夸张&#xff0c;你若不信&#xff0c;且听我娓娓道来&#xff0c;看完…