第七节1:Java集合框架之二叉排序树和哈希表

Posted 快乐江湖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第七节1:Java集合框架之二叉排序树和哈希表相关的知识,希望对你有一定的参考价值。

文章目录

一:二叉排序树(二叉搜索树)基本概念及实现

(1)定义

二叉排序树(Binary Sort Tree)::又称之为二叉搜索树,它具有下面的性质

  • 若其左子树不空,则左子树上所有结点的值均小于根结点的值
  • 若其右子树不空,则右子树上所有结点的值均大于根结点的值
  • 其左、右子树也分别是二叉排序树

由以上性质可知,二叉排序树的中序遍历是一个递增序列

(2)二叉排序树操作

A:查找

二叉排序树查找:若树非空,让目标值与根节点的值进行比较。查找成功返回结点指针,失败则返回NULL

  • 如果相等,那么查找成功
  • 如果小于,则在左子树上继续查找
  • 如果大于,则在右子树上继续查找

B:插入

二叉排序树插入:其本质就是将关键字放到树中的合适位置,和查找思想一致,而且插入的地方总在叶子结点处

  • 注意:二叉排序树内不能存在两个相同的数据

C:删除

二叉排序树删除:二叉排序树的删除操作需要仔细分析,因为插入操作能保证每次插入后仍然是一颗二叉排序树,但是删除操作可能导致整个树的特性发生变化。二叉树排序树删除某结点时需要考虑三种情况

  • 待删除结点为叶子结点
  • 待删除结点的左子树或右子树为空
  • 待删除结点 的左子树和右子树都存在

当然叶子结点可以归结为左子树为空或右子树为空那一种情况,因此共有左为空,右为空和左右都不为空这么三种情况

①:如果左子树为空

处理办法:如果待删除结点左子树为空,那么让父亲的左子树或者右子树指向我的右子树


  • 需要注意,如果删除的是根结点,那么就让根结点的右孩子结点直接作为根结点

②:如果右子树为空

处理办法:如果待删除结点右子树为空,那么让父亲的左子树或者右子树指向我的左子树

③:如果左右子树都不为空

处理办法:从要删除的结点位置开始,寻找左子树的最右结点(也就是左子树的最大结点)或右子树的最左结点(也就是右子树的最小节点)替代要删除的结点。替代后,这个问题就转化为了删除左为空或右为空的结点了

如下,以寻找右子树的最左结点为例,这里删除根结点5。首先寻找5的右子树的最左结点,是6,submin标记,同时记录6的父亲结点7,submin_pre标记然后将submin处的6直接赋值给要删除的结点5,这样结点5等于就删除了,接着只需要将submin删除即可。在这种情况下找到的submin一定满足左子树为空,所以符合上面的那种情况,删除后让其父亲结点的左子树或右子树连接到它的右子树11即可

但是要注意一个特殊情况:submin本身就是要删除结点cur右子树的最小结点,因此submin_pre在赋值时,一定要赋值为cur

(3)二叉排序树实现

package myBinarySearchTree;

public class MyBinarySearchTree 
    //节点定义
    static class TreeNode
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val)
            this.val = val;
        
    

    public TreeNode root;

    //查找
    public TreeNode search(int key)
        TreeNode cur = root;
        while(cur != null)
            if(key > cur.val)
                cur = cur.right;
            else if(key < cur.val)
                cur = cur.left;
            else
                return cur;
            
        
        return null;
    

    //插入
    public Boolean insert(int key)
        TreeNode node = new TreeNode(key);
        //如果当前BST为空
        if(root == null)
            this.root = node;
            return true;
        
        TreeNode cur = root;
        TreeNode pre = null;
        while(cur != null)
            if(key > cur.val)
                pre = cur;
                cur = cur.right;
            else if(key < cur.val)
                pre = cur;
                cur = cur.left;
            else
                return false;//数据相同不能插入
            
        
        if(key > pre.val)
            pre.right = node;
        else
            pre.left = node;
        
        return true;
    

    //删除
    public void remove(int key)
        TreeNode cur = root;
        TreeNode pre = null;
        while(cur != null)
            if(key > cur.val)
                pre = cur;
                cur = cur.right;
            else if(key < cur.val)
                pre = cur;
                cur = cur.left;
            else
                //找到待删除结点
                removeNode(cur, pre);
                return;;
            
        
    
    //删除结点方法
    private void removeNode(TreeNode cur, TreeNode pre) 
        //情况1:如果待删除的结点左子树为空
        if(cur.left == null) 
            if(cur == root)this.root = this.root.right;//特殊情况:如果待删除结点本身就是根节点,那么直接让该结点的右结点作为根节点
            else
                //正常情况下就让其父节点的左子树或者右子树指向待删除结点的右子树即可
                if(pre.left == cur)pre.left = cur.right;
                if(pre.right == cur)pre.right = cur.right;
            
        //情况2:如果待删除的结点右子树为空
        else if(cur.right == null)
            if(cur == root)this.root = this.root.left;//特殊情况:如果待删除结点本身就是根节点,那么直接让该结点的左结点作为根节点
            else
                //正常情况下就让其父节点的左子树或者右子树指向待删除结点的左子树即可
                if(pre.left == cur)pre.left = cur.left;
                if(pre.right == cur)pre.right = cur.left;
            
        
        //情况3:如果左右子树都不为空
        /*从待删除结点开始寻找其右子树的最左结点(用submin标记该结点,并用submin_pre标记该结点的父节点)
         然后把submin结点复制到cur处,接着再删除submin结点即可
         在这种情况下找到的submin一定满足左子树为空,所以删除submin时就等价于情况1
         */
        else
            TreeNode submin_pre = cur;
            TreeNode submin = cur.right;
            while(submin.left != null)
                submin_pre = submin;
                submin = submin.left;
            
            cur.val = submin.val;
            if(submin_pre.left == submin)
                submin_pre.left = submin.right;
            else
                submin_pre.right = submin.right;
            
        
    


二:哈希表(散列表)基本概念及实现

(1)定义

哈希表(Hash table):在元素与其存储位置之间唯一确定一对应关系 f f f,称之为哈希函数,使得每个关键字 k e y key key对应一个存储位置 f ( k e y ) f(key) f(key)。若把所有记录存储在一块连续的空间上,那么这样的结构就称之为哈希表,关键字对应的存储位置则称之为哈希地址

(2)建立一个简单的哈希表(快速入门以及相关术语)

给出这样一组数据: 19 19 19 14 14 14 23 23 23 1 1 1 68 68 68 20 20 20 84 84 84 27 27 27 55 55 55 11 11 11 10 10 10 79 79 79,设哈希函数为
H ( k e y ) = k e y % 13 H(key)=key\\%13 H(key)=key%13

  • 这样确定地址的方法称之为除留余数法,当然还有其他方法后面会细谈

由于所有数据全部对13取余,所以其结果会被映射在0~12这个范围内,如下

  • 14 14 14% 13 13 13= 1 1 1
  • 19 19 19% 13 13 13= 6 6 6
  • 20 20 20% 13 13 13= 10 10 10

继续进行,会发现数据 1 1 1% 13 13 13也等于1,这就意味着 1 1 1 14 14 14会映射在相同的地址处,这里 1 1 1 14 14 14就称之为同义词,而它们产生的现象称之为冲突

解决冲突的方法也有很多种,这里以链地址法为例

  • 具体细节后面细说,但是通过下面的图相信大家也能瞬间明白链地址法的思想

哈希表建立好之后,就可以进行查找了。比如查找关键字 27 27 27,计算 27 27 27% 13 = 1 13=1 13=1,因此在地址为 1 1 1处进行查找即可

如果查找关键字 21 21 21,计算 21 21 21% 13 = 8 13=8 13=8,但是8的位置处是一个空指针,所以直接判定查找失败

如果查找关键字 66 66 66,计算 66 66 66% 13 13 13=1,在1的位置处的链表上逐个比较后,发现该元素也不存在

(3)常见哈希函数

A:直接定址法

取关键字的某个线性函数为哈希地址,即
H a s h ( K e y ) = A ∗ K e y + B Hash(Key)=A*Key+B Hash(Key)=AKey+B

优点: 简单、快速、均匀
缺点: 需要事先知道关键字的分布情况
适用情形: 适合查找比较小且连续的情况

比如LeetCode387:字符串中的第一个唯一字符展现的就是直接定址,字母与数组下标一一映射,映射函数就是每个字母与小写字母 a a a的相对位置,地址就是数组下标

class Solution 
public:
    int firstUniqChar(string s) 
    
        int count[26]=0;
        for(int i=0;i<s.size();i++)
        
            count[s[i]-'a']++;
        

        for(int i=0;i<s.size();i++)
        
            if(count[s[i]-'a']==1)
                return i;
        
        return -1;
    

;

B:除留余数法

设哈希表中允许的地址数为 m m m ,取一个不大于m,但是最接近或者等于 m m m的质数 p p p作为除数,按照哈希函数
H a s h ( K e y ) = k e y % p ( p ≤ m ) Hash(Key)=key\\%p(p\\leq m) Hash(Key)=key%p(pm)

将关键码转换为哈希地址

这种方法最大的缺陷就是会产生哈希冲突——不同的关键字映射到了相同的地址

C:平方取中法

取关键字平方后的中间几位作为 H a s h Hash Hash地址。一个数平方后的中间几位数和数的每一位都相关,由此得到的Hash地址随机性会更大,适用于不知道关键字的分布,而位数又不是很大的情况

比如关键字 1234 1234 1234,其平方后的结果为 1522756 1522756 1522756,可以取227作为哈希地址

D:数字分析法

设有 n n n d d d位数,每一位可能有 r r r种不同的符号取值,这 r r r种不同的符号在各个位上出现的频率不一定相同,可能在某些位上分布较为均匀,每种符号出现的机会均等,而在某些位上分布不均匀只有某种符号经常出现。可以根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。比较适合于处理关键字位数较大的情况,以及事先知道关键字的分布情况

生活中常见的就是手机号

E:折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短一点),然后这几部分重叠求和,并按照散列表表长,取后几位作为散列地址。
比较适合于事先不知道关键字的分布,以及关键字位数较多的情况。

F:随机数法

选择一个随机数,取关键字的随机函数值作为其哈希地址,即
H ( K e y ) = r a n d o m ( k e y ) H(Key)=random(key) H(Key)=random(key)
其中 r a n d o m random random为随机函数

(4)解决Hash冲突的方法

A:闭散列(开放定址法)

当产生哈希冲突时,如果哈希表没有被装满,那么哈希表中必然还会有空的位置,所以开放定址法就会不断寻找空的位置,找到空的位置将关键字塞入进去。根据找的方法不同,开放定址法又分为线性探测法和二次探测法

①:线性探测法

从没有发生冲突的位置开始,依次向后探测&#

以上是关于第七节1:Java集合框架之二叉排序树和哈希表的主要内容,如果未能解决你的问题,请参考以下文章

第七节:Java集合框架之map和set

算法之二叉树各种遍历

第七节2:Java集合框架之map和set

数据结构之二叉搜索树和二叉平衡树学习笔记

数据结构(Java描述)之二叉树

二叉树和哈希表的优缺点对比与选择