文章目录
203.移除链表元素
文章链接:203.移除链表元素
视频链接:手把手带你学会操作链表 | LeetCode:203.移除链表元素
状态:循环条件的设置while(cur)和while(cur->next != nullptr)
虚拟头结点在链表内容中非常实用,这里分别用两种方法解决该问题。
循环条件的设置while(cur)和while(cur->next != nullptr)有什么不同呢?
主要是关注我们在操作链表时,cur应该出现在哪里,如果我们待删除的结点是targetNode,那么我们的cur应该在targetNode的前一个位置,所以循环条件必须是while(cur->next != nullptr)
直接操作原链表
不使用虚拟头结点的话,我们就要考虑如果要删除的结点就是头结点怎么办?这个时候我们需要分情况讨论。
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;
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;
}
};
有以下几个重要的点:
- 一定要先检测头结点,并且,很有可能从头开始连续好几个结点都是val,所以我们一定要用
while
循环来删除头结点 - 注意删除非头结点时
while
的条件:首先要保证cur不等于空然后cur的下一个结点也不是空,因为这代表了最后一个结点,我们是无法进行操作的。
虚拟头结点
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;
while (cur->next != nullptr) // 更改循环条件为cur->next不为nullptr
{
if (cur->next->val == val) {
ListNode* temp = cur->next; // 保存当前要删除的节点
cur->next = cur->next->next; // 删除操作
delete temp; // 释放内存
} else {
cur = cur->next; // 当前节点不需要删除,cur指针前进
}
}
ListNode* resultHead = dummyHead->next;
delete dummyHead; // 删除虚拟头节点
return resultHead;
}
};
需要注意的是循环体内的if…else逻辑,因为如果如果执行完删除操作,cur应该位置不变,再看下一个结点是否为要删除的元素。所以一定要接else cur=cur->next
if ()
{
}
else{
}
707.设计链表
文章链接:707.设计链表
视频链接:帮你把链表操作学个通透!LeetCode:707.设计链表
状态:如何定义链表结构体不知道,初始化链表没有定义_size。然后许多加减操作都忘记操作_size了。
关于函数
void deleteAtIndex(int index)
其中对于temp = nullptr
的操作非常重要
定义链表结构体
struct LinkedNode
{
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
}
MyLinkedList()
初始化 MyLinkedList
对象。
MyLinkedList()
{
_dummyhead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结
_size = 0;
}
int get(int index)
获取链表中下标为 index
的节点的值。如果下标无效,则返回 -1
。
int get(int index)
{
//下标无效
if (index > (_size - 1) || index < 0)
return -1;
LinkedNode* cur = _dummyhead->next; //指向cur指向真正的头结点,因为真正头结点的下标为0
while(index--)// 如果--index 就会陷入死循环
{
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val)
将一个值为 val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
void addAtHead(int val)
{
LinkedNode* newNode = new LinkedNode(val);
//并不需要tempListNode* temp = _dummyhead->next;
newNode->next = _dummyhead->next;
_dummyhead->next = newNode;
_size++;
}
void addAtTail(int val)
将一个值为 val
的节点追加到链表中作为链表的最后一个元素。
void addAtTail(int val)
{
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyhead;
while (cur->next != null)
{
cur = cur->next;
}
//该句话可以省略newNode->next = nullptr;
cur->next = newNode;
_size++;
}
void addAtIndex(int index, int val)
将一个值为 val
的节点插入到链表中下标为 index
的节点之前。如果 index
等于链表的长度,那么该节点会被追加到链表的末尾。如果 index
比长度更大,该节点将 不会插入 到链表中。
void addAtIndex(int index, int val)
{
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
//判断插入条件
if (index > _size)
return ;
if (index < 0) index = 0;
while (index--)
{
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为 index
的节点。
void deleteAtIndex(int index)
{
//下标无效的情况
if (index >= _size || index < 0)
return;
LinkedNode* cur = _dummyHead;
while(index--) {
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()
打印链表
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
整体CPP代码
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
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++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
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;
206.反转链表
文章链接:206.反转链表
视频链接:帮你拿下反转链表 | LeetCode:206.反转链表
状态:基本能用双指针完成,但是代码还是不够优美。下一个结点的保存被设置在循环里面,代码相对比较复杂;
再一个递归的方法必须掌握,对于链表操作的许多题目都是能够使用递归方法的。
一刷代码
一刷用的是双指针,但是其实本题是不需要虚拟头结点的
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head) return head;
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* right = dummyHead->next;
ListNode* left = dummyHead;
while (right->next) {
ListNode* nextRight = right->next;
right->next = left;
left = right;
right = nextRight;
}
//此时right的下一个结点还指向空,我们让他指回前一个结点,完成最后一步
right->next = left;
//一定要先把head的下一个指向置为空,再删虚拟头结点,不然会导致内存泄漏
head->next = nullptr;
delete dummyHead;
return right;
}
};
双指针法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
}
递归法
迭代法应该怎么去想呢?就是说,我们双指针法的本质,也不过就是不停得尽兴交换,这个时候想直接写出迭代函数最好的方法其实就是模拟一下前几个的操作。
假设有一个不停迭代操作pre、cur的函数。这个函数应该怎么写呢?先写出它的终止条件
reverse(pre, cur)
//先写明迭代法的终止条件
if (cur == NULL) return pre;//这是由于此时pre已经是新的头结点了
ListNode* temp = cur->next;
cur->next = pre;
//之前我们做的操作是pre = cur; cur = temp;来进行指针的移动
//如果利用迭代函数,其实就是把这两个参数按照对应位置传进去就好了
return reverse(cur, temp)
C++代码如下
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 层栈空间