细说 单链表、双向链表 、LinkedList类(附 add 源码解读)和 ArrayList 和 LinkedList 的区别 —— 数据结构

请添加图片描述


前言:

​上一篇文章我们初步介绍了 List 以及 ArrayList,我们不难发现使用 ArrayList 过程中,对元素进行操作可能会涉及到大量数据的改变,所以LinkedList “临危受命”,本篇文章将从链表的相关概念入手,对单向、双线链表进行模拟实现,再回到 LinkedList 集合内当中进行简单分析,最后结合上一篇文章,阐述四点 LinkedList 和 ArrayLIst 区别。如果有需要快速了解两者区别的朋友可以直接跳转 ArrayList 和 LinkedList的区别

  • 希望能对各位看官朋友有所裨益,如有问题欢迎批评指正

重点:



1 单链表


1.1 链表和单链表

1)链表

(1)是什么

  • 链表是线性表的一种表现形式
  • 链表在逻辑上是连续的,在物理上 不一定连续
  • 节点资源是从堆中申请的,多次申请可能连续也可能不连续

(2)分类

通过下面的三种特征可以自由组合成 8 种链表结构

  • 带头 / 不带头

    • 带头双向链表常出现与对于 链表结构有修改操作 的题目中,可以省去很多特殊情况的判断

    • 常见是创建一个虚拟头结点(哨兵节点)dummy,头插至链表中

      删除有序链表中重复的元素-II 有兴趣可以尝试一下删除链表元素经典题目

请添加图片描述


  • 单向 / 双向
    • 笔试中多用单向链表(双向链表容易很多怎么舍得考呢(doge)),后面的部分也会介绍关于双向链表的内容

请添加图片描述


  • 循环 / 非循环
    • 虽然非循环是主流,但循环链表也有不少的题型,要有所注意

请添加图片描述


2)单链表
  • 这里我们指的是 单向不带头非循环 链表,也是面试笔试的高频问题之一

  • 实际情况中,单链表一般是作为其他数据结构(哈希桶、图…)的子结构

    有兴趣可以尝试一下 牛客网面试必刷 TOP101 的链表题目,都很具有代表性,博主后续也会跟进个人关于单链表的刷题记录,可以点个关注不迷路哦


1.4 MyLinkedList 模拟实现

不带头单向非循环链表

  • Node 类

    • 成员变量

      • int val
      • Node next
    • 构造方法

      • Node (int val) { this.val = val}
  • Node head

请添加图片描述

1)addFirst

头插

请添加图片描述

2)addLast

尾插

  • 先判断是否是第一个节点
  • 遍历到尾部

请添加图片描述

3)addIndex

在 index 位置插入元素

  • 先判读下标是否合法,判断是否是头插尾插
  • 找到下标元素的前一个元素进行插入

请添加图片描述

4)contains

查找元素
请添加图片描述

5)remove

删除第一个出现指定元素

  • 先判空、判断首元素是否删除
  • 遍历后续元素找到删除元素的前一个元素,进行删除

请添加图片描述

6)removeAllKey

删除所有出现的指定元素

  • 从第二个元素开始寻找删除元素
  • 遍历链表,未找到元素 pre cur 同时向前;找到元素,跳过删除元素
    • 可能有多个删除元素聚合在一起,使用 while 跳过,跳出 while 再向前一步,pre.next 指向该节点
  • 最后判断头结点是否是删除节点

请添加图片描述

7)size

链表长度

请添加图片描述

8)display

打印链表所有元素

请添加图片描述

9)clear

清空链表

  • 删除每个节点

请添加图片描述





2 双向链表


2.1 是什么

  • 双向链表中保存了当前节点的前驱和后继,相较于单链表,它能够找到上一个节点
  • Java 集合框架库中 LinkedList 类底层实现是 无头双向循环链表

2.2 MyDoubleLinkedList模拟实现

我们先对双线链表中的常用操作进行模拟实现,有利于我们后续对源码的阅读分析

  • Node 类

    • 成员变量
      • int val
      • Node next
      • Node prev
    • 构造方法
      • Node(int val) { this.val = val}
  • Node head

  • Node tail

请添加图片描述


1) addFirst

头插法

  • 第一次 插入:head, tail
  • 后续修改值:node.next; head.prev;head
    代码

请添加图片描述


2) display

打印单链表

  • 遍历打印(记得 换行 )

请添加图片描述


3)addLast尾插法

尾插法

  • 第一次:head;tail
  • 后续修改值:tail.next;node.prev;tail

请添加图片描述


4)contains

查找关键字 key

  • 遍历 判断

请添加图片描述


5)size

单链表长度

  • 遍历 累加

请添加图片描述


6)addIndex

任意位置插入

  • Index 合法性(单独写 private chkIndex()返回 throw )
  • 判断 Index 是 否等于 0(头插)或size(尾插)
  • 中间插入改变 四个值,cur.prev.next、node.prev、node.next、cur.prev
    • Index 位置 cur 单独写 private searchIndex

请添加图片描述


7)remove

删除关键字为key 的节点

  • 遍历找到 cur
  • 判断是否为 head/tail
  • cur.prev.next = cur.next;cur.next.prev = cur.prev

请添加图片描述


8)clear

删除链表

  • head一个一个删除,最后 this.tail = null

请添加图片描述


2.4 遍历操作

ListIterator 遍历

  • 顺序遍历

    • 使用 listIterator() 构造 ListIterator 对象
    • it.next() 循环得到每个元素,条件是 对象.hasNext()
  • 逆序遍历

    • 使用 listIterator(链表长度) 构造 ListIterator 对象
    • it.previous() 循环得到每个元素,条件是 对象.hasPrevious()

请添加图片描述




3 LinkedList 类


3.1 单链表的继承关系

请添加图片描述

3.2 成员变量

  • transient Node first;
    • 头结点
  • transient Node last;
    • 尾节点
  • transient int size = 0;
    • 链表长度,就地初始化为0
  • tip:Node 类

请添加图片描述


3.3 构造方法

  • LinkedList ()
    • 构造一个空链表

请添加图片描述

  • LinkedList (Collection<? extends E> c)
    • 使用 addAll(),按迭代器的返回元素顺序放入指定集合 c

请添加图片描述


3.3 成员方法

1)add(E e) 源码
  • 传入 E e, 返回值为 boolean

  • 调用尾插方法 void linkLast(e)

    • 和上述 MyDoubleList 的尾插方式类似,先拿到指向 last 的 l 和 构造好的 newNode
    • last 指向 newNode,再判断 l 是否为空,进行尾插,size++

请添加图片描述
请添加图片描述


2)add(int index, E e) 源码

指定位置插入元素时间复杂度为 O(N),使用 node(index) 查找 index 位置元素最坏要查找 N / 2 次,时间复杂度为 O(N)

请添加图片描述

  • 调用 checkPositionIndex(index) 方法判断 index 的 合法性

    • 通过 isPositionIndex(index) 判断返回的布尔值,返回 true 没有影响,返回 false 直接抛出异常
      • isPositionIndex 返回 index >= 0 && index <= size

请添加图片描述

  • 判断是否 size == index,直接进行 linkLast 尾插,否则调用 linkBefore(element, node(index)) 方法进行插入

    • linkLast 尾插 add(E e) 中已有所述

    • linkBefore 参数为( E e, Node succ

      node 方法中,先确定 index 是在前半部分还是后半部分,从头或者从尾遍历查找 index 位置的元素进行返回,加快查找速度

      • linkedBefore 方法中,先拿到 succ 前一个节点 pred 和 构造好的新节点 newNode
      • 将 succ.prev 指向新节点,判断 pred 是否为空 (succ 可能为第一个元素),进行插入,size++
        • 为空,将首元素指向 newNode
        • 非空,pre.next = newNode

请添加图片描述
请添加图片描述


3)其他常用方法
方法名具体功用
boolean addAll (Collection <? extends E> c)尾插 集合 c 中的元素
E remove (int index)删除 下标为 index 位置的元素
boolean remove (Object o)删除 第一个出现的元素 o
void clear ()清空 所有元素
boolean contains (Object o)查询 是否包含元素 o
E get (int index)查询 index 位置的元素
int indexOf (Object o)查询 第一个出现的元素 o 的下标
int lastIndexOf (Object o)查询 最后一个出现的元素 o 的下标
E set (int index, E element)设置 index 位置元素为 o
List subList (int fromIndex, int toIndex)截取 从 from 到 to 的元素(左闭右开)

和 ArrayList 常用方法类似,所以如果刷题过程中有构造 List 的需求,可以使用 ArrayList 也可也使用 LinkedList




4 ArrayList 和 LinkedList的区别


紧承上一篇关于 顺序表 的内容,这篇文章从单链表讲起,同时剖析了 LinkedList 集合类。在回顾过程中,我们最后来总结一下 链表 和 顺序表的区别到底几何

注意我们这里说指的是具体的集合类,不是指顺序表和链表

(1)空间存储

  • ArrayList 是连续的储存空间
  • LinkedList 不一定 是连续的储存空间

(2)随机访问

  • ArrayList 实现了 RandomAccess 接口,支持随机访问
  • LinkedList 没有实现 RandomAccess 接口,不支持随机访问
    • 具体继承关系可以查看 3.1 部分

(3)插入操作

  • 时间复杂度
    • ArrayList 头插 和 index 位置插入时间复杂度都为O(N)
    • LinkedList 头插 时间复杂度为 O(1),index 位置插入元素时间复杂度为O(N)
      • 因为LinkedList 不支持随机访问,上述我们分析过 add(int index, E e) 源码, index 位置插入需要先找到index 位置元素再进行插入,所以时间复杂度为O(N)
  • 扩容
    • ArrayList 插入会先尝试进行扩容
      • 先判断扩容后的合法性,再进行相应的扩容
    • LinkedList 没有容量的限制

(4)应用场景

  • ArrayList 因为支持随机访问,所以适合需要频繁访问的场景
  • LinkedList 在任意位置插入和删除过程中,只需要对目标位置前后元素进行更改,所以 适合任意位置插入和删除的场景


文章至此就结束啦,如果看官朋友觉得还不错,博主求 点赞、评论、收藏 三连,十分感谢

  • 23
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值