03、第二章 链表part01

本节内容

  • 链表理论基础
  • 203.移除链表元素
  • 707.设计链表 
  • 206.反转链表

链表理论基础※

建议:了解一下链接基础,以及链表和数组的区别 

文章链接:https://programmercarl.com/%E9%93%BE%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html

203.移除链表元素※

建议: 本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。

题目链接: https://leetcode.cn/problems/remove-linked-list-elements/
文章讲解: https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html

https://www.yuanql.top/2023/06/04/02_leetcode/203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0/

题目分析

方案一

设置一个虚拟的头节点,根据头节点去操作会更加的顺滑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public ListNode removeElements(ListNode head, int val) {

ListNode temp = new ListNode();
temp.next = head;
head = temp;

while (temp != null && temp.next != null) {
// 此处循环是用于防止链表中有同时多个数值时,将其判断去除
while (temp.next != null && temp.next.val == val) {
temp.next = temp.next.next;
}
temp = temp.next;
}

return head.next;
}
}

结果

解答成功:
执行耗时:1 ms,击败了45.66% 的Java用户
内存消耗:44.2 MB,击败了28.54% 的Java用户

分析

时间复杂度:
O( n )

空间复杂度:
O( 1 )

方案二

其在方案一上进行了优化,其实就是修改了一下判断逻辑,不至于向方案一那么复杂,减少判断,可以有效减少实际消耗时间。

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
class Solution {
public ListNode removeElements(ListNode head, int val) {

ListNode temp = new ListNode();
temp.next = head;
head = temp;

// while (temp != null && temp.next != null) {
// while (temp.next != null && temp.next.val == val) {
// temp.next = temp.next.next;
// }
// temp = temp.next;
// }

while (temp.next != null) {
if (temp.next.val == val) {
temp.next = temp.next.next;
} else {
temp = temp.next;
}
}

return head.next;
}
}

结果

解答成功:
执行耗时:0 ms,击败了100.00% 的Java用户
内存消耗:43.9 MB,击败了61.02% 的Java用户

分析

时间复杂度:
O( n )

空间复杂度:
O( 1 )

代码随想录

https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html

文章中介绍了两种方式,一种是直接对链接进行操作,另一种是设置一个虚拟头节点,对虚拟头节点进行操作。

使用直接对链接进行操作的方式操作,需要的头节点进行特殊判断
设置一个虚拟头节点,可以很好的解决此问题,因为链表中的头节点变为了虚拟节点,其不需要特殊判断,只需要进行统一的处理。

代码实现

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
/**
* 添加虚节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
// 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
ListNode dummy = new ListNode(-1, head);
ListNode pre = dummy;
ListNode cur = head;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return dummy.next;
}
/**
* 不添加虚拟节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
while (head != null && head.val == val) {
head = head.next;
}
// 已经为null,提前退出
if (head == null) {
return head;
}
// 已确定当前head.val != val
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
/**
* 不添加虚拟节点and pre Node方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
while(head!=null && head.val==val){
head = head.next;
}
ListNode curr = head;
while(curr!=null){
while(curr.next!=null && curr.next.val == val){
curr.next = curr.next.next;
}
curr = curr.next;
}
return head;
}

707.设计链表※

建议: 这是一道考察 链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点

题目链接: https://leetcode.cn/problems/design-linked-list/
文章讲解: https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html

https://www.yuanql.top/2023/06/04/02_leetcode/707.%20%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8/

题目分析

方案一

本方案通过单链表的方式实现。
重点考察代码的编写能力

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
class MyLinkedList {

private int val;

private MyLinkedList next;

public MyLinkedList() {

}

public int get(int index) {
MyLinkedList temp = this;
for (int i = 0; i <= index; i++) {
if (temp.next == null ) {
return -1;
}
temp = temp.next;
}
return temp.val;
}

public void addAtHead(int val) {
MyLinkedList temp = this.next;
this.next = new MyLinkedList();
this.next.val = val;
this.next.next = temp;
}

public void addAtTail(int val) {
MyLinkedList temp = this;
while (temp.next != null) {
temp = temp.next;
}
temp.next = new MyLinkedList();
temp.next.val = val;
}

public void addAtIndex(int index, int val) {
MyLinkedList temp = this;
for (int i = 0; i < index; i++) {
if (temp.next == null ) {
return;
}
temp = temp.next;
}
MyLinkedList temp1 = temp.next;
temp.next = new MyLinkedList();
temp.next.val = val;
temp.next.next = temp1;
}

public void deleteAtIndex(int index) {
MyLinkedList temp = this;
for (int i = 0; i < index; i++) {
if (temp.next == null ) {
return;
}
temp = temp.next;
}
if (temp.next != null){
temp.next = temp.next.next;
}
}
}

结果

解答成功:
执行耗时:9 ms,击败了87.73% 的Java用户
内存消耗:43.5 MB,击败了12.62% 的Java用户

代码随想录

https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html

这道题目设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目

链表操作的两种方式:

  1. 直接使用原来的链表来进行操作。
  2. 设置一个虚拟头结点在进行操作。

代码实现

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
//单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
class MyLinkedList {
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;

//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}

//获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
//包含一个虚拟头节点,所以查找第 index+1 个节点
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}

//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}

//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val);
}

// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pred.next;
pred.next = toAdd;
}

//删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
if (index == 0) {
head = head.next;
return;
}
ListNode pred = head;
for (int i = 0; i < index ; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}

//双链表
class ListNode{
int val;
ListNode next,prev;
ListNode() {};
ListNode(int val){
this.val = val;
}
}


class MyLinkedList {

//记录链表中元素的数量
int size;
//记录链表的虚拟头结点和尾结点
ListNode head,tail;

public MyLinkedList() {
//初始化操作
this.size = 0;
this.head = new ListNode(0);
this.tail = new ListNode(0);
//这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
head.next=tail;
tail.prev=head;
}

public int get(int index) {
//判断index是否有效
if(index<0 || index>=size){
return -1;
}
ListNode cur = this.head;
//判断是哪一边遍历时间更短
if(index >= size / 2){
//tail开始
cur = tail;
for(int i=0; i< size-index; i++){
cur = cur.prev;
}
}else{
for(int i=0; i<= index; i++){
cur = cur.next;
}
}
return cur.val;
}

public void addAtHead(int val) {
//等价于在第0个元素前添加
addAtIndex(0,val);
}

public void addAtTail(int val) {
//等价于在最后一个元素(null)前添加
addAtIndex(size,val);
}

public void addAtIndex(int index, int val) {
//index大于链表长度
if(index>size){
return;
}
//index小于0
if(index<0){
index = 0;
}
size++;
//找到前驱
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
//新建结点
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;

}

public void deleteAtIndex(int index) {
//判断索引是否有效
if(index<0 || index>=size){
return;
}
//删除操作
size--;
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
}
}

/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/

206.反转链表※

题目链接: https://leetcode.cn/problems/reverse-linked-list/
文章讲解: https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html

https://www.yuanql.top/2023/06/05/02_leetcode/206.%20%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/

题目分析

需要三个变量,两个用于遍历,一个用于存储中间值

temp:用于存储中间的数值
left:用于指向需要将next指针改变的节点
right:用于记录原链表的下一个节点,主要目的是 left 的next指向改变的时候其就无法找到原链表的下一个节点了。

方案一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public ListNode reverseList(ListNode head) {

if (head == null) {
return head;
}
ListNode temp = null, left = head, right = head.next;
while (right != null) {
left.next = temp;
temp = left;
left = right;
right = right.next;
}
left.next = temp;
return left;
}
}

结果

解答成功:
执行耗时:0 ms,击败了100.00% 的Java用户
内存消耗:40.3 MB,击败了51.04% 的Java用户

分析

时间复杂度:
O( n )

空间复杂度:
O( 1 )

代码随想录

https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html

双指针

拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur)

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 双指针
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;
while (cur != null) {
temp = cur.next;// 保存下一个节点
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}
}

递归

递归遍历三步走:
1、确认递归函数的参数和返回值
2、确定终止条件
3、确认单层递归的逻辑

全面理解递归

递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。

具体可以看代码(已经详细注释),双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 递归 
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}

private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
temp = cur.next;// 先保存下一个节点
cur.next = prev;// 反转
// 更新prev、cur位置
// prev = cur;
// cur = temp;
return reverse(cur, temp);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 从后向前递归
class Solution {
ListNode reverseList(ListNode head) {
// 边缘条件判断
if(head == null) return null;
if (head.next == null) return head;

// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
}

03、第二章 链表part01
http://yuanql.top/2023/07/14/02_1_代码随想录算法训练营18期/03、第二章 链表part01/
作者
Qingli Yuan
发布于
2023年7月14日
许可协议