【数据结构】哈希表二叉搜索树详解

  💎 欢迎大家互三2的n次方_ 

 💎所属专栏数据结构与算法学习 

在这里插入图片描述

 

🍁1. 二叉搜索树 

二叉搜索树也称为二叉查找树或二叉排序树,是一种特殊的二叉树结构,它的特点是:

1. 若左树不为空,则左树所有节点的值都小于根节点的值

2. 若右树不为空,则右树所有节点的值都小于根节点的值

3. 不存在键值相等的节点

 

 接下来就模拟实现一下二叉搜索树

首先,和之前二叉树的实现一样,都是一个节点包括值和指向左右节点的引用

public class BinarySearchTree {static class TreeNode {int val;TreeNode left;TreeNode right;public TreeNode(int val) {this.val = val;}}
}

 之后就是插入,删除,搜索等一些方法了

🍁1.1 search()

根据二叉搜索树的性质,只需要在遍历的时候进行判断目标值在左子树还是在右子树

    public TreeNode search(int key) {//从根节点开始往下搜索TreeNode cur = root;while (cur != null) {if (cur.val > key) {cur = cur.left;} else if (cur.val < key) {cur = cur.right;} else {return cur;}}return null;}

🍁1.2 insert(int key)

插入也是一样的过程,这里定义了两个节点,一个用来遍历,另一个用来判断最后插入的位置,需要注意的是,由于二叉搜索树不能有重复节点,在遍历的过程中,如果发现当前节点和要插入的元素的值相同,直接退出方法

    public void insert(int key) {if (root == null) {root = new TreeNode(key);return;}TreeNode parent = null;TreeNode cur = root;//定义要插入的节点TreeNode node = new TreeNode(key);while (cur != null) {if (cur.val > key) {parent = cur;cur = cur.left;} else if (cur.val < key) {parent = cur;cur = cur.right;} else {return;//不能有重复的值,直接返回}}//判断作为左树还是右树if (parent.val > key) {parent.left = node;} else {parent.right = node;}}

🍁1.3 remove(int key)

删除操作是有些麻烦的,因为删除节点之后还需要保证是二叉搜索树,首先找到要删除的节点,找到之后调用删除节点的方法

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

可以分为三种情况:

要删除的节点左树为空,接着又可以分为三种情况

右树为空,同理,也可以分为三种情况

左右都不为空

这里采用替换删除的方法,找到一个合适的数据替换cur.val,这个数据替换之后还要保证二叉搜索树的特性,所以就要找左子树的最大值或者右子树的最小值来进行替换

左子树的最大值也就是左树最右边的节点,即右树为空

右子树的最小值也就是右树最左边的节点,即左树为空

以右子树的最小值为例,找到之后替换cur,接着删除原来的节点

 找到之后还需要判断是右子树或者是左子树,因为二者的删除方式是不一样的

 

    private void removeNode(TreeNode parent, TreeNode cur) {if (cur.left == null) {//左树为空if (cur == root) {root = cur.right;} else if (cur == parent.left) {parent.left = cur.right;} else {parent.right = cur.right;}} else if (cur.right == null) {//右树为空if (cur == root) {root = cur.left;} else if (cur == parent.left) {parent.left = cur.left;} else {parent.right = cur.left;}} else {//左右都不为空   // t:要交换的目标元素的  tp:要交换的目标元素的双亲节点,方便后续删除TreeNode tp = cur;TreeNode t = cur.right;while (t.left != null) {tp = t;t = t.left;}cur.val = t.val;if (tp.left == t) {tp.left = t.right;} else {tp.right = t.right;}}}

 🍁2. 哈希表

哈希表(Hash table,也叫散列表)是一种根据关键码值(Key value)而直接进行访问的数据结构。它通过哈希函数(也叫散列函数)将关键码值映射到表中一个位置来访问记录,以加快查找的速度。

哈希表的插入、删除和查找操作的时间复杂度在理想情况下是O(1),比我们之前学过的数据结构都要快

🍁2.1 实现原理

哈希表通过哈希函数将元素的键名映射为数组下标(转化后的值叫做哈希值或散列值),然后在对应下标位置存储记录值。当我们按照键名查询元素时,可以使用同样的哈希函数,将键名转化为数组下标,从对应的数组下标位置读取数据。

🍁2.2 哈希函数的构造

哈希函数的设计规则:

哈希函数的定义域必须包括需要存储的全部关键码

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

哈希函数应该简单设计

关于哈希函数的构造介绍一下两种最常用的方法

直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况

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

 

🍁2.3 哈希冲突

哈希冲突是指不同的键名通过哈希函数计算后得到相同的哈希值,导致它们被映射到散列表中的同一个位置,例如下面的4,和14通过除留余数的哈希函数映射到了同一个位置

🍁2.3.1 哈希冲突的避免

避免哈希冲突有以下需要注意的:

1. 引起哈希冲突的一个原因可能是哈希函数设计的不合理,需要设计合理的哈希函数

2. 调节负载因子

哈希表的负载因子用于衡量哈希表的填充程度

 其实很好理解,填的越满越容易挤

🍁2.3.2 哈希冲突的解决方法

我们有以下几种解决办法

闭散列(开放寻址法):

 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

 很明显,这种方式有一个弊端:冲突元素都聚集到了一起,这与其找下一个空位置有关系

二次探测:当哈希函数计算出的位置已被占用时,二次探测通过计算一个二次方递增的步长来探测下一个可能的哈希地址,直到找到一个空槽或遍历完整个表。

其中:i = 1,2,3…, H₀是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

 

(4+ 1^2)%10  ,     (4 + 2^2)%10
 无论是线性探测还是二次探测,都有一个问题:空间利用率低,就有了下面的一种方法:

开散列(哈希桶)

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

 HashSet就是采用的链表数组+链表的方式存储的,并且在特定的情况下会变为红黑树

🍁3. 哈希桶的实现

🍁3.1 创建哈希桶

我们这里根据key-value模型来实现一下哈希桶

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[] arr = new Node[10];public int usedSize;//负载因子private final double DEFAULT_LOAD_FACTOR = 0.75;
}

 这也和我们之前说的数组+链表是一样的,接下来就是其中的一些方法

🍁3.2 push()

首先通过哈希函数计算出要插入的数组下标,接着再顺着链表进行判断,如果插入元素已经存在,需要更新val之后再返回,不存在的话就用头插的方法插入

    public void push(int key, int val) {//哈希函数int index = key % arr.length;//根据哈希函数算出来数组的位置后进行判断Node cur = arr[index];while (cur != null) {//如果要插入元素已经存在,更新val后直接返回if (cur.key == key) {cur.val = val;return;}cur = cur.next;}//如果没有找到相同的元素,调用头插法插入Node node = new Node(key, val);node.next = arr[index];arr[index] = node;usedSize++;//超过负载因子进行扩容if (doLoadFactor() >= DEFAULT_LOAD_FACTOR) {resize();}}

 接下来讲一下扩容的方法

    //扩容private void resize() {//重新定义一个扩容之后的数组Node[] newArr = new Node[arr.length * 2];for (int i = 0; i < arr.length; i++) {Node cur = arr[i];while (cur != null) {//提前记录cur.next,避免之后头插时无法再遍历原来的节点Node curn = cur.next;//重新记录扩容后的下标int index = cur.key % newArr.length;cur.next = newArr[index];newArr[index] = cur;cur = curn;}}arr = newArr;}//计算存储的比例private double doLoadFactor() {return usedSize * 1.0 / arr.length;}

 由于采用了数组+链表的形式,不能简单的进行扩容+拷贝,这样链表上的元素无法处理,这里采用的是定义一个扩容之后的数组,接着遍历原数组上链表的每一个元素,并重新根据哈希函数进行计算,并排列到新的数组中合适的位置

🍁3.3 hashCode()方法

上面我们是先用int类型实现了哈希桶,但是如果是其他非数值的类型怎么去根据哈希函数计算地址呢,这时就用到了hashCode方法,hashCode方法是Java中Object类的一个方法,用于返回对象的哈希码,可以利用哈希码来进行计算,对于同一个对象,在其生命周期内,只要对象的内容没有发生变化,多次调用hashCode方法应该返回相同的值,理想情况下,hashCode方法应该为每个不同的对象生成不同的哈希码,但实际上由于哈希码的值域有限(int类型),不同的对象可能会生成相同的哈希码,称为哈希冲突

class Person{public String name;public Person(String name) {this.name = name;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(name, person.name);}@Overridepublic int hashCode() {return Objects.hash(name);}
}public class Text {public static void main(String[] args) {Person person1 = new Person("LiHua");Person person2 = new Person("LiHua");//重写hashCode之前,两个对象的hashCode值不一样System.out.println(person1.hashCode());System.out.println(person2.hashCode());//在重写equals前,这是两个不同的对象,重写后为trueSystem.out.println(person1.equals(person2));//两个不一样的对象拥有了相同的哈希值System.out.println("abc".hashCode());//96354System.out.println("acD".hashCode());//96354}
}

 

 不重写的话即使两个对象属性值一样也不是同一个对象,哈希值也就不相同

 

 🍁3.4 实现泛型哈希桶

 根据hashCode方法,就可以实现一个泛型类的哈希桶,传入其他类型的值也可以

public class HashBuck2<K, V> {static class Node<K, V> {public K key;public V val;public Node<K, V> next;public Node(K key, V val) {this.key = key;this.val = val;}}public Node<K, V>[] arr = (Node<K, V>[]) new Node[10];public int usedSize;private final double DEFAULT_LOAD_FACTOR = 0.75;public void push(K key, V val) {int index = key.hashCode() % arr.length;Node<K, V> cur = arr[index];while (cur != null) {//如果要插入元素已经存在,更新val后直接返回if (cur.key.equals(key)) {//由于是引用数据类型,就需要用equals方法判断cur.val = val;return;}cur = cur.next;}//如果没有找到相同的元素,调用头插法插入Node<K, V> node = new Node<>(key, val);node.next = arr[index];arr[index] = node;usedSize++;if (doLoadFactor() >= DEFAULT_LOAD_FACTOR) {resize();}}private void resize() {Node<K, V>[] newArr = (Node<K, V>[]) new Node[arr.length * 2];for (int i = 0; i < arr.length; i++) {Node<K, V> cur = arr[i];while (cur != null) {//提前记录cur.next,避免之后头插时无法再遍历原来的节点Node<K, V> curn = cur.next;//重新记录扩容后的下标int index = cur.key.hashCode() % newArr.length;cur.next = newArr[index];newArr[index] = cur;cur = curn;}}arr = newArr;}private double doLoadFactor() {return usedSize * 1.0 / arr.length;}public V getVal(K key) {int index = key.hashCode() % arr.length;Node<K, V> cur = arr[index];while (cur != null) {if (cur.key.equals(key)) {return cur.val;}cur = cur.next;}return null;}
}

需要注意的还有,由于传入的值为引用数据类型,就不能用"=="比较两个对象的值了,这时就需要调用equals方法进行判断

在这里插入图片描述

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

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

相关文章

顺序表的代码实现

顺序表的代码实现 1.认识什么是顺序表1.1顺序表的优缺点 2.实现顺序表代码准备3.顺序表的代码实现3.1 顺序表结构体的定义3.2 顺序表的初始化3.3 顺序表的销毁3.4 顺序表的输出打印3.5顺序表的扩容3.6 顺序表的头部插入(头插)3.7 顺序表的头部删除(头删)3.8 顺序表的尾部插入(尾…

2种常用的取消word文档”打开密码“方法

在日常工作中&#xff0c;我们有时会遇到需要取消Word文档“打开密码”的情况。无论是因为忘记密码&#xff0c;还是出于文档共享的需要&#xff0c;掌握几种有效的取消密码方法都显得尤为重要。以下是2种常用的方法来取消Word文档的“打开密码”。 方法一&#xff1a;文件另存…

二叉树--堆(上卷)

二叉树–堆&#xff08;上卷&#xff09; 树 树的概念与结构 树是⼀种⾮线性的数据结构&#xff0c;它是由 n&#xff08;n>0&#xff09; 个有限结点组成⼀个具有层次关系的集合。把它叫做 树是因为它看起来像⼀棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;⽽…

智慧医院信息系统思维导图

智慧医院信息系统 "思维导图智慧医院信息系统, 用一张图解析智慧医疗信息系统 本文转载&#xff1a;有了这个智慧医院信息系统思维导图&#xff0c;没人不明医疗信息化

是时候学习Grid布局了

一、序言 先说什么&#xff1f;当然先说大家最关心的兼容性了 CanIUse 嗯&#xff0c;对于非要兼容IE的开发者&#xff0c;我建议&#xff0c;量力而行&#xff01;兼容性还是不如Flex 当然&#xff0c;如果你flex够熟悉了&#xff0c;但却被一些布局有时候难倒&#xff0c;我…

学习react-登录状态验证

1.创建三个页面LoginPage, HomePage,NotFoundPage用于Router 创建LoginPage.tsx用于做登录页面 // LoginPage.tsx const LoginPage (props:LoginProp) > {const navigate useNavigate();return( <h1 onClick{ ()>{navigate("/");}}>Hello Login, {pr…

昇思25天学习打卡营第1天 | 快速入门教程

昇思大模型平台&#xff0c;就像是AI学习者和开发者的超级基地&#xff0c;这里不仅提供丰富的项目、模型和大模型体验&#xff0c;还有一大堆经典数据集任你挑。 AI学习有时候就像找不到高质量数据集的捉迷藏游戏&#xff0c;而且本地跑大数据集训练模型简直是个折磨&#xf…

JQuery简单实现ul li点击菜单项被选中的菜单项保持高亮状态(导航ul li点击切换样式)

效果&#xff1a; JS&#xff1a; $(function () {//遍历list&#xff08;一般为ul li&#xff09;$("#menu a").each(function () {//给当前项添加点击事件&#xff08;点击后切换样式&#xff09;$(this).bind(click,function () {// 移除其他所有项的active类$(&…

挑战房市预测领头羊:KNN vs. 决策树 vs. 线性回归

挑战房市预测领头羊&#xff08;KNN&#xff0c;决策树&#xff0c;线性回归&#xff09; 1. 介绍1.1 K最近邻&#xff08;KNN&#xff09;&#xff1a;与邻居的友谊1.1.1 KNN的基础1.1.2 KNN的运作机制1.1.3 KNN的优缺点 1.2 决策树&#xff1a;解码房价的逻辑树1.2.1 决策树的…

【日常设计案例分享】通道对账

今天跟同事们讨论一个通道对账需求的技术设计。鉴于公司业务线有好几个&#xff0c;为避免不久的将来各业务线都重复竖烟囱&#xff0c;因此&#xff0c;我们打算将通道对账做成系统通用服务&#xff0c;以降低各业务线的开发成本。 以下文稿&#xff08;草图&#xff09;&…

局部变量,在使用时再定义

关于局部变量&#xff0c;适时定义局部变量&#xff0c;可提高代码清晰度和可读性&#xff0c;并能规避不必要的代码bug 局部变量&#xff0c;在使用时再定义&#xff0c;提高代码可读性 下面代码中的2个方法&#xff0c;第1个 verifyTaskApply 调用第2个 existAppliedTask 。…

20240730 每日AI必读资讯

&#x1f3ac;燃爆&#xff01;奥运8分钟AI影片火了&#xff0c;巴赫主席&#xff1a;感谢中国黑科技 - 短片名为《永不失色的她》&#xff08;To the Greatness of HER&#xff09;&#xff0c;由阿里巴巴和国际奥委会联合推出。 - 百年奥运史上伟大女性的影响故事在此被浓缩…

Rust语言入门第七篇-控制流

文章目录 Rust语言入门第七篇-控制流If 表达式基本结构特点和规则示例 let 语句中使用 ifloop 循环基本结构特点示例综合示例 while 循环基本结构特点示例综合示例 与 loop 循环的区别 for 循环基本结构详细说明特点示例综合示例 Rust语言入门第七篇-控制流 Rust 的控制流是指…

Oracle Database 23.5 - for Engineered Systems版本发布

要尝鲜的可以在https://edelivery.oracle.com/下载。对于x86的本地版本再等等吧。 安装可参考飞总的&#xff1a;oracle 23ai&#xff08;23.5.0.24.07&#xff09;完整功能版安装体验 – 提供7*24专业数据库(Oracle,SQL Server,MySQL,PostgreSQL等)恢复和技术支持Tel:1781323…

Python数值计算(12)

本篇说说Neville方法。Neville方法的基础是&#xff0c;插值多项式可以递归的生成&#xff0c;有时进行插值的目的是为了计算某个点的值&#xff0c;这个时候并不需要将拟合曲线完全求出&#xff0c;而是可以通过递归的方式进行计算&#xff0c;具体操作如下&#xff1a; 例如…

OpenGL学习 1

一些唠叨&#xff1a; 很多时候&#xff0c;都被Live2d吸引&#xff0c;去年想给网页加个live2d看板娘&#xff0c;结果看不懂live2d官方给的SDK&#xff0c;放弃了。今天又想弄个live2d桌宠&#xff0c;都已经在网上找到Python 的 Live2D 拓展库了&#xff0c;并提供了用QT实现…

昇思25天学习打卡营第19天|ResNet50 图像分类案例:数据集、训练与预测可视化

目录 环境配置 数据集加载 数据集可视化 Building Block Bottleneck 构建ResNet50网络 模型训练与评估 可视化模型预测 环境配置 首先指出实验环境预装的 mindspore 版本以及更换版本的方法。然后&#xff0c;它卸载了已安装的 mindspore 并重新安装指定的 2.3.0rc1 版本…

值得买科技与MiniMax达成官方合作伙伴关系,共建融合生态

7月29日&#xff0c;值得买科技与大模型公司MiniMax宣布达成官方合作伙伴关系。 MiniMax旗下大模型产品海螺AI现已接入值得买“消费大模型增强工具集”&#xff0c;基于海螺AI比价策略&#xff0c;用户可通过海螺AI“悬浮球”功能实现快速比价及跳转购买。 此次合作也标志着值…

操作系统重点总结

文章目录 1. 操作系统重点总结1.1 操作系统简介1.1.1 操作系统的概念和功能1.1.2 操作系统的特征1.1.2.1 并发1.1.2.2 共享1.1.2.3 虚拟1.1.2.4 异步 1.1.3 操作系统的发展与分类1.1.4 中断和异常1.1.5 系统调用1.1.6 操作系统的体系结构1.1.7 操作系统简介总结 1.2 进程1.2.1 …

使用YApi平台来管理接口

快速上手 进入YApi官网&#xff0c;进行注册登录https://yapi.pro/添加项目 3. 添加分类 4. 添加接口 5. 添加参数 添加返回数据 可以添加期望 验证 YAPI&#xff08;Yet Another Practice Interface&#xff09;是一个现代化的接口管理平台&#xff0c;由淘宝团队…