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

news2024/9/8 23:46:09

  💎 欢迎大家互三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;
    }

    @Override
    public 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);
    }

    @Override
    public 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前,这是两个不同的对象,重写后为true
        System.out.println(person1.equals(person2));
        
        //两个不一样的对象拥有了相同的哈希值
        System.out.println("abc".hashCode());//96354
        System.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.coloradmin.cn/o/1957922.html

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

相关文章

SCI算法!发文首选!参数优化下的BiLSTM-KAN模型回归预测,Python代码

声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类算法的家人&#xff0c;可关注我的VX公众号&#xff1a;python算法小当家&#xff0c;不定期会有很多免费代码分享~ KAN是2024年最新的算法&#xff0c;是近期非常热门的一…

leetcode112. 路径总和 leetcode113. 路径总和II,图文并茂,教你完全弄懂DFS,附详细代码

leetcode112. 路径总和 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 …

实验2-3-8 计算火车运行时间

//实验2-3-8 计算火车运行时间 /* 输入格式&#xff1a;输入在一行中给出2个4位正整数&#xff0c;其间以空格分隔&#xff0c;分别表示火车的出发时间和到达时间。 每个时间的格式为2位小时数&#xff08;00-23&#xff09;和2位分钟数&#xff08;00-59&#xff09;&#xff…

从0到1,AI我来了- (4)AI图片识别的理论知识-II

上篇文章&#xff0c;我们理解了我们程序的神经网络设计&#xff0c;这篇我们继续&#xff0c;把训练迭代过程分析一下&#xff0c;完成这两篇文章&#xff0c;下面问题&#xff0c;应该能回答了。 一张图片&#xff0c;如何被计算机读懂&#xff1f;pytorch 封装的网络&#…

C语言实现K均值聚类

K均值聚类(K_means)基础理论 K_means聚类是一种简单且广泛使用的聚类算法&#xff0c;它旨在将数据集中的样本划分为k个不同的聚类&#xff0c;其中k是事先指定的聚类数量&#xff0c;该算法的核心思想是迭代地优化聚类中心&#xff0c;以最小化每个样本与其所属聚类中心之间的…

数据仓库中的DIM层-定义、设计与最佳实践

在当今数据驱动的商业环境中,构建高效的数据仓库架构至关重要。本文将深入探讨数据仓库中的维度层(DIM层),帮助您了解其定义、重要性以及设计最佳实践。 目录 什么是DIM层?DIM层的重要性DIM层设计最佳实践1. 选择适当的粒度2. 实施慢速变化维度(SCD)3. 使用代理键4. 规范化v…

x264编解码库 -介绍和使用示例

目录 1&#xff1a;X264简单介绍 1.1&#xff1a;编译x264 1.2&#xff1a;x264简单介绍 1.3&#xff1a;x264的优势 1.4&#xff1a;x264与FFmpeg的关系 1.5&#xff1a;x264 编解码原理 1.6 进一步学习资源 2&#xff1a;demo效果 3&#xff1a;完整代码 4&#xff1a;附件…

Vite + Vue3 + TS项目配置前置路由守卫

在现代前端开发中&#xff0c;使用 Vue 3 和 TypeScript 的组合是一种流行且高效的开发方式。Vite 是一个极速的构建工具&#xff0c;可以显著提升开发体验。本文博主将指导你如何在 Vite Vue 3 TypeScript 项目中配置前置路由守卫&#xff08;Navigation Guards&#xff09;…

学习日记:数组

1. 概念 一组相同类型的数据的集合&#xff0c;也是一种数据类型。 2. 一维数组 2.1 语法 类型说明符 数组名 [常量表达式] &#xff08;1&#xff09; &#xff08;2&#xff09; &#xff08;3&#xff09; &#xff08;1&#xff09;类型说明符&#x…

Leetcode - 周赛407

目录 一&#xff0c;3226. 使两个整数相等的位更改次数 二&#xff0c;3227. 字符串元音游戏 三&#xff0c;3228. 将 1 移动到末尾的最大操作次数 四&#xff0c;3229. 使数组等于目标数组所需的最少操作次数 一&#xff0c;3226. 使两个整数相等的位更改次数 本题可以暴力…

刷题了:144.二叉树的前序遍历心|145.二叉树的后序遍历心|94.二叉树的中序遍历己

递归遍历 文章讲解:https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86.html#%E6%80%9D%E8%B7%AF 视频讲解:https://www.bilibili.com/video/BV1Wh411S7xt/?spm_id_from333.788&vd_sourcee70917aa6392827d1ccc8d85e1…

ubuntu20.04安装nginx,mysql8,php7.4详细教程,包成功

目录 1.更新索引 2.安装 Nginx 1.安装 Nginx&#xff1a; 2.启动 Nginx 服务并设置为开机自启&#xff1a; 3.开放防火墙的 80 端口&#xff1a; 4.检查 Nginx 是否正常运行&#xff1a; 3.安装 MySQL 8.0 1.首先&#xff0c;安装 MySQL 的仓库&#xff1a; 安装过程中你会看…

Python --Pandas库基础方法(2)

文章目录 Pandas 变量类型的转换查看各列数据类型改变数据类型 重置索引删除行索引和切片seriesDataFrame取列按行列索引选择loc与iloc获取 isin()选择query()的使用排序用索引排序使用变量值排序 修改替换变量值对应数值的替换 数据分组基于拆分进行筛选 分组汇总引用自定义函…

TinyVue 组件库官网焕然一新!

本文由体验技术团队Kagol原创~ 之前有一些朋友吐槽我们 TinyVue 组件库的 UI 不够美观&#xff0c;于是我们请了设计师小姐姐给我们的组件和网站进行优化&#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…

如何在Selenium Webdriver中点击SVG元素?

我们将在URL上单击下面突出显示的SVG元素&#xff1a;https&#xff1a;//testkru.com/Elements/SVGelemnts。 有几种方法可以点击SVG元素&#xff0c;我们将在这篇文章中讨论它们&#xff0c;并讨论它们之间应该首选哪一种。 使用 WebElement Click() 通过使用Action Click() …

前端JS特效第53集:带声音的烟花模拟绽放特效插件

带声音的烟花模拟绽放特效插件&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下(全部代码在文章末尾)&#xff1a; <!DOCTYPE html> <html lang"en" > <head><meta charset"UTF-8"><title>Firework Simulator v2&…

五、SpringIoC/DI的使用

1. 类注解、方法注解 告诉spring管理bean—>bean的存储 1、类注解&#xff1a;五大注解 Controller&#xff08;控制器存储&#xff09;、 Service&#xff08;服务存储&#xff09;、 Repository&#xff08;仓库存储&#xff09;、 Component&#xff08;组件存储&#xf…

一种多策略改进黑翅鸢智能优化算法IBKA(2024年新出优化算法)种群初始化精英反向+透镜成像反向学习+黄金正弦变异策略

一种多策略改进黑翅鸢智能优化算法IBKA&#xff08;2024年新出优化算法&#xff09; 种群初始化精英反向策略透镜成像反向学习策略黄金正弦变异策略 文章目录 前言一种多策略改进黑翅鸢智能优化算法IBKA&#xff08;2024年新出优化算法&#xff09; 种群初始化精英反向策略透镜…

35_YOLOX网络详解

1.1 简介 YOLOX是YOLO系列&#xff08;You Only Look Once&#xff09;目标检测模型的一个最新变种&#xff0c;由阿里云团队和旷视科技在2021年提出。YOLO系列以其快速、准确的目标检测能力而闻名&#xff0c;而YOLOX在此基础上进行了多方面的改进和优化&#xff0c;旨在提供…