xiaobenchi

导航

Map的使用

Map的使用

在上一篇文章中主要讲了Collections中的一些接口的使用

本篇文章将讲述另一个用于存储数据的结构分支——Map

Map分支

HashTable

什么是哈希表

​ 哈希表(HashTable)又叫散列表,是根据关键码值(即键值对)而直接访问的数据结构。也就是说,它通过把关键码映射到表中的一个位置来访问记录,以加快查找速度。这个映射函数就叫做散列(哈希)函数,存放记录的数组叫做散列表。

为什么哈希表的速度快

​ 在数据结构中有两种被人熟知的数据结构:数组与链表。数组的特点就是容易查找,删除和插入困难;而链表的特点就是插入删除容易,但是查找困难。将二者的优点结合弥补各自的缺点就成为了哈希表。

哈希表的查找

哈希表的查找分为两个步骤:

  1. 使用哈希函数将被查找的键转化为数组的索引。在理想状态下,不同的键值会转换成不同的索引值。但那时理想状态,我们实践当中不可能一直是理想状态的。当不同的键生成了相同的索引的时候,也就是发生了冲突,我们这个时候就要处理冲突。

  2. 处理冲突的方法有很多

    ​ 哈希表是一个时间和空间上平衡的例子。如果没有空间的限制,我们可以直接用键来作为数组的索引,这样就可以将查找时间做到最快O(1).如果没有时间限制,我们可以使用无序链表进行顺序查找,这样只需要很少的内存。

什么是哈希函数

​ 哈希函数其实就是我们常说的哈希算法,主要应用在以下这几个方面:文件校验、数字签名、鉴权协议,常用的哈希算法如下:

  1. MD5:Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整性一致。MD5是输入不定长度信息,输出固定长度128bits的算法。
  2. SHA-1: 常用于HTTPs传输和软件签名。
  3. SHA-2: SHA-224/SHA-256/SHA-384/SHA-512并成为SHA-2.
  4. SHA-3: 之前名为Keccak算法,是一个加密杂凑算法。

如何避免哈希冲突

这里介绍两种方法

  1. 拉链法

    将大小为M的数组的每一个元素指向一个链表,链表中的每一个节点都储存散列值为该索引的键值对,这个就是拉链法。

    image-20220417114640972

    ​ 该方法的基本思想就是选择足够大的M,使得所有的链表都尽可能短小,以保证查找的速度。对采用拉链法的哈希表实现的查找分为两步,首先是根据散列值找到对应的链表,然后沿着链表找到相应的键。

  2. 线性探索法

    ​ 线性探测法是开放寻址解决哈希冲突的一种方法,基本原理为,使用大小为M的数组来保存N个键值对,其中M>N,我们需要使用数组中的空位来解决碰撞冲突。

    image-20220417115051670

    ​ 对照前面的拉链法,在该图中,“Ted Baker”有唯一的hash值153,但是153被“Sandra Dee”占用了。而原先“Sandra Dee”和”John Smith”的hash值都是152的,但是在对”Sandra Dee”进行hash的时候偶然发现152已经被占用了,所以就往下找发现153没有被占用,就将其存放在153。后面“Ted Baker”hash到153上发现被占用了,就会往下找,发现154没有被占用,所以将其存放到154上面。

开放寻址法中最简单的是线性探测法:当碰撞发生时,即一个键的散列值被另一个键占用时,直接查散列表中下一个位置,即将索引值+1,这样的线性探测有三种结果:

  • 命中,该位置的键根被查找的键相同;
  • 未命中,键为空。
  • 继续查找,该位置的键和被查找的键不同。

​ 这两种方法的性能上有一定的区别:对于拉链法,查找的效率在于链表的长度,一般我们应该保证长度的M/8M/2之间,如果链表的长度大于M/2,我们可以扩充数组长度。如果长度在0M/8时,我们可以缩短数组的长度。对于线性探索法,动态调整数组的大小需要对所有的值重新进行散列并插入新的表中。

不管是拉链法还是散列法,这种动态调整链表或者数组的大小以提高查询效率的同时,还应该考虑动态改变链表或者数组大小的成本。散列表长度加倍的插入需要进行大量的探索,这种均摊成本很多时候需要考虑。

哈希碰撞攻击

​ 哈希攻击就是通过精心构造哈希函数,使得所有的键经过哈希函数哈希之后都会映射到同一个或几个索引上,将哈希表退化为一个单链表这样哈希表的各种操作,比如插入,查找都会从O(1)退化到链表的查询操作,这样会消耗大量的CPU资源,导致系统无法响应,从而达到拒绝服务供给的目的。

HashMap

HashMap的特点

  1. HashMap底层是哈希表,查询的速度非常块

    JDK1.8之前:数组+单向链表

    JDK1.8之后: 数组+单向链表/红黑树(链表长度超过8):提高查询速度

  2. HashMap是一个无序集合,存储元素和取出元素的顺序可能不一致

LinkedHashMap的特点

  1. LinkedHashMap集合底层是哈希表+链表(保证迭代的顺序)
  2. LinkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序是一致的

主要方法

方法声明 功能描述
void put(Object key,Object value) 向Map集合中添加指定键值映射的元素
int size() 返回Map集合中键值对映射的个数
Object get(Object key) 返回指定键值所映射的值,如果映射不包含盖键的映射关系,则返回null
boolean containsKey(Object key) 查看Map集合中是否存在指定的键对象key
boolean containsValue(Object value) 查看Map结合中是否存在指定的值对象value
Object remove(Object key) 删除并返回Map集合中指定键对象Key的键值映射元素
void clear() 清空整个Map集合中的键值映射元素
Set keySet() 以Set集合的形式返回Map集合中所有的键对象Key
Collection values() 以Collection集合的形式返回Map集合中所有的值对象Value
Set<Map.Entry<Key,Value>>entrySet 将Map集合转换为存储元素类型为Map类型的set集合
Object getOrDefault(Object key,Object defaultValue) 返回Map集合指定键所映射的值,如果不存在则返回默认值defaultValue
void forEach(BiConsumer action) 通过传入一个函数式接口对Map集合元素进行遍历
Object putIfAbsent(Object key,Object value) 向Map集合中添加指定键值映射的元素。如果集合中已存在该键值映射的元素,则不再添加而是返回已存在的值对象value
boolean remove(Object key,Object value) 删除Map集合中键值映射同时匹配的元素
boolean replace(Object key,Object value) 将Map集合中指定键对象Key所映射的值修改为value

HashMap集合分析

​ HashMap集合是Map接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。HashMap底层是由哈希表结构组成,其实就是”数组+链表“的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构,正因为这样特殊的存储结构,HashMap集合对于元素的增删改查表现出的效率都比较高。

​ HashMap集合底层结构中,水平方向以数组结构为主体并在竖直方向以链表结构进行结合。在哈希表结构中,水平方向数组的长度称为HashMap集合的容量,竖直方向每个元素对应的链表结构称为一个桶。每个桶的位置在集合中都有对应的桶值,用于快速定位集合元素添加、查找时的位置。

存储元素的原理:

​ 当向HashMap集合添加元素时,首先会调用键对象k的hash(k)方法,快速定位并寻址到该元素在集合中要存储的位置。定位到存储元素键对象k的哈希值所对应的桶位置后,会出现两种情况:

(1) 键对象的hash值所在桶位置为空,则可以直接向该桶位置插入元素对象;

(2)键对象的hash值所在桶位置不为空,则还需要继续通过键对象k的equals(k)方法比较新插入的元素k和已存在的元素键对象k是否相同,如果键对象k相同,就会对原有元素的值对象v进行替换并返回原来的旧值,否则会在该桶的链表结构头部新增一个节点来插入新元素对象。

​ 由于HashMap存在桶的结构,这就使得其在添加的时候十分快速,但当数据量过大而桶的数量过少的时候就会出现桶链表过长从而导致查询变慢的情况。负载因子的存在就是用来平衡桶的数量和整体数据量。通过负载因子,根据Map集合中的数据量动态的分配桶的数量。

HashMap和Hashtable的对比

  • 相同点

    • 基于哈希表实现
    • 通过单链表解决冲突问题
  • 不同点

    • 继承的父类不同HashMap继承自AbstractMap类。但二者都实现了Map接口。Hashtable继承自Dictionary类,Dictionary类是一个已经废弃类。父类都被废弃,自然而然也没人用它的子类Hashtable类。

    • HashMap线程不安全,HashTable线程安全

    • 包含的Contains方法不同

      HashMap是没有contains方法的,包括containsValue和containsKey方法;hashtable则保留了contains方法。

    • 是否允许null值

      HashMap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;HashTable键值对都不能为空,否则包含空指针异常。

    • 计算hash值方式不同

      HashMap有个hash方法重新计算了key的hash值,因为hash冲突变高,所以通过一种方法重算hash值,用来降低冲突;

      HashTable通过计算key的hashCode()来得到hash值就为最终的hash的值。

    • 扩容方式不同

    • 解决hash冲突方式不同(地址冲突)

      HashMap可以通过红黑树和链表之间的转换解决冲突导致的链表过长问题

      hashTable中都是以链表的方式存储

    • 当容量不足时要进行resize方法,而resize的两个步骤:

      • 扩容

      • rehash: 这里HashMap和HashTable都会重新计算hash值,而计算方式不同

      ​ HashMap哈希扩容必须要求为原容量的2倍,而且一定是2的幂次倍扩容结果,而且每次扩容时,原数组中的元素一次重新计算存放位置,并重新插入

      ​ Hashtable扩容为原容量2倍加1;

TreeMap

TreeMap概述

TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;
TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;
TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;
TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序;

红黑树介绍

红黑树

红黑树规则特点

  1. 节点分为红色或者黑色;
  2. 根节点必为黑色;
  3. 叶子节点都为黑色,且为null;
  4. 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
  5. 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
  6. 新加入到红黑树的节点为红色节点;

红黑树自平衡基本操作

  1. 变色:在不违反上述红黑树规则的情况下,将红黑树某个node节点颜色进行变化;
  2. 左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点;
  3. 右旋:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点;

TreeMap构造

  • 主要成员变量
//排序用的Comparator的实现类
private final Comparator<? super K> comparator;

//红黑树有唯一的根节点。
private transient Entry<K,V> root;

//key-val对的数量
private transient int size = 0;

//红黑树结构的调整次数
private transient int modCount = 0;

Entry静态内部类实现了Map的内部接口Entry,提供了红黑树存储结构的java实现,通过left属性可以建立左子树,通过right属性可以建立右子树,通过parent可以往上找到父节点。

  • TreeMap构造函数
//默认构造函数,按照key的自然顺序排列
public TreeMap() {comparator = null;}

//传递Comparator具体实现,按该实现规则进行排序
public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;}

// 传递一个map实体构建TreeMap,按照默认规则排序
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

//传递一个map实体构建TreeMap,按照传递的map的排序规则进行排序
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

put方法

put方法流程

get方法

get方法是通过二分查找的思想

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
//默认排序情况下的查找
final Entry<K,V> getEntry(Object key) {
    
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

//自定义排序规则下的查找
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
    K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

remove方法

步骤: 找到节点,删除节点,找到节点直接调用上面的getEntry(Object key)

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;
 
    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

通过deleteEntry(p)进行删除操作

删除原理

  1. 删除的是根节点,则直接将根节点置为null;
  2. 待删除节点的左右节点都是null,删除的时候将该节点置为null;
  3. 待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可;
  4. 待删除节点的左右子节点都不为null,则找前驱或者后继,将前驱或者后继的值赋值到该节点中,然后删除前驱或者后继(前驱:左子树中值最大的节点,后继:右子树中值最小的节点);
函数 功能
V putAll(Map map) 放入指定map
K firstKey() 返回该TreeMap的第一个(最小)的映射
K lastKey() 返回该TreeMap的最后一个(最大)的映射
Map.Entry<K, V> firstEntry() 返回该TreeMap的第一个(最小的)映射
SortedMap<K, V> headMap(K toKey) 返回该TreeMap中严格小于指定key的映射集合
SortedMap<K, V> subMap(K fromKey, K toKey) 返回该TreeMap中指定范围的映射集合(大于等于fromKey,小于toKey)
Collection<V> values() 返回由该TreeMap中所有的values构成的集合
Object clone() 返回TreeMap实例的浅拷贝
Comparator<? super K> comparator() 返回给该TreeMap的keys排序的comparator,若为自然排序则返回null

遍历方式

  • for 循环
for (Map.Entry entry : treeMap.entrySet()) {
      System.out.println(entry);
}
  • 迭代器循环
Iterator iterator = treeMap.entrySet().iterator();
while (iterator.hasNext()) {
      System.out.println(iterator.next());
}

如何选取合适的Map

  1. HashMap可实现快速存储和检索,但其缺点是其包含的元素是无序的,这导致它在存在大量迭代的情况下表现不佳。

  2. LinkedHashMap保留了HashMap的优势,且其包含的元素是有序的。它在有大量迭代的情况下表现更好。

  3. TreeMap能便捷的实现对其内部元素的各种排序,但其一般性能比前两种map差。

posted on 2022-04-17 12:49  小迟在努力  阅读(82)  评论(0)    收藏  举报