二叉搜索树 和 哈希表 (JAVA)

目录

二叉搜索树

二叉搜索树的插入 

二叉搜索树的查找

 二叉搜索树的删除

哈希表 

哈希冲突

闭散列

线性探测法

二次探测法

开散列

开散列代码实现:

插入元素 

删除元素

查找元素


二叉搜索树

先了解以下二叉搜索树是啥,概念如下:

二叉搜索树又称二叉排序树,它具有以下性质的二叉树空树:

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

这就是一颗简单的二叉搜索树: 

二叉搜索树的插入 

二叉搜索树的插入非常简单:

  1. 从根节点开始比较,如果大于根节点就遍历右子树,小于根节点就遍历左子树
  2. 对所有的子树都进行如上操作
  3. 直到遍历到空节点,将待插入元素插入此空节点

先创建一个二叉搜索树的类,然后创建一个描述节点的内部类:

public class BinarySearchTree {//描述节点的内部类public static class TreeNode {public int key;public TreeNode left;public TreeNode right;TreeNode(int key) {this.key = key;}}//搜索树的根节点public TreeNode root;}
}

添加一个插入元素的方法,注:二叉搜索树的插入只会出现在null节点处,也就是插入的新节点都会成为搜索书的叶子节点 。

public class BinarySearchTree {public static class TreeNode {public int key;public TreeNode left;public TreeNode right;TreeNode(int key) {this.key = key;}}public TreeNode root;/*** 插入一个元素*/public boolean insert(int key) {TreeNode tmp = new TreeNode(key);//树如果为空就直接令其成为根节点if (this.root == null) {this.root = tmp;//插入成功返回truereturn true;}TreeNode privat = this.root;TreeNode p1 = this.root;//寻找新元素的插入位置while (p1 != null) {privat = p1;if (p1.key > key) {p1 = p1.left;}else {p1 = p1.right;}}//插入新元素if (privat.key > key) {privat.left = tmp;}else {privat.right = tmp;}//插入成功返回truereturn true;}
}

二叉搜索树的查找

在二叉搜索树中查找一个元素分为两步:

  1. 从根节点开始比较,如果大于根节点就遍历右子树,小于根节点就遍历左子树
  2. 对所有的子树都进行如上操作
    //查找key是否存在public TreeNode search(int key) {//判断树是否为空,为空就直接返回nullif (this.root == null){return null;}TreeNode tmp = this.root;while (tmp != null) {if (tmp.key > key) {tmp = tmp.left;}else if (tmp.key < key) {tmp = tmp.right;}else {//找到该元素后将其作为返回值返回return tmp;}}//当出循环之后代表没找到该元素返回nullreturn null;}

 二叉搜索树的删除

二叉搜索树的删除相比于插入和查找还是比较复杂的,因为要保证删除待删除结点之后,树依旧是一颗二叉搜索树。

分两种情况:

  • 待删除节点只有一颗子树即左子树或者右子树为空
  1. 当待删除节点左树为空就令待删除节点的双亲指向其右子树
  2. 当待删除节点右树为空就令待删除节点的双亲指向其左子树

  • 左右子树都不为空(此处我们利用“替罪羊”的删除方法)
  1. 找到待删除节点的右(左)子树的最左(右)端的节点
  2. 交换两个结点的值
  3. 删除该节点

    //删除key的值public boolean remove(int key) {if (this.root == null) {return false;}TreeNode tmp = this.root;TreeNode privat = tmp;//寻找待删除节点while (tmp != null) {if (tmp.key > key) {privat = tmp;tmp = tmp.left;}else if (tmp.key < key) {privat = tmp;tmp = tmp.right;}else {break;}}if (tmp == null) {return false;}//判断待删除节点是否只有一颗子树if (tmp.left == null || tmp.right == null) {判断该子树为左右哪颗子树if (tmp.left == null) {if (tmp == this.root) {this.root = tmp.right;}else {if (privat.left == tmp) {privat.left = tmp.right;}else {privat.right = tmp.right;}}}else {if (tmp == this.root) {this.root = tmp.left;}else {if (privat.left == tmp) {privat.left = tmp.left;}else {privat.right = tmp.left;}}}}else {//待删除节点有两棵子树时//寻找右子树的最左端的节点TreeNode a = tmp.right;while(a.left != null) {privat = a;a = a.left;}//将右子树的最左端的节点的值赋值给待删除节点tmp.key = a.key;//删除右子树的最左端的节点if (privat.left == a) {privat.left = a.right;}else {privat.right = a.right;}}return true;}

哈希表 

在顺序结构和平衡树中,顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(\log_{2}N)。

而哈希表是一种插入/删除/查找时间复杂度都是O(1)的数据结构,它的查询之所以也可以如此之快的的原因就是它在查找时不需要经过任何比较,直接一次从表中得到要搜索的元素。

实现这种数据结构的方法就是通过某种函数使元素的存储位置与它的关键码之间能够建立一一对应的映射关系,在查找时通过该函数就可以很快找到该元素。而这种数据结构被称作哈希表或散列表,这种函数被称为哈希(散列)函数

设计一个哈希表最关键的一步就是设计哈希函数。

哈希函数的设计有以下要求: 

  1. 哈希函数的定义域必须包括需要存储的全部关键码,其值域必须在0到m-1之间(散列表允许有m个地址)
  2. 哈希函数计算出来的地址能均匀分布在整个空间中
  3. 哈希函数应该比较简单

例如将数据集合{1,7,6,4,5,9}存入哈希表

存入的时候将每个元素都带入哈希函数计算出下标然后插入,查找时也是同理。

但是此时又出现了一个新问题如果此时要插入元素15 就会发现没地方可以放了,而这也是哈希表中经常会发生的问题,被称为:哈希冲突或哈希碰撞

冲突的发生是必然的,而我们能做的应该是尽量的降低冲突率。

哈希冲突

降低哈希冲突有以下几个方法:

  • 采用更优的哈希函数,哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突;
  • 减小负载因子(负载因子 = 填入表中的元素个数 / 表的大小     JDK中负载因子被设置成里0.75)也就是增大哈希表的存储地址。

解决哈希冲突的两种常见的方法是:闭散列和开散列

闭散列

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

常见的闭散列有两种:

线性探测法

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

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

例如在前面的例子中插入元素14:

因为下标为4的地方已经有值了,所以就看下一个位置即下标为5的地方,此处因为也有值所以继续向后寻找直到出现空位。

线性探测的缺陷:产生冲突的数据会堆积在一起,这与其找下一个空位置的方式关系,因为找空位置的方式就是挨着往后逐个去找。

二次探测法

二次探测法的本质与线性探测法相同,它们的区别是当发生哈希冲突时找下一个空位置的方法不同。

二次探测法找空位置的方法为:H_{i} = (H_{0}+i^{2}) % m   其中:i = 1,2,3…, H0是通过散列函数对元素的关键码 key 进行计算得到的位置其中m是表的大小。 

例如在前面的例子中插入元素14:

虽然插入位置和线性探测法相同但是原理不同:

  1. 先计算得到插入位置H0 = 4,因为4下标处已有值所以带入利用H_{i} = (H_{0}+i^{2}) % m 公式代入i=1;
  2. 计算得到的新下标为5,但5下标处也有值所以带入i = 2;
  3. 计算得到新下标为8,插入;

但是闭散列还有一个问题:如果此时删除元素4那么就找不到元素14了。

开散列

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

比如采用开散列的方式存储集合{1,7,6,4,5,9,14}

而开散列最重要的是控制负载因子,因为当负载因子过大就无法使哈希表的速度达到O(1)。JAVA中的哈希表就是采用的开散列的方式,在JDK中负载因子为0.75。

开散列代码实现:

散列函数和上面的例子相同

class MyHash{//哈希表中存储元素的节点static class node{public int data;public node next;public node(int data) {this.data = data;}}//哈希表,默认大小为10node[] arr = new node[10];//数组中的元素个数int size = 0;//最大负载因子,默认为0.75double maxLoadNum = 0.75;
}
插入元素 

加入插入元素的方法,注:开散列最重要的是控制负载因子,因为当负载因子过大就无法使哈希表的速度达到O(1)所以插入元素之后必须进行负载因子的判断

    public void insert(int data) {//先利用散列函数计算出插入位置int index = data % this.arr.length;//创建节点node tmp = new node(data);if (this.arr[index] == null) {this.arr[index] = tmp;}else {//头插法tmp.next = this.arr[index];this.arr[index] = tmp;}this.size++;//计算负载因子是否过大,如果过大就必须进行扩容,然后将所有元素重新哈希if (((double)this.size / this.arr.length) >= this.maxLoadNum) {this.size = 0;node[] arr1 = new node[this.arr.length * 2];for (int i = 0; i < this.arr.length; i++) {while (this.arr[i] != null) {int index1 = this.arr[i].data % arr1.length;node tmp1 = this.arr[i];//在原表中删除该节点this.arr[i] = this.arr[i].next;tmp1.next = null;//将节点插入新表中if (arr1[index1] == null) {arr1[index1] = tmp1;}else {//头插法tmp1.next = arr1[index1];arr1[index1] = tmp1;}this.size++;}}//用新表替换原表this.arr = arr1;}}

利用该组样例进行简单测试之后插入和扩容均无误。 

 

删除元素
    public void remove (int data) {//先利用散列函数计算出插入位置int index = data % this.arr.length;node p = this.arr[index];if (p == null) {return;}if (p.data == data) {this.arr[index] = p.next;}else {while (p.next != null){if (p.next.data == data) {p.next = p.next.next;break;}p = p.next;}}}
查找元素
    //找到该元素返回该元素的值,没找到返回-1public int check(int data) {//先利用散列函数计算出插入位置int index = data % this.arr.length;node p = this.arr[index];if (p == null) {return -1;}while (p != null){if (p.data == data) {return p.data;}p = p.next;}return -1;}

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

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

相关文章

光强的检测与控制系统设计

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、实习内容二、实习方法2.1 proteus仿真部分2.2 使用Altium designer软件绘制原理图2.2.1 工程创建2.2.2 绘制封装以及链接封装与原件原理图2.2.3检查原件原理…

【深入浅出】寄存器精讲第一期

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、算法模板、汇编语言 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. ⛳️开篇1.1 &#x1f514;CPU 概述&#xff08;简单了解&#xff09…

分布式消息队列:RabbitMQ(1)

目录 一:中间件 二:分布式消息队列 2.1:是消息队列 2.1.1:消息队列的优势 2.1.1.1:异步处理化 2.1.1.2:削峰填谷 2.2:分布式消息队列 2.2.1:分布式消息队列的优势 2.2.1.1:数据的持久化 2.2.1.2:可扩展性 2.2.1.3:应用解耦 2.2.1.4:发送订阅 2.2.2:分布式消息队列…

mathtype7.4破解永久激活码

MathType(数学公式编辑器)是由Design Science公司研发的一款专业的数学公式编辑工具。MathType功能非常强大&#xff0c;尤其适用于专门研究数学领域的人群使用。使用MathType让你在输入数学公式的时候能够更加的得心应手&#xff0c;各种复杂的运算符号也不在话下。 MathType最…

SpringBoot小项目——简单的小区物业后台管理系统 认证鉴权 用户-角色模型 AOP切面日志 全局异常【源码】

目录 引出一、应用到的技术栈Spring、Spring MVC、Spring Boot基础SpringBoot进阶、SpringMVC原理、AOP切面MyBatis 数据库相关JavaWeb基础&#xff1a;Session等前端Vue、JavaScript、Bootstrap 二、后台管理系统的功能登录功能1.用户名密码登录2.验证码的登录 报修业务的处理…

贝叶斯变分方法:初学者指南--平均场近似

Eric Jang: A Beginners Guide to Variational Methods: Mean-Field Approximation (evjang.com) 一、说明 变分贝叶斯 (VB) 方法是统计机器学习中非常流行的一系列技术。VB 方法允许我们将 统计推断 问题&#xff08;即&#xff0c;给定另一个随机变量的值来推断随机变量的值&…

服务熔断保护实践--Hystrix

概述 微服务有很多互相调用的服务&#xff0c;构成一系列的调用链路&#xff0c;如果调用链路中某个服务失效或者网络堵塞等问题&#xff0c;而有较多请求都需要调用有问题的服务时&#xff0c;这是就会造成多个服务的大面积失效&#xff0c;造成服务“雪崩”效应。 服务“雪…

【C语言】优化通讯录管理系统

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家优化上一篇的通讯录&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 一. 前言二. 动态通讯录2.1 通讯录结构体2.2 初始化通讯录2.3 增加联系人2.4 销毁通讯…

Redis原理-IO模型和持久化

高性能IO模型 为什么单线程Redis能那么快 一方面&#xff0c;Redis 的大部分操作在内存上完成&#xff0c;再加上它采用了高效的数据结构&#xff0c;例如哈希表和跳表&#xff0c;这是它实现高性能的一个重要原因。另一方面&#xff0c;就是 Redis 采用了多路复用机制&#…

HTML简单实现v-if与v-for与v-model

Vue启动&#xff01;&#xff01; 首先VIewModel将View和Model连接一起&#xff0c;Model的数据改变View的数据也变 使用Visual Studio Code 启动Vue需要vue.js插件和导入CDN(包) vue.js插件&#xff1a;CTRL shift x 在搜索栏搜 索vue.js安装即可 CDN&#xff1a; http…

orb-slam3编译手册(Ubuntu20.04)

orb-slam3编译手册&#xff08;Ubuntu20.04&#xff09; 一、环境要求1.安装git2.安装g3.安装CMake4.安装vi编辑器 二、源代码下载三、依赖库下载1.Eigen安装2.Pangolin安装3.opencv安装4.安装Python & libssl-dev5.安装boost库 三、安装orb-slam3四、数据集下载及测试 写在…

k8s集群升级

目录 1. 部署cri-docker &#xff08;所有集群节点&#xff09; 2. 升级master节点 3. 升级worker节点 4. 部署containerd 1. 部署cri-docker &#xff08;所有集群节点&#xff09; k8s从1.24版本开始移除了dockershim&#xff0c;所以需要安装cri-docker插件才能使用docker …

MySQL6:索引使用原则,联合索引,联合主键/复合主键,覆盖索引、什么是回表?索引条件下推,索引的创建与使用,索引的创建与使用,索引失效

MySQL6&#xff1a;索引使用原则&#xff0c;联合索引&#xff0c;联合主键/复合主键&#xff0c;覆盖索引、什么是回表&#xff1f;索引条件下推&#xff0c;索引的创建与使用&#xff0c;索引的创建与使用&#xff0c;索引失效 索引使用原则列的离散(sdn)度 联合索引创建联合…

Harbor私有镜像仓库搭建

本文基于&#xff1a;https://zhuanlan.zhihu.com/p/143779176 1.环境准备 IP&#xff1a;192.168.10.136/24 操作系统:centos7 2.安装Docker、Docker-compose 2.1安装Docker-CE $ wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.re…

数据库简史:多主数据库架构的由来和华为参天引擎的机遇

注&#xff1a;本文发表后&#xff0c;收到了很多后台反馈&#xff0c;其中关于大型机的早期成就不容省略。微调重发本文&#xff0c;纯属个人观点&#xff0c;错谬之处&#xff0c;仍然期待指正。 2023年10月13日&#xff0c;在北京举办的“2023金融业数据库技术大会"上&…

redis6.0源码分析:跳表skiplist

文章目录 前言什么是跳表跳表&#xff08;redis实现&#xff09;的空间复杂度相关定义 跳表&#xff08;redis实现&#xff09;相关操作创建跳表插入节点查找节点删除节点 前言 太长不看版 跳跃表是有序集合zset的底层实现之一&#xff0c; 除此之外它在 Redis 中没有其他应用。…

电力巡检/电力抢修行业解决方案:AI+视频技术助力解决巡检监管难题

一、行业背景 随着国民经济的蓬勃发展&#xff0c;工业用电和居民用电需求迅速增加&#xff0c;电厂、变电站、输电线路高负荷运转&#xff0c;一旦某个节点发生故障&#xff0c;对生产、生活造成巨大的影响。目前电力行业生产现场人员、设备较多&#xff0c;而生产监督员有限…

基于vue小红书平台用户数据分析与可视化

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

【马蹄集】—— 搜索专题

搜索专题 目录 MT2238 数的增殖MT2239 二维矩阵中的最长下降序列MT2240 传染病MT2241 循环空间BD202303 第五维度 MT2238 数的增殖 难度&#xff1a;黄金    时间限制&#xff1a;1秒    占用内存&#xff1a;128M 题目描述 给定一个数 n ( n < 1000 ) n (n<1000) n…

Java I/O (输入/输出)

1.流的概念 流是一种有序的数据序列&#xff0c;根据操作类型&#xff0c;可以分为输入流和输出流两种。I/O流&#xff08;输入输出&#xff09;提供了一条通道程序&#xff0c;可以使用这条通道把源中的字节序列送到目的地。 1.1 输入流&#xff1a; 程序从指向源的输入流中读…