Map与Set

 

目录

 

一、全局看关系

二、搜索树

概念

查找

插入

 删除

三、Map

概念:

使用方式:

 底层结构图解:

四、Set

常见的方法:

​编辑 底层结构图解

五、哈希表

概念:

 冲突

用哈希函数降低冲突率:

负载因子调节(重点)

 负载因子概念:

闭散列

开散列(重点)

实现哈希桶

六、和JAVA类集的联系


一、全局看关系

 继承自Iterable的TreeSet和HashSet可以进行继承和迭代,TreeMap和HashMap方法则没有。

 TreeSet和TreeMap用到了红黑树,时间复杂度为O(logN);

HashSet和HashMap用到了哈希桶,时间复杂度为O(1)。

二、搜索树

概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

查找

1.先判断根节点,与根节点相等则找到

2.不相等时判断左右子树,比根节点小找左子树,比根节点大找右子树,由此重复操作,直到找到为止。

public boolean search(int value) {
        //没有值
        if(root==null){
            return false;
        }
        //遍历树
        TreeNode current=root;
        while(current!=null){
            //根节点为所找的值
            if(current.value==value){
                return true;
            }
            //如果比根节点值大,向右子树移动
            if (current.value<value){
                current=current.left;
            }
            //如果比根节点值小,向左子树移动
            if (current.value>value){
                current=current.right;
            }
        }
        //遍历所有值没有找到返回false
        return false;
    }

插入

1.如果树为空,证明数中没有元素,直接插入

2.树不为空时,先判断与根节点大小,大向右子树移动,小向左子树移动,直到找到合适的位置插入,相同则插入失败。

public boolean insert(int value) {
        //定义插入的数值为新的节点
        TreeNode node=new TreeNode(value);
        //根节点为空直接插入
        if(root==null){
            root=node;
            return true;
        }
        //遍历节点
        TreeNode current=root;
        //用来保存根节点
        TreeNode prev=null;
        while (current!=null){
            //如果根节点的值和插入的值相等,则不能插入
            if (current.value==value){
                return false;
            }
            prev=current;
            //如果比根节点的值大,向右子树移动
            if(current.value<value){
                current=current.right;
            }
            //如果比根节点的值小,向左子树移动
            if (current.value>value){
                current=current.left;
            }
        }
        if (value<prev.value){
            node=prev.left;
        }else {
            node=prev.right;
        }
        return true;
    }

 删除

设待删除结点为 cur, 待删除结点的双亲结点为 parent
1. cur.left == null
1. cur 是 root,则 root = cur.right
2. cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
3. cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
2. cur.right == null
1. cur 是 root,则 root = cur.left
2. cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
3. cur 不是 root,cur 是 parent.right,则 parent.right = cur.left
3. cur.left != null && cur.right != null
1. 需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被
删除节点中,再来处理该结点的删除问题

 public boolean remove(int value) {
        if (root == null) {
            return false;
        }
        // 找到要删除元素
        TreeNode current = root;
        TreeNode parent = null;
        while (current != null) {
            if (current.value == value) {
                removeNode(parent, current);
                return true;
            }
            // 记录父节点
            parent = current;
            if (value < current.value) {
                current = current.left;
            } else {
                current = current.right;
            }
        }

        return false;
    }

    private void removeNode(TreeNode parent, TreeNode current) {
        if (current.left == null) {
            // 当左孩子节点为空时进入
            if (current == root) {
                // 把当前要删除节点的右孩子赋给root
                root = current.right;
            } else if (current == parent.left) {
                // 当前节点是父节点的左孩子节点时
                parent.left = current.right;
            } else {
                // 当前节点是父节点的右孩子节点时
                parent.right = current.right;
            }
        } else if (current.right == null) {
            // 当前节点是根节点时
            if (current == root) {
                root = current.left;
            } else if (current == parent.left) {
                // 当前节点是父点的左孩子节点时
                parent.left = current.left;
            } else {
                // 当前节点是父节点的右孩子节点时
                parent.right = current.left;
            }
        } else {
            // 定义用来遍历的几个变量
            TreeNode target = current.right;
            TreeNode parentTarget = current;
            // 向左去找最小值
            while (target.left != null) {
                parentTarget = target;
                target = target.left;
            }
            // 到达叶子节点时
            current.value = target.value;
            // 删除target节点
            if (target == parentTarget.left) {
                parentTarget.left = target.right;
            } else {
                parentTarget.right = target.right;
            }
        }
    }

最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: log2N

最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2

三、Map

概念:

Map 是一个接口类,该类没有继承自 Collection ,该类中存储的是 <K,V> 结构的键值对,并且 K 一定是唯一的,不 能重复。
Map.Entry<K, V> Map 内部实现的用来存放 <key, value> 键值对映射关系的内部类 ,该内部类中主要提供了 <key, value>的获取, value 的设置以及 Key 的比较方式。

使用方式:

 底层结构图解:

注:
1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
2. Map中存放键值对的Key是唯一的,value是可以重复的
3. Map中的Key可以全部分离出来,存储到Set来进行访问(因为Key不能重复)
4. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)
5. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行 重新插入。
6. TreeMapHashMap的区别

四、Set

Set与map最大的不同:

1.set继承自collection的接口类

2.set只存储key,而map存储key,value。

常见的方法:

 底层结构图解

 

 注:

1. Set 是继承自 Collection 的一个接口类
2. Set 中只存储了 key ,并且要求 key 一定要唯一
3. Set 的底层是使用 Map 来实现的,其使用 key Object 的一个默认对象作为键值对插入到 Map 中的
4. Set 最大的功能就是对集合中的元素进行去重
5. 实现 Set 接口的常用类有 TreeSet HashSet ,还有一个 LinkedHashSet LinkedHashSet 是在 HashSet 的基础 上维护了一个双向链表来记录元素的插入次序。
6. Set 中的 Key 不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7. Set 中不能插入 null key
8. TreeSet HashSet 的区别

五、哈希表

概念:

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经过关键 码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O(log2N
) ,搜索的效率取决于搜索过程中 元素的比较次数。

 当向该结构中:

插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称为哈希表 (Hash Table)( 或者称散列表 )
例如:数据集合 {1 7 6 4 5 9}
哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。

 冲突

在运用上述方法进行查找位置时,难免会出现存储位置相同的情况,这就产生了冲突。

不同关键字通过相同哈 希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一

个问题, 冲突的发生是必然的 ,但我们能做的应该是尽量的 降低冲突率

由此,我们可以通过设计更合理的哈希函数来降低冲突率

用哈希函数降低冲突率:

1. 直接定制法 --( 常用 )
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况 
2. 除留余数法 --( 常用 )
设散列表中允许的地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址
3. 平方取中法 --( 了解 )
假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址; 再比如关键字为 4321 ,对它平方就是18671041 ,抽取中间的 3 671( 710) 作为哈希地址
平方取中法比较适合:不知道关键字的分 布,而位数又不是很大的情况
4. 折叠法 --( 了解 )
折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和, 并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法 --( 了解 )
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中 random 为随机数 函数。
通常应用于关键字长度不等时采用此法
6. 数学分析法 --( 了解 )
设有 n d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某 些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均 匀的情况

负载因子调节(重点)

 负载因子概念:

解决哈希冲突 两种常见的方法是: 闭散列 开散列

闭散列

概念:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 key 存放到冲突位置中的 下一个 空位置中去。
方法:
1. 线性探测
比如上面的场景,现在需要插入元素 44 ,先通过哈希函数计算哈希地址,下标为 4 ,因此 44 理论上应该插在该位置,但是该位置已经放了值为4 的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
2.二次探测

开散列(重点)

概念:开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

实现哈希桶

public class HashBucket {
     private static class Node {
          private int key;
          private int value;
          Node next;
          public Node(int key, int value) {
              this.key = key;
              this.value = value;
          }
     }
     private Node[] array;
     private int size; // 当前的数据个数
     private static final double LOAD_FACTOR = 0.75;
     public int put(int key, int value) {
          int index = key % array.length;
          // 在链表中查找 key 所在的结点
          // 如果找到了,更新
          // 所有结点都不是 key,插入一个新的结点
          for (Node cur = array[index]; cur != null; cur = cur.next) {
              if (key == cur.key) {
                   int oldValue = cur.value;
                   cur.value = value;
                   return oldValue;
              }
          }
          Node node = new Node(key, value);
          node.next = array[index];
          array[index] = node;
          size++;
          if (loadFactor() >= LOAD_FACTOR) {
              resize();
          }
          return -1;
     }
     private void resize() {
          Node[] newArray = new Node[array.length*2];
          for (int i = 0; i < array.length; i++) {
              Node next;
              for (Node cur = array[i]; cur != null; cur = next) {
                   next = cur.next;
                   int index = cur.key % newArray.length;
                   cur.next = newArray[index];
                   newArray[index] = cur;
              }
          }
          array = newArray;
     }
     private double loadFactor() {
          return size * 1.0 / array.length;
     }
     public HashBucket() {
          array = new Node[8];
          size = 0;
     }
     public int get(int key) {
          int index = key % array.length;
          Node head = array[index];
          for (Node cur = head; cur != null; cur = cur.next) {
              if (key == cur.key) {
                   return cur.value;
              }
          }
          return -1;
     }
}

六、和JAVA类集的联系

1. HashMap HashSet java 中利用哈希表实现的 Map Set
2. java 中使用的是哈希桶方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key equals 方 法。所以如果要用自定义类作为 HashMap key 或者 HashSet 的值, 必须覆写 hashCode equals ,而且要做到 equals 相等的对象, hashCode 一定是一致的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LAKURRAA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值