1、为什么数组索引从0开始呢?假如从1开始不行吗?
- 再根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组首地址 + 索引 * 存储数据的类型大小
- 如果数组的索引从1开始,寻址公式中就需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高。
索引从0开始:a[i] = baseAddress + i * dataTypeSize
索引从1开始:a[i] = baseAddress + (i - 1) * dataTypeSize
2、关于数组的时间复杂度
查找的时间复杂度:
- 随机(通过下标)查询的时间复杂度是O(1)
- 查找元素(未知下标)的时间复杂度是O(n)
- 查找元素(未知下标但排序),通过二分查找的时间复杂度是O(logn)
插入和删除时间复杂度:
- 插入和删除的时候,为了保证数组的内存连续性,需要挪动数组元素,平均时间复杂度是O(n)
3、ArrayList底层的实现原理是什么?
- ArrayList底层是用动态的数组实现的
- ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
- ArrayList在添加数据的时候:
- 确保数组已使用的长度(size)加 1 之后足够存下下一个数据
- 计算数组的容量,如果当前数组已使用长度 +1后大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
- 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
- 返回添加成功布尔值
4、ArrayList list = new ArrayList(10)中的list扩容了几次?
该语句只是声明和实例了一个ArrayList,指定了容量为10,并未扩容。
5、如何实现数组和List之间的转换?
- 数组转List:使用java.util.Arrays工具类的asList方法
- List转数组:使用List的toArray方法,无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
1)用Arrays.asList转List之后,如果修改了数组的内容,list受影响吗?
2)List用toArray转数组之后,如果修改了List的内容,数组受影响吗?
- Arrays.asList转换list之后,如果修改了数组的内容,list会受到影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址。
- list用了toArray转数组之后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层它是进行了数组的拷贝,跟原来的元素就没啥关系了。所以即使list修改了以后,数组也不受影响。
6、单向链表和双向链表的区别是什么?
- 单向链表只有一个方向,结点只有一个后继指针next
- 双向链表它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点
7、链表操作数据的时间复杂度是多少?
8、ArrayList和LinkedList的区别是什么?(重点)
1)底层数据结构:
- ArrayList是动态数组的数据结构实现
- LinkedList是双向链表的数据结构实现
2)操作数据效率:
- ArrayList按照下标查询的时间复杂度是O(1)【内存是连续的,根据寻址公式】,LinkedList不支持下标查询
- 查找(未知索引):ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)
- 插入和删除:
- ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
- LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
3)内存空间占用:
- ArrayList底层是数组,内存连续,节省内存
- LinkedList是双向链表需要存储数据,还有存储两个指针,更占用内存。
4)线程安全:
- ArrayList和LinkedList都不是线程安全的
- 如果需要保证线程安全,有两种方案:
- 在方法内使用,局部变量则是线程安全的
- 使用线程安全的ArrayList和LinkedList:
-
List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>()); List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
二叉树
- 每个节点最多有两个“叉”,分别是左子节点和右子节点。
- 不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点
- 二叉树每个节点的左子树和右子树也分别满足二叉树的定义
二叉搜索树
- 二叉搜索树又名二叉查找树,有序二叉树
- 在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值而右子树节点的值都大于这个节点的值
- 没有键值相等的节点
- 通常情况下二叉树搜索的时间复杂度为O(logn)
红黑树
- 红黑树是一种自平衡的二叉搜索树
- 所有的红黑规则都是希望红黑树能够保证平衡
- 红黑树的时间复杂度:查找、添加、删除都是O(logn)
红黑树的性质:
- 节点要么是红色,要么是黑色
- 根节点是黑色
- 叶子节点都是黑色的空节点
- 红黑树中红色节点的子节点都是黑色
- 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点
散列表
- 散列表又称为哈希表/Hash表
- 根据键(Key)直接访问在内存存储位置值(Value)的数据结构
- 由数组演化而来的,利用了数组支持按照下标进行随机访问数据
散列冲突
- 散列冲突又称哈希冲突,哈希碰撞
- 指多个Key映射到同一个数组下标位置
解决散列冲突-链表法
- 数组的每个下标位置称之为桶或者槽
- 每个桶(槽)会对应一个链表
- hash冲突后的元素都放到相同槽位对应的链表中或红黑树中
9、HashMap的实现原理?(重点)
HashMap的数据结构:底层使用hash表数据结构,即数组+链表/红黑树
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况:
- 如果key相同,则覆盖原始值
- 如果key不同(出现冲突),则将当前的key-value放入链表或者红黑树中
- 获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值
- 当链表的长度大于8且数组长度大于64时,此时链表转为红黑树
10、HashMap的JDK1.7和JDK1.8有什么区别?
- JDK1.8之前采用的是拉链法,就是 数组 + 链表
- JDK1.8之后采用的是 数组 + 链表 + 红黑树,当链表长度大于8且数组长度大于64时,链表会转化为红黑树,以减少搜索时间
11、HashMap的put方法的具体流程(重点)
- 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引
- 判断table[i] == null,条件成立,直接新建节点添加
- 如果table[i] == null,不成立:
- 判断table[i]的首个元素是否和key一样,如果相同则直接覆盖value
- 判断table[i]是否为红黑树,如果是红黑树,则直接在树中插入键值对
- 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入
- 插入成功后,判断实际存在的键值对数量size是否超过了最大容量 threshold(数组长度*0.75),如果超过,进行扩容
12、讲一讲HashMap的扩容机制?(重点)
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度 * 0.75)
- 每次扩容的时候,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
- 如果是红黑树,走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap) 是否为0,该元素的位置要么停留在原始位置,要么移动到(原始位置 + 增加的数组大小) 这个位置上
13、hashMap的寻址算法
- 计算对象的hashCode()
- 在进行调用hash()方法进行二次哈希,hashCode值右移16位再异或运算,让哈希分步更为均匀
- 最后(capacity - 1) & hash 得到索引
14、为何HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高:如果是2的n次幂可以使用位于运算代替取模
- 扩容时重新计算索引效率更高:hash & oldCap == 0 的元素留在原来位置,否则新位置 = 旧位置 + oldCap
15、HashMap 和 Hashtable 的区别?
相同点: 都是实现来Map接口(hashTable还实现了Dictionary 抽象类)。 不同点:
- 历史原因:Hashtable 是基于陈旧的 Dictionary 类的,HashMap 是 Java 1.2 引进的 Map 接口 的一个实现,HashMap把Hashtable 的contains方法去掉了,改成containsvalue 和containsKey。因为contains方法容易让人引起误解。
- 同步性:Hashtable 的方法是 Synchronize 的,线程安全;而 HashMap 是线程不安全的,不是同步的。所以只有一个线程的时候使用hashMap效率要高。
- 值:HashMap对象的key、value值均可为null。HahTable对象的key、value值均不可为null。
- 容量:HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。
- HashMap扩容时是当前容量翻倍即:capacity * 2,Hashtable扩容时是容量翻倍+1 即:capacity * 2+1。
16、ConcurrentHashMap和Hashtable的区别?
JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类 似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
17、Set和List的区别?
- Set 接口实例存储的是无序的,不重复的数据。List 接口实例存储的是有序的,可以重复的元素。都可以存储null值,但是set不能重复所以最多只能有一个空元素。
- Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。
- List和数组类似,可以动态增长,根据实际存储的数据的长度自动增长List的长度。查找元素效率高,插入删除效率低,因为会引起其他元素位置改变 <实现类有ArrayList,LinkedList,Vector> 。
18 、Collection和Collections的区别?
Collection是单列集合的顶层接口,Map是双列集合的顶层接口
Collections是一个集合的工具类,提供了排序、查找等操作集合的一些常用方法。