前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >离程序猿又近了一步:HashMap全解析

离程序猿又近了一步:HashMap全解析

作者头像
用户5546570
发布2020-07-16 15:42:51
3410
发布2020-07-16 15:42:51
举报

离程序猿又近了一步:HashMap全解析

HashMap是键值对的集合。为什么要写它呢? 首先是因为HashMap日常使用比较多,并且面试中是大概率被问到的面试题。 所以我们对它的设计和源码来做一个分析。

准备的技术点

单链表、双链表、红黑树、二叉搜索树,hash

单链表

单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。 单链表的实际使用场景并不多,比如只是频繁对头/尾结点进行操作,单链表最佳

双链表

双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。 双链表的主要优点是对于任意给的结点,都可以很轻易的获取其前驱结点或者后继结点,而主要缺点是每个结点需要添加额外的next域,因此需要更多的空间开销,同时结点的插入与删除操作也将更加耗时,因为需要更多的指针指向操作。

二叉搜索树

又称二叉查找树,亦称二叉搜索树,满足下面性质 若左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若右子树不空,则右子树上所有结点的值均大于它的根结点的值; 左、右子树也分别为二叉排序树; 没有键值相等的结点。

红黑树

红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

性质1. 节点是红色或黑色。 性质2. 根节点是黑色。 性质3.所有叶子都是黑色。(叶子是NUIL节点) 性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。 性质4导致路径上不能有两个连续的红色节点。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长

hash算法

希算法并不是一个特定的算法而是一类算法的统称。哈希算法也叫散列算法,一般来说满足这样的关系:f(data)=key,输入任意长度的data数据,经过哈希算法处理后输出一个定长的数据key。同时这个过程是不可逆的,无法由key逆推出data。 如果是一个data数据集,经过哈希算法处理后得到key的数据集,然后将keys与原始数据进行一一映射就得到了一个哈希表。一般来说哈希表M符合M[key]=data这种形式。 哈希表的好处是当原始数据较大时,我们可以用哈希算法处理得到定长的哈希值key,那么这个key相对原始数据要小得多。我们就可以用这个较小的数据集来做索引,达到快速查找的目的。 稍微想一下就可以发现,既然输入数据不定长,而输出的哈希值却是固定长度的,这意味着哈希值是一个有限集合,而输入数据则可以是无穷多个。那么建立一对一关系明显是不现实的。所以"碰撞"(不同的输入数据对应了相同的哈希值)是必然会发生的,所以一个成熟的哈希算法会有较好的抗冲突性。同时在实现哈希表的结构时也要考虑到哈希冲突的问题。

源码

本质上就是数组+链表

数组

代码语言:javascript
复制
transient Node<K,V>[] table;

单链表

代码语言:javascript
复制
static class Node<K,V> implements Map.Entry<K,V> {        final int hash;        final K key;        V value;        Node<K,V> next;        Node(int hash, K key, V value, Node<K,V> next) {            this.hash = hash;            this.key = key;            this.value = value;            this.next = next;        }        public final K getKey()        { return key; }        public final V getValue()      { return value; }        public final String toString() { return key + "=" + value; }        public final int hashCode() {            return Objects.hashCode(key) ^ Objects.hashCode(value);        }        public final V setValue(V newValue) {            V oldValue = value;            value = newValue;            return oldValue;        }        public final boolean equals(Object o) {            if (o == this)                return true;            if (o instanceof Map.Entry) {                Map.Entry<?,?> e = (Map.Entry<?,?>)o;                if (Objects.equals(key, e.getKey()) &&                    Objects.equals(value, e.getValue()))                    return true;            }            return false;        }    }

双链表-红黑树

代码语言:javascript
复制
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {        LinkedHashMapEntry<K,V> before, after;  // 在hashMap中并没有使用这两个节点信息        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {            super(hash, key, value, next);        }    }
代码语言:javascript
复制
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {        TreeNode<K,V> parent;        TreeNode<K,V> left;        TreeNode<K,V> right;        TreeNode<K,V> prev;        boolean red;                ............ // 省略方法代码    }

扩容原理resize

代码语言:javascript
复制
final Node<K,V>[] resize() {        Node<K,V>[] oldTab = table;        int oldCap = (oldTab == null) ? 0 : oldTab.length;        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) < MAXIMUM_CAPACITY &&                     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            newCap = DEFAULT_INITIAL_CAPACITY;            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);        }        if (newThr == 0) {            float ft = (float)newCap * loadFactor;            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                      (int)ft : Integer.MAX_VALUE);        }        threshold = newThr;        @SuppressWarnings({"rawtypes","unchecked"})        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];        table = newTab;        if (oldTab != null) {            for (int j = 0; j < oldCap; ++j) {                Node<K,V> e;                if ((e = oldTab[j]) != null) {                    oldTab[j] = null;                    if (e.next == null)                        newTab[e.hash & (newCap - 1)] = e;                    else if (e instanceof TreeNode)                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                    else { // preserve order                        Node<K,V> loHead = null, loTail = null;                        Node<K,V> hiHead = null, hiTail = null;                        Node<K,V> next;                        do {                            next = e.next;                            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;                        }                        if (hiTail != null) {                            hiTail.next = null;                            newTab[j + oldCap] = hiHead;                        }                    }                }            }        }        return newTab;    }

容器大小为2的n次方,扩容因子为m;默认n = 4也就是默认大小16, m = 0.75。 存在3种情况下会扩容

容器内数组未初始化或者大小为0 此时已存储元素个数大于2^n * m 会扩容 容器个数小于64,某个位置的单链表长度大于等于7时,这时不直接转换为树结构,而是进行扩容

插入操作put

是否为第一次加入新元素,初始化容器(也是调用扩容方法) 如果加入key的hash值对应索引位置无数据,直接插入 如果key的hash值对应索引位置有数据,且节点为红黑树结构,则查看2节中关于红黑树插入的内容 如果key的hash值对应索引位置有数据,且节点为单链表结构,则进行for循环处理: a)链表存在数据,其key与当前物理地址相同或者key的equal比较相同,则重置数据返回, b) 若是已经到链表尾部,则新增节点,加入到尾部;如果加入后数据大小>= 7 则进行树化,查看2节中树化方法 尺寸+1,判断是否达到阈值,达到则进行扩容

获取元素

获取元素比较简单,以key的hash值找到索引位置,然后根据位置的节点特点来查找元素节点为空,则无此元素 节点为红黑树节点,查看2章节中getTreeNode方法分析 节点为单链表,从头到尾进行比对,如果比对成功,则退出,否则返回null;比对依据:hash值相同且物理地址相同或者equal方法相同

删除方法

如果未加入过元素,则不处理 如果删除key的hash对应索引位置元素为空,则不处理 如果索引节点的key和要删除的key比对相同,则删除节点即为索引节点 索引节点后驱为空,则不需要处理 索引节点后驱不为空,则在链表或者红黑树中查找此节点 红黑树结构使用removeTreeNode删除元素,单链表,删除元素后,其前驱的后继为其后继;并大小减1

清空

所有索引位置置空,也即断开单链表或者双链表-红黑树的根节点,进而删除所有元素

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 准备的技术点
  • 单链表
  • 双链表
  • 二叉搜索树
  • 红黑树
  • hash算法
  • 源码
  • 数组
  • 单链表
  • 双链表-红黑树
  • 扩容原理resize
  • 插入操作put
  • 获取元素
  • 删除方法
  • 清空
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档