【数据结构】哈希表二叉搜索树详解
💎 欢迎大家互三: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方法进行判断
相关文章:

【数据结构】哈希表二叉搜索树详解
💎 欢迎大家互三:2的n次方_ 💎所属专栏:数据结构与算法学习 🍁1. 二叉搜索树 二叉搜索树也称为二叉查找树或二叉排序树,是一种特殊的二叉树结构,它的特点是: 1. 若左树不为空&am…...

【SpringBoot】参数传递之@ModelAttribute
ModelAttribute标注的方法会在Controller类的每个映射URL的控制执行方法之前执行。 ModelAttribute public void findUserById(PathVariable("userId") Long userId,Model model){ model.addAttribute("user",userService.findUserById(userId)); } GetM…...

frp搭建ssh内网穿透
frp软件包下载 检查外网服务器架构 uname -i官网下载对应的版本 https://github.com/fatedier/frp/releases 使用wget或拷贝文件到外网服务器/opt目录下并解压 解压得到frp_0.59.0_linux_amd64文件夹 tar -zxvf frp_0.59.0_linux_amd64.tar.gzfrpc 这是 frp 的客户端可执…...

OpenCV库学习之cv2.normalize函数
OpenCV库学习之cv2.normalize函数 一、简介 cv2.normalize是OpenCV库中的一个函数,用于对图像进行归一化处理。归一化是一种线性变换,可以将图像像素值的范围缩放到指定的区间。这种操作在图像处理中非常有用,特别是在需要将图像数据用于某些…...

LINUX操作系统安全
一、概述内容 操作系统负责计算机系统的资产管理,支撑和控制各种应用程序运行,为用户提供管理计算机系统管理接口。操作系统也是构成网络信息系统的核心关键组件,其安全可靠性决定了计算机系统的安全性和可靠性。 操作系统安全是指满足安全…...

vue3.0学习笔记(三)——计算属性、监听器、ref属性、组件通信
1. computed 函数 定义计算属性: computed 函数,是用来定义计算属性的,计算属性不能修改。 计算属性应该是只读的,特殊情况可以配置 get set 核心步骤: 导入 computed 函数 执行函数 在回调参数中 return 基于响应…...

Elasticsearch面试三道题
针对Elasticsearch的面试题,从简单到困难,我可以给出以下三道题目: 1. Elasticsearch的基本概念与优势 问题:请简要介绍Elasticsearch是什么,并说明它相比传统数据库的优势有哪些? 答案: El…...

大厂面经:大疆嵌入式面试题及参考答案(4万字长文:持续更新)
目录 Linux 系统调用的过程,中间发生了什么? 表格总结 Linux 中断流程,谈谈你对中断上下文的理解 中断流程 中断上下文理解 Linux schedule() 函数的原理和调用的时机 schedule() 函数原理 调用时机 页表实现机制,分页的缺点? 页表机制 分页的缺点 介绍操作系…...

数据结构【有头双向链表】
目录 实现双向链表 双向链表数据 创建双向链表 初始化双向链表创建(哨兵位) 尾插 打印双向链表 头插 布尔类型 尾删 头删 查询 指定位置后插入 指定位置删除数据 销毁 顺序表和链表的分析 代码 list.h list.c test.c 注意:…...

docker 安装jenkins详细步骤教程
Jenkins 是一个开源的持续集成(CI)和持续部署(CD)工具,用于自动化软件开发过程中的构建、测试和部署。 特点和功能: 持续集成:Jenkins 可以自动触发构建过程,检查代码变更并进行构建、测试和部署,以确保团队的代码始终保持可集成状态。 插件生态系统:Jenkins 拥有丰富…...

C++模板函数
C模板函数 函数模板简单的函数模板模板类型推导返回输入的类型,模板返回的类型由输入的决定返回类型的模板参数返回值使用auto,编译器自动推导 默认模板实参模板参数重载函数模板 constexpr关键字 函数模板 简单的函数模板 typename 可以使用class代替…...

c#中的正则表达式和日期的使用(超全)
在 C# 中,正则表达式(Regular Expressions)是一种强大的文本处理工具,用于执行各种字符串搜索、替换和验证任务。以下是一些常用的正则表达式示例及其用途: 1. 邮箱地址验证 string emailPattern "^[^\s][^…...

论文阅读【检测】:商汤 ICLR2021 | Deformable DETR
文章目录 论文地址AbstractMotivation技术细节多尺度backbone特征MSDeformAttention 小结 论文地址 Deformable DETR 推荐视频:bilibili Abstract DETR消除对目标检测中许多手工设计的组件的需求,同时表现出良好的性能。然而,由于Transfor…...

dpdk发送udp报文
dpdk接收到udp报文后,自己构造一个udp报文,将收到的报文中的源mac,目的mac,源ip,目的ip,源端口和目的端口交换下顺序填充到新的udp报文中,报文中的负载数据和收到的udp保持一致。 注࿱…...

网站后端管理和构建java项目的工具-Maven
maven是用于管理和构建java项目的工具。 管理Jar包 无论是使用eclipse、IDEA创建的maven项目,格式都是统一的。 不同开发工具创建的maven项目兼容。 test是对main测试的代码。main中的resources中放置配置文件。 对于Maven,一个Maven项目就是一个对象…...

深入理解计算机系统 CSAPP 家庭作业11.10
A: //home.html <form action"/cgi-bin/adder" method"GET"><ul><li><label for"n1">n1:</label><input type"text" id"n1" name"n1" /> //name的值决定页面提交后…...

Unity3D 二进制序列化器详解
前言 在Unity3D开发中,二进制序列化是一种重要的数据持久化和网络传输技术。通过二进制序列化,游戏对象或数据结构可以被转换成二进制格式,进而高效地存储于文件中或通过网络传输。本文将详细介绍Unity3D中的二进制序列化技术,包…...

js_拳皇(上)
文章目录 架构设计:一图胜千言绪论不能正常加载动图设计的思路渲染画布开发感想角色抽象为矩形ctx 是 canvas 的对象键盘控制角色Set键盘事件流程图在 canvas 里面使用 gif 图片继承存储动作ReferenceError: gif is not definedTypeError: Cannot read properties o…...

TCP请求如何获取客户端真实源IP地址
应用场景 在基于TCP的应用程序中,获取客户端真实源IP地址可以用于以下应用场景: 访问控制和安全策略:通过获取客户端真实源IP地址,应用程序可以实施访问控制策略,限制或允许特定IP地址的访问。这可以用于身份验证、防…...

【b站-湖科大教书匠】6 应用层 - 计算机网络微课堂
课程地址:【计算机网络微课堂(有字幕无背景音乐版)】 https://www.bilibili.com/video/BV1c4411d7jb/?share_sourcecopy_web&vd_sourceb1cb921b73fe3808550eaf2224d1c155 目录 6 应用层 6.1 应用层概述 6.2 客户-服务器方式和对等方…...

QT串口和数据库通信
创建串口 串口连接客户端并向服务器发送消息 client.pro #------------------------------------------------- # # Project created by QtCreator 2024-07-02T14:11:20 # #-------------------------------------------------QT core gui network QT core gui…...

WebKitWebKit简介及工作流程
简介 引擎能够解析HTML、CSS、JavaScript等网页标准,从而将互联网内容呈现给用户。 WebKit的主要特点包括: 开源性:它是一个开源项目,任何人都可以查看、修改和贡献代码。跨平台:WebKit可以在多个操作系统上运行&am…...

架构分析(CPU:ARM vs RISC-V)
ARM N2 ARM V2 对比 N2和V2,整体架构具有一致性。保证 SiFive P870 P870 Pipeline Veyron V1...

使用 Docker Compose 部署 RabbitMQ 的一些经验与踩坑记录
前言 RabbitMQ 是一个功能强大的开源消息队列系统,它实现了高效的消息通信和异步处理。 本文主要介绍其基于 Docker-Compose 的部署安装和一些使用的经验。 特点 成熟,稳定消息持久化灵活的消息路由高性能,高可用性,可扩展性高支…...

前端八股速通(持续更新中...)
1、深拷贝和浅拷贝的区别 浅拷贝:浅拷贝是拷贝一层,引用类型共享地址。 如果属性是基本类型,拷贝的就是基本类型的值。 如果属性是引用类型,拷贝的就是内存地址。 意思是,当进行浅拷贝时,对于对象的每一…...

【语音识别和生成】语音识别和语音合成技术
语音识别和生成:语音识别和语音合成技术 目录 引言语音识别技术 语音识别的基本原理语音识别系统的组成语音识别的关键技术 语音合成技术 语音合成的基本原理语音合成系统的组成语音合成的关键技术 语音识别和生成的应用 智能助理智能家居语音翻译医疗健康教育和学…...

Redis#架构师面试题
1、Redis锁存在哪些问题及如何解决? 1、死锁问题 加过期时间设定 2、原子性问题 通过“set…nx...ex…”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。 3、释放其他线程的锁问题 当过期时间设置小于线程…...

关于#define的使用方法总结
文章目录 #define 预处理指令一、#define宏定义二、查看预处理文件三、#define 的使用方法四、C语言宏中“#”和“##”的用法五、常见的宏定义总结六、常考题目 #define 预处理指令 #define 是 C 和 C 编程语言中的预处理指令,用于定义宏(macro…...

Unity顶点动画(Vertex Animation):创造动态视觉效果
在Unity中,顶点动画(Vertex Animation)是一种强大的技术,它允许开发者直接在顶点级别上操作和变形网格,从而实现各种动态视觉效果。顶点动画不依赖于骨骼绑定,因此非常适合模拟布料、流体、面部表情等复杂的动画效果。本文将探讨顶…...

WSL for Windows
1、安装 超详细Windows10/Windows11 子系统(WSL2)安装Ubuntu20.04(带桌面环境)_wsl安装ubuntu20.04-CSDN博客https://blog.csdn.net/weixin_44301630/article/details/122390018 注意,安装之后首次启动 Ubuntu 时&…...