文章目录
取自力扣图书区
先学习下预备知识 然后再开始刷题咯~
链表是面试非常高频的问题
尤其是那道经典的反转链表 虽然我目前看的面经不是很多 但是这四个字就没少看见 所以——这部分要好好刷一下!
与数组相似,链表也是一种线性
数据结构
这里有一个例子:
正如你所看到的,链表中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。
链表有两种类型:单链表和双链表。上面给出的例子是一个单链表,这里有一个双链表的例子:
我们将在接下来的章节中介绍更多内容。完成后,你将:
- 了解单链表和双链表的结构
- 在单链表和双链表中实现遍历、插入和删除
- 分析在单链表或双链表中的各种操作的复杂度;
- 在链表中使用双指针技巧(快指针慢指针技巧)
- 解决一些经典问题,例如反转链表
- 分析你设计的算法的复杂度
- 积累设计和调试的经验。
1.单链表
正如我们在概览中提到的那样,链表是一种线性数据结构,它通过引用字段将所有分离的元素链接在一起。有两种常用的链表:单链表
和双链表
。
本章节中,我们将从单链表开始,并帮助您:
- 了解单链表的
结构
; - 在单链表中执行
遍历
,插入
和删除
操作; - 分析单链表中不同操作的
时间复杂度
。
【1】单链表简介
单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段
。通过这种方式,单链表将所有结点按顺序组织起来。
面是一个单链表的例子:
蓝色箭头显示单个链接列表中的结点是如何组合在一起的。
结点结构
以下是单链表中结点的典型定义:
public class SinglyListNode{
int val;
SinglyListNode next;
SinglyListNode(int x){val = x;}
}
在大多数情况下,我们将使用头结点(第一个结点)来表示整个列表。
单链表的操作
与数组不同,我们无法在常量时间内访问单链表中的随机元素。 如果我们想要获得第 i 个元素,我们必须从头结点逐个遍历。我们按索引
来访问元素
平均要花费 O(N)
时间,其中 N 是链表的长度。
例如,在上面的示例中,头结点是 23。访问第 3 个结点的唯一方法是使用头结点中的“next”字段到达第 2 个结点(结点 6) 然后使用结点 6 的“next”字段,我们能够访问第 3 个结点。
你可能想知道为什么链表很有用,尽管它在通过索引访问数据时(与数组相比)具有如此糟糕的性能。
在接下来我们将介绍插入和删除操作,你将了解到链表的好处。
之后,我们将为你提供练习设计自己的单链表。
【2】单链表的添加操作
如果我们想在给定的结点 prev
之后添加新值,我们应该:
-
使用给定值初始化新结点
cur
;
-
将
cur
的next
字段链接到prev
的下一个结点next
;
-
将
prev
中的next
字段链接到cur
。
一句话——让新来的节点有所指向(
next
),从头遍历找到前一个节点(prev
)指向新来的节点
与数组不同,我们不需要将所有元素移动到插入元素之后。(和前面的数组专题做一个对比~)因此,您可以在 O(1)
时间复杂度中将新结点插入到链表中,这非常高效。
示例
让我们在第二个结点 6 之后插入一个新的值 9 。
我们将首先初始化一个值为 9 的新结点。然后将结点 9 链接到结点 15 。最后,将结点 6 链接到结点 9 。
插入之后,我们的链表将如下所示:
在开头添加结点
众所周知,我们使用头结点来代表整个列表。
因此,在列表开头添加新节点时更新头结点 head
至关重要。
-
初始化一个新结点
cur
; -
将新结点链接到我们的原始头结点
head
。 -
将
cur
指定为head
。
例如,让我们在列表的开头添加一个新结点 9 。
- 我们初始化一个新结点 9 并将其链接到当前头结点 23 。
- 指定结点 9 为新的头结点。
思考一个问题
如何在列表的末尾添加新的结点?我们还能使用类似的策略吗?
while(head.next != null){
//移动head指针
head = head.next;
}
//移动到尾部之后 将新节点插入尾部
head.next = newNode;
newNode.next = null;
【3】单链表的删除操作
如果我们想从单链表中删除现有结点 cur
,可以分两步完成:
-
找到 cur 的上一个结点
prev
及其下一个结点next
;
-
接下来链接
prev
到 cur 的下一个节点next
。
在我们的第一步中,我们需要找出 prev
和 next
。
使用 cur
的参考字段很容易找出 next
,但是,我们必须从头结点遍历链表,以找出 prev
—— 它的平均时间是 O(N)
,其中 N 是链表的长度。因此,删除结点的时间复杂度将是 O(N)
。
空间复杂度为 O(1)
,因为我们只需要常量空间来存储指针。
示例
让我们尝试把结点 6从上面的单链表中删除。
-
从头遍历链表,直到我们找到前一个结点
prev
,即结点 23 -
将
prev
(结点 23)与next
(结点 15)链接
结点 6 现在不在我们的单链表中。
删除第一个结点
如果我们想删除第一个结点,策略会有所不同。
正如之前所提到的,我们使用头结点 head
来表示链表。我们的头是下面示例中的黑色结点 23。
如果想要删除第一个结点,我们可以简单地将下一个结点分配给 head
。也就是说,删除之后我们的头将会是结点 6。
链表从头结点开始,因此结点 23 不再在我们的链表中。
思考一个问题
删除最后一个结点呢?我们还能使用类似的策略吗?
——把倒数第二个节点的next指向NULL即可
2.双链表
本段内容都来自 leetbook
【1】双链表的简介
我们在前面的章节中介绍了单链表。
单链接列表中的结点具有 Value 字段,以及用于顺序链接结点的“Next”引用字段。
在本文中,我们将介绍另一种类型的链表:双链表
。
定义
双链表以类似的方式工作,但还有一个引用字段
,称为“prev”
字段。有了这个额外的字段,您就能够知道当前结点的前一个结点。
让我们看一个例子:
绿色箭头表示我们的“prev”
字段是如何工作的。
结点结构
下面是双链表中结点结构的典型定义:
//cpp
struct DoubleListNode{
int val;
DoubleListNode *next, *prev;
DoubleListNode(int x): val(x), next(NULL),prev(NULL){}
}
//java
class DoubleListNode{
int val;
DoubleListNode next, prev;
DoubleListNode(int x){
VAL = X;
}
}
与单链接列表类似,我们将使用头结点
来表示整个列表。
操作
与单链表类似,我们将介绍在双链表中如何访问数据、插入新结点或删除现有结点。
我们可以用 和单链表相同的方式访问数据:
同单链表一样——
- 我们不能在常量级的时间内
访问随机位置
。 - 我们必须从头部遍历才能得到我们想要的第一个结点。
- 在最坏的情况下,时间复杂度将是
O(N)
,其中N
是链表的长度。
对于添加和删除,会稍微复杂一些,因为我们还需要处理“prev”
字段。在接下来的两篇文章中,我们将介绍这两个操作
之后,我们提供练习,让你使用双链表重新设计链表。
【2】添加操作-双链表
如果我们想在现有的结点 prev
之后插入一个新的结点 cur
,我们可以将此过程分为两个步骤:
- 链接
cur
与prev
和next
,其中next
是prev
原始的下一个节点;
- 用
cur
重新链接prev
和next
。
与单链表类似,添加操作的时间和空间复杂度都是O(1)
示例
让我们在现有结点 6 之后添加一个新结点 9:
- 链接 ==
cur
(结点 9)==与prev
(结点 6)和next
(结点 15)
- 用==
cur
(结点 9)==重新链接prev
(结点 6)和next
(结点 15)
如果我们想在
开头
或结尾
插入一个新结点怎么办?“使用虚拟头节点和尾节点,添加操作就统一了。”
【3】删除操作 - 双链表
如果我们想从双链表中删除一个现有的结点 cur
,我们可以简单地
将它的前一个结点 prev
与下一个结点 next
链接起来。
与单链表不同,使用“prev”字段可以很容易地在常量时间内获得前一个结点。
因为我们不再需要遍历链表来获取前一个结点,所以时间和空间复杂度都是O(1)
。
示例
我们的目标是从双链表中删除结点 6。
因此,我们将它的前一个结点 23 和下一个结点 15 链接起来:
结点 6 现在不在我们的双链表中,达成效果。
如果我们要删除
第一个结点
或最后一个结点
怎么办?这部分内容可以再好好思考下