抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

在之前刷力扣的过程中,我每刷道题都是先看题解然后再去边写边看,很多内容根本没消化,在进度上欺骗自己,感觉之前刷的都忘了,能力没有什么提升,现在还是一道都不会做。所以这次报了卡哥的算法训练营,希望能够按计划地有效刷题。本次刷题,我用的是 Java 语言解题,有余力的话也可能会加上 C++ 的题解。以下是算法训练营第三天的刷题记录和思考笔记。

一、链表理论基础

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向 null(空指针)。链表的入口节点称为链表的头结点也就是 head

链表节点的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ListNode {
int val;
ListNode next;
public ListNode() { // 节点的构造函数
}
public ListNode(int val) {
this.val = val;
}
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}

链表在内存中的存储方式是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

链表和数组的区别:数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

除此以外,还有双链表、循环链表和静态链表,以及它们的 C/C++ 的定义方式可以参考这篇文章

双链表结点的定义:

1
2
3
4
5
6
7
8
class ListNode{
int val;
ListNode next,prev;
ListNode() {};
ListNode(int val){
this.val = val;
}
}

链表一定要分清节点和指针的概念。 new ListNode() 是真实存在的一个节点,head = new ListNode() 相当于 head 指针指向了一个真实的节点,node = head 相当于 nodehead 同时指向了这个真实的节点。

二、203 移除链表元素

题目

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点。

img

1
2
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

解题思路

第一种操作:直接使用原来的链表来进行移除。移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点(head = head -> next)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public ListNode removeElements(ListNode head, int val) {
while (head != null && head.val == val){
head = head.next; // 删除头结点
}
ListNode current = head;
while (current != null){ //注意这里不是 if
while (current.next != null && current.next.val == val){
current.next = current.next.next;
}
current = current.next;
}
return head;
}
}

其实可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。最后呢在 return 头结点的时候,return dummyNode->next;, 这才是新的头结点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null){
return head;
}
ListNode dummy = new ListNode(-1, head); // 设置一个虚拟头结点
ListNode previous = dummy;
ListNode current = head;
while (current != null){
if(current.val == val){
previous.next = current.next;
}else{
previous = current;
}
current = current.next;
}
return dummy.next;
}
}

三、707 设计链表

题目

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
1
2
3
4
5
6
7
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3

解题思路

这道题目设计链表的五个接口,覆盖了链表的常见操作:

  • 获取链表第 index 个节点的数值,注意 index 是从 0 开始的,第 0 个节点就是头结点。
  • 在链表的最前面插入一个节点,等价于在第 0 个元素前添加。
  • 在链表的最后面插入一个节点,等价于在 (末尾+1) 个元素前添加。
  • 在链表第 index 个节点前面插入一个节点,例如 index 为 0,那么新插入的节点为链表的新头节点。如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点。如果 index 大于链表的长度,则返回空。
  • 删除链表的第 index 个节点

可以设置一个虚拟头结点再进行操作。

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
class MyLinkedList {
int size;
ListNode head; //虚拟头结点

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

public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
for (int i = 0; i <= index; i++) { //包含一个虚拟头节点,所以查找第 index+1 个节点
currentNode = currentNode.next;
}
return currentNode.val;
}

public void addAtHead(int val) {
addAtIndex(0, val);
}

public void addAtTail(int val) {
addAtIndex(size, val);
}

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;
}
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;
}
}

补充(双链表)

如果本题目是双链表进行操作:

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
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) {
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) {
addAtIndex(0,val);
}

public void addAtTail(int val) {
addAtIndex(size,val);
}

public void addAtIndex(int index, int val) {
if(index>size){
return;
}
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;
}
}

四、206 反转链表

题目

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

img

1
2
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

解题思路

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。其实只需要改变链表的 next 指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。

首先定义一个 cur 指针,指向头结点,再定义一个 pre 指针,初始化为 null。然后就开始反转,首先要把 cur->next 节点用 tmp 指针保存一下,也就是保存一下这个节点。接下来要改变 cur->next 的指向,将 cur->next 指向 pre ,即反转第一个节点。接下来循环遍历继续移动 precur 指针。最后,cur 指针已经指向了 null,循环结束,链表也反转完毕了。 此时,pre 指针就指向了新的头结点,我们 return pre 指针就可以了。

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

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

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
// 递归 
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);
}
}
// 从后向前递归
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;
}
}

评论



Copyright © 2020 - 2022 Zhihao Zhuang. All rights reserved

本站访客数: 人,
总访问量: