Day3 链表:203.移除链表元素 707.设计链表 206.翻转链表

83 阅读8分钟

203.移除链表元素

题目链接:203.移除链表元素

给你一个链表,移除链表中节点等于某个 target 的所有的节点,然后返回这个链表的头节点。

在实现这道题目的代码逻辑时,就要做一个判断:我们要删除的这个节点是不是头节点?

如果是头节点就按照这个方式删除,不是头节点就按照这种方式删除。

这样就有一个小问题:删除节点的方式不统一。

因此有了虚拟头节点法:就是在链表中再加入一个头节点 dummy head

这样删除链表中的节点的方法就可以得到统一:即让虚拟头节点直接指向该节点的下一个节点,然后释放掉该节点的内存。

这样代码会更加简洁。

本题可以采用如下2种链表操作的方式:

  • 直接使用原来的链表来进行删除操作。
  • 设置一个虚拟头结点再进行删除操作。

直接使用原来的链表来进行移除节点操作:

head != NULL && head->val == target

首先要判断头结点一定要不为空;

因为接下来要取头结点的值,如果这个头结点是空的话,我们相当于是操作空指针,(编译出错)。

同时,当头结点指向的数值等于要删除的值,

我们就可以进行删除操作。

思考:上面这个条件该使用 if 还是 while ? (⚠️很多人经常在这犯错!)

A:使用 while 循环

示例:[1, 1, 1, 1, 1, 1] target = 1

先判断头结点不为空,头结点的数值是1,然后进行移除头结点的操作。

移到第二个发现还是1,那么写 if 是不符合题目要求的。

移除头结点是一个持续移除的过程。

剩下的就是删除非头结点的情况了:

⚠️注意:cur要从head开始,而不是从head的next开始。

设置一个虚拟头结点再进行移除节点操作:

头结点的指针是不能改的,我们要遍历链表的时候,需要定义一个临时的指针cur,用来遍历这个链表。

AC代码: (核心代码模式)

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* removeElements(ListNode* head, int val) {
         ListNode* dummyHead = new ListNode(0);  //设置一个虚拟头结点
         dummyHead->next = head;  //让虚拟头结点的下一个节点指向链表的头节点head
         ListNode* cur = dummyHead;  //cur指针用来遍历链表
         while (cur->next != NULL) {
             if (cur->next->val == val) {
                 ListNode* tmp = cur->next;
                 cur-next = cur->next->next;
                 delete tmp;
             }
             else {
                 cur = cur->next;
             }
         }
         head = dummyHead->next;
         delete dummyHead;
         return head;
     }
 };

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

707.设计链表

题目链接:707.设计链表

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

1️⃣获取第n个节点的数值

n的合法判断:n < 0 || n > size - 1 均不合法。

Q:如何获取第n个节点? (这是遍历链表最基础的一个操作)

A:定义一个(临时的)指针cur,让它指向虚拟头结点

说一嘴:为什么在遍历链表的时候,要定义一个指针来遍历,而不是直接操作头指针?

A:因为我们操作完链表之后,需要返回头结点。如果你上来就操作头结点,那么头结点的值都改了,我们就不能够返回链表的头结点。

因此,需要定义一个临时的指针cur

cur = dummyHead->next;

我们要操作的就是获取当前这个节点的数值,让cur直接指向这里。

03.01.png

 while (n--) {
     cur = cur->next;
 }
 return cur->val;

2️⃣头部插入节点

⚠️坑点:

dummyHead->next = newNode;

newNode = ❌

顺序不对!!!

03.04.png

应该是:

 newNode->next = dummyHead->next;  
 dummyHead->next = newNode;
 size++;

3️⃣尾部插入节点

03.05.png

4️⃣第n个节点前插入节点

这个思路很重要: 只有保证第n个节点是 cur->next ,我们才能用cur来操作在 cur->next 之前添加一个节点。

03.02.png

 while (n) {
     cur = cur->next;
     n--;
 }
 newNode = cur->next;
 cur->next = newNode;
 size++;

5️⃣删除第n个节点

n < 0 || n >= size

首先对n进行合法性的判断, 判断完成之后进行删节点的操作。

03.03.png

🦄要清楚这个思路:第n个节点一定是cur->next,第n - 1个节点才是cur

我们通过操作cur来删掉cur->next这个节点。

 cur = dummyHead;
 while (n) {
     cur = cur->next;
     n--;
 }
 ​
 tmp = cur->next;
 cur-> next = cur->next->next;
 size--;

这个写法能不能保证第n个节点是cur->next?

举一个极端的例子:如果n = 0,即这个链表只有一个节点,

关键点:我们要明白假如操作的是第n个点,第n个点一定是cur->next,这也才能用cur来操作这个点,是增加还是删除。

本题总结:

在插入节点的时候,一定要注意先更新哪条边,后更新哪条边,只有清楚这一点指针才不会指乱。

⚠️本题在1️⃣获取第n个结点的数值,以及5️⃣删除第n结点,均要先对n进行合法性判断。

AC代码: (核心代码模式)

 class MyLinkedList {
 public:
 ​
     //定义链表节点结构体
     struct LinkedNode {
         int val;  //节点上存储的元素
         LinkedNode* next;  //指向下一个节点的指针
         LinkedNode(int val) : val(val), next(nullptr) {}  //节点的构造函数
             
     };
 ​
     //初始化链表
     MyLinkedList() {
         _dummyHead = new LinkedNode(0);  //虚拟头结点
         _size = 0;
     }
     
     //获取第n个节点的数值
     int get(int index) {
         if (index < 0 || index > (_size - 1)) {
             return -1;
         }
         LinkedNode* cur = _dummyHead->next;
         while (index--) {  //--index会死循环
             cur = cur->next;
         }
         return cur->val;
     }
 ​
     //头部插入节点
     void addAtHead(int val) {
         LinkedNode* newNode = new LinkedNode(val);
         newNode->next = _dummyHead->next;
         _dummyHead->next = newNode;
         _size++;
     }
     
     //尾部插入节点
     void addAtTail(int val) {
         LinkedNode* newNode = new LinkedNode(val);
         LinkedNode* cur = _dummyHead;
         while (cur->next != nullptr) {
             cur = cur->next;
         }
         cur->next = newNode;
         _size++;
     }
     
     //第n个节点前插入节点
     void addAtIndex(int index, int val) {
         if (index < 0 || index > _size) {
             return;
         }
         LinkedNode* newNode = new LinkedNode(val);
         LinkedNode* cur = _dummyHead;
         while (index--) {
             cur = cur->next;
         }
         newNode->next = cur->next;
         cur->next = newNode;
         _size++;
     }
     
     void deleteAtIndex(int index) {
         if (index < 0 || index >= _size) {
             return;
         }
         LinkedNode* cur = _dummyHead;
         while (index--) {
             cur = cur->next;
         }
         LinkedNode* tmp = cur->next;
         cur->next = cur->next->next;
         delete tmp;
         _size--;
     }
 ​
     //打印链表
     void PrintLinkedList() {
         LinkedNode* cur = _dummyHead;
         while (cur->next != nullptr) {
             cout << cur->next->val << " ";
             cur = cur->next;
         }
         cout << endl;
     }
 ​
 private:
     int _size;
     LinkedNode* _dummyHead;
 };
 ​
 /**
  * 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.翻转链表

题目链接:206.反转链表

考查对基础数据结构操作的一道很好的题目。

我们可以先看看双指针的解法是如何实现这个翻转的过程,然后对着双指针解法的代码,写出一个递归的版本。

优先掌握双指针写法

03.06.png

双指针解法:

动画:

03.07.gif

算法的大概思路:

首先,要有一个指针 cur 指向头结点,还需要一个指针 pre (定义在 cur 的前面,方便 cur 指向后一位改成指向前一位),然后 prvcur 移动到下一个点。

因此,需要对上述2个指针进行初始化cur = head; pre = NULL; (很多同学对这个初始化掌握得并不是很好)

初始化就是为了让head能够直接指向它的前一位,指向的就应该是个 null

接下来就是遍历的过程:while ( )

遍历到最后,当pre指向了尾结点,cur指向了null,这个遍历操作(while循环)就结束了,循环条件:while (cur)

⚠️需要一个临时指针tmp,趁还没赋值(即cur指向的结点和下一个结点还存在连接),把cur的下一个结点保存下来: tmp = cur->next;

然后就可以赋值了:cur->next = pre; ,这样就改变了2个结点之间指针的方向。

改变完方向后需要分别将 precur 指针向后移一位:pre = cur;cur = tmp; (先移动pre,后移动cur)

注意:⚠️顺序不能反! 一旦顺序互换,cur指向tmp(cur指向的值被改了),导致pre不能移动到先前cur的位置。

最后,返回新链表的头结点:return pre;

梳理一下:while循环控制遍历的终止位置,一次循环改变一次结点的方向,然后将pre、cur指向下一个位置。如此循环操作,直到cur指向null,循环结束。

AC代码: (核心代码模式)

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* reverseList(ListNode* head) {
         ListNode* temp;
         ListNode* cur = head;
         ListNode* pre = nullptr;
 ​
         while (cur) {
             temp = cur->next;  //保存cur的下一个结点
             cur->next = pre;  //翻转操作
             
             //更新pre和cur指针,分别向后移一位 (顺序不能反)
             pre = cur;
             cur = temp;
         }
         return pre;
     }
 };

递归解法:

Carl板书:

03.07.png

AC代码: (核心代码模式)

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 ​
 ​
 class Solution {
 public:
     ListNode* reverse(ListNode* cur, ListNode* pre) {
         if (cur == nullptr) {
             return pre;
         }
         ListNode* temp = cur->next;
         cur->next = pre;
         return reverse(temp, cur);
     }
 ​
     ListNode* reverseList(ListNode* head) {
         return reverse(head, nullptr);
     }
 };