043:HashMap1.7扩容死循环问题与二叉搜索树结构
1 HashMapJdk1.7面试题总结
HashMap默认的初始容量? 16
HashMap默认情况每次容量扩容多少? 2倍
HashMap如何解决hashCode冲突问题? 单向链表,新值指向旧值。
Index冲突会产生哪些问题? 会导致值被覆盖,使用链表解决。
HashMap底层如何减少index冲突问题? return h&(length-1);位移运算取模分摊
HashMap线程是否安全,与HashTable区别? 不安全;线程安全、null、继承HashMap
是否可以存放key为空的对象? 支持,存放在数组第0个位置(第一个链表)
HashMap底层实现扩容的时候存在哪些问题? jdk7并发情况扩容可能出现死循环问题
HashMap是否可以存放键值对为自定义对象? 可以,底层使用泛型
HashMap加载因子为什么是0.75而不是其他? 加载因子越大,hash冲突概率越大,空间利用率高但是链表长度更长导致查询效率更低;加载因子越小,hash冲突概率越小,频繁扩容导致空间利用率不是很高。0.75是兼顾“冲突机会”与“空间利用率”相对来说最合适的值。
HashMap中index冲突与hash冲突有什么区别? Index冲突是因为底层做二进制运算时产生相同的index,对象不同但是二级制运算得出index相同;hash冲突,对象不同但是hashCode相同。
同一个链表中存放哪些内容? index冲突或者hash值冲突
HashMap链表长度过长会产生哪些问题? 导致整个查询效率降低,时间复杂度o(n)
Jdk7HashMap存在哪些问题? HashMap线程不安全;链表如果过长会导致查询效率降低;底层扩容的时候可能会存在死循环问题
Jdk7HashMap计算hash值与jdk8区别? Jdk7中HashMap计算得出的hash值非常均摊,能够减少hash冲突问题,因为链表查询效率低;Jdk8中计算hash非常简单,存在hash冲突的概率相比jdk7更大,但是不影响,因为java8使用红黑树解决查询效率问题。
描述Jdk7中HashMap查询? Jdk7中HashMap根据key查询分两种情况:如果该key没有发生hash冲突,根据key计算index,直接从数组中查询;如果该key存在hash冲突问题,会形成一个链表,遍历链表查询,查询效率低。
2 HashMapJdk1.7扩容死循环问题及原理分析
HashMap在并发情况下,扩容时候会存在哪些问题?
Jdk7使用链表头插赋值法,在多线程的情况下扩容可能会导致出现死循环的问题,jdk8已经解决。
原理分析:在多线程情况下同时对HashMap进行扩容,因为每次数组在扩容的时候,新的数组长度发生了变化,需要重新计算index值,需要将原来table中的数据移动到新table中。在HashMap1.7版本598行“e.next = newTable[i];”代码,直接改变原来的next关系,导致出现脏读的数据。
误区:扩容的时候不是重新计算hash值而是重新计算index值。
3 HashMap1.8为什么需要使用红黑树
课程内容:
1.为什么Java8HashMap需要引入红黑树
2.时间复杂度与空间复杂度之间区别
3.折半查找、二叉树、平衡二叉树区别
4.写一个史上最牛逼的二叉搜索树
为什么Java8HashMap需要引入红黑树
- Jdk7中HashMap通过链表+数组实现,当产生hashCode冲突的时候会使用同一个链表存放数据,如果链表的长度过长会导致查询效率非常慢,时间复杂度为o(n);在Java8中HashMap使用数组+链表+红黑树,在链表长度大于8的时候,将后面的数据以红黑树实现存放,从而加快检索速度;
- 修复多线程情况下扩容可能出现的死循环bug;
4 数据结构中时间查询复杂度对比
时间复杂度与空间复杂度基本概念
O(1):最低的时空复杂度,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)。
O(n):比如时间复杂度为O(n),就代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法,要找到一个数组里面最大的一个数,你要把n个变量都扫描一遍,操作次数为n,那么算法复杂度是O(n)。
O(log n):当数据增大n倍时,耗时增大log n倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。二分查找就是O(log n)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。
HashMap根据key查询,数据量的增多会导致根据key查询效率降低吗?
HashMap根据key获取hash值,计算index,分两种场景:
- 如果当前的key没有发生index冲突问题,不管怎么增加HashMap中的数据,都是可以一次性查询,时间复杂度o(1);
- 如果当前的key发生了index冲突的问题,形成一个链表,需要重头开始遍历查询,时间复杂度为o(n)。
5 常用集合时间复杂度原理对比
在Java集合中ArrayList、LinkeList、HashMap时间复杂度分别为多少?
ArrayList的底层是采用数组实现,根据下标查询时间复杂度为o(1)、根据元素值查询时间复杂度o(n);
LinkeList底层采用链表实现,根据下标查询时间复杂度为o(n)、根据元素值查询时间复杂度为o(n);
Jdk7HashMap底层数组+链表实现,根据key查询时间复杂度为o(1)或者o(n);
总结:直接通过数组下标查找元素的情况下o(1),不断遍历o(n)。
6 折半查找算法底层实现原理分析
思考:比如在链表中有1-100个节点,那么如何根据下标查询到某个节点?
举例查询链表中第63个节点
For(int i=0; i<63; i++){
//查询63次 效率非常低
}
二分查找
第一次查询 中间值50 63>50,从51-100之间查询
第二次查询 中间值75 63<75,从51-75之间查询
第三次查询 中间值63 命中
时间复杂度o(log n),查询效率非常高
存在问题:必须保证数据有序性
7 二叉搜索树基本实现原理与思想
二叉搜索树基本特征
- 节点的左子树只包含小于当前根节点的数。
- 节点的右子树只包含大于当前根节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
查找效率:20亿个数,2^32=21亿+,查询21次。
时间复杂度:2^x=n ==> x=log2^n=log n
二叉搜索树存在哪些问题:不平衡。使用第一次添加的节点作为根节点,如果后面添加节点值都大于根节点值的情况下,会形成链表。查找时间复杂度其实就是树的深度,也就是变为O(n)。
使用平衡二叉树(通过旋转的形式找平衡点)
8 完全手写二叉搜索树(添加、查询)功能
public class MayiktBinarySearchTree {
// 二叉搜索树特征:添加第一个节点作为平衡值,也就是最底层根节点
/**
* 元素节点值
*/
int data;
/**
* 左子树
*/
MayiktBinarySearchTree left;
/**
* 右子树
*/
MayiktBinarySearchTree right;
public MayiktBinarySearchTree(int data) {
this.data = data;
}
// 左小右大
public void insert(MayiktBinarySearchTree root, int data) {
if (data > root.data) {
// 如果当前根节点右边为空,说明第一次添加右子树
if (root.right == null) {
root.right = new MayiktBinarySearchTree(data);
} else {
// 如果右边有值,使用递归
insert(root.right, data);
}
} else {
// 如果当前添加的节点小于根节点,存放到左边
if (root.left == null) {
root.left = new MayiktBinarySearchTree(data);
} else {
insert(root.left, data);
}
}
}
public void inSearch(MayiktBinarySearchTree root) {
if (root != null) {
// 递归找到最小的数
inSearch(root.left);
System.out.println(root.data);
inSearch(root.right);
}
}
public static void main(String[] args) {
int data[] = {3, 6, 5, 2, 1, 4};
// 3为data中的平衡值
MayiktBinarySearchTree root = new MayiktBinarySearchTree(data[0]);
for (int i = 1; i < data.length; i++) {
root.insert(root, data[i]);
}
root.inSearch(root);
}
}
运行结果: