List、HashMap学习

1 List

1.1 ArrayList

  • 首先需要清楚:
    • ArrayList的底层为一个elementData[]数组;
    • 起始插入第一个值时,数组容量默认为10;
    • 每次新增元素前会判断是否需要扩容,当插入元素后的长度大于数组容量时,触发扩容
      1. 首先获取原有数组elementData的长度;
      2. 新增oldCapacity的一半整数长度作为newCapacity的额外增长长度;
      3. 若新的长度newCapacity依然无法满足需要的最小扩容量minCapacity,则新的扩容长度为minCapacity;
      4. 扩展数组长度为newCapacity,并且将旧数组中的元素赋值到新的数组中!
  • 在这里插入图片描述

1.2 LinkedList

  • 首先LinkedList内部定义了一个节点类
private static class Node<E> {
        E item; // 结点元素
        Node<E> next; // 后置结点指针
        Node<E> prev; // 前置结点指针

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
  • 当初始化一个LinkedList时,内部生成指向第一个节点的指针first,和指向最后一个节点的指针last
  • 当新添加一个元素e,并维护进链表
    1. 获取当前最后一个节点last

      final Node l = last;
      final Node newNode = new Node<>(l, e, null);

    2. 将新添加进链表的节点作为last;
    3. 判断是否为第一个元素,并将其维护斤链表。

      if (l == null) {
      /** 如果是第一个添加的元素,则first指针指向该结点*/
      first = newNode; // eg1: first指向newNode
      } else {
      /** 如果不是第一个添加进来的元素,则更新l的后置结点指向新添加的元素结点*/
      l.next = newNode;
      }

  • 当移除一个一个元素e;

linkedList.remove(index);

1. 首先判断index是否越界;
2. 通过index找到该元素,如果需要获取的index小于总长度size的一半,则从头部开始向后遍历查找,否则从尾部开始向前遍历查找;
3. 断开该元素的连接。

2 HashMap

2.1 什么是哈希表?

  • 在讨论哈希表之前,先看一下其他数据结构。
    1. 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
    2. 线性表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
    3. 二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
  • 为了高效率的查找元素,我们的主干使用数组的形式来存储数据。比如我们要新增一个元素时,我们通过某个函数将其映射到数组某个位置,这样我们在查找的时候,通过数组下标即可完成定位。
    在这里插入图片描述
  • 查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。
  • 然而,若在不断hash后插入是发现插入位置已经有元素该怎么办?这就是我们所谓的哈希冲突。观察源码后发现,代码中解决该问题的方法为链地址法,当我们插入时发现该地址已有一个其他元素时,将该位置转换成一个链表形势,然后连接其中。因此,hashMap采用的是数组+链表的形式构成的。
    在这里插入图片描述

2.2 HashMap的成员变量

  • 由上文可以了解到,HashMap的主干是一个(Node)数组的形式,而Node元素时HashMap的基本组成单元,每一个Node包含一个key-value键值对。
  • // 存储Node数组。
    transient Node<K, V>[] table;

  • Node是HashMap中的一个静态内部类
 static class Node<K, V> implements 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;
        
    }
  • HashMap其他成员变量:

transient int size; //记录实际上Key-Value个数
int threshold; //阈值,用于判断是否需要扩容,默认值为16
final float loadFactor; //负载因子,默认值为0.75,用于计算阈值
transient int modCount; // HashMap被改变次数,用于在非线程安全下抛出异常

2.3 从一次put来理解HashMap:

  • 首先初始化一个table数组存放Node;
// 如果是空的table,那么默认初始化一个长度为16的Node数组
if ((tab = table) == null || (n = tab.length) == 0) {
// eg1: resize返回(Node<K, V>[]) new Node[16],所以:tab=(Node<K, V>[]) new Node[16], n=16
    n = (tab = resize()).length;
}
  • 通过异或运算hash下标,如果此时tab中此下标没有数据,则新增在此位置,若此时tab数组中已存在数据,则…
  • p = tab[i = (n - 1) & hash]//异或运算

    • 判断value是否与待插入value相同,若相同则直接覆盖
    • 如果与已存在的Node是相同的key值,并且是普通节点,则循环遍历链式Node,并对比hash和key,如果都不相同,则将新的Node拼装到链表的末尾。如果相同,则进行更新。(当Node节点链表大于8个Node,并且table长度大于64,则转换为红黑树)
    • /** 如果Node链表大于8个Node,那么变为红黑树 */
      if (binCount >= TREEIFY_THRESHOLD - 1) {
      // eg6: tab={newNode(0, 0, “a0”, [指向后面1个链表中的7个node]), newNode(1, 1, “a1”, null)}, hash=128
      treeifyBin(tab, hash);
      }

    • 将元素插入hashMap后,size++并与阈值threshold对比,若大于threshold,则触发resize()扩容。容量扩大一倍后检查是否小于2^30,并且大于默认值16。扩容后将当前table数组的所有Node全部传输过去,扩容后的新数组是table数组的两倍大小。

2.4 hash函数

  • 首先上源码:
static final int hash(Object key) {
        int h;
        /**
         * 按位异或运算(^):两个数转为二进制,然后从高位开始比较,如果相同则为0,不相同则为1。
         *
         * 扰动函数————(h = key.hashCode()) ^ (h >>> 16) 表示:
         *      将key的哈希code一分为二。其中:
         *      【高半区16位】数据不变。
         *      【低半区16位】数据与高半区16位数据进行异或操作,以此来加大低位的随机性。
         * 注意:如果key的哈希code小于等于16位,那么是没有任何影响的。只有大于16位,才会触发扰动函数的执行效果。
         * */
        // egx: 110100100110^000000000000=110100100110,由于k1的hashCode都是在低16位,所以原样返回3366
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 从源码中可以看到核心为key.hashCode()) ^ (h >>> 16),即将key的hashCode右移16位后异或操作。那么可以得出:将key的哈希code一分为二。【高半区16位】数据不变。【低半区16位】数据与高半区16位数据进行异或操作,以此来加大低位的随机性,减少hash冲突的可能。
    hash函数

2.5 resize()扩容

  • 当我们构造一个hashMap或者当hashMap中table数组的容量不足以存放我们所需要的值时,就会用到扩容,即扩充hashMap的容量,或者说是扩充hashMap中table数组的容量。
  • 由上文得知,当申请一个hashMap或者当前table的size大于阈值threshold时,就会触发扩容。这里我们把扩容分成两个阶段:
    1. 重新解析计算table的容量newCap和重新计算阈值的大小newThr;
    2. 根据newCap和newThr重新构建一个新的数组,并将旧的table转移进去。
2.5.1 扩容第一阶段

resize扩容第一阶段

  • 首先我们现在需要知道当前触发扩容是由于申请了新的hashMap还是当前超过阈值引起的。因此我们用oldCap>0?去判断触发扩容的原因;
  • 若是申请一个新的hashMap,则将newCap和newThr分别设置为默认值16和12;阈值=容量*负载因子;
  • 若因超出阈值而触发扩容,则判断此时的容量是否已经达到最大值,若已达到最大值,则将阈值threshold设置为integer的最大值;反之,则容量翻倍、阈值翻倍
2.5.2 扩容第二阶段
  • 扩容第二阶段为将原数组放入扩容后的数组。
  • 这里涉及到一个链表转化后重新分配地址的内容,因我自身还没理清楚,暂不讨论。

2.6 HashMap一次put大致流程图

在这里插入图片描述

  • 红黑树部分不太了解…后面补充
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值