Map&Set

本文详细介绍了Java中的Set(特别是HashSet)和Map(包括HashMap、TreeMap)的数据结构特点、常用方法,以及哈希表的工作原理、哈希函数的设计原则、冲突解决策略(如线性探测和链地址法)和负载因子的管理。还分析了JDKHashMap的实现和与其它Map子类的区别。
摘要由CSDN通过智能技术生成

一. Set

1. 特点

Collection的子接口, 和List并列. 是保存单个不重复元素的集合, 主要有去重的作用.

Set接口的本质就是Map, Set将元素保存在了Map的Key上, 因此Set是不重复元素集合

2. 常用方法

方法

说明

boolean add(E e)

添加元素, 但重复元素不会添加成功同时返回false

void clear()

清空集合

boolean contains(Object o)

判断o是否在集合中

Iterator<E> iterator()

返回迭代器

boolean remove(Object o)

删除集合中的o

int size()

返回set中元素的个数

boolean isEmpty()

检测Set是否为空

Object[] toArray()

将Set里的元素转换为数组返回

boolean containsAll(Collection<?> c)

检测集合c中的元素在Set里是否全部存在

boolean addAll(Collection<? extends E> c)

将集合c里的元素添加到Set中,可达到去重的效果

二. Map

1. 特点

Map接口保存一对元素, 形如key=value的映射关系. 具有高效搜索的特点.

2. 常用方法

方法

说明

V get(Object key)

返回key对应的value, key不存在返回null

V getOrDefault(Object key, V defaultValue)

返回key对应的value, key不存在, 返回defaultValue

V put(K key, V value)

设置key对应的value. 若key存在, 返回更新前的value

V remove(Object key)

删除key对应的映射关系

Set<K> keySet()

返回所有key的不重复集合

Collection<V> values()

返回所有value的可重复集合

Set<Map.Entry<K,V>> entrySet()

返回所有的key-value映射关系

boolean containsKey(Object key)

判断是否包含key

boolean containsValue(Object value)

判断是否包含value

3. 关于Map.Entry<K,V>的说明

Map.Entry<K,V> 是Map内部实现的, 用来存放<key,value>键值对映射关系的内部类, 该内部类中主要提供了<key,value>的获取,value的设置以及key的比较方式

方法

说明

K getKey()

返回entry中的key

V getValue()

返回entry在的value

V setValue(V value)

将键值对中的value替换成指定的value

注意: 当修改Entry中的value值, 会同步影响Map的value !

三. 哈希表

1. 认识哈希表

1.1. 背景

哈希表, 源于数组的随机访问特性

关于搜索问题

链表的查找, 只能从链表开头遍历到结尾, 时间复杂度O(n)

搜索树的查找(平衡搜索树), 查找的时间复杂度O(logn)

数组, 如果知道元素的索引,查找的时间复杂度O(1)

利用数组的随机访问特性来查找元素,这个思想就是哈希表产生的背景

1.2. 示例

若需要在一组数据 [9.5,2,7,3,6,8] 这样的一组元素中判断某个数字是否存在?使用链表或BST保存这些数字, 然后查找. 若能将元素的值和数组的下标建立联系,根据数组的索引查找元素立即得到答案。

创建一个大小为10的boolean数组,扫描一遍原数组的内容,将原数组的值保存到对应布尔数组的下标将数组的具体数值转换为了新数组的下标~用空间换时间的思想

扫描原数组,将碰到的每个元素将其对应的boolean置为true.

2. 哈希函数

哈希函数的作用: 将任意的数据类型的值, 转为正整数(转换后的正整数就可以作为数组的下标) 一般来说,哈希函数不需要我们自己设计,用现成的方案即可。

2.1. 设计的3个规则

①一致性。

对于两个相同的数字x和y, 当x == y时, hash(x) == hash(y)

②稳定性

对于相同的数字x,无论啥时候计算哈希值, 得到的结果均相同

③均匀性

不同的数据x和y, 经过哈希函数计算之后的数据尽量分散

2.2. 设计思路

2.2.1. 关于整数的哈希函数计算: 取模运算

将数据取模运算放入数组.

但是存在一些情况, 经过两个不相同的数字x和y,经过哈希函数计算之后得到相同的值, 这种情况叫做哈希冲突(哈希碰撞)

显著降低哈希冲突的几率, 比较好的方式就是取一个素数作为因子

2.2.2. 关于字符串的哈希计算

业内有很多关于字符串的哈希函数, 例如: MD3, MD4, MD5, Base64, SHA1, SHA256

以MD5为例, 他的三个特点:

①定长: 无论输入多长的字符串,经过MD5函数运算后得到的值是定长的(有16位, 常用32位)

②分散: 原字符串只改动一点内容,得到的MD5值差距很大

③不可逆: 经过字符串得到MD5数值很容易, 但是通过MD5倒推原内容, 非常困难(基本不可能)

所以说, 在工程中, 两个数据x和y经过md5运算后得到相同值, 就可以认为x和y是相同的数据

MD5在平常工程中的应用:

①作为hash值

②用于加密领域

③对比文件内容: 下载一些大文件的时候

3. 哈希冲突(哈希碰撞)

3.1. 定义

当原始的两个key值x和y原本不相同, 经过哈希函数运算之后, 得到了两个相同的数字, 就称为发生了哈希冲突/碰撞

3.2. 解决方案

3.2.1. 闭散列(线性探测)

当发生冲突时, 找到冲突位置旁边的空闲位置放入冲突元素. 闭散列方案, 好想好实现, 但难查更难删, 工程中很少采用此方案

3.2.2. 开散列(链地址法)

若出现哈希冲突, 让冲突的位置变为一个链表

当元素个数不断变大, 哈希冲突的概率也会越来越大, 在当前数组中, 某些链表的长度会很长. 查找效率又从O(1)变为O(n), 所以有两种解决方案:

①针对整个数组扩容, 扩容为原来的一倍, 大概率原先冲突的元素再次哈希之后就不再冲突(C++采用的方案)

②将长度过长的链表转为BST/哈希表(JDK8+的方案)

4. 负载因子factor

描述哈希冲突的严重情况. 一般来说当哈希表的元素个数size >= 哈希表长度length * factor, 就认为当前哈希表的冲突比较严重, 需要进行处理

4.1. 结论

负载因子越大, 冲突越严重, 节省空间(保存的元素个数多)

负载因子越小, 冲突越轻微, 浪费空间(保存元素少)

负载因子需要在时间和空间上取平衡。

JDK的HashMap的默认负载因子就是0.75

阿里的实验室论证, 在一般商用系统中, 负载因子取10比较合适

5. 基于开散列的HashMap的实现

哈希表: 数组+链表, 只不过普通的数组都是保存单个元素, 现在的哈希表保存的是一个个链表节点, 数值保存在链表的节点中

5.1. 代码

public class MyHashMap {
    // 定义内部的Node节点
    private static class Node {
        int key;
        int value;
        Node next;
        Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    // 当前哈希表中有效元素个数
    private int size;
    // 取模数,简单起见,和数组长度保持一致
    private int M;
    // 保存Node元素的数组
    private Node[] data;
    // 负载因子
    private static final double LOAD_FACTOR = 0.75;
    public MyHashMap() {
        this(16);
    }
    public MyHashMap(int capacity) {
        this.data = new Node[capacity];
        this.M = capacity;
    }

    // 判断当前哈希表中是否包含指定的key值
    public boolean containsKey(int key) {
        int index = hash(key);
        for (Node x = data[index];x != null;x = x.next) {
            if (x.key == key) {
                return true;
            }
        }
        return false;
    }

    // 判断当前哈希表中是否包含指定的value值
    public boolean containsValue(int value) {
        // 全表扫描
        for (int i = 0; i < data.length; i++) {
            // 内层循环就是每个子链表的遍历
            for (Node x = data[i];x != null;x = x.next) {
                if (x.value == value) {
                    return true;
                }
            }
        }
        return false;
    }

    //新增 & 修改
    public int put(int key,int value) {
        // 1.首先计算出当前新元素的下标
        int index = hash(key);
        // 2.在当前的子链表中判断key值是否存在,若存在,只需要更新value即可
        for (Node x = data[index];x != null;x = x.next) {
            if (x.key == key) {
                // 存在,只需要更新value
                int oldValue = x.value;
                x.value = value;
                return oldValue;
            }
        }
        // 3.此时key第一次出现,头插到当前的子链表中
        Node node = new Node(key,value);
        node.next = data[index];
        data[index] = node;
        size ++;
        // 4.判断当前哈希表的冲突情况,是否需要扩容
        if (size >= this.data.length * LOAD_FACTOR) {
            resize();
        }
        return -1;
    }

    private void resize() {
        this.M = data.length << 1;
        Node[] newData = new Node[data.length << 1];
        // 搬移原数组的所有节点
        for (int i = 0; i < data.length; i++) {
            for (Node x = data[i];x != null;) {
                Node next = x.next;
                // 当前x搬移到新数组的对应位置
                int newIndex = hash(x.key);
                // 头插到新数组的对应位置
                x.next = newData[newIndex];
                newData[newIndex] = x;
                // 继续搬移原数组的下一个节点
                x = next;
            }
        }
        // 更新data的指向
        data = newData;
    }
    
    // 在当前哈希表中指定的key值节点
    public boolean removeKey(int key) {
        // 1.先求索引
        int index = hash(key);
        // 先判空
        if(data[index] == null) {
            // 不存在对应的key值
            return false;
        }
        // 剩下就是链表的删除问题
        if (data[index].key == key) {
            data[index] = data[index].next;
            size --;
            return true;
        }
        Node prev = data[index];
        while (prev.next != null) {
            if (prev.next.key == key) {
                prev.next = prev.next.next;
                size --;
                return true;
            }
        }
        // 此时不存在指定的key值
        return false;
    }
	//哈希函数, 本哈希表采用取模
    private int hash(int key) {
        return key % this.M;
    }
}

5.2. JDK HashMap源码解析

5.2.1. 结构

JDK8之前, HashMap就是数组 + 链表

JDK8以及之后, HashMap数组+链表+红黑树

5.2.2. 树化和扩容时机

某个子链表的长度 >= 8并且整个哈希表的元素个数 >= 64才会将当前链表进行树化操作

若子链表长度 >= 8但是整个元素个数 <64 这个时候, 进行整表扩容

当红黑树中节点数量小于等于6时, 会恢复成链表

6. HashMap源码详解

6.1. 对比Map常用子类关系

TreeMap

HashMap

内部数据结构

RBTree

哈希表

key与value是否允许为null

key不允许为null, value允许为null

key必须具备可比较的性质或者传入比较器对象

key和value都允许null

是否有序

对于key"有序", 这个大小关系由Comparable或者比较器决定

无序

是否线程安全

不安全

不安全

若需要用到线程安全的Map集合, 请使用java.util.ConcurrentHashMap

6.2. Set和Map的关系

Set是保存单个不重复的元素的集合, 不重复的原因在于Set保存元素实际上保存在了对于Map集合的key值上. 因为Map的key不可重复, 因此Set不可重复.

实际上HashSet使用的是HashMap保存元素, TreeSet使用的是TreeMap保存元素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值