阅读视图

发现新文章,点击刷新页面。
🔲 ☆

链表常见算法题及解析

目录:


翻转链表

问题

对于一个这样的链表:

希望经过函数处理后,变成这样:

链表结构定义

节点的定义为:

type Node struct {    Value int    Next  *Node}

构造链表方法为:

func createLinkedList(n int) *Node {    head := &Node{Value: 0}    node := head    for i := 0; i < n; i++ {        if i < n {            node.Next = &Node{Value: i + 1}        }        node = node.Next    }    return head}

函数会返回一个链表的指针。使用指针而不是结构体类型是因为,Go 语言的某些关于变量的设计,无法使用 Node{} == nil 的形式判断变量是否为空,因为理论上 Node{} 不是 nil。这就造成了如果使用Node{}作为链表头部的变量类型,在遍历的时候找不到一个合理的结束时机,只能使用类似 Node{}.Next == nil 这样的形式,还会遗漏掉最后一个节点。

迭代翻转链表

这里不能使用直接改变节点值的方式,比如遍历一次后把链表节点的值按照顺序储存到数组中,然后再遍历一次,一次修改链表节点的值。这个违背了数据结构的意义。可以使用递归完成翻转链表的操作。

比如第一个节点,使用 temp 变量储存翻转前的下一个节点的位置,然后把 head.Next 指向翻转后应该有的节点位置,第一个节点的下一个节点是空节点,第二个节点的下一个节点是节点 1。完成 head.Next 的指向后,head 要指向 temp 也就是原来的下一个节点用以完成遍历。这时还需要要用一个 curr 变量来储存head 跳转前的位置,方便下一次 head.Next 指向上一个节点的位置。这应该是一个简单的过程。

func reverseLinkedList(head *Node) *Node {    curr := new(Node)    for head != nil {        temp := head.Next        head.Next = curr        curr = head        head = temp    }    return curr}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

执行

执行程序后结果和预期一致:

func main() {    head := createLinkedList(4)    head = reverseLinkedList(head)    for head != nil {        fmt.Println(head.Value)        head = head.Next    }}



判断链表是否有环

问题

链表有环是指链表中“最后”的一个节点,它的下一个节点指向了链表中位于它之前的节点。

当程序遍历这个链表,会发现进入了死循环,永远找不到出口了。怎么判断一个链表,是否存在这样的环呢?

分析

常用的解决思路是双指针。设想一个在赛道上的场景,两个人 A 和 B 处于同样的起点,但是他们跑步的速度并不相同,A 的速度 v1=1,B 的速度 v2=2,也就是 B 比 A 快。在这样的情况下,只要他们不停,B 一定会超过 A 一圈然后再次追上 A,这是一种生活中的常识。

在一个圈里,一快一慢的两个点一定会再次相遇,而且他们经过的路程是可以计算的,路程 s1 和 s2 应该满足这样的关系:

s2 - s1 = nR

R 是圆圈的周长,n 是正整数,他们位于出发点时 n=0,第一次相遇的时候 B 比 A 多跑了一圈,多出了 1 倍周长的路程,n=1。

和链表的情景相比较,赛道的场景还少了开始的一段距离,在进入赛道之前,A 和 B 会先从赛道外的小路进入赛道,然后再开始绕圈跑步。他们的起点在赛道外,为了便于计算,他们的速度从始至终不发生变化,那么当他们进入赛道之后,就已经不是同样的起点了。

在这种情况下,他们经过的路程 s1 和 s2 还有规律可循吗?设圆形赛道外的直道距离为 d,相比上面的关系式,他们在圆圈内的路径依然满足 n 倍的周长 R,只不过现在的表达式不同了:

(s2 - d) - (s1 - d) = nR    s2 - d - s1 + d = nR            s2 - s1 = nR

结果表达式在相互抵消路径 d 之后,和之前的一样。

A 的路程 s1=v1t,B的路程 s2=v2t,时间 t 是一样的,速度 v1 和 v2 是已知的 1 和 2,有:

    s2 - s1 = nR  v2t - v1t = nR     2t - t = nR          t = nR

取 n = 1,t = R

解决

回到链表的问题,其实我们只要用快慢指针就可以判断链表是否有环了,并不需要知道他们具体相遇的点在哪儿,不过计算路径关系的公式可以辅助我们验证结果的正确性。

回到这个链表,用两个指针 A 和 B 从节点 1 分别以速度 1 和 2 出发:

他们的位置关系将会是:

时间 t01234
A 的位置节点 1节点 2节点 3节点 4节点 5
B 的位置节点 1节点 3节点 5节点 3节点 5

在第 4 个时间点的时候,A 和 B 相遇了,环的周长正好等于 4,满足 t = R 的关系。



链表如果有环,找到环的起点

问题

这个问题是上一个问题的延伸,在判断链表已经有环的基础上,找到环的起点。比如这样的一个链表,环的起点是节点 3。

分析

(1)

在判断链表是否有环的问题中,我们得到了一个至关重要的结论:

t = R

两个快慢指针将会在等于环长度的时间点相遇。对于上图的链表,快慢指针的位置关系是这样:

时间 t0123456
A 的位置节点 1节点 2节点 3节点 4节点 5节点 6节点 7
B 的位置节点 1节点 3节点 5节点 7节点 3节点 5节点 7

我们可以观察到,环的长度是 6,快慢指针也会在第 6 秒相遇,他们交点位置是节点 7:

(2)

根据上面提到的之前的结论,按照慢指针 v1 = 1 的速度,它经过的路程和时间是一样的,也就是说,从出发点到两指针相遇的路径长度,根据 t = R,此刻的时间是 t,正好是环的长度 R:

(3)

做一个假设,慢指针保持着这个长度为 R 的走过的路径,向前移动一步,会变成这样:

再走一步,变成了这样:

(4)

到这里似乎还不知道我们要干什么。现在对路径设一个变量,从 出发点环的起点 之间的距离设为 l1,整个链表的长度设为 l,环的长度仍然为 R。

这 3 个变量将满足这样的关系:

l - l1 = R

这是太显而易见的事情。

(5)

记得我们一开始的结论吗?从 出发点快慢指针的交点 之间的距离,等于环的长度 R:

变量 l 和 l1 保持不变,图就成了这样:

此时的 l 仍然等于 l1 + R,不同的是,l1R 重合了。

(6)

l - l1 = R

重合之后,等式关系还成立吗?当然成立,因为整个链表没有变,变量的大小没有变。但好像又觉得哪里奇怪。

现在新设一个变量,设从 快慢指针的交点环的起点 的距离为 l2

此时:

l - l2 = R

(7)

经过这样一些比较,发现 l1 == l2,也就是从 出发点环的起点 的距离,等于 快慢指针的交点环的起点 的距离。

解决

出发点 -> 环的起点 == 快慢指针的交点 -> 环的起点

这是一个很重要的结论,因为我们此时的快慢指针就在 快慢指针的交点 上,在节点 7 的位置。

如果这个时候在新增一个指针 p3,在快慢指针相交的时刻,从整个链表的 出发点 1 出发(速度为 1),那么 p3 和慢指针一定会相交,因为 p3环的起点 的距离等于慢指针到 环的起点 的距离。p3 遇到慢指针的位置,就是环的起点。



判断两个链表是否相交

问题

存在两个链表,分别在某一个节点指向了同一个节点作为下个节点:

这里有两个链表:

1 -> 2 -> 3 -> 4     5 -> 3 -> 4

怎么判断两个链表是否相交?

分析

一种简单的做法是,分别遍历每条链表到最后一个节点,判断最后一个节点是否相同。如果两个链表在中间节点相交,则最后一个节点一定相同。



链表如果相交,找到交点

问题

对于这样两个链表:

如何找到第一个交点 3 ?

分析

一种简单的解决思路是,把这个链表的尾节点和任意一个链表的头节点连起来:

可以是链表 1 的尾节点到链表 2 的头节点,或者链表 2 的尾节点到链表 2 的头节点,总之连起来以后,问题就转变成了,找到链表环的起点。



合并两个有序链表

问题

给出两个有序链表,将两个链表合并为一个有序链表。

分析

思路暴力简单,同时迭代两个链表,按照顺序依次合并就可以了。控制好边界条件。

代码

node 结构定义:

type Node struct {    Value int    Next  *Node}

构建两条链表:

func main() {    root1 := &Node{        Value: 1,    }    root1.Next = &Node{        Value: 1,    }    root1.Next.Next = &Node{        Value: 3,    }    root1.Next.Next.Next = &Node{        Value: 5,    }    root2 := &Node{        Value: 1,    }    root2.Next = &Node{        Value: 2,    }    root2.Next.Next = &Node{        Value: 4,    }    root := merge(root1, root2)    for root != nil {        fmt.Println(root.Value)        root = root.Next    }}

合并链表:

func merge(root1 *Node, root2 *Node) *Node {    var root *Node    var temp *Node    if root1.Value <= root2.Value {        root = root1        temp = root2    } else {        root = root2        temp = root1    }    p1 := root    p2 := p1.Next    for {        if p2 == nil || temp == nil {            break        }        if p2.Value <= temp.Value {            p1.Next = p2            p1 = p1.Next            p2 = p2.Next        } else {            p1.Next = temp            p1 = p1.Next            temp = temp.Next        }    }    return root}
🔲 ⭐

数据结构:链表(LinkedList)- 上

本文将简单介绍链表的概念,给出它们的 Java 实现以及原理解析。

对象的引用(指针)

在开始讲链表之前,我们先来看复习一下 Java 中的对象引用(Reference)。

考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
public int value;

public MyClass(int value) {
this.value = value;
}
}

public class Main {
private static void changeValue(MyClass myClass) {
myClass.value = 20;
}

public static void main(String[] args) {
MyClass myClass = new MyClass(10);
System.out.println(myClass.value);
changeValue(myClass);
System.out.println(myClass.value);
}
}

第一次调用 System.out.println(myClass.value); 时,输出的是 10,但是第二次调用 System.out.println(myClass.value); 应该输出什么呢?

思考一下,两个可能的答案 1020,哪个是正确的?

答案

正确答案是 20

虽然 Java 中没有明确定义指针(Pointer)这个概念,但是在 Java 中,对象引用可以看作是指向对象的指针。

一个非常简化的解释:

简单来讲,Java 中的每个对象都需要使用一整块内存存储,但是我们并不能永远找到这个对象从内存的哪个存储位置(地址)开始。所以,我们还会在内存中存储一个指向这个对象的指针(这个对象存储块开始位置的地址)。我们每次在代码中使用对象时,实际上是在使用这个指针。(这也是为什么没有 toString 方法的对象在输出时会显示一个十六进制的地址。)

在上述代码中,我们实际上并没有将对象 myClass 传递给 changeValue 方法,而是将指向对象 myClass 的指针传递给了 changeValue 方法。所以,当我们在 changeValue 方法中修改了对象的值时,实际上仍然在修改这个指针指向的对象的值,所以当我们回到 main 方法时,对象的值仍然是修改后的值。

理解这个概念对于理解链表的实现非常重要。

链表是什么?

链表,与数组、栈以及队列一样,是一种线性数据结构。

与数组不同的是,链表中的元素顺序并不以数组的下标为准,而以每个元素中包含的一个指向下一个元素的指针为准。这样的话,链表可以作为一个简单且灵活的数据结构,并支持搜索、插入、删除等操作(虽然链表的实现并不一定是最高效的)。

Coremen, et al. (2022, p. 258) [1]

在链表中,每个元素都是一个节点(Node)。每个节点都包含两个数据:一个是该节点存储的数据 data ,另一个是指向下一个节点的指针 next。这样的设计允许我们以比数组高效的多的方式插入和删除元素。[2]

一个简单的数组示例 [3]
一个简单的数组示例 [3]
一个简单的链表示例 [3]
一个简单的链表示例 [3]

这种每个节点都只有一个指向下一个节点的指针的链表称为单链表(Singly Linked List)。

一个单链表示例 [4]
一个单链表示例 [4]

(这个单链表示例与上面的链表示例其实一样,只不过这个链表没有把指向下一个节点的指针画到上一个节点里,所以与实际上的实现有些不同。不过由于这个图更加简洁,而且后面关于操作的示例也是基于这个图的,所以我还是在这里展示一下。)

链表也有其他类型,包括双向链表(Doubly Linked List,意味着每个节点有两个指针,一个指向前一个节点,一个指向后一个节点)、循环链表(Circular Linked List,意味着链表的最后一个节点的“下一个”指针指向第一个节点)以及双向循环链表(Doubly Circular Linked List,意味着链表的最后一个节点的“下一个”指针指向第一个节点,而第一个节点的“上一个”指针指向最后一个节点)。

一个双向链表示例 [4]
一个双向链表示例 [4]
一个循环链表示例 [4]
一个循环链表示例 [4]
一个双向循环链表示例 [4]
一个双向循环链表示例 [4]

为了简单起见:本文将主要讨论单链表的操作原理和实现。

链表的操作

链表主要支持以下几种操作:

  • 遍历(Traverse):遍历链表中的所有节点。
  • 插入(Insert):在链表中插入一个新节点。
  • 删除(Delete):在链表中删除一个节点。
  • 搜索(Search):在链表中搜索一个节点。
  • 更新(Update):更新链表中的一个节点。
  • 排序(Sort):对链表中的节点进行排序。

本文将仅讨论遍历、插入和删除操作。

遍历

遍历其实非常简单,只需要从链表的第一个节点开始,每次访问完一个节点,将目前正在操作的节点指针指向换成当前节点的“下一个”指针指向的节点即可。

1
2
3
4
5
6
Node current = head; // 从链表的第一个节点(head)开始
while(current.next != null) { // 当前节点的“下一个”指针不为空时(即当前节点不是最后一个节点)
System.out.println(current.data); // 获取当前节点的数据(如果需要的话)
current = current.next; // 将当前节点指针指向下一个节点
}
System.out.println(current.data); // 输出最后一个节点的数据(如果需要的话)

插入

插入操作分为三种情况:

  1. 在链表的头部插入一个新节点。
  2. 在链表的尾部插入一个新节点。
  3. 在链表的中间插入一个新节点。

头部插入

头部插入仍然相对简单,只需要创建一个新节点,将新节点的“下一个”指针指向原来的头节点,然后将新节点设置为头节点即可。

1
2
3
4
5
// 头部插入方法需要一个参数 data 作为新节点的数据
Node newNode = new Node(); // 创建一个新节点
newNode.data = data; // 设置新节点的数据
newNode.next = head; // 将新节点的“下一个”指针指向原来的头节点
head = newNode; // 将新节点设置为头节点
在链表头部插入一个新节点 [5]
在链表头部插入一个新节点 [5]

尾部插入

尾部插入相对复杂一些,因为我们需要遍历整个链表,找到最后一个节点,然后将最后一个节点的“下一个”指针指向新节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 尾部插入方法也需要一个参数 data 作为新节点的数据
Node newNode = new Node(); // 创建一个新节点
newNode.data = data; // 设置新节点的数据
newNode.next = null; // 将新节点的“下一个”指针设置为 null
if (head == null) { // 如果链表为空
head = newNode; // 将新节点设置为头节点
} else {
Node current = head; // 从头节点开始遍历
while (current.next != null) { // 当前节点的“下一个”指针不为空时(即当前节点不是最后一个节点)
current = current.next; // 将当前节点指针指向下一个节点
}
current.next = newNode; // 将最后一个节点的“下一个”指针指向新节点
}
在链表尾部插入一个新节点 [5]
在链表尾部插入一个新节点 [5]

中间插入

中间插入操作与尾部插入操作类似,只不过我们需要在找到要插入的位置后停下来,将新节点的“下一个”指针指向当前节点的“下一个”指针指向的节点,然后将当前节点的“下一个”指针指向新节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 中间插入方法需要两个参数 data 和 index,分别表示新节点的数据和要插入的位置(从 0 开始)
if (index == 0) {
// 实际上就是头部插入
insertAtBeginning(data); // 由于已经有了头部插入方法,所以可以直接调用头部插入方法
} else {
Node newNode = new Node(); // 创建一个新节点
newNode.data = data; // 设置新节点的数据
Node current = head; // 从头节点开始遍历
for (int i = 1; i < index; i++) { // 遍历到要插入的位置的前一个位置
current = current.next; // 如果还没到要插入的位置,就将当前节点指针指向下一个节点
}
// 到了要插入的位置后(此时 current 指向要插入的位置的前一个位置)
newNode.next = current.next; // 将新节点的“下一个”指针指向当前节点的“下一个”指针指向的节点
current.next = newNode; // 将当前节点的“下一个”指针指向新节点
}

在链表中间插入一个新节点 [5]
在链表中间插入一个新节点 [5]

删除

删除操作基本只有两种情况:

  1. 删除头节点。
  2. 删除中间或尾部节点。

删除头节点相当简单,只要将 head 指针指向头节点的“下一个”指针指向的节点即可。

而删除中间节点则需要遍历链表,找到要删除的节点的前一个节点,然后将前一个节点的“下一个”指针指向要删除节点的“下一个”指针指向的节点。

如果我们要删除的节点是最后一个节点,那么其实就是将前一个节点的“下一个”指针指向 null,不需要特殊处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 删除方法需要一个参数 index,表示要删除的节点的位置(从 0 开始)
if (index == 0) {
// 实际上就是删除头节点
Node temp = head; // 保存头节点
head = head.next; // 将头节点指针指向头节点的“下一个”指针指向的节点
temp = null; // 删除头节点,这一步实际上不是必要的
} else {
Node current = head; // 从头节点开始遍历
for (int i = 1; i < index; i++) { // 遍历到要删除的位置的前一个位置
current = current.next; // 如果还没到要删除的位置,就将当前节点指针指向下一个节点
}
// 到了要删除的位置的前一个位置后(此时 current 指向要删除的位置的前一个位置)
Node temp = current.next; // 保存要删除的节点
current.next = temp.next; // 将前一个节点的“下一个”指针指向要删除节点的“下一个”指针指向的节点
temp = null; // 删除要删除的节点
}
在链表中间删除一个节点 [5]
在链表中间删除一个节点 [5]

示例实现与测试

下面是一个上述链表实现的示例代码以及一个简单的测试代码,用于测试链表的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* 链表的节点
*/
class Node {
/**
* 节点的数据
*/
public int data;
/**
* 节点的“下一个”指针
*/
public Node next;
}

/**
* 链表
*/
class MyLinkedList {
private Node head = null;

/**
* 遍历链表,显示链表中的所有元素
*/
public void traverse() {
Node current = head; // 从链表的第一个节点(head)开始
while (current.next != null) { // 当前节点的“下一个”指针不为空时(即当前节点不是最后一个节点)
System.out.print(current.data + " "); // 获取当前节点的数据(如果需要的话)
current = current.next; // 将当前节点指针指向下一个节点
}
System.out.print(current.data + " "); // 输出最后一个节点的数据(如果需要的话)
System.out.println(); // 换行
}

/**
* 在链表的开头插入一个新数据
* @param data 新节点的数据
*/
public void insertAtBeginning(int data) {
// 头部插入方法需要一个参数 data 作为新节点的数据
Node newNode = new Node(); // 创建一个新节点
newNode.data = data; // 设置新节点的数据
newNode.next = head; // 将新节点的“下一个”指针指向原来的头节点
head = newNode; // 将新节点设置为头节点
}

/**
* 在链表的末尾插入一个新数据
* @param data 新节点的数据
*/
public void insertAtEnd(int data) {
// 尾部插入方法也需要一个参数 data 作为新节点的数据
Node newNode = new Node(); // 创建一个新节点
newNode.data = data; // 设置新节点的数据
newNode.next = null; // 将新节点的“下一个”指针设置为 null
if (head == null) { // 如果链表为空
head = newNode; // 将新节点设置为头节点
} else {
Node current = head; // 从头节点开始遍历
while (current.next != null) { // 当前节点的“下一个”指针不为空时(即当前节点不是最后一个节点)
current = current.next; // 将当前节点指针指向下一个节点
}
current.next = newNode; // 将最后一个节点的“下一个”指针指向新节点
}
}

/**
* 在链表的指定位置插入一个新数据
* @param data 新节点的数据
* @param index 新节点的位置
*/
public void insertAt(int data, int index) {
// 中间插入方法需要两个参数 data 和 index,分别表示新节点的数据和要插入的位置(从 0 开始)
if (index == 0) {
// 实际上就是头部插入
insertAtBeginning(data); // 由于已经有了头部插入方法,所以可以直接调用头部插入方法
} else {
Node newNode = new Node(); // 创建一个新节点
newNode.data = data; // 设置新节点的数据
Node current = head; // 从头节点开始遍历
for (int i = 1; i < index; i++) { // 遍历到要插入的位置的前一个位置
current = current.next; // 如果还没到要插入的位置,就将当前节点指针指向下一个节点
}
// 到了要插入的位置后(此时 current 指向要插入的位置的前一个位置)
newNode.next = current.next; // 将新节点的“下一个”指针指向当前节点的“下一个”指针指向的节点
current.next = newNode; // 将当前节点的“下一个”指针指向新节点
}
}

/**
* 删除链表指定位置的节点
* @param index 要删除的节点的位置
*/
public void deleteAt(int index) {
// 删除方法需要一个参数 index,表示要删除的节点的位置(从 0 开始)
if (index == 0) {
// 实际上就是删除头节点
Node temp = head; // 保存头节点
head = head.next; // 将头节点指针指向头节点的“下一个”指针指向的节点
temp = null; // 删除头节点,这一步实际上不是必要的
} else {
Node current = head; // 从头节点开始遍历
for (int i = 1; i < index; i++) { // 遍历到要删除的位置的前一个位置
current = current.next; // 如果还没到要删除的位置,就将当前节点指针指向下一个节点
}
// 到了要删除的位置的前一个位置后(此时 current 指向要删除的位置的前一个位置)
Node temp = current.next; // 保存要删除的节点
current.next = temp.next; // 将前一个节点的“下一个”指针指向要删除节点的“下一个”指针指向的节点
temp = null; // 删除要删除的节点
}
}
}

public class Main {
public static void main(String[] args) {
MyLinkedList list = new MyLinkedList();
list.insertAtBeginning(10);
list.insertAtBeginning(5);
list.insertAtEnd(15);
list.traverse(); // 5 10 15
list.insertAt(8, 2);
list.traverse(); // 5 10 8 15
list.deleteAt(1);
list.traverse(); // 5 8 15
}
}

例题

Coming soon…

预告

你有没有发现,我们上面的代码(以及之前关于栈和队列的文章中的代码)中都只提到了如何存储 int 类型的数据?那么,我们如果需要存储其他类型的数据怎么办呢?为每个类型都写一个实现显然是不现实的,所以我们需要使用泛型(Generic)。

在下一篇文章中,我们将讨论 Java 中的泛型,以及如何实现对应的栈、队列和链表。

再之后,我们会讨论链表的其他操作。

GL & HF!

🔲 ⭐

LeetCode腾讯精选练习50题-061.旋转链表

题目描述

  • 给定一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
exampleinput  : head = [1,2,3,4,5], k = 2output : [4,5,1,2,3]input  : head = [0,1,2], k = 4output : [2,0,1]

解题思路

  • 如果满足以下条件,可以直接返回原链表
    • 链表为空,即 head = null
    • 链表只有一个节点,即 head.next = null
    • 旋转次数 k 为0
    • 旋转次数 k 是链表长度的倍数,即旋转后的链表还是原样
思路1 双指针/快慢指针
  • 第一次遍历获取链表长度

  • 用快慢指针拉开遍历差距,并借助慢指针获取结果链表的起始节点,即

    • 先考虑简单情况 k 严格小于 链表长度 n,那么就是要找到原链表的倒数第k个节点作为结果链表的起始节点
    • 再拓展到k > n的情况,此时只需要刷新k,即令 k = k % n,转变成第一种简单情况即可
  • 使链表闭合,并断开新的起始节点与其之前节点的链接

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

    思路2 双指针的反向思考
  • 其实就是双指针思路的变形

  • 第一次遍历获取链表长

  • 反向思考旋转后结果链表起始节点的位置,使用单个指针遍历来获取结果链表的起始节点,即

    • 先考虑简单情况 k 严格小于 链表长度 n,那么就是要找到原链表的倒数第k个节点原始链表的正数第 n - k 个节点的后继结点作为结果链表的起始节点
    • 再拓展到k > n 的情况,此时只需要将 k % n,转变成第一种简单情况即可,即刷新k,令 k = n - k % n
  • 使链表闭合,并断开新的起始节点与其之前节点的链接

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

代码(Java)

思路1代码

public class Solution1 {    public ListNode rotateRight(ListNode head, int k) {        if (k == 0 || head == null || head.next == null) {            return head;        }        ListNode iter = head;        int len = 0;        ListNode tail = null;        while (iter != null) {            if (iter.next == null) {                tail = iter;            }            len++;            iter = iter.next;        }        // 如果旋转次数恰好是链表长度的倍数,说明旋转后也是原样,所以不需要移动        if (k % len == 0) {            return head;        }        // 初始令计数器延后一位(即-1),以获取结果链表的起始位置的前一个位置        // 此处若初始令计数器为0,则获得的是结果链表的其实位置,无法将它与之前的节点断开        int cnt = -1;        // 先令快指针向前遍历k%len步        ListNode fast = head;        while (cnt != k % len) {            cnt++;            fast = fast.next;        }        // 再同步开启慢指针的遍历        // 当快指针为空时,说明慢指针已经到达了目标结果链表的起始节点的前一个位置        ListNode second = head;        while (fast != null) {            fast = fast.next;            second = second.next;        }        // 使原始链表闭合为环        tail.next = head;        // 获得新起点        ListNode res = second.next;        // 从新起点之前断开        second.next = null;        // 返回结果        return res;    }}

思路2代码

public class Solution2 {    public ListNode rotateRight2(ListNode head, int k) {        if (k == 0 || head == null || head.next == null) {            return head;        }        ListNode first = head;        int len = 1;        while (first.next != null) {            len++;            first = first.next;        }        if (k % len == 0) {            return head;        }        // 第二次遍历        ListNode second = head;        int cnt = 1;        // 移动k个位置,其实就是以 第len - k % len个节点的后继节点 作为新链表的头节点,并使其与其之前的节点断开链接        // 所以此处可以做减法来替代快慢指针的同步遍历,只需要遍历一次即可。        int step = len - k % len;        while (cnt < step) {            cnt++;            second = second.next;        }        // 使之闭合为环        first.next = head;        ListNode res = second.next;        second.next = null;        return res;    }}
❌