数据结构——二叉搜索树、Map和Set

news/2024/11/13 6:35:58/

对于不同的数据结构,他们的使用场景是不一样的,map和set这两种数据结构主要用在搜索相关的场景中。学习这些之前我们先来了解一下二叉搜索树,

一、搜索树

1.1概念

二叉搜索树 又称 二叉排序树 ,它或者是一棵空树,或者是具有以下性质的二叉树 :
<1>左子树上所有的值都小于根节点的值;
<2>右子树上所有的值都大于根结点的值;
<3>子树也是一颗二叉搜索树;


1.2 搜索树操作

1.2.1树的查找操作

代码如下:

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


1.2.2树的插入操作

与树的查找操作一样,也需要与树的节点进行大小比较,cur的移动方法与查找相同,当我们还需要定义一个parent记录cur的父亲节点,当cur为空时,要插入的值就可以通过与parent节点的值比较大小来确定插入到parent的左节点还是右节点

public void insert(int val) {if (root == null) {root = new TreeNode(val);return;}TreeNode cur = root;//记录cur的父亲节点TreeNode parent = null;while (cur != null) {parent=cur;if (val > cur.val) {cur = cur.right;} else if(val<cur.val){cur = cur.left;}else{//二叉搜索树不能有两个一样的值return;}}if(val>parent.val){parent.right=new TreeNode(val);}else{parent.left=new TreeNode(val);}}

1.2.3树的删除操作

实际上,在情况一和情况三中还分别有三种小情况:

代码如下:

private void removeNode(TreeNode parent, TreeNode cur) {//情况1:cur的左边为空if(cur.left==null){if(cur==root){//cur是根结点root=cur.right;}else if(parent.right==cur){//parent的右节点是curparent.right=cur.right;}else if(parent.left==cur){//parent的左节点是curparent.left=cur.right;}}else if(cur.right==null){//情况2:cur的右边为空if(cur==root){//cur是根结点root=cur.left;}else if(parent.right==cur){//parent的右节点是curparent.right=cur.left;}else{//parent的左节点是curparent.left=cur.left;}}else{//情况3:cur的左右都不为空TreeNode rightMin = cur.right;TreeNode parent2=cur;//记录rightMin的父亲节点while(rightMin.left!=null){parent2=rightMin;rightMin=rightMin.left;}//找到右树最小值,将cur.val覆盖,相当于删除了这个节点cur.val=rightMin.val;//删除rightMin节点,与情况一方法相同if(parent2.left==rightMin) {parent2.left = rightMin.right;}else{parent2.right=rightMin.right;}}}

1.2.4复杂度分析
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: O( logN)
最差情况下,二叉搜索树退化为单支树,其平均比较次数为: O(N)
(当插入的数据有序时,二叉搜索树退化成一颗单分支的树,时间复杂度最大)
1.2.5  java 类集的关系
TreeMap TreeSet java 中利用搜索树实现的 Map Set ;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容后序再进行讲解。

二、搜索

2.1概念与场景

Map set 是一种 专门用来进行搜索的容器或者数据结构 ,其搜索的效率与其具体的实例化子类有关 。以前常见的
搜索方式有:
1. 直接遍历 ,时间复杂度为 O(N) ,元素如果比较多效率会非常慢
2. 二分查找 ,时间复杂度为O(logN),但搜索前必须要求序列是 有序
上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:
1. 根据姓名查询考试成绩
2. 通讯录,即根据姓名查询联系方式
3. 不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,本节介绍的 Map和Set是一种适合动态查找的集合容器

2.2模型

2.2 模型
一般把搜索的数据称为关键字( Key ),和关键字对应的称为值( Value ),将其称之为 Key-value 的键值对,所以模型会有两种:
1. 纯 key 模型 ,比如:
    <1>有一个英文词典,快速查找一个单词是否在词典中
    <2>快速查找某个名字在不在通讯录中
2. Key-Value 模型 ,比如:
    <1>统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数: < 单词,单词出现的次数 >
    <2>梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
Map中存储的就是key-value的键值对,Set中只存储了Key

三、Map的使用

Map 是一个接口类,该类没有继承自 Collection ,该类中存储的是 <K,V> 结构的键值对,并且 K 一定是唯一的,不可以重复。

3.1Map.Entry<k,v>

Map.Entry<K, V> Map 内部实现的用来存放 <key, value> 键值对映射关系的内部类 ,该内部类中主要提供了<key, value>的获取, value 的设置以及 Key 的比较方式。

!!!Map.Entry<k,v>中没有设置key的方法。 


3.1Map的常用方法

注意:
1. Map 是一个 接口 ,不能直接实例化对象 ,如果 要实例化对象只能 实例化其实现类TreeMap或者HashMap
2. Map 中存放键值对的 Key是唯一的,value是可以重复
3. TreeMap中插入键值对时,key不能为空 ,否则就会抛 NullPointerException 异常 value 可以为空。但是 HashMap的key和value都可以为空
4. Map 中的 Key 可以全部分离出来,存储到 Set 来进行访问 ( 因为 Key 不能重复 )
5. Map 中的 value 可以全部分离出来,存储在 Collection 的任何一个子集合中 (value 可能有重复 )
6. Map 中键值对的 Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入
7. TreeMap HashMap 的区别(HashMap后面会说到)
!!! entrySet方法中返回的映射关系相当于搜索树的每一个节点

四、Set的使用

4.1Set的常用方法

!!!Map不能使用迭代器,只有Set才可以,原因是Map没有实现Iterable接口,但是我们可以使用entrySet方法将Map的键值对放入Set中,在通过Set使用迭代器间接遍历Map

 public static void main(String[] args) {TreeSet<String> treeSet = new TreeSet<>();treeSet.add("abcd");treeSet.add("hello");treeSet.add("def");//判断元素是否存在于集合中,存在返回trueSystem.out.println(treeSet.contains("hello"));//迭代器遍历Iterator<String> it = treeSet.iterator();while(it.hasNext()){System.out.println(it.next()+" ");}}

注意:
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可以
8. TreeSet HashSet 的区别【 最后会讲到】


五、哈希表

在TreeSet和TreeMap中查找值时,需要通过对关键码的多次比较,而红黑树的高度为logN,因此它们查询的时间复杂度为O(logN),

如果构造一种存储结构, 通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系 ,那么在查找时通过该函数可以很快 找到该元素。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称为哈希表 (Hash  Table)( 或者称散列表 )

5.1哈希冲突


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

由于填入表中的元素个数是无法改变的,因此我们只能通过增加散列表的长度来降低负载因子。 


5.3冲突-解决-开散列/哈希桶 

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

5.4HashMap代码实现 

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 static final double DEFAULT_LOADFACTER = 0.75f;//数组public Node[] array = new Node[10];public int usedSize;//存放值public void push(int key,int val){int index = key % 10;//随便举例的一个哈希函数//首先要遍历对应链表,看是否已经存在keyNode cur = array[index];while(cur!=null){if(cur.key==key){cur.val=val;}cur=cur.next;}//说明没有key节点Node node = new Node(key,val);node.next=array[index];array[index]=node;usedSize++;//计算负载因子大小if(doloadFactor()>=DEFAULT_LOADFACTER){//扩容reSize();}}private void reSize() {//注意不能直接拷贝到新数组,因为扩容后哈希函数已经发生了改变//需要重新遍历桶中的所有节点,重新把这些节点放到合适的桶中Node[] newArray = new Node[array.length*2];for (int i = 0; i < array.length; i++) {Node cur=array[i];while(cur!=null){int newIndex= cur.key% newArray.length;Node curN=cur.next;cur.next=newArray[newIndex];newArray[newIndex]=cur;cur=curN;}}//将array指向newArray即可array=newArray;}private double doloadFactor() {return usedSize*1.0/array.length;}public int getVal(int key){int index = key % 10;Node cur = array[index];while(cur!=null){if(cur.key==key){return cur.val;}cur=cur.next;}//没找到return -1;}}

 

在对散列表进行reSize过程中,可能会遇到下面的问题:

 


5.5性能分析

  实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个 常数 ,所以,通常意义下,我们认为 哈希表的插入/删除/查找时间复杂度是O(1)

5.6哈希表和Java类集的关系

1. HashMap HashSet java 利用哈希表实现 Map Set
2. java 中使用的是 哈希桶 方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表 转变为搜索树 (红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key equals 方法。所以如果要用自定义类作为 HashMap key 或者 HashSet 的值 必须覆写 hashCode 和 equals 方法 ,而且要做到 equals 相等的对象, hashCode 一定是一致的。

http://www.ppmy.cn/news/1527549.html

相关文章

Redis 底层数据结构,一文详解

Redis 底层用 C 语言实现&#xff0c;不同版本的数据类型使用的数据结构也不同&#xff0c;下面详细看 SDS 字符串在 Redis 中很常用&#xff0c;键值对的所有键都是字符串&#xff0c;值有时候也是字符串 Redis 是用 C 语言实现的&#xff0c;但是字符串没有直接用 C 语言的…

功能测试干了三年,快要废了。。。

8年前刚进入到IT行业&#xff0c;到现在学习软件测试的人越来越多&#xff0c;所以在这我想结合自己的一些看法给大家提一些建议。 最近聊到软件测试的行业内卷&#xff0c;越来越多的转行和大学生进入测试行业&#xff0c;导致软件测试已经饱和了&#xff0c;想要获得更好的待…

基于鸿蒙API10的RTSP播放器(八:音量和亮度调节功能的整合)

一、前言&#xff1a; 笔者在前面第六、七节文章当中&#xff0c;分别指出了音量和屏幕亮度的前置知识&#xff0c;在本节当中&#xff0c;我们将一并实现这两个功能&#xff0c;从而接续第五节内容。本文的逻辑分三大部分&#xff0c;先说用到的变量&#xff0c;再说界面&…

【TypeScript】 ts控制语句

文章目录 ts控制语句1. 条件语句1.1 if 语句1.2 if...else 语句1.3 if...else if...else 语句1.4 switch...case 语句 2. 循环2.1 for 循环2.2 for...in 循环2.3 for...of、forEach、every 和 some 循环2.4 while 循环2.5 do...while 循环2.6 break 语句2.7 continue 语句 3. 函…

Golang | Leetcode Golang题解之第406题根据身高重建队列

题目&#xff1a; 题解&#xff1a; func reconstructQueue(people [][]int) (ans [][]int) {sort.Slice(people, func(i, j int) bool {a, b : people[i], people[j]return a[0] > b[0] || a[0] b[0] && a[1] < b[1]})for _, person : range people {idx : pe…

C编程控制PC蜂鸣器方法2

在《C编程控制PC蜂鸣器》一文中,我们了解并使用了通过IO端口控制的方式操作硬件,而有些时候这对于一些朋友来说太模糊了,很容易让人迷糊,这次采用最基本的write系统调用来写入input_event数据实现相同功能。这里涉及到的input_event可参考《C编程实现键盘LED闪烁方法2》一文…

[数据集][目标检测]红外微小目标无人机直升机飞机飞鸟检测数据集VOC+YOLO格式7559张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;7559 标注数量(xml文件个数)&#xff1a;7559 标注数量(txt文件个数)&#xff1a;7559 标注…

Dify 中的讯飞星火平台工具源码分析

本文主要对 Dify 中的讯飞星火平台工具 spark 进行了源码分析&#xff0c;该工具可根据用户的输入生成图片&#xff0c;由讯飞星火提供图片生成 API。通过本文学习可自行实现将第三方 API 封装为 Dify 中工具的能力。 源码位置&#xff1a;dify-0.6.14\api\core\tools\provide…