常见集合篇

常见集合篇

Java集合框架体系

image-20241125211533388

image-20241125211618895

算法复杂度分析

image-20241125212103155

image-20241125212208981

image-20241125212733844

面试总结

常对幂指阶
执行时间/存储空间 与 数据规模 之间的增长关系

image-20241125212851945

List相关面试题

image-20241125213004624

数组

image-20241125213148084

image-20241126121409609

image-20241126121549418

image-20241126121739908

image-20241126121817438

面试总结

数组:用连续存储空间存储相同数据类型数据的线性数据结构

数组下标为什么从0开始?
	寻址公式:baseAddress + i*dataTypeSize
	计算下标的内存地址效率更高,相比较而言,CPU不会增加一个减法指令

ArrayList源码分析

image-20241126122049246

image-20241126122221061

image-20241126122504891

image-20241126123010585

image-20241126123204817

image-20241126132857181

面试总结

ArrayList扩容原理

ArrayList底层是用动态数组实现的
ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都要拷贝数组

ArrayList添加数据的时候:
1)确保数组已使用长度(size)加1之后足够存下下一个数据(即数据存入之前先判断有没有多余位置)
2)计算数组的容量,若当前已使用长度(size)加1之后大于当前的数组长度,则调用grow方法扩容(即填满最后一个位置不扩容,超过才扩容)
3)确保新元素有地方存储之后,才将新元素添加到指定位置
4)返回添加成功布尔值



Q2:ArrayList list=new ArrayList(10)中的list扩容几次?
该语句只是声明和实例了一个ArrayList,指定容量为10,未扩容

image-20241126133231913

如何实现数组和List之间的转换

image-20241126133755921

image-20241126134127325

面试总结

1) 数组转list,可以使用jdk自带的一个工具类Arrays,里面有一个asList方法可以转换为数组. List<String> list = Arrays.asList(strs);

2) List 转数组,可以直接调用list中的toArray方法,需要给一个参数,指定数组的类型,需要指定数组的长度。
String[] array = list.toArray(new String[list.size()]);


Q3:用Arrays.asList转List后,如果修改了数组内容,list受影响吗?
Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址。

Q4:List用toArray转数组后,如果修改了List内容,数组受影响吗?
list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。

LinkedList结构

单向链表

image-20241126134425461

image-20241126134531475

双向链表

image-20241126134750346

image-20241126134844991

image-20241126134939614

ArrayList和LinkedList的区别

image-20241126135140057

image-20241126135252594

面试总结

ArrayList和Linkedlist的区别?
相同的地方:两者都是线程不安全的

image-20241222213623723

HashMap相关面试题

image-20241126135449873

二叉树

image-20241126135617824

image-20241126135648576

二叉搜索树

image-20241126135950489

image-20241126140126989

image-20241126140204802

image-20241126140258550

红黑树

image-20241126140407312

image-20241126140551498

image-20241126140723846

面试总结

红黑树
概述 一种自平衡的二叉搜索树(BST)
性质 口诀:左根右,根叶黑,不红红,黑路同
性质1:节点要么是红色的,要么是黑色的
性质2:根节点是黑色的
性质3:叶子节点都是黑色的空节点
性质4:红黑树中红色节点的子节点都是黑色
性质5:从任一节点到叶子节点的所有路径都包含相同数目的黑色节点
在添加或删除节点的时候,如果不符合这些性质会发生旋转,以达到所有的性质,保证红黑树的平衡
复杂度 查找: 红黑树也是一棵BST(二叉搜索树)树,查找操作的时间复杂度为:O(log n)
添加: 添加先要从根节点开始找到元素添加的位置,时间复杂度O(log n);添加完成后涉及到复杂度为 O(1)的旋转调整操作故整体复杂度为:O(log n)
删除: 首先从根节点开始找到被删除元素的位置,时间复杂度O(log n);删除完成后涉及到复杂度为O(1)的旋转调整操作故整体复杂度为:O(log n)

散列表

image-20241126140932484

image-20241126141047458

image-20241126141142894

image-20241126141301872

image-20241126141412508

image-20241126141450381

image-20241126141534594

image-20241126141703162

image-20241126141804337

面试总结

散列表能够解决Hash冲突。
散列表:根据键(Key)直接访问在内存存储位置值(Value)的数据结构
解决Hash冲突方法有:开放定址法、再哈希法、链地址法。 HashMap中采用的是 链地址法 。
开放定址法 当发生哈希冲突时,也就是要插入的元素经过哈希函数计算得到的存储位置已经被其他元素占用时,开放定址法会去寻找下一个空闲的存储位置来存放该元素。有线性探测法、二次探测法等。
举例:元素 A 的哈希位置是 3,但是位置 3 已经被其他元素占用,所以通过探测序列将 A 存放在位置 5。如果现在将位置 3 的元素删除,当查找 A 时,按照探测序列先检查位置 3,发现为空,就会错误地认为 A 不在哈希表中。
再散列法 它使用两个哈希函数和。当发生冲突时,先用第二个哈希函数计算一个偏移量,然后通过这个偏移量来寻找下一个可能的空闲位置。
举例:当要插入关键字为的元素时,首先计算得到初始位置,如果该位置被占用,则计算下一个位置为 image-20241210151850843。通过不断地改变i值,就可以逐步寻找空闲位置。
链地址法 将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

HashMap的实现原理

image-20241126142408744

image-20241126142504069

面试总结

Q1:HashMap的实现原理?
底层使用Hash表数据结构,即数组+(链表|红黑树)
添加数据时,计算Key的值确定元素在数组中的下标
	Key相同则替换
	   不同则存入链表或红黑树中
获取数据通过Key的Hash计算数组下标获取元素



Q2:HashMap的jdk1.71.8有什么区别?
1)jdk1.7
JDK7中的HashMap,是基于数组+链表来实现的。
	它的底层维护一个Entry数组。它会根据计算的hashCode将对应的KV键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的以有元素的后面, 此时便形成了一个链表式的存储结构。
	
2)jak1.8
JDK8中的HashMap,是基于数组+链表+红黑树来实现的。
	它的底层维护一个Node数组。当链表长度大于阈值(默认为 8)外加数组长度大于64时时,将链表转化为红黑树,以减少搜索时间。这么做主要是在查询 的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。

补充:Entry是一个内部类,用于存储键值对(key - value)。它包含了hash(哈希值)、key、value和next(指向下一个Entry的引用)这几个重要的成员变量。Node和Entry的功能类似.

HashMap的put方法的具体流程

image-20241126170549271

image-20241126170705651

image-20241126171116650

image-20241126171537574

image-20241126171940696

面试总结

Q1:put方法的具体流程
1)判断键值对数组table是否为空或为null,若为空第一次扩容(resize:默认情况下,如果没有指定初始容量,则扩容的大小为16)
2)通过hash算法根据键值key计算hash值得到数组索引i
3)判断索引处有没有存在元素,没有就直接插入
4)如果存在元素且该key已存在,则直接覆盖其value;
5)如果索引处存在元素且该key不存在,则遍历插入,有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
6)链表的数量大于阈值8且数组长度大于64,就要转换成红黑树的结构
7)插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容。

理解小结:若是第一次插入,可能要为数组扩容;对key进行Hash映射得到哈希值以及在数组中的下标;判断索引处是否存在元素,不存在直接插入,若存在,判断key是否存在,存在则直接覆盖,不存在则按照(链表形式或红黑树形式)插入其中;若链表长度大于8且数组长度大于64,则转换成红黑树结构。插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容。



Q2:get方法的具体流程
HashMap的get方法的具体流程涉及查找哈希表以定位键值对。具体步骤如下:
(综上所述,HashMap的get方法是通过计算哈希值和比较键值来快速定位并返回对应value的过程。)

HashMap的扩容机制

image-20241126172744808

image-20241126173256360

面试总结

在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(当前数组长度 * 0.75)
每次扩容的时候,都是扩容之前容量的2倍;
扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
没有hash冲突节点,则直接使用 e.hash & (newCap - 1) (值等于e.hash%newCap)计算新数组的索引位置
如果是红黑树,走红黑树的添加
如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,若为0该元素的位置停留在原始位置,否则移动到原始位置+增加的数组大小这个位置上
Q3:为什么要判断e.hash & oldCap==0?
举例:原数组容量为16,存有3个元素,他们hash值分别为52137,他们都存在下标为5的链表里,扩容后数组容量为32,他们对应的新数组下标分别为5215,可用发现,hash值为537的都满足e.hash & oldCap==0,因此他们下标不变;而hash21的不满足e.hash & oldCap==0,此时他的下标变为原下标+oldCap。这样可以省去重新计算hash值的时间

HashMap的寻址算法

image-20241126174300997

image-20241126174654987

面试总结

Q1:HashMap的寻址算法
	1、计算对象的hashcode()
	2、调用Hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更为均匀
	3、最后(capacity-1) & hash得到索引
	
(计算hashCode是为了将对象转换为数字标识,便于在数据结构中初步定位,不然难以快速确定对象存储位置。
二次哈希是为了混合hashCode高低位信息,减少哈希冲突,让对象更均匀地分布在存储位置。
计算索引是为了确定键值对在数组中的存储位置,使用位运算来计算索引更快,且能准确存储和检索数据。)


Q2:HashMap的数组长度一定是2的次幂?
1、计算索引时效率更高:如果是2的n次幂可以使用位与运算代替取模
2、扩容时重新计算索引效率更高:Hash & oldCap == 0的元素留在原来位置,否则新位置=旧位置+oldCap

HashMap在1.7情况下多线程死循环问题

image-20241126175130960

image-20241126175440782

面试总结

在 jdk1.7 的 hashmap 中,在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环。
比如说,现在有两个线程
线程一:读取到当前的 hashmap 数据,数据中一个链表,在准备扩容时,线程二介入
线程二:也读取 hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是 AB,扩容后的顺序是 BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环的问题。
线程一先将 A 移入新的链表,再将 B 插入到链头,由于另外一个线程的原因,B 的 next 指向了 A,所以 B->A->B, 形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头 (而是保持与扩容前一样的顺序),尾插法,就避免了 jdk7 中死循环的问题。
posted @   墨羽寻觅  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
点击右上角即可分享
微信分享提示