二叉搜索树 和 哈希表 (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,一经查实,立即删除!

相关文章

代码随想录day4:链表总结

两两交换链表中的节点 一开始自己的思路只是两两交换&#xff0c;并没有说涉及到前一个节点。实际上两两交换涉及到了三个节点 使用虚拟头结点&#xff0c;这样一次性处理三个节点。且每次组里第一个节点其实数值没变。 class Solution { public:ListNode* swapPairs(ListNod…

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

提示&#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最…

云计算概述笔记

目录 IT发展趋势&#xff1a; IT定义&#xff1a;IT是信息处理的总集&#xff0c;包括&#xff1a;软件&#xff0c;硬件&#xff0c;通信和相关服务等。 传统IT架构的核心&#xff1a;以数据中心为基础的核心架构。 传统IT面临的挑战&#xff1a; IT发展趋势&#xff1a; …

力扣42.接雨水(java,暴力法、前缀和解法)

Problem: 42. 接雨水 文章目录 思路解题方法复杂度Code 思路 要能接住雨水&#xff0c;感性的认知就是要形成一个“下凹区域”&#xff0c;则此时我们就要比较当前柱子和其左右柱子高度的关系&#xff0c;易得一个关键的式子&#xff1a;当前小区域的积水 min&#xff08;当前…

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

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

LLM-Embedder

1. 目标 训出一个统一的embedding模型LLM-Embedder&#xff0c;旨在全面支持LLM在各种场景中的检索增强 2. 模型的四个关键检索能力 knowledge&#xff1a;解决knowledge-intensive任务memory&#xff1a;解决long-context modelingexample&#xff1a;解决in-context learn…

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

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

常见的配置文件格式:yaml,json,xml,ini,csv等

目录 1、配置文件的作用 2、什么是硬编码&#xff1f; 3、常见的配置文件格式 1、配置文件的作用 为什么需要配置文件&#xff1a; 主要作用是将应用程序或系统的配置参数和设置从源代码中分离出来&#xff0c;使它们变得易于修改和管理。通过将配置信息存储在配置文件中&#…

服务熔断保护实践--Hystrix

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

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

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

Mybatis中执行Sql的执行过程

MyBatis中执行SQL的过程可以分为以下几个步骤&#xff1a; 解析配置文件&#xff1a;在运行时&#xff0c;MyBatis会加载并解析配置文件&#xff08;通常为mybatis-config.xml&#xff09;&#xff0c;获取数据库连接信息、映射文件等。 创建SqlSessionFactory&#xff1a;MyB…

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…

UDP编程

UDP编程&#xff1a; 用packet和socket完成 ● 流 程&#xff1a; DatagramSocket与DatagramPacket 建立发送端&#xff0c;接收端 建立数据报&#xff0c;用于储存数据 调用Socket的发送、接收方法 关闭Socket ● 发送端与接收端是两个独立的运行程序 发送端&#xf…

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四、数据集下载及测试 写在…

Python selenium模块简介

视频版教程&#xff1a;一天掌握python爬虫【基础篇】 涵盖 requests、beautifulsoup、selenium 有些网站的数据是js动态渲染的&#xff0c;我们无法通过网页源码直接找到数据&#xff0c;只能通过找接口方式来获取数据&#xff0c;但是很多时候&#xff0c;数据又是json格式的…

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 …