集合
1.数组
- 数组:连续内存空间存储的相同数据类型的线性数据结构
- 寻址公式:baseAddress + i*dataTypeSize
- 查找时间复杂度:
- 随机查找O(1)
- 查找未知下标O(n)
- 查找未知下标但排序O(logn)
- 插入和删除时间复杂度
- O(n)
2.ArrayList
- 动态数组实现
- 初始为0,第一次添加的时候初始化为10
- 扩容为1.5倍,每次拷贝
- 添加数据逻辑:
- 确保数组已使用的size+1之后足够存下下一个数据
- 计算数组的容量,如果当前的数组已使用长度+1后大于(等与不会扩容)当前的数组长度,扩容1.5倍
- 确保有地方存数据时,添加到size
- 返回true
3.如何实现数组和List的转换
- 数组转List:使用JDK中的Arrays工具类的asList方法
- 数组转List后,修改数组,新的List会受影响
- 因为:底层是使用内部类ArrayList来构造的集合,是对我们传入的集合进行包装,最终指向的都是同一个内存地址
- List转数组:使用List的toArray方法,无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
- List转数组后,修改List,新的数组不会受影响
- 因为:底层是进行了数组的拷贝,跟原来的元素就没啥关系了
4.链表
链表 | 方向 | 时间复杂度 |
---|---|---|
单向链表 | 单向 | 头O(1),其他O(n) |
双向链表 | 双向 | 头尾O(1),其它O(n),给定节点O(1) |
5.ArrayList和LinkedList的区别是什么
- 同:都线程不安全
- 异:底层数据结构,时间复杂度,内存空间占用
- ArrayList:底层数组
- LinkedList:底层双向链表
6.红黑树
- 红黑数:自平衡的二叉搜索树(BST),也叫平衡二叉B树
- 红黑规则:
- 1.节点要么红,要么黑
- 2.根是黑色
- 3.叶子节点都是黑色的空节点
- 4.红黑树中的红色节点的子节点都是黑色
- 5.从任意一节点到叶子节点的路径都包含数目相同的黑色节点
- 时间复杂度:
- 查找:O(logn)
- 添加:整体O(logn),因为得先查找O(logn)再添加后旋转O(1)
- 删除:整体O(logn),因为得先查找O(logn)再删除后旋转O(1)
7.散列表
- 散列表:哈希 (hash) 表,根据键 (key) 直接访问内存存储位置值 (value) 的数据结构,是根据数组演化而来的,利用了数组支持按照下标随机访问数据的特性
- 散列函数:讲键 (key) 映射为数组下标的函数
- 哈希碰撞:散列冲突,多个key映射到同一个下标的位置
- 解决冲突的办法:
- 拉链法:每个下标的位置都称为桶 (bucker) 或槽 (slot) ,没个桶都会对应一个链表,存放相同散列值相同的元素
- 时间复杂度:插入O(1),查找删除O(1),O(n)或O(logn)
- 拉链法:每个下标的位置都称为桶 (bucker) 或槽 (slot) ,没个桶都会对应一个链表,存放相同散列值相同的元素
8.HashMap的实现原理
- 数据结构:底层使用的是hash表的数据结构,数组和链表,1.8以后加入了红黑树
- 1.当向HashMap中put数据时,利用key的hashCode重新计算hash计算出当前对象的元素在数组中的下标
- 2.存储时,如果出现hash值相同的key,两种情况:
- a.如果key相同,则覆盖原始值
- b.如果key不同(出现冲突),则将key-value放入链表或红黑树中
- 3.获取时,直接找到hash对应的下标,再进一步判断key是否相同,从而找到对应值
9.HashMap的jdk1.7和1.8有什么区别
- jdk1.7及之前是拉链法,采用头插法加入数据
- jdk1.8及之后加入了红黑树,采用尾插法加入数据
- 当链表的长度大于等于8,且数组的长度大于64时转化为红黑树
- 扩容时,红黑树拆分成的树结点数小于等于临界值6个,则退化为链表
10.HashMap的put方法的具体流程
- 1.判断键值对数组table是否为null,否则执行resize()进行扩容(初始化)
- 2.根据键值key计算hash值得到数组索引
- 3.判断table[i]==null,条件成立,直接新建节点添加
- 4.如果table[i]==null,条件不成立
- 4.1判断table[i]的首个元素是否和key一样,如果相同则直接覆盖value
- 4.2不相同,再判断table[i]是否是红黑树,如果是红黑树,则直接在数中加入键值对
- 4.3若不是树,再遍历table[i],判断链表长度是否大于8且数组的长度大于64,不满足条件在链表的尾部插入数据,满足条件则将链表转化为红黑树,在红黑树中插入操作,遍历过程中若是发现key则直接覆盖value
- 5.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容
11.HashMap的扩容
- 在添加元素或初始化的时候都需要调用resize方法进行扩容,第一次添加数据初始化数组长度是16,以后每次都是达到了扩容阈值(数组长度*0.75)
- 每次扩容的时候,都是扩容之前的2倍
- 扩容之后,会创建新的数组,将老数组中的数据挪动到新的数组中
- 没有hash冲突的节点,则使用e.hash & (newCap - 1)计算新数组的索引位置
- 如果是红黑树,走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & odlCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组的大小的这个位置
12.HashMap的寻址算法
- 扰动算法:计算hashCode值后,再调用方法进行二次hash,hashCode值右移16位再异或运算,使hash值更加均匀,减少hash冲突
- hash & (n-1):得到的数组中的索引,代替取模,性能更好,数组长度必须是2的n次幂
13.为什么HashMap的数组长度一定是2的次幂
- 计算索引时效率更高:如果是2的n次幂可以使用位于运算代替取模
- 扩容时重新计算索引效率更高:hash & oldCap == 0元素留在原来的位置,否则新位置 = 旧位置 + oldCap
12.HashMap在jdk1.7情况下的多线程死循环问题
- jdk1.7使用的数据结构是:数组+链表,扩容是头插法,在数据迁移过程中,没有新对象产生,只是改变了对象的引用
- 问题的发生:
- 1.有线程1和线程2准备扩容拷贝链表
- 2.线程1的e1和next1和线程2的e2和next2分别指向同一个位置
- e是指将要迁移的节点
- next是指下一个要迁移的节点
- 3.线程1先开始,线程2先睡
- 4.线程1将原来的链表运用头插法拷贝到了新的位置
- 5.然后线程2醒来继续操作,原先线程2指向的e2和next2,由于线程1的操作,已经逆序,此时next2只指向的下一个节点是e2
- 6.线程2继续执行头插法进行拷贝,会导致e2指向next2,next2指向e2的请况,然后导致死循环
- 解决方案:jdk8扩容使用尾插法