集合面试题整理

本文详细分析了各种数据结构如数组、链表(单向和双向)、二叉搜索树和红黑树的操作时间复杂度,并深入探讨了HashMap的实现细节,包括插入、删除和扩容机制。此外,还讨论了ArrayList和LinkedList在不同场景下的性能差异以及HashMap在JDK1.7和1.8中的变化。
摘要由CSDN通过智能技术生成

集合

数据结构

算法复杂度分析

为什么要对算法的复杂度进行分析呢?

  • 教你写出性能更好的代码
  • 知道代码的好坏

时间复杂度分析

评估代码执行耗时

大O表示法:表示代码执行时间随数据规模增长的变化趋势 O(n)
常见表示形式
常对指幂阶
空间复杂度

空间复杂度全称渐进空间复杂度,表示算法占用的额外存储空间与数据规模之间的增长关系

在这里插入图片描述

数组

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

在这里插入图片描述
获取其他元素地址值

寻址公式 a[i] = 数组首地址 + 元素下标 * 元素类型大小

在这里插入图片描述
索引从 0 或 从 1 的区别

计算机底层内存空间的分配是从 0 开始的,如果 从 1 开始要对每个下标减1 ,多了一次运算

在这里插入图片描述

操作数组的时间复杂度

查找

  • 索引查找通过寻址公式查找 O(1)
  • 查找索引内的元素 遍历数组 O(n)
  • 排序后查找 O(log n)
    在这里插入图片描述
    在这里插入图片描述

插入 删除

数组是一段连续的内存空间,插入 和删除 会影响后面的内存空间,需要挪动后面的元素,效率慢

在这里插入图片描述

链表
单向链表
  • 链表中的每一个元素称之为 结点(Node)
  • 物理存储单元上 ,非连续 非顺序的存储结构
  • 每个结点包含两部分:一个是存储数据元素的数据域,另一个是存储下一个节点的指针域

在这里插入图片描述
在这里插入图片描述

单向链表时间复杂度分析

查询在这里插入图片描述
插入 删除在这里插入图片描述

双向链表
  • 每个结点不止有一个后继指针 next 指向后面的结点
  • 有一个前驱指针 prev 指向前面的结点
    在这里插入图片描述
双向链表时间复杂度分析

在这里插入图片描述

二叉搜索树

二叉搜索树(Binary Search Tree,BST)又名二叉查找树,有序二叉树或者排序二叉树
在树中的任意一个节点,其左子树的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值

二叉搜索树 时间复杂度分析

在这里插入图片描述
极端情况在这里插入图片描述

红黑树

红黑树(Red Black Tree) 自平衡二叉搜索树

性质

在添加或删除节点的时候,如果不符合这些性质会发生旋转,以达到所有的性质

  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)

散列表

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

在这里插入图片描述
将键(Key)映射为数组下标的函数叫做散列函数。 hashvalue = hash(key)
基本要求

  • 散列函数计算得到的散列值必须是大于等于0的正整数,因为hashValue需要作为数组的下标。
  • 如果key1==key2,那么经过hash后得到的哈希值也必相同即:hash(key1) == hash(key2)
  • 如果key1 != key2,那么经过hash后得到的哈希值也必不相同即:hash(key1) != hash(key2)(散列冲突)
散列冲突

散列函数对于不同的key计算得到的散列值都不同几乎不可能,指将多个key映射到同一个数组下标位置

在这里插入图片描述

链表法解决散列冲突

在散列表中,数组的每个下标位置我们可以称之为桶(bucket)或者槽(slot),每个桶(槽)会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。

在这里插入图片描述

链表法时间复杂度

插入
插入操作,通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,插入的时间复杂度是 O(1)
查找 删除
当查找、删除一个元素时,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除

  • 平均情况下基于链表法解决冲突时查询的时间复杂度是O(1)
  • 散列表可能会退化为链表,查询的时间复杂度就从 O(1) 退化为 O(n)
  • 将链表法中的链表改造为其他高效的动态数据结构,比如红黑树,查询的时间复杂度是 O(log n)

Collection 单列集合

ArrayList
ArrayList源码分析

怎么分析源码?
成员变量 构造函数 关键方法

  • 成员变量
    在这里插入图片描述

  • 构造函数
    在这里插入图片描述

添加和扩容操作
List<Integer> list = new ArrayList<Integer>();
list.add(1);

第一次添加数据

执行add方法时会设置动态数组初始长度 10

在这里插入图片描述

第2至10次添加数据
10 - 10 不大于 0 ,则不扩容
在这里插入图片描述

第 11次添加数据
11 -10 > 0 扩容 久数组长度 + 老数组长度/2
在这里插入图片描述

  • 确保数组已使用长度(size)加1之后足够存下下一个数据​
  • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
  • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。​
  • 返回添加成功布尔值。
ArrayList list = new ArrayList(10)中的list扩容几次

在这里插入图片描述

数组和List之间的转换

在这里插入图片描述在这里插入图片描述

LinkedList
ArrayList和LinkedList的区别
  • 底层数据结构

    • ArrayList 是动态数据的数据结构实现
    • LinkedList 是双向链表的数据结构实现
  • 操作数据效率

    • ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】, LinkedList不支持下标查询
    • 查找(未知索引): ArrayList需要遍历,链表也需要链表,时间复杂度都是O(n)
    • 新增和删除
      • ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
      • LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
  • 内存占用空间

    • ArrayList底层是数组,内存连续,节省空间
    • LinkedList是双向链表需要存储数据,和两个指针,更占用内存
  • 线程安全

    • ArrayList和LinkedList都不是线程安全的
    • 如果需要保证线程安全,有两种方案:
      • 在方法内使用,局部变量则是线程安全的
      • 使用线程安全的ArrayList和LinkedList
      • 在这里插入图片描述

Map 双列集合

HashMap

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

  1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。
    a. 如果key相同,则覆盖原始值;
    b. 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
    在这里插入图片描述
HashMap的jdk1.7和jdk1.8有什么区别
  • JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
  • jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表
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] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
    3. 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value
  5. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。
    在这里插入图片描述
源码分析在这里插入图片描述

在这里插入图片描述

讲一讲HashMap的扩容机制

在这里插入图片描述

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍;
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    • 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
    • 如果是红黑树,走红黑树的添加
    • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
HashMap的寻址算法

在这里插入图片描述

为何HashMap的数组长度一定是2的次幂
  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值