前言:
上一篇文章我们初步介绍了 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
- 通过 isPositionIndex(index) 判断返回的布尔值,返回 true 没有影响,返回 false 直接抛出异常
-
判断是否 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 没有容量的限制
- ArrayList 插入会先尝试进行扩容
(4)应用场景
- ArrayList 因为支持随机访问,所以适合需要频繁访问的场景
- LinkedList 在任意位置插入和删除过程中,只需要对目标位置前后元素进行更改,所以 适合任意位置插入和删除的场景
文章至此就结束啦,如果看官朋友觉得还不错,博主求 点赞、评论、收藏 三连,十分感谢