Java集合
1. 数据结构:树、数组、链表、堆、栈、队列
1. 树
- 二叉树:节点存值
- 容易形成单链表形式,如果是单链表形式,查询时间复杂度On,如果是二叉树形式,查询时间复杂度Ologn
- 遍历方式
- 前序:根左右
- 中序:左根右
- 后序:左右根
- BST二叉搜索树:节点存值
- 节点值按照左根右升序排序,其中序遍历是递增序列,容易形成单链表形式,如果是单链表形式,查询时间复杂度On,如果是二叉树形式,查询时间复杂度Ologn
- AVL二叉平衡树
- 能够维持二叉树结构,查询时间复杂度Ologn,但是增删改需要通过旋转来维持平衡树的结构,开销大
- 红黑树
- 根节点是黑节点,每一层红黑相间
- 红黑树是一种特殊的二叉平衡树,能够维持二叉平衡树的结构,查询时间复杂度Ologn,增删改需要旋转来维持,开销大
- B树
- 是一种多路平衡树,节点存多个值,M阶B树一个节点可以存M-1个值,通过将节点读入内存,然后进行二分查找来搜索
- 缺点:查询效率不稳定,数据可能在树的任意一层,时间复杂度Ologn
- B+树
- 是一种多路平衡树,节点存多个值,非叶子节点存数据的索引,叶子节点存数据,叶子节点的数据按照升序排序
- 优点:查询效率稳定,数据必定在叶子节点,查询时间复杂度Ologn
2. 数组
- 在内存中连续存储,通过索引来随机访问元素,查询快增删慢,增删需要调用System.arrayCopy()实现,开销大
- 查询O1,头插On,尾插O1,删除On
- 初始容量,扩容需要新建数组
3. 链表
- 在内存中非连续存储,通过指针指向来固定元素位置,只能顺序访问,查询慢增删快,增删只需要改变前后指针的指向就能实现
- 查询On,头插On,尾插O1,删除O1
- 无初始容量,不需要扩容
4. 堆栈队列
- 堆:特殊的完全二叉树,有大根堆和小根堆
- 栈:先进后出
- 队列:先进先出
2. Java集合
1. List:有序可重复
- ArrayList:数组
- LinkedList:双向链表
- Vector:数组,线程安全
2. Set:无序无重复
- HashSet:哈希表,底层是HashMap
- TreeSet:红黑树,底层是TreeMap
- LinkedHashSet:双向链表+哈希表
3. Queue
- LinkedList
- PriorityQueue
4. Map:kv键值对,key无重复,value可重复
- HashMap:1.8之前数组+链表,1.8之后数组+链表+红黑树,无序无重复
- TreeMap:红黑树,有序无重复
- LinkedHashMap:双向链表+哈希表
- HashTable:哈希表,线程安全
- ConcurrentHashMap:数组+链表+红黑树,线程安全
3. List列表
1. ArrayList和Vector
- 数据结构:数组
- 初始容量和扩容:
- ArrayList初始容量10,扩容1.5倍。
- Vector初始容量10,扩容2倍
- 线程安全:ArrayList线程不安全,Vector的add和get方法加了synchronized实现线程安全
- ArrayList的add、get和grow都需要调用System.arrayCopy()
2. ArrayList和LinkedList
- 数据结构
- ArrayList:数组
- LinkedList:双向链表
- 初始容量
- ArrayList初始容量10,扩容1.5倍
- LinkedList无
- 时间复杂度
- ArrayList:查询O1、删除On、头插On、尾插O1
- LinkedList:查询On、删除O1、头插On、尾插O1
- 存100W数据
- 头插:LinkedList完胜,因为ArrayList的add和grow都要调用System.arraycopy()进行转移元素,开销大
- 尾插:ArrayList完胜,且效率越来越高。因为ArrayList前期扩容频繁,但是后期扩容越来越少。
3. ArrayList删除元素
- 正序for遍历,不行,删除之后会将后一个元素前移,i++后会忽略这个元素
- 倒序for遍历,不行,会报错
- foreach遍历,不行,会报错
- Iterator迭代器,通过hasNext()判断是否还有元素,正确的方式
4. Set集合
- HashSet:底层是封装了一个HashMap,通过hashcode和equals实现元素的唯一,无序唯一
- TreeSet:底层是封装了一个TreeMap,通过树结构实现有序,hashcode和equals实现唯一,有序唯一
5. Map
1. HashMap:数据结构 、初始容量、扩容机制、树化、查询时间复杂度
- 1.8之前
- 数据结构:数组+链表,table数组存放Entry节点
- 初始容量:16,加载因子0.75,扩容阈值12
- 不会树化
- 时间复杂度:无hash冲突时O1,发生hash冲突形成链表On
- 1.8之后
- 数据结构:数组+链表+红黑树,table数组存放Node节点
- 初始容量:16,加载因子0.75,扩容阈值12
- 当链表节点数大于等于8并且table数组长度大于64时转换为红黑树,当红黑树节点数小于等于6时退化回链表
- 时间复杂度:链表On,红黑树Ologn,无hash冲突O1
2. 为什么要转化为红黑树:红黑树时间复杂度Ologn,链表时间复杂度On
3. 为什么在8时转化:链表的节点数量分布符合泊松分布,当节点数量为8时,概率已经非常低了,因此将8作为上限
4. 为什么在6时退化:红黑树的空间开销比链表大,退化能减少空间开销
5. 为什么在8时转化,在6时退化:设置一个缓冲区防止频繁进行转化和退化,节省时间开销
6. 负载因子为什么是0.75
- 是空间利用率和时间复杂度的折中权衡
- 如果是0.5,使用一半数组就扩容,虽然减少了出现hash冲突的概率,避免了形成链表导致时间复杂度变高,但是空间利用率却降低了
- 如果是1,数组使用完之后才扩容,虽然空间利用率变高了,但是容易出现hash冲突形成链表导致时间复杂度变高
7. hash扰动函数
- 为了得到一个更加散列的hash值,然后再跟数组长度-1进行与运算得到index位置,为了减少hash冲突的概率
- 8之前:将高20位和高12位右移进行异或运算,再将高7位和高4位右移进行异或运算。一共5次异或
- 8之后:将高低16位分别进行右移异或运算。一共1次异或
- 与运算的目的:因为hash值是正负21亿的int值,table数组明显不能存放这么多数据,因此通过hash值和数组长度-1进行与运算,消除hash值的高位
8. HashMap解决hash冲突
- 拉链法,1.8之前,如果出现hash冲突,在table[index]下形成链表,1.8之后加入了红黑树的数据结构,当出现hash冲突时,先在table[index]下形成链表,当链表节点数到达8个并且table数组长度大于64时转化为红黑树
9. 为什么数组长度是2的n次方,扩容是2倍
- 减少hash冲突的概率
- 数组长度为2的n次方,数组长度-1的二进制数全部为1,然后跟hash值进行与运算计算index位置时,能够让节点在数组上的分布更均匀,减少了hash冲突的概率
- 如果初始容量设置不是2的n次方,也会通过左移运算得到第一个比它大的2的n次方的值作为容量
10. HashMap线程不安全问题
- 1.8之前扩容采用头插法,将旧数组的链表翻转,转移时可能出现线程切换,可能导致两个节点指向同一个位置造成死循环
- 1.8之后采用尾插法,解决了死循环问题。但是Put存放元素时可能出现数据覆盖问题。比如线程A计算得到index位置后,发生线程切换,线程B也计算得到相同的index位置,线程B存入元素。然后线程A继续执行,直接将数据覆盖而不形成链表
11. Put机制
1. 8之前
- 如果数组为空,新建数组
- 如果key为null,存入table[0]
- 如果key不为null,将key的hash值传入indexFor计算index位置,存入table[index]
- 如果table[index]为空,直接存入
- 如果table[index]有一个值,判断是否相同,否则形成链表
- 如果table[index]是链表,遍历判断是否相同
- addentry头插,容量+1,判断是否需要扩容
1.8之后
- 如果数组为空,新建数组
- 如果key为null,存入table[0]
- 如果key不为null,将key的hash值和数组长度-1进行与运算得到index,存入table[index]
- 如果table[index]为空,直接存入
- 如果table[index]只有一个值,判断是否相同,否则形成链表
- 如果table[index]是链表,遍历判断是否相同,否则尾插法存入,判断链表长度是否大于等于8,如果是,进行树化
- 如果table[index]是红黑树,调用putTreeVal存入
- 容量+1,判断是否需要扩容
12. Get机制
1.8之前
- 如果数组为空,返回null
- 如果key为null,查询table[0]
- 如果key不为null,将key的hash值和数组长度传入indexFor计算index,查询table[index]
- 如果table[index]为空,返回null
- 如果table[index]只有一个值,判断是否相同,相同返回value,否则返回null
- 如果table[index]是链表,遍历链表,判断是否有相同,有就返回value,否则返回null
1.8之后
- 如果数组为空,返回null
- 如果key不为null,将key的hash值和数组长度-1进行与运算得到index,查询table[index]
- 如果table[index]为空,返回null
- 如果table[index]只有一个值,判断是否相同,相同返回value,否则返回null
- 如果table[index]是链表,遍历链表,判断是否有相同,有就返回value,否则返回null
- 如果table[index]是红黑树,调用getTreeVal查询
13. Resize扩容
1.8之前
- addEntry中判断是否需要扩容,调用resize进行扩容
- resize先判断旧数组长度是否大于最大容量,如果是,将阈值设置为Integer.MAX_VALUE,不扩容,返回。否则就新建一个数组,长度为原来的两倍,调用transfer进行转移
- transfer遍历旧数组,提取Entry节点,如果需要进行rehash,重新计算hash值,调用indexFor方法重新计算index,利用头插法将元素转移到新数组
1.8之后
- 重新规划长度
- 如果旧数组长度大于最大容量,不扩容,返回
- 如果旧数组的两倍小于MAX_VALUE,扩容两倍
- 如果阈值大于0,将阈值设置为新数组长度
- 进行初始化,长度16,阈值12
- 重新计算位置
- 遍历旧数组,提取Node节点
- 如果Node节点没有next指针,计算新下标的位置存入新数组
- 如果是树节点,调用split方法
- 如果是链表,分为超出旧数组和未超出旧数组两部分。如果是未超出旧数组的部分,存入原来的位置。如果是超出旧数组的部分,计算新下标=旧数组长度+旧下标,存入新数组
14. HashMap和HashTable
- 数据结构
- HashMap:8之前数组+链表,8之后数组+链表+红黑树
- HashTable:数组+链表
- 线程安全
- HashMap不是线程安全
- HashTable在put和get加synchronized实现线程安全
- HashMap的key和value可以为null,HashTable的key和value不能为null
- 初始容量
- HashMap初始容量16,扩容2倍
- HashTable初始容量11,扩容2倍+1
15. ConcurrentHashMap如何解决线程安全
- 8之前通过Segment+Entry解决线程安全,Segment分段锁实现了ReentrantLock,多个Segment分段锁共同维护整个table数组,多个线程可以同时访问不同的分段锁
- 8之后通过synchronized+CAS+Node实现,当table[index]下没有元素时使用CAS方式存入,synchronized只锁住当前Node节点,锁粒度小,效率高。并且Node节点的next指针和value值都是使用了volatile修饰的,多线程下修改是线程间可见的,因此get方法不需要加synchronized。
16. Put机制
- 判断key和value是否为null,如果为null,返回
- 计算key的index位置,提取Node数组对象
- while true循环
- 如果数组为空,新建数组
- 提取Node节点
- 如果table[index]为空,CAS方式存入
- 如果正在发生扩容,提取扩容后的数组对象
- synchronized对Node节点上锁
- 如果Node节点是树节点,调用putTreeval存入key和value
- 如果Node节点是链表节点,遍历table[index]下的链表,判断key是否相同,相同就替换value,否则遍历到链表尾部,尾插法存入key和value。
判断链表长度是否大于等于8,如果是,进行树化