算法第3天| 203 移除链表元素、707 设计链表、206 反转链表
一、 203 移除链表元素
对链表不熟悉,做算法题蛮吃力,抱着学习链表的基础知识和算法思路的态度认真学习今天的三道题目。
链表:一种通过指针串联在一起的线性结构,每个节点由数据域和指针域两部分组成,指针域存放的是指向一个节点的指针,最后一个节点的指针域指向 null(空指针)。链表分为单链表、双链表和循环链表,如图为单链表,链表的入口节点称为链表的头结点 head。
C++中单链表的定义:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
// 通过自己定义构造函数初始化节点
ListNode* head = new ListNode(5);
// 若不定义节点的构造函数,则C++默认生成一个构造函数。但是这个构造函数在初始化的时候不能直接给变量赋值
// 使用默认构造函数初始化节点:
ListNode* head = new ListNode();
head->val = 5;
看完讲解视频自己写的还是有困难,主要问题在于链表的定义,以及while的条件,以及修改next后内存空间的释放
/**
* 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) {
struct ListNode{
int val;
ListNode *next;
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
// 一般的链表删除要根据是否为头节点分两种情况处理,头节点只需头指针后移一位即可,如果不是头节点则需要;
while (head != NULL && head -> next -> val == val) {
head = head -> next; // 要释放掉内存空间 // 头节点所指向的 head -> next
}
cur = head; // 注意这里是head而不是head -> next, 因为要找当前节点的前一个节点;即第一个非头节点的前一个节点--head节点
// 操作cur -> next,所以要保证cur不为空,否则会报空指针错误;因为要把cur -> next的值与目标值 val 进行对比,所以要保证cur -> next不为空,否则又会报操作空指针的错误
while (cur != NULL & cur -> next != NULL) {// 注意这里要持续遍历,因此是 while 而不是 if
if (cur -> next -> val == val) {
cur -> next = cur -> next ->next;
delete ; // 这里也要释放掉内存空间
}
}
return head;
}
};
正解——直接用原来的链表来进行移除节点操作:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) { // 注意这里不是if
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
ListNode* cur = head;
// 空指针可以赋值,但是不能访问,注意这里必须要保证cur != NULL,只有这样才可以访问cur->next,接着必须保证cur->next!= NULL,才可以访问cur->next->val。
while (cur != NULL && cur->next!= NULL) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head; // head的值并没有改变,一直指向头节点。(遍历链表用的是另外定义的cur)
}
};
设置虚拟头结点进行移除节点操作,好处是可以将头节点和非头节点统一处理,添加和删除节点的操作方式都可以统一。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 把dummyHead实例化,new设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,方便后面做删除操作
// ListNode* cur = dummyHead 而不是 dummyHead->next,因为要想删除元素,必须要知道该元素的上一个元素的指针,所以我们要删除的是dummyHead->next,需要将指针指向dummyHead
ListNode* cur = dummyHead; // 定义一个临时指针,用于遍历链表
while (cur->next != NULL) { // while循环控制持续遍历链表,直到尾节点为null
if(cur->next->val == val) { // 判断cur->next的数值等于目标值val
ListNode* tmp = cur->next; // 相等则进行
cur->next = cur->next->next; // 删除元素的操作
delete tmp; //释放内存空间
} else { // 未找到删除的目标值
cur = cur->next; // 继续往下遍历
}
}
head = dummyHead->next; // 因为原来的head有可能已经被删除,因此新链表的头节点是虚拟头节点的下一个即dummyHead->next而不是Head。
delete dummyHead;
return head;
}
};
二、 707 设计链表
题目链接
代码随想录链接
关键是注意两个问题:
1.cur->next应该指向第n个节点,这样才能用cur进行正确的添加和删除操作;
2. 在插入节点时,应该先更新 新节点指向后面节点,再更新前面的节点指向新节点
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
// LinkedNode(int val): 构造函数,定义了一个接受一个整数参数val的构造函数
// val(val): 使用传入的val初始化类成员val
// 使用nullptr初始化类成员next(nullptr是C++11引入的空指针常量,用于初始化指针类型的成员)
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
// 使用构造函数创建一个新的LinkedNode对象,并使用整数值0初始化它的val成员,同时将next成员初始化为nullptr
_dummyHead = new LinkedNode(0); // 定义虚拟头结点
_size = 0;
}
int get(int index) {
// index是从0开始的,头节点的index为0
if (index < 0 || index > _size - 1) {
return -1;
}
// 定义临时指针cur来遍历链表找到下标为index的节点
LinkedNode* cur = _dummyHead->next;
// 在考虑边界条件的时候,可以用特殊情况 index=0 来验证写的条件是否正确
while (index--) { // 注意中止条件,需要循环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=null
while (cur->next!=nullptr) {
cur=cur->next;
}
cur->next = NewNode;
_size++;
// NewNode->next = null; 因为在定义一个新的node时,默认它的next就是null,所以这个就不用了
}
// 要在第n个节点前插入,需要将cur指向第n-1个节点,然后在第n-1个节点后插入
// 也就是要保证第n个节点是cur->next,才能用cur在第n个节点前插入,才不会插入错误
void addAtIndex(int index, int val) {
if(index > _size) return; // 如果index大于链表的长度,则返回空
if(index < 0) index = 0; // 若index为0,那么在头节点前插入,即新插入的节点为链表的新头节点;考虑当index小于0时,同样在头节点前插入,直接将Index设置为0(不写这两个会报错 Line 63: Char 30: runtime error: member access within null pointer of type 'LinkedNode' (solution.cpp) SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:68:30
LinkedNode* NewNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while (index) {
cur = cur->next;
index--; //while(index)... index--;等价于while(index--)
}
NewNode->next = cur->next; // 在第n个节点前插入也就是 NewNode是第n个节点
cur->next = NewNode; // 重新让cur指向第n个节点NewNode
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
} // 保证下标有效 (不写这个会报错 Line 74: Char 32: runtime error: member access within null pointer of type 'LinkedNode' (solution.cpp) SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:79:32
LinkedNode *cur = _dummyHead;
while (index--) { // 循环结束时,cur->next就是第n个节点,要想删除第n个节点,就是要直接让cur指向第n+1个节点
cur = cur->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针,如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp=nullptr;
_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 反转链表
题目链接
代码随想录链接
面试常考第一题—考察对基础数据结构的操作
两种解法:双指针法 递归法
法一:双指针法 cur->pre
初始化:当前节点cur=head, 当前节点的前一个节点pre=NULL;
遍历:终止条件:cur=NULL;(终止条件不正确可能会导致出现空指针异常或死循环等报错)
提前保存原cur的下一个节点:temp = cur->next;
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 初始化指针cur、pre
ListNode* cur = head;
ListNode* pre = NULL;
ListNode* temp;
// 临时指针temp用于保存原cur的下一个节点
while (cur!=NULL) { // 循环终止的位置;一次循环改变一个节点的方向
temp = cur->next; // 将cur->next提前保存在temp
cur->next = pre; // cur->next 指向 pre 即反转(从原来pre->cur 反转为cur->pre)
pre = cur; // 反转后要将pre和cur后移,以处理下一个节点;注意必须先移动pre,原因是pre要移动到cur的位置,cur要移动到temp的位置
cur = temp; //cur后移,即移动到temp的位置
}
// 循环结束后,cur指向NULL,pre为新的头节点
// 返回新链表的头节点 pre
return pre;
}
};
时间复杂度: O(n)
空间复杂度: O(1)
法二:递归法
递归代码更简洁,但直接看的话比较晦涩难懂,按照双指针的思路写递归的代码
class Solution {
public:
ListNode* reverse(ListNode* cur, ListNode* pre) {
ListNode* temp;
// 当cur为空的时候循环终止,返回新链表的头节点
if (cur==NULL) {
return pre;
}
// 改变每个节点的指向之前,仍要借用temp来保存cur此时的下一个节点(否则无法获取到下一个要反转的节点的位置)
else {temp = cur->next;
cur->next = pre;
// 一次循环 -> 一次递归
// 循环中的pre = cur; cur = temp; 用函数调用来实现
reverse(temp, cur);
}
}
ListNode* reverseList(ListNode* head) {
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(head, NULL);
}
};
正解——从前往后翻转指针指向
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}
};
时间复杂度: O(n), 要递归处理链表的每个节点
空间复杂度: O(n), 递归调用了 n 层栈空间
正解——从后往前翻转指针指向:
class Solution {
public:
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;
}
};
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 若链表为空或者只有一个节点就直接返回
if (!head || !head->next) {
return head;
}
// 递归,直接对子链表(子问题)开始递归,对子链表进行反转
ListNode* newHead = reverseList(head->next);
// 回溯,从后往前,令head->next指向head,就实现了反转
head->next->next = head;
// 原来的head指向NULL
head->next = nullptr;
return newHead;
}
};
递归方法更简洁且不易出错
时间复杂度: O(n), 要递归处理链表的每个节点
空间复杂度: O(n), 递归调用了 n 层栈空间(因为会调用到最后一个节点)
参考链接:
作者:力扣官方题解
链接:https://leetcode.cn/problems/remove-linked-list-elements/solutions/813358/yi-chu-lian-biao-yuan-su-by-leetcode-sol-654m/
https://leetcode.cn/problems/design-linked-list/solutions/1840997/she-ji-lian-biao-by-leetcode-solution-abix/
https://leetcode.cn/problems/reverse-linked-list/solutions/551596/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。