散列表和树快速入门

本文深入浅出地介绍了散列表和树的基本概念,包括散列函数的设计、散列冲突及其解决方法,以及二叉树、二叉查找树、AVL树的结构与特性。特别强调了散列表和二叉查找树在实际应用中的优缺点,如查找效率、数据有序性以及平衡调整策略。文章适合初学者入门学习。
摘要由CSDN通过智能技术生成

概述

你可能不知道一篇文章把散列表和树入门讲透是什么概念,我们一般把这种人称为 土块

我经常说一句话,当年林纳斯花了几个月就写出了Linux,我JY用一篇文章讲个入门 不是问题

埋伏他一手,这句骚话不能写,这个标签不用创,他死定了。

反手给个概述,闷声发大财!他也来概述?但是不用怕,他赢不了我。

如果把这个概述换成结语,我这波将绝杀,但是换不得。

(未完待续 To Be Continue…)

散列表

1.概念

散列表(Hash Table)又名哈希表/Hash 表,是根据键(Key)直接访问在内存存储位置的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性。

2.散列函数

2.1散列函数的要求及特点

散列函数就是一个函数(方法),能够将给定的 key 转换为特定的散列值,我们可以表示为:

hash(key) = hashValue

散列函数要满足的几个基本要求

  1. 散列函数计算得到的散列值必须是大于等于 0 的正整数,因为 hash 值需要作为数组的下标。
  2. 如果 key1== key2,那么经过 hash 后得到的哈希值也必相同即: hash(key1) == hash(key2)
  3. 如果 key1 != key2,那么经过 hash 后得到的哈希值也必不相同即:hash(key1) != hash(key2)

好的散列函数应该满足以下特点

  1. 散列函数不能太复杂,因为太复杂度势必要消耗很多的时间在计算哈希值上,
    也会间接影响散列表性能。
  2. 散列函数计算得出的哈希值尽可能的能随机并且均匀的分布,这样能够将散列
    冲突最小化。

2.2散列函数的设计方法

2.2.1直接寻址法:

我们可以取关键字 key 的某个线性函数值为散列地址,即:hash(key) = a x key + b,其中 a,b 为常量

例子:比如我们现在要对 0-100 岁的人口数字统计表,那么我们对年龄这个关键字 key就可以直接用年龄的数字作为地址。 此时 hash(key) = key。这个时候,我们可以得出这么个哈希函数: hash(0) = 0, hash(1) = 1, ……, hash(20) = 20。

这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字 key 的分布情况,适合査找表较小且连续的情况。由于这样的限制,在现实应用中,直接寻址法虽然简单,但却并不常用。

2.2.2除留余数法

除留余数法此方法为最常用的构造散列函数方法。 对于散列表长为 m 的散列函数公式为:
hash( key ) = key % p ( p ≤ m )

使用除留余数法的一个经验是,若散列表表长为 m,通常 p 为小于或等于表长(最好接近 m)的最大质数或不包含小于 20 质因子的合数。总之实践结果证明:当 P 取小于哈希表长的最大质数时,产生的哈希函数较好。

2.2.3平方取中法

这是一种常用的哈希函数构造方法。这个方法是先取关键字的平方,然后根据可使用空间的大小,选取平方数是中间几位为哈希地址。 hash(key) = key 平方的中间几位

这种方法的原理是通过取平方扩大差别,平方值的中间几位和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。

2.2.4折叠法

这个没太搞懂,解释不起来,我直接截取原话大家理解理解

有时关键码所含的位数很多,采用平方取中法计算太复杂,则可将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址,这方法称为折叠法,折叠法可分为两种:
移位叠加:将分割后的几部分低位对齐相加。
边界叠加:从一端沿分割界来回折叠,然后对齐相加。

2.3散列冲突

又名Hash冲突Hash碰撞

两个不同的关键字(key),由于散列函数值相同,因而被映射到同一表位置上。该现象称为散列冲突或哈希碰撞。

即key1 != key2 hash(key1)==hash(key2)

3.散列冲突的解决方案

3.1开放寻址法

开放寻址法的核心思想是:一旦出现了散列冲突,我们就重新去寻址一个空的散列地址。

3.1.1线性检测 :

我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。

上述过程其实就是向散列表中插入数据,如果要从散列表中查找是否存在某个元素,这个过程跟插入类似,先根据散列函数求出要查找元素的 key 的散列值,然后比较数组中下标为其散列值的元素和要查找的元素,如果相等则表明该元素就是我们想要的元素,如果不等还要继续向后寻找遍历,如果遍历到数组中的空闲位置还没有找到则说明我们要找的元素并不在散列表中。

散列表删除操作: 从散列表中查找是否存在某个元素一旦在对应 hash 值下标下的元素不是我们想要的就会继续在散列表中向后遍历,直到找到数组中的空闲位置,但如果这个空闲位置是我们刚刚删除的,那就会中断向后查找的过程,那这样的话查找的算法就会失效,本来应该认定为存在的元素会被认定为不存在,那删除的问题如何解决呢?我们可以将删除的元素特殊标记为 deleted,当线性检测遇到标记 deleted的时候并不停下来而是继续向后检测。

因此,使用线性检测的方式存在很大的问题:那就是当散列表中的数据越来越多的时候,散列冲突发生的可能性就越来越大,空闲的位置越来越少,那线性检测的时间就会越来越长,在极端情况下我们可能需要遍历整个数组,所以最坏的情况下时间复杂度为 O(n)

3.1.2二次检测

所谓的二次检测跟线性检测的原理一样,只不过线性检测每次检测的步长是 1,每次检测的下标依次是: hash(key)+0, hash(key)+1, hash(key)+2,hash(key)+3…,所谓的二次检测指的是每次检测的步长变为原来的二次方,即 hash(key)+0, hash(key)+1^2, hash(key)+22,hash(key)+32…

3.1.3双重散列

所谓的双重散列,意思就是不仅要使用一个散列函数。我们使用一组散列函数hash1(key), hash2(key),hash3(key)……我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

比如先用除余发现此位置有元素,再用平方法

3.2链表法

在散列表中,数组的每个下标位置我们可以称之为“桶(bucket) ”或者“槽( slot) ”,每个桶(槽)会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。

基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

4.应用(HashMap底层解析)

HashMap 的数据结构图 :

面试必问:

jdk1.8 中关于 HashMap 的实现跟 jdk1.7 的几点差别:

1.数据结构引入了红黑树,好处是可以提高查询效率(jdk1.7 中极端情况下查询是 O(n),如果引入红黑树在极端情况下的查询可以降低为 O(log n)),当散列表某一桶内链表节点数>=8 时链表树化成红黑树,红黑树太小时退化成链表,退化的阈值为 6

2.计算 key 的 hash 值的方式不一样,但是思路和原理一样都是对 key 的hashCode 进行扰动让高位和低位一起参与运算计算出更加均匀的 hash 码,降低hash 冲突的概率。

3.插入数据时如果发送了 hash 冲突,优先判断该位置上是否是红黑树,如果是则存入红黑树中,如果是链表则插入链表尾节点上(jdk1.7 是插入到链表头节点上),插入完成后还判断链表的节点数是否大于等于设定好的链表转红黑树的阈值,如果满足则将链表转换为红黑树。

4.两个版本都会产生扩容操作,只不过 jdk1.8 中扩容涉及到对红黑树的操作以及优化了在 hash 冲突时计算元素新下标的代码,使其非常简单高效!

5.哈希算法

5.1定义

哈希算法又称为摘要算法,它可以将任意数据通过一个函数转换成长度固定的数据串,这个映射转换的规则就是哈希算法,而通过原始数据映射之后得到的二进制值串就是哈希值。 可见,摘要算法就是通过摘要函数 f()对任意长度的数据 data 计算出固定长度的摘要 digest,目的是为了发现原始数据是否被人篡改过

摘要函数是一个单向函数,计算 f(data)很容易,但通过 digest 反推 data 却非常困难。而且,对原始数据做
一个 bit 的修改,都会导致计算出的摘要完全不同。

那有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要呢?完全有可能!因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况就是我们说的碰撞。

5.2要求

  1. 将任何一条不论长短的信息,计算出唯一的一摘要(哈希值)与它相对应,对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同
  2. 摘要的长度必须固定,散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小
  3. 摘要不可能再被反向破译。也就是说,我们只能把原始的信息转化为摘要,而不可能将摘要反推回去得到原始信息,即哈希算法是单向的
  4. 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值

1.概念

1.1定义

树在维基百科中的定义为:, 树(英语: Tree)是一种无向图(undirected graph),其中任意两个顶点间存唯一一条路径。或者说,只要没有回路的连通图就是树。在计算机科学中, 树(英语: tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由 n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

1.2高度,深度及层

节点的高度:节点到叶子节点的最长路径(边数),所有叶子节点的高度为 0。
节点的深度:根节点到这个节点所经历的边的个数,根的深度为 0。
节点的层数:节点的深度+1
树的高度:根节点的高度

2.二叉树

2.1定义

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。

叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树

叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树

2.2为什么偏偏把最后一层的叶子节点靠左排列的叫完全二叉树

想要存储一棵二叉树,我们有两种方法,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。

链式存储法:

顺序存储法:

根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 * i = 2 的位置,右子节点存储在 2 * i + 1 = 3 的位置。

如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。

如果不按靠左排列

这样无疑多浪费了很多内存空间

如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。

2.3二叉树的遍历

前序遍历:对于树中的任意节点来说,先打印这个节点, 然后再打印它的左子树,最后打印它的右子树。

中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。

后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

二叉树的前序,中序,后序遍历是一个递归的过程,比如前序遍历,就是先答应根节点,然后递归的打印其左子树,然后递归的打印其右子树。

通过我们分析的二叉树的遍历流程我们可以发现,遍历二叉树的时间复杂度跟二叉树节点的个数 n 成正比,因此, 二叉树遍历的时间复杂度是 O(n)

3.二叉查找树

3.1结构及特点

二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值,有如下四个特点:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;
  4. 没有键值相等的节点。

3.2查找操作

准备工作:

public class Node {
   

    public int value;
    public Node left;
    public Node right;
    public Node(Node left,int value, Node right) {
   
        super();
        this.value = value;
        this.left = left;
        this.right = right;
    }
    public int getValue() {
   
        return value;
    }
    public void setValue(int value)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值