1. 二叉树、BST、AVL、B树、B+树、红黑树:节点存储方式、时间复杂度、特点
- 二叉树:节点存值
- 遍历方式:前(根左右)、中(左根右)、后(左右根)
- 时间复杂度
- 查找、插入、删除都是On
- 容易形成单向链表
- BST:节点存值,节点值按照左根右从小到大排序,中序遍历为递增
- 时间复杂度
- 查找、插入、删除都是On
- 时间复杂度
- AVL:节点存值,左右子树高度不超过1
- 时间复杂度
- 查找、插入、删除都是Ologn
- 时间复杂度
- B树:节点可以存m-1个值,叶子节点位于同一层
- 时间复杂度:O(logn),二分查找比较节点的值
- B+树:节点可以存m个值,叶子节点存数据且升序,非叶子节点存数据索引。
- 查找效率稳定,数据必定在叶子节点
- 叶子节点数据通过指针指向连接,形成升序链表。
- 时间复杂度:O(logn)
- 红黑树:节点红黑相间,根节点为黑,从任一节点出发到叶子节点都有相同数量的黑节点
- 红黑树不是严格的AVL树,查询效率可能比AVL树低,但是插入操作进行的旋转次数比AVL树要少,至多3次。
- 适合频繁插入和删除且数据量大的情况
- 时间复杂度:同AVL
2. 数组和链表:内存存储、时间复杂度、优缺点、使用场景
- 数组:在内存中连续存储,通过索引来随机获取元素,查询快增删慢。
- 时间复杂度:查询O1,删除On,头插On,尾插O1
- 大小固定,扩容要新建数组,类型单一
- 适用于数据量少且查询较多
- 链表:在内存中非连续存储,只能顺序访问,查询慢增删快
- 时间复杂度:查询On,删除O1,头插On,尾插O1
- 无初始容量,无上限,但内存消耗大,因为存在大量的指针
- 适合数据量少增删多
3. 集合类:List接口、Set接口、Queue接口、Map。从数据结构以及特点来讲
- List
- ArrayList:数组,查询快增删慢,线程不安全
- LinkedList:双向链表,查询慢增删快,线程不安全
- Vector:线程安全版的ArrayList
- Set
- TreeSet:红黑树,有序且唯一,底层是treemap,compareTo实现唯一,比较器实现有序
- HashSet:哈希表,无序且唯一,底层是hashmap,compareTo实现唯一
- LinkedHashSet:双向链表维护HashSet集合
- Queue
- LinkedList
- PriorityQueue:优先队列,大根堆小根堆
- Map
- HashMap:数组+链表+红黑树,key无序且唯一,通过hashcode和equals实现唯一
- TreeMap:红黑树,key有序且唯一,compareTo实现唯一,比较器实现有序
- LinkedHashMap:双向链表+HashMap,有序的HashMap
- HashTable:数组+链表,线程安全
- concurrentHashMap:线程安全的HashMap。
4. HashMap概述:数据结构、初始容量、扩容、树化、时间复杂度、为什么线程不安全。好文:https://zhuanlan.zhihu.com/p/76735726
1. 1.8之前
- 数据结构:数组+链表,数组存KV键值对,查询时间复杂度:On,最好O1。存储时间复杂度O1。
- 初始容量:16,加载因子0.75。扩容2倍。当threshold == 加载因子 * 当前数组长度时,扩容
- 节点存入方式:头插法,多线程先可能出现节点翻转时导致循环链表
- 数组是用来确定桶的位置,利用key的hash值与数组长度-1取模得到. 链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置通过头插法形成一条链表。
2. 1.8之后
- 数据结构:数组+链表+红黑树,当链表长度大于等于8时,树化。存储时间复杂度O1,查询时间复杂度降为Ologn
- 初始容量不变,扩容也不变。但是扩容会重新计算长度和阈值。
- 节点存入方式:尾插法,解决了环形链表问题,但是多线程下可能会发生数据覆盖,依旧是线程不安全
- hashmap的rehash采用的是位运算函数
5. HashMap的put机制:1.8前后
- 1.8之前:
- 先判断数组是否为空,如果为空,新建一个数组,长度为16
- 如果key为null,存入table[0],遍历table[0]数组下的链表,通过hashcode和equals判断是否存在相同的key,如果是,替换,否则,存入链表头
- 如果key不为null,将key的hashcode传入indexFor,计算出index位置,遍历table[index]下的链表,通过hashcode和equals判断是否存在相同的key,如果是,替换value。
- 如果key不重复,addEntry添加节点到链表头,判断是否需要扩容
- 1.8之后:调用putVal方法
- 先判断数组是否为空,如果为空,新建一个数组,长度为16
- 根据key的hashcode和数组长度-1进行与运算得到index位置,如果table[index]位置下没有Node节点,将key和value封装成Node节点存入。如果第一个节点的hash和key相同,替换value值
- 否则先提取节点,如果节点是树节点,调用树方法将key和value存入树中。如果是链表节点,如果遍历链表的过程中通过hashcode和equals没有发现相同的key,将key和value封装成Node节点,存入链表尾部。如果发现相同key,替换value。判断链表长度是否大于等于8,如果是,进行树化。
- 判断数组是否需要扩容
6. HashMap的get机制:1.8前后
- 1.8之前
- 先判断数组是否为空,如果为空,直接返回null
- 判断key是否为null,如果为null,调用getForNullKey查找table[0]数组,遍历链表,通过hashcode和equals查找key,找到,返回value,否则返回null
- 如果key不为null,调用getForEntry,将key的hashcode传入indexfor计算得到index,遍历table[index]下的链表,通过hashcode和equals查找key,找到返回value,否则返回null
- 1.8之后
- 调用getNode
- 如果数组为空,返回null
- 如果数组不为空,查找第一个Node节点是否与key相同,如果相同,直接返回
- 如果节点是树节点,调用getTreeNode查找
- 如果节点是链表节点,遍历链表,通过hashcode和equals查找key,找到返回value,否则返回null
- 调用getNode
7. HashMap的resize机制:1.8
- 1.8之前的resize
- 在put的addEntry中判断是否需要扩容,先判断数组长度是否大于threshold阈值,如果是,调用resize扩容
- resize先提取旧数组容量,判断旧容量是否已经到达MAXVALUE,如果是,将threshold设置为Integer.MAX_VALUE,不再扩容,直接返回。否则新建一个数组,长度为原来的两倍,调用transfer进行转移
- transfer遍历旧数组元素,调用rehash判断是否需要重新进行hash,调用indexFor计算index,头插法将节点转移到新数组中
- 1.8之后要重新规划长度和阈值,会重新排序
- 重新规划长度和阈值
- 最大容量:先判断旧容量是否到达最大容量,如果是,将threshold设置为Integer.MAX_VALUE,不再扩容,返回
- 两倍扩容:如果当前容量的两倍小于最大容量并且当前容量大于16,当前容量扩大两倍
- threshold:如果都不满足且threshold大于0,将threshold设置为容量
- 初始16和12:否则将长度设置为16,threshold为12
- 重新排序节点
- 如果节点为null,不处理
- 如果是树节点,调用split方法处理。
- 如果是链表节点:将链表拆分为hash值超出旧容量和未超出旧容量的情况。如果hash计算结果为0,原位置,不处理。否则将节点存入到新下标,新下标 = 旧容量+旧下标
- 重新规划长度和阈值
8. 线程安全问题:环形链表和数据覆盖
- 1.8之前:
- 环形链表:transfer采用头插法,多线程下可能出现节点翻转指向同一个位置
- 1.8之后
- 尾插法可能导致数据覆盖。
9. HashMap的初始容量为什么是2的n次方:防止数组越界、hash分别更均匀、转换为与运算
- 防止数组越界,因为数组index位置是通过key的hash值跟数组的长度-1进行与运算得到的,如果数组长度为2的n次方,那么长度-1的二进制就是11111...的数,与运算,最大的结果也不会发生数组越界
- 为了使hash分布更均匀,长度-1的二进制位1111....,这种二进制数进行与运算时,出现重复数的概率低,hash冲突发生概率低
- 将取模运算转换为与运算,效率更高
10. hash冲突如何解决(hash碰撞是指两个不同的对象计算出同一个hashcode。hash冲突是指hashmap中桶位置重叠),hash函数类型:开放定址法、rehash、拉链法
- 开放定址法:线性探测,当发生hash冲突时,从该hash值的地址往下搜索,直到找到一个不冲突的hash值为止
- rehash:定义多个hash函数,当发生hash冲突时,轮番调用函数重新进行计算,直到不发生冲突为止。常见的hash函数有:位运算(HashMap)、乘法(String)、加法
- 拉链法:头插和尾插
-
。紫色的为数组,绿色的为链表
11. HashTable如何解决线程安全:put和get方法加synchronized、初始容量、扩容、key-value
- hashtable的初始容量为11,扩容2倍+1。key和value都不能为null
- 通过在put和get方法加synchronized实现线程安全
12. ConcurrentHashMap如何解决线程安全问题:1.8前后
- hashtable主要的问题就是对整个put方法加锁,并发低。
- concurrentHashMap在1.8之前通过Segment分段锁 + Entry节点实现,每个分段锁维护一段数组,多个线程可以同时访问不同的分段锁,并发提高
- concurrentHashMap在1.8之后通过Cas + synchronized + Node节点实现,锁住Node节点,锁粒度小。当数组index位置下没有元素时,通过CAS方式存入节点
13. ConcurrentHashMap的put机制:1.8版本
- 先判断key和value是否为null,concurrentHashMap的key和value不能为null,如果为null,break
- 计算key的hash值,确定index位置。
- while true循环,因为可能会发生扩容和初始化数组
- 提取节点
- 如果数组为空,创建一个数组
- 如果数组不为空,但是table[index]位置下没有元素,将key和value封装成Node节点,CAS方式存入
- 如果正在发生扩容,table数组提取扩容后返回的数组
- 对节点上锁
- 遍历链表,如果在链表中找到相同的key,替换value,如果没有找到,尾插法存入链表
- 如果节点是树节点,putTreeVal存入
- 判断链表长度是否大于等于8,如果是,进行树化
- 容量+1,判断数组是否需要扩容
14. ConcurrentHashMap的get机制要加synchronized吗:Node节点加volatile,线程间可见
- 因为Node节点的value和Node节点.next都是使用volatile修饰,多线程下修改value或者增删Node节点都是线程间可见的
15. ArrayList概述:数据结构、初始容量、扩容、时间复杂度、线程不安全
- 数据结构:数组
- 初始容量:10
- 扩容:1.5倍,通过Arrays.copyOf()复制到新数组
- 线程不安全
- 时间复杂度:查询O1,删除On,头插On,尾插O1
16. ArrayList的grow:创建新数组,1.5倍,通过Arrays.copyOf()复制,底层调用了System.arraycopy(),扩容开销大
- add时先判断容量,如果容量不够,调用grow进行扩容
- grow会创建一个新数组,长度为1.5倍,使用Arrays.copyOf转移元素。底层调用System.arraycopy(),开销大
17. ArrayList的add:先判断是否越界,判断是否需要扩容,调用System.arraycopy()添加
- 先判断index是否越界,如果是,抛出越界异常
- 判断数组是否需要扩容
- 调用System.arraycopy()添加
18. ArrayList的remove:先判断是否越界、提取数组、调用fastRemove()、调用System.arraycopy()删除
- 先检查index是否越界,如果越界,抛出越界异常
- 提取数组对象
- 调用fastRemove,调用System.arraycopy()删除,返回删除后的数组对象
19. 如何解决ArrayList线程不安全问题:使用Vector、Collections.synchronizedList()、原子类CopyOnWriteArrayList
- 使用Vector,效率低
- 使用Collection接口的线程安全方法Collections.synchronizedList()
- 使用原子类CopyOnWriteArrayList,使用synchronized实现
20. Vector和ArrayList有什么区别,Vector如何解决线程不安全:就是一个线程安全的ArrayList,通过在add和get方法加synchronized解决,锁粒度大,效率低
- Vector就是一个线程安全的ArrayList,初始容量10,扩容2倍,也是通过Arrays.copyOf()
- 通过对add和get方法加synchronized实现线程安全。
21. ArrayList和LinkedList比较:数据结构、初始容量扩容、时间复杂度、线程安全、100W数据插入(头擦和尾插)
- 数据结构:
- ArrayList:数组,可以通过索引随机访问
- LinkedList:链表,只能顺序访问
- 初始容量:
- ArrayList初始容量10,扩容1.5倍
- LinkedList无
- 时间复杂度:
- ArrayList
- 查询O1
- 删除On
- 头插On
- 尾插O1
- LinkedList
- 查询On
- 删除O1
- 头插On
- 尾插O1
- ArrayList
- 都是线程不安全
- 存100W数据
- 头插:LinkedList完胜,因为ArrayList的add和grow都要调用System.arraycopy()进行转移元素
- 尾插:ArrayList完胜,且效率越来越高。因为ArrayList需要扩容,前期扩容频繁,但是后期扩容越来越少。
22. ArrayList的删除方法:使用Iterator方式、倒序遍历、remove方法
- 正序for循环:元素前移后忽略元素,不可取
public static void remove(ArrayList list) {
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if (s.equals("b")) {
list.remove(s);
}
}
}
- foreach循环:不能用
- Iterator迭代器:正确方式
public static void remove3(ArrayList list) {
Iterator it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("b")) {
it.remove();
}
}
}
- 倒序for循环:解决了前移忽视元素的情况
public static void remove14(List list, String target){
for(int i = list.size() - 1; i >= 0; i--){
String item = list.get(i);
if(target.equals(item)){
// List 删除元素的逻辑是将目标元素之后的元素往前移一个索引位置,最后一个元素置为 null,同时 size - 1
list.remove(item);
}
}
print(list);
}
- 使用remove方法
- 按照索引删除:List.remove(index)
- 按照value值删除:List.remove(Integer.valueOf(value))