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

news2024/12/25 1:04:46

目录

二叉搜索树

二叉搜索树的插入 

二叉搜索树的查找

 二叉搜索树的删除

哈希表 

哈希冲突

闭散列

线性探测法

二次探测法

开散列

开散列代码实现:

插入元素 

删除元素

查找元素


二叉搜索树

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

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

  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;
            //插入成功返回true
            return 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;
        }
        //插入成功返回true
        return true;
    }
}

二叉搜索树的查找

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

  1. 从根节点开始比较,如果大于根节点就遍历右子树,小于根节点就遍历左子树
  2. 对所有的子树都进行如上操作
    //查找key是否存在
    public TreeNode search(int key) {
        //判断树是否为空,为空就直接返回null
        if (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;
            }
        }
        //当出循环之后代表没找到该元素返回null
        return 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;
        }
    }
    //哈希表,默认大小为10
    node[] arr = new node[10];
    //数组中的元素个数
    int size = 0;
    //最大负载因子,默认为0.75
    double 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;
            }
        }
    }
查找元素
    //找到该元素返回该元素的值,没找到返回-1
    public 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.coloradmin.cn/o/1147729.html

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

相关文章

L2-2 老板的作息表

L2-2 老板的作息表 分数 25 全屏浏览题目 切换布局 作者 陈越 单位 浙江大学 新浪微博上有人发了某老板的作息时间表&#xff0c;表示其每天 4:30 就起床了。但立刻有眼尖的网友问&#xff1a;这时间表不完整啊&#xff0c;早上九点到下午一点干啥了&#xff1f; 本题就请你…

30个前端和设计必备网站,让你的工作更轻松!

说在前面 当今互联网时代&#xff0c;前端开发和设计领域变化迅速&#xff0c;每天都会有新技术和工具不断涌现。无论你是一名前端工程师还是一名设计师&#xff0c;都需要不断了解最新的前沿技术和工具。下面是30个前端和设计经常会用到的网站&#xff0c;以及每个网站的功能介…

程序设计语言

编译解释 传参还是传值 编译原理

洛谷 B2029 大象喝水 C++代码

题目描述 AC Code #include<bits/stdc.h> using namespace std; int main() {int h,r;cin>>h>>r;double val;val3.14*r*r*h;int ans20000/val;ans;cout<<ans<<endl;return 0; }

Springmvc 讲解(1)

文章目录 前言一、SpringMvc1、简介2、核心组件和调用流程2.1 涉及组件的理解 3、小案例快速体验3.1场景需求3.1.1 导入依赖3.1.2 controller声明3.1.3 核心配置类3.1.4 环境搭建3.1.6 配置tomcat3.1.7 测试 二、SpringMvc 接收参数1.路径设置注解2、param接收参数四种类型2.1 …

Linux下的IMX6ULL——开发板的第一个APP和驱动实验(三)

前言&#xff1a; 万事开头难&#xff0c;如果我们在开发板上开发出第一个应用程序&#xff0c;第一个驱动程序&#xff0c;那么后续的开发就会稍微简单点&#xff0c;下面让我们来进行第一个应用程序和第一驱动程序的开发吧。 目录 一、开发板的第1个APP实验 1.通过Git仓库…

Day 13 python学习笔记

模块 内置模块 加密 加密是成本与时间的问题 不可逆加密 哈希加密&#xff0c;单向性&#xff0c;唯一性。 可逆加密 对称加密&#xff1a;加密与解密采用同一密匙&#xff08;如文件压缩时设置密码&#xff09; DES算法 非对称加密&#xff1a;采用一对密匙&#xff0c;公匙、…

邦邦资讯|邦邦机器人10月新鲜事来袭,请查收

01 邦邦机器人研发中心徐汇新址落成 立足新起点 实现新跨越 10月初&#xff0c;邦邦机器人成立徐汇研发中心&#xff0c;历时半年的策划、设计、讨论、施工&#xff0c;正式落定上海市徐汇区桂林路406号华鑫中心1号楼505室&#xff0c;“这是邦邦机器人发展史的又一座重要里程…

java代码审计-不安全的配置-Tomcat任意文件写入(CVE-2017-12615)

Tomcat任意文件写入&#xff08;CVE-2017-12615&#xff09; 影响范围&#xff1a;Apache Tomcat 7.0.0 - 7.0.79 (windows环境) 当 Tomcat 运行在 Windows 操作系统时&#xff0c;且启用了 HTTP PUT 请求方法&#xff08;例如&#xff0c;将 readonly 初始化参数由默认值设置…

部署K8S

防火强的初始化&#xff1a; [rootk8s-node-12 ~]# systemctl stop firewalld NetworkManager [rootk8s-node-12 ~]# systemctl disable firewalld NetworkManager Removed symlink /etc/systemd/system/multi-user.target.wants/NetworkManager.service. Removed symlink /et…

【Android】MQTT入门——服务器部署与客户端搭建

目录 MQTT 协议简介应用场景优点缺点 部署服务端下载安装包启动服务器 搭建客户端下载SDK添加依赖配置MQTT服务和权限建立连接订阅主题发布消息取消订阅断开连接 MQTT客户端工具最终效果实现传感器数据采集与监测功能思路 MQTT 协议 简介 MQTT&#xff08;Message Queuing Te…

剑指JUC原理-6.wait notify

wait nofity 小故事 - 为什么需要 wait 由于条件不满足&#xff0c;小南不能继续进行计算 但小南如果一直占用着锁&#xff0c;其它人就得一直阻塞&#xff0c;效率太低 于是老王单开了一间休息室&#xff08;调用 wait 方法&#xff09;&#xff0c;让小南到休息室&#xf…

C++可视化 有穷自动机NFA 有穷自动机DFA

一、项目介绍 根据正则表达式,可视化显示NFA&#xff0c;DFA&#xff1b;词法分析程序 二、项目展示

洛谷 B2007 A+B问题 C++代码

目录 题目描述 AC Code 题目描述 AC Code #include<bits/stdc.h> using namespace std; typedef long long ll; int main() { int a,b;cin>>a>>b;cout<<ab<<endl;return 0; }

F5修复了允许远程代码执行攻击的BIG-IP认证绕过漏洞

图片 导语 近日&#xff0c;网络安全公司Praetorian Security的研究人员发现了一项影响F5 BIG-IP配置工具的严重漏洞&#xff0c;该漏洞被命名为CVE-2023-46747。攻击者可以通过远程访问配置工具来执行未经身份验证的远程代码&#xff0c;从而对系统进行攻击。本文将详细介绍该…

基于 Python 的个性化电影推荐系统的研究与实现

1 简介 本毕业设计的内容是设计并且实现一个电影个性化推荐系统。它是在Windows下&#xff0c;以MYSQL为数据库开发平台&#xff0c;Python技术和Tomcat网络信息服务作为应用服务器。电影个性化推荐系统的功能已基本实现&#xff0c;主要实现首页&#xff0c;个人中心&#xf…

热力学第三定律

热力学第三定律能斯特定理 凝聚系的熵在等温过程中的改变随绝对温度趋于零 如果有 即有&#xff1a; 也就是说&#xff1a;当绝对温度趋于零时&#xff0c;熵和状态参量y无关。 普朗克绝对熵 当绝对温度趋于零时&#xff0c;一个化学均匀系统的熵趋向于一个极限值 也就是说&a…

【数据结构】面试OJ题——时间复杂度2

目录 一&#xff1a;移除元素 思路&#xff1a; 二&#xff1a;删除有序数组中的重复项 思路&#xff1a; 三&#xff1a;合并两个有序数组 思路1&#xff1a; 什么&#xff1f;你不知道qsort&#xff08;&#xff09; 思路2&#xff1a; 一&#xff1a;移除元素 27. 移…

离线语音通断器开发-稳定之后顺应新需求

使用云知声的US516p6方案开发了一系列的离线语音通断器&#xff0c;目前已经取得了不小的收获&#xff0c;有1路的&#xff0c;3路的&#xff0c;4路的&#xff0c;唛头和扬声器包括唛头线材也在不断的更新打磨中找到了效果特别好的供应商。 离线语音通断器&#xff0c;家用控…