hashmap排序_数据结构与算法(33)HashMap

Map

Map 与 Collection 有些许不同。在 Collection 中,元素单独存在;而在 Map 中,元素成对存在,每个元素都由一对键值对组成。而且,Map 不能包含相同的键,但值可以重复,每个键对应一个值。

HashMap

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable
  • 继承了 AbstractMap 类,实现了 Map 接口

  • 实现了 Cloneable 接口,表示可以克隆

  • 实现了 Serializable 接口,表示支持序列化

HashMap的属性

/**
 * 默认HashMap长度为16.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 <4; // aka 16

/**
 * 最长为2的29次方
 */
static final int MAXIMUM_CAPACITY = 1 <30;

/**
 * 默认加载因子 = 0.75,是在扩容时需要用到,均衡了时间和空间损耗算出来的值,较高的值会减少空间开销(扩容减少,数组大小增长速度变慢),但增加了查找成本(hash 冲突增加,链表长度变长)
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 表示链表转为红黑树的临界值是8
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 表示红黑树转链表的临界值为6
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 数组转化为红黑树的最小长度为64
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * HashMap的数组
 */
transient Node[] table;/**
 * 链表节点类Node的Set集合
 */transient Set> entrySet;/**
 * HashMap的长度
 */transient int size;// 扩容阈值,当size >= threshold时候,有可能会被扩容int threshold;// 自定义负载因子final float loadFactor;/**
 * 链表的节点类
 */static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;
        V value;
        Node next;
}/**
 * 红黑树的节点类
 */static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode parent;  // red-black tree links
        TreeNode left;
        TreeNode right;
        TreeNode prev;    // needed to unlink next upon deletionboolean red;
}

构造方法

  • 传入初始容量和加载因子构造HashMap

  • 传入初始容量进行构造HashMap,构造因子默认为0.75

  • 初始容量为16,构造因子默认为0.75

  • 以一个Map集合为元素构造HashMap

    /**
     * 传入初始容量和加载因子构造HashMap
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    /**
     * 传入初始容量进行构造HashMap,构造因子默认为0.75
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 初始容量为0,构造因子默认为0.75
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * 以一个Map集合为元素构造HashMap
     */
    public HashMap(Map extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

在构造方法中,HashMap还调用了tableSizeFor(initialCapacity)方法,这个方法可以说特别玄妙、精彩。它的目的就是为了找到最接近传入的初始容量并大于初始容量的一个 2^n 数,并让 threshold = 这个数。

static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

public static int numberOfLeadingZeros(int i) {
    // HD, Count leading 0's
    if (i <= 0)
        return i == 0 ? 32 : 0;
    int n = 31;
    if (i >= 1 <16) { n -= 16; i >>>= 16; }
    if (i >= 1 <8) { n -=  8; i >>>=  8; }
    if (i >= 1 <4) { n -=  4; i >>>=  4; }
    if (i >= 1 <2) { n -=  2; i >>>=  2; }
    return n - (i >>> 1);
}

小伙伴如果想要理解这个方法,可以自行研究,或者找相关视频去了解这个方法,需要一些位运算的知识,这里我就不展开叙述,可以回看我的文章:位运算。

HashMap底层结构

  • 数组:查找快,增删慢,对于一般的增删操作,时间复杂度为 O(n)

  • 链表:增删快,查找慢,对于一般的查找操作,时间复杂度为 O(n)

  • 红黑树:增删查找的速度都比较快,平均时间复杂度为 O(log N)

HashMap 就是基于这三种数据结构实现的,我们使用上面的构造方法来创建一个长度为 6 的 HashMap

701a9f705778d7792441930073105312.png

我们可以发现,HashMap 的主干部分是数组,并且不是按照插入顺序进行排序,而是根据 hash值进行排序的。hashcode 是根据以下公式进行计算

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

小伙伴们这时候会发现,这样子算出来的值不是很大吗?如何进行排序呀?

HashMap 会将值插入到 (哈希值 % 数组长度) 的位置中

小伙伴们这时候发现,这样子只有数组长度只有6,那么两个元素的插入位置相同了怎么办?

这个时候,HashMap会判断当前这个位置是否为空,如果已经有元素存在,则将此元素置为头结点,使用尾插法将新元素置为next节点

9f88efe7ebc9d231e837a00ba759876c.png

在 jdk1.8 之后,由于链表太长导致查找速度降低,于是引进了 红黑树,红黑树的文章之前也有进行学习:点击访问

f846c4054c0741dcb42acdfe41bb05a5.png

HashMap的主要方法

插入元素

put(K,V)

HashMap的初始化不在构造方法中,而在 put 方法中

  • 如果HashMap为空,则先执行扩容

  • 通过key的hash值和数组的长度来确定当前key的下标,并获取到对应下标下的根节点

  • 如果根节点为空,说明该位置没有链表或是红黑树,则将key、value封装成Node节点直接存放到该节点

  • 如果根节点为树的根节点,则进行红黑树的插入操作,红黑树的插入之前有文章:点击访问

  • 如果根节点为链表的根节点,则执行链表的插入

  • 如果满足扩容条件,则进行扩容

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * 参数解析:onlyIfAbsent表示,如果为true,那么将不会替换已有的值,否则替换
 * evict:该参数用于LinkedHashMap,这里没有其他作用(空实现)
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node[] tab; Node p; int n, i;// 如果数组为空,则证明还没有初始化,先进行初始化,对HashMap进行resizeif ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;// 获取对应的节点key对应的下标,并复制给p,如果节点为空,那么创建一个Node节点作为头结点if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);// 如果结点不为空,则执行以下操作else {// e是节点临时变量
        Node e; K k;if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))// 如果头结点的key与传入的key相同,那么直接覆盖key对应的value值
            e = p;else if (p instanceof TreeNode)// 如果p为树节点,则调用putTreeVal方法,执行红黑树的插入
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);else {// 否则,插入的就是链表结点// 进入循环,找到合适插入新结点的位置for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {// 将新结点插入到链表的尾部
                    p.next = newNode(hash, key, value, null);// 如果链表的长度大于8,则将联保转化为红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);break;
                }if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))// 如果找到key与传入的key相同,则直接退出break;// 更改循环的当前元素,使 p 在遍历过程中,一直往后移动
                p = e;
            }
        }// 如果e不为空,也就是确定了新结点的新增位置if (e != null) { // existing mapping for key
            V oldValue = e.value;// 因为onlyIfAbsent传进来的是false,因此可以对值进行覆盖if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);// 返回原来的值return oldValue;
        }
    }
    ++modCount;if (++size > threshold)// 如果满足扩容条件,则进行扩容
        resize();
    afterNodeInsertion(evict);return null;
}

我们查看了插入的流程,突然萌发了一个问题:什么时候执行链表转化为红黑树?

转化为红黑树的条件就是发生了哈希冲突,且新结点插入链表之后链表长度正好达到8,并且哈希表的长度要达到64,这样在插入的时候就会将链表转化为红黑树

final void treeifyBin(Node[] tab, int hash) {
    int n, index; Node e;if (tab == null || (n = tab.length)         resize();else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode hd = null, tl = null;do {
            TreeNode p = replacementTreeNode(e, null);if (tl == null)
                hd = p;else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

为什么链表要转化为红黑树?

因为链表查找的时间复杂度为O(n),查找节点数为6的链表时间复杂度是 6,而查找节点数为8的红黑树时间为 8/2 = 4。在查找方面红黑树比链表有更大的优势,因此有转化为红黑树的必要。

为什么要设置6和8,而不直接设置7?

如果只设置一个值7,那么在 HashMap 的长度刚好=7 的时候进行增删,那么会一直产生红黑树和链表的交换,这个交换也是一个浪费时间的过程,因此我们选择7作为中间缓存,让HashMap不会经常进行转化。

删除元素

  • 判断HashMap是否为空,是则直接返回null

  • 判断该节点是存在数组、链表或是红黑树

  • 根据对应的数据结构进行查找

  • 如果查找到值,并且判断不为空,则分别对应执行删除

public V remove(Object key) {
    Node e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
    Node[] tab; Node p; int n, index;// 当数组不为空时,再进入操作,如果数组为空则直接返回nullif ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node node = null, e; K k; V v;if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))// 如果头结点 = 传入的key,则将node置为p
            node = p;// 如果头结点不为key,则进入树或是链表的判断else if ((e = p.next) != null) {if (p instanceof TreeNode)// 如果p为红黑树的根节点,则调用红黑树的查找方法
                node = ((TreeNode)p).getTreeNode(hash, key);else {// 如果p为链表的根节点,则进行链表的查找do {if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {// 如果判断key值不为空且相等,则该节点就是要移除的节点
                        node = e;break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }// 如果结点不为空if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {if (node instanceof TreeNode)// 如果该节点是红黑树节点,则执行红黑树的移除
                ((TreeNode)node).removeTreeNode(this, tab, movable);else if (node == p)// 如果该节点是数组,则将该索引指向该节点的下一个结点
                tab[index] = node.next;else// 如果该节点是链表,则执行链表的删除
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);return node;
        }
    }return null;
}

查找元素

  • 如果HashMap为空,直接返回null

  • 否则,判断传入的key值对应的节点是索引头结点、链表、或是红黑树

  • 分别对他们执行查找

public V get(Object key) {
    Node e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;// 如果HashMap非空,则继续执行操作,否则返回nullif ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {// 如果索引首位节点与传入的key相等,则直接返回索引头结点if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果不是首节点,则继续if ((e = first.next) != null) {if (first instanceof TreeNode)// 如果根节点为红黑树节点,则进行红黑树的查找return ((TreeNode)first).getTreeNode(hash, key);do {// 如果头结点是链表结点,则进行链表的查找if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))return e;
            } while ((e = e.next) != null);
        }
    }return null;
}

扩容机制

之前就有提过扩容条件:

  • 节点数 > threshold 时

  • 链表长度超过了8,HashMap 数组的长度达到64

  • HashMap 为空,即初始化的时候

final Node[] resize() {
    Node[] oldTab = table;// 计算原来HashMap的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;// threshold = capacity * loadFactor,// 初始化的时候,threshold为HashMap的长度int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {// 当容量已经到达最大的时候,不再进行扩容if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;return oldTab;
        }else if ((newCap = oldCap <1)                  oldCap >= DEFAULT_INITIAL_CAPACITY)// 否则将数组扩大为两倍
            newThr = oldThr <1; // double threshold
    }else if (oldThr > 0) // initial capacity was placed in threshold// 这里表示为刚初始化的时候,此处扩容就等于传入的长度或者是默认长度
        newCap = oldThr;else {               // zero initial threshold signifies using defaults// 如果以上两种情况都不是,那么直接使用16作为默认容量,0.75f作为加载因子,16*0.75 = 12作为扩容阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }if (newThr == 0) {// 这里是为了计算扩容阈值,在第一个和第二个if的时候没有进行扩容阈值的计算float ft = (float)newCap * loadFactor;
        newThr = (newCap float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;// 到这里位置就完成了扩容后长度和扩容阈值的计算@SuppressWarnings({"rawtypes","unchecked"})
    Node[] newTab = (Node[])new Node[newCap];// 将扩容完成后的数组赋给table
    table = newTab;// 如果原来的数组不为0,即不是初始化,则继续执行操作if (oldTab != null) {// 这里进行HashMap扩容后的重新赋值for (int j = 0; j             Node e;if ((e = oldTab[j]) != null) {// 将数组内的节点设置为null
                oldTab[j] = null;if (e.next == null)// 如果只有头结点,则直接重新计算索引放入新数组中
                    newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)// 如果结点为红黑树节点,则执行红黑树的split方法
                    ((TreeNode)e).split(this, newTab, j, oldCap);else { // preserve order// 如果是链表结点,则执行链表的操作// 在下面的变量中,lo开头的变量代表原数组的节点,hi的变量代表新数组的节点// head表示头结点,tail表示尾结点
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;do {
                        next = e.next;// 利用 e.hash & oldCap  == 0 进行判断// 将满足条件的值通过尾插法插入原链表中// 将不满足条件的值通过尾插法插入新链表中if ((e.hash & oldCap) == 0) {if (loTail == null)
                                loHead = e;else
                                loTail.next = e;
                            loTail = e;
                        }else {if (hiTail == null)
                                hiHead = e;else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);// 将原链表放在原来数组的位置上if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }// 将新链表放在数组原位置 + j的位置if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }return newTab;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值