天天看点

Java Map和Set

目录

    • 1. 二叉排序树(二叉搜索树)
      • 1.1 二叉搜索树的查找
      • 1.2 二叉搜索树的插入
      • 1.3 二叉搜索树的删除(7种情况)
      • 1.4 二叉搜索树和TreeMap、TreeSet的关系
    • 2. Map和Set的区别与联系
      • 2.1 从接口框架的角度分析
      • 2.2 从存储的模型角度分析【2种模型】
    • 3. 关于Map
      • 3.1 Map的注意事项
      • 3.2 TreeMap 和 HashMap的对比
      • 3.3 Map.Entry<k,v>的用法 (遍历)
    • 4. 关于Set
      • 4.1 Set的注意事项
      • 4.2 TreeSet 和 HashSet的对比
      • 4.3 迭代器遍历Set
    • 5. 哈希表
      • 5.1 哈希函数
      • 5.2 冲突
      • 5.3 冲突的解决:开放地址法和链地址法
      • 5.4 哈希表与Java集合类的关系
    • 6. HashMap源码分析
      • 6.1 成员变量
      • 6.2 构造方法
      • 6.3 put方法源码

1. 二叉排序树(二叉搜索树)

二叉搜索树又称二叉排序树

  • 若左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 左右子树也分别是二叉搜索树

    【左小右大】

1.1 二叉搜索树的查找

【二叉搜索树的查找,一般情况下一次可以干掉很多数据】

为了保证每次可以干掉很多数据

思路:取遍历节点为cur,每次要查找的值val和cur的val进行比较,如果要查找的值比cur的val小,则去cur的左边继续查找,如果大则去cur的右边继续查找。

public boolean search(int val) {
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val == val) {
                return true;
            }else if (cur.val > val) {
                cur = cur.left;
            }else {
                cur = cur.right;
            }
        }
        return false;
    }
           

1.2 二叉搜索树的插入

思路:整体思路与查找的思路类似,但是二叉搜索树的每次插入的节点一定是叶子节点,因此需要记录待插入节点的双亲。

当cur为空时,parent记录了待插入结点的父亲位置,再用val和parent的val比较进行插入。

public void insert (int val) {
        Node node = new Node(val);
        if (root == null) {
            root = node;
            return;
        }
        Node cur = root;
        Node parent = root;
        while (cur != null) {
            if (cur.val == val) {
                return;
            }else if (cur.val > val) {
                parent = cur;
                cur = cur.left;
            }else {
                parent = cur;
                cur = cur.right;
            }
        }
        if (parent.val > val) {
            parent.left = node;
        }else {
            parent.right = node;
        }
    }
           

1.3 二叉搜索树的删除(7种情况)

设待删除结点为cur,待删除结点的双亲结点为parent

整体看分以下3种大情况:

①cur的左为空

②cur的右为空

③cur的左和右都不为空

继续细分:cur是不是root;cur不是root的话,是parent的左还是parent的右

故有3 + 3 + 1 = 7种情况。

  1. cur.left == null

    ①cur是root

    则 root = cur.right;

    ②cur不是root,cur是parent.left

    则 parent.left = cur.right;

    ③cur不是root,cur是parent.right

    则 parent.right = cur.right;

  2. cur.right == null

    ①cur是root

    则 root = cur.left;

    ②cur不是root,cur是parent.left

    则 parent.left = cur.left;

    ③cur不是root,cur是parent.right

    则 parent.right = cur.left;

  3. cur.left != null && cur.right != null

    此时需要使用替换法进行删除,即在它的右子树种寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题。

    1.4 二叉搜索树和TreeMap、TreeSet的关系

    TreeMap和TreeSet是Java种利用搜索树实现的Map和Set,实际上用的是红黑树,红黑树是一颗近似平衡的二叉搜索树,即在二叉搜索树的基础之上+颜色以及红黑树性质。

2. Map和Set的区别与联系

2.1 从接口框架的角度分析

Java Map和Set

Map是一个独立的接口,而Set继承自Collection接口。

TreeMap 和 TreeSet 都继承了一个Sorted接口,说明 Tree某某 都是经过排序的,即 Tree某某 都是关于key有序的。

2.2 从存储的模型角度分析【2种模型】

  1. Map中存储的是键值对key-value
  2. Set中只存储了key

3. 关于Map

Map是一个接口类,该类没有继承collection,该类中存储的是<k,v>结构的键值对,并且k一定是唯一的,不能重复。

3.1 Map的注意事项

  1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
Map<Integer, Integer> map1 = new HashMap<>();
        Map<Integer, Integer> map2 = new TreeMap<>();
           
  1. Map中存放键值对的键key是唯一的,key不可以重复,但是value可以重复。
  2. 在TreeMap中插入键值队的时候,key不能为空,否则会抛出NPE(空指针异常),但是value可以为空,因为TreeMap中的key是要进行比较的;反而HashMap的key和value都可以为空,因为HashMap不涉及比较。
  3. Map中的key可以全部分离出来,存储到Set中进行访问(因为key不能重复,set是天然的去重),Map中的value可以全部分离出来,存储到Collection的任何一个子集合中(value可能有重复)

3.2 TreeMap 和 HashMap的对比

Map底层结构 TreeMap HashMap
底层结构 红黑树 哈希桶
插入/删除/查找时间复杂度 O(log2N) O(1)
是否有序 关于key有序 无序
线程安全 不安全 不安全
插入/删除/查找区别 需要进行元素比较 通过哈希函数计算哈希地址
比较与覆写 key必须能够比较,否则会抛出类型转换异常 自定义类型需要覆写equals和hashcode方法
应用场景 需要key有序的场景下 key是否有序不关心,需要更高的时间性能

3.3 Map.Entry<k,v>的用法 (遍历)

Set<Map.Entry <k,v>> entrySet(), 返回所有的key-value映射关系

Java Map和Set
Map<Integer, Integer> map = new TreeMap<>();
        map.put(1,2);
        map.put(2,3);
        map.put(3,6);
        //key一定是可以进行比较的

        for(Map.Entry<Integer,Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
           

分析:

打印所有的键值对

entrySet():将Map中的键值对放在Set中返回了。

Java Map和Set

4. 关于Set

Set与Map主要的不同有2点:

①Set是继承自Collection的接口类

②Set中只存储了Key

Map不能使用迭代器遍历,但是Set可以,因为Set实现了Iterable接口

4.1 Set的注意事项

  1. Set是继承自Collection的一个接口类
  2. Set中只存储了key,并且要求key一定要唯一
  3. Set的底层使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中
    Java Map和Set
  4. Set最大的功能就是对集合中的元素进行去重
  5. TreeSet中不能插入null的key,但是HashSet可以

4.2 TreeSet 和 HashSet的对比

Set底层结构 TreeSet HashSet
底层结构 红黑树 哈希桶
插入/删除/查找时间复杂度 O(log2N) O(1)
是否有序 关于key有序 不一定有序
线程安全 不安全 不安全
插入/删除/查找区别 按照红黑树的特性进行插入和删除 通过哈希函数计算哈希地址
比较与覆写 key必须能够比较,否则会抛出类型转换异常 自定义类型需要覆写equals和hashcode方法
应用场景 需要key有序的场景下 key是否有序不关心,需要更高的时间性能

4.3 迭代器遍历Set

Iterator< E> iterator() 返回迭代器,可以利用其进行遍历

Set<Integer> set = new TreeSet<>();
        set.add(1);
        set.add(3);
        set.add(2);
        set.add(8);
        set.add(4);
        Iterator<Integer> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
           

运行结果:

Java Map和Set

5. 哈希表

5.1 哈希函数

  1. 哈希表的效率非常高,查找、删除、插入的时间复杂度都是O(1)。
  2. 理想的搜索方法:不经过任何比较,一次直接从表中得到想要搜索的元素,即一一映射关系。
  3. 哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表(Hash Table)。
  4. 哈希函数计算出来的地址能均匀分布在整个空间中。

5.2 冲突

  1. 冲突: 不同关键字通过相同哈希函数计算出相同的哈希地址
  2. 冲突的发生是必然的,冲突是不可避免的,只能降低冲突率。
  3. 避免冲突:负载因子调节,其值= 填入表中的元素个数/哈希表的长度,因此可以增加哈希表的长度避免冲突。Java的系统库限制了荷载因子为0.75。

5.3 冲突的解决:开放地址法和链地址法

  1. 开放地址法(闭散列)

    ①线性探测

    ②二次探测

    闭散列最大的缺陷:空间利用率比较低

  2. 链地址法(开散列) 数组+链表+红黑树

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

    注意:数组长度>=64,链表长度>=8,就会变成红黑树。

5.4 哈希表与Java集合类的关系

  1. HashMap和HashSet是用哈希表实现的Map和Set。
  2. Java会在冲突链表长度大于一定阈值后,将链表转变成红黑树(搜索树)。
  3. Java中计算哈希值实际上调用的是类的hashcode方法,进行key的相等性比较是调用key的equals方法。
  4. 所有自定义类的HashMap的key或者HashSet的值,必须重写hashcode和euqals方法,而且必须做到euqals相等的对象,hashcode一定也是一样的。
  5. 扩容之后要注意每个元素都要重新计算hashcode哈希值。
面试问题:
  1. 问:如果2个对象hashcode一样,equals一定一样吗?

    答:不一定,hashcode一样只能证明我们要找的位置一样,位置一样的下面有很多值,无法确定2个对象的euqals。

  2. 问:如果2个对象equals一样,hashcode一定一样吗?

    答:一定,equals一样则hashcode一定一样。

6. HashMap源码分析

6.1 成员变量

  1. 默认容量:16,最大容量:2的30次方
    Java Map和Set
  2. 默认的负载因子:0.75
    Java Map和Set
  3. 树化的条件(数组+链表变成红黑树)

    链表长度超过8,数组的容量大于等于64

    Java Map和Set
  4. 解树的条件(红黑树退化为链表+数组)

    链表的阈值为6的时候

    Java Map和Set
  5. table数组的每一个元素是node类型的结点地址
    Java Map和Set

6.2 构造方法

  1. 不带参数的构造方法

    负载因子等于默认的负载因子0.75,没有给table数组进行初始化,意味着table数组没有分配内存,数组的大小是0。

    (所以,为什么等会put元素可以put进去?----> 说明在第一次put的时候,会把数组进行初始化为默认容量16)

    Java Map和Set
  2. 带2个参数的构造方法

    一个是给定的容量参数,一个是给定的负载因子参数

    Java Map和Set

    如果给定的参数容量大于2的30次方,则容量为2的30次方;

    如果给定的参数容量小于0或者给定的负载因子小于0那么就抛异常。

    最后负载因子满足条件直接赋值,但是容量还需要经过tableSizeFor进一步筛选。

    Java Map和Set

    tableSizeFor里面:返回最接近目标的一个二次幂整数。

    例如:

    传入10:2的3次方=8,2的4次方16,因此返回16。

面试题:调用构造方法给了1000,请问最后哈希数组的长度是多少?

答:1024

Java Map和Set

HashMap的最大容量保证是2的n次方,但是如果没有传入一个2的次方怎么办?

答:HashMap会将传入的参数做校验,返回距离传参最近的一个2的n次方的值,例如传入15会初始化为16。

那么,为什么返回二次幂呢?

分析put源码。

6.3 put方法源码

Java Map和Set

①先把key给到hash函数中,hash(key),调用hash方法,将引用类型key转换成整数类型

Java Map和Set

②hash这个方法中调用了hashcode方法,如果key重写了hashcode方法则会调用自己的hashcode方法,如果没有重写,则会调用Object类的hashcode方法。

h是通过hashcode方法得到的32位的整数,h 异或上 h右移16位

为什么要右移16位?

答:为了更好的均匀分布,低16位和高16位异或,可以让结果更均匀。

Java Map和Set

③i = (n - 1) & hash 等价于 n % len,位运算一定是最快的,一定要保证n是2的次幂,这样2个公式才等价。【所以初始化的长度为2的整数次幂】

Java Map和Set