八股文-java集合相关

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)

红黑树的性质:

  1. 节点要么是红色,要么是黑色
  2. 根节点是黑色
  3. 叶子节点都是黑色的空节点
  4. 红黑树中红色节点的子节点都是黑色
  5. 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点

散列表

  • 散列表又称为哈希表/Hash表
  • 根据键(Key)直接访问在内存存储位置值(Value)的数据结构
  • 由数组演化而来的,利用了数组支持按照下标进行随机访问数据

散列冲突

  • 散列冲突又称哈希冲突,哈希碰撞
  • 指多个Key映射到同一个数组下标位置

解决散列冲突-链表法

  1. 数组的每个下标位置称之为或者
  2. 每个桶(槽)会对应一个链表
  3. hash冲突后的元素都放到相同槽位对应的链表中或红黑树中

9、HashMap的实现原理?(重点)

HashMap的数据结构:底层使用hash表数据结构,即数组+链表/红黑树

  1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况:
    1. 如果key相同,则覆盖原始值
    2. 如果key不同(出现冲突),则将当前的key-value放入链表或者红黑树中
  3. 获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值
  4. 当链表的长度大于8且数组长度大于64时,此时链表转为红黑树

10、HashMap的JDK1.7和JDK1.8有什么区别?

  • JDK1.8之前采用的是拉链法,就是 数组 + 链表
  • JDK1.8之后采用的是 数组 + 链表 + 红黑树,当链表长度大于8且数组长度大于64时,链表会转化为红黑树,以减少搜索时间

11、HashMap的put方法的具体流程(重点)

  1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
  2. 根据键值key计算hash值得到数组索引
  3. 判断table[i] == null,条件成立,直接新建节点添加
  4. 如果table[i] == null,不成立:
    1. 判断table[i]的首个元素是否和key一样,如果相同则直接覆盖value
    2. 判断table[i]是否为红黑树,如果是红黑树,则直接在树中插入键值对
    3. 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入
  5. 插入成功后,判断实际存在的键值对数量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 抽象类)。 不同点:

  1. 历史原因:Hashtable 是基于陈旧的 Dictionary 类的,HashMap 是 Java 1.2 引进的 Map 接口 的一个实现,HashMap把Hashtable 的contains方法去掉了,改成containsvalue 和containsKey。因为contains方法容易让人引起误解。
  2. 同步性:Hashtable 的方法是 Synchronize 的,线程安全;而 HashMap 是线程不安全的,不是同步的。所以只有一个线程的时候使用hashMap效率要高。
  3. 值:HashMap对象的key、value值均可为null。HahTable对象的key、value值均不可为null。
  4. 容量:HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。
  5. 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的区别?

  1. Set 接口实例存储的是无序的,不重复的数据。List 接口实例存储的是有序的,可以重复的元素。都可以存储null值,但是set不能重复所以最多只能有一个空元素。
  2. Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。
  3. List和数组类似,可以动态增长,根据实际存储的数据的长度自动增长List的长度。查找元素效率高,插入删除效率低,因为会引起其他元素位置改变 <实现类有ArrayList,LinkedList,Vector> 。


18 、Collection和Collections的区别?

Collection是单列集合的顶层接口,Map是双列集合的顶层接口

Collections是一个集合的工具类,提供了排序、查找等操作集合的一些常用方法。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

开心懒羊羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值