题目描述:给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1 输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7 输出:[]
解题思路:
·最普通的链表操作,只需要将节点next指针直接指向下下一个节点就可以了
·有一个特殊情况就是删除头节点,有两种删除方式
1.直接使用原来的链表来进行删除操作
2.设置一个虚拟头结点再进行删除操作
直接使用原来的链表进行操作的代码如下:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//删除头结点
while(head != NULL && head->val == val){
ListNode* temp = head;
head = head->next;
delete temp;//删除结点释放空间
}
//删除非头结点
ListNode* cur = head;
while(cur != NULL && cur->next != NULL){
if(cur->next->val == val){
ListNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;//删除结点释放空间
}else{
cur = cur->next;
}
}
return head;
}
};
·时间复杂度:O(n)
·空间复杂度:O(1)
设置一个虚拟头结点再进行移除节点操作的代码如下:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
ListNode* cur = dummyHead;
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;
}
};
·时间复杂度:O(n)
· 空间复杂度:O(1)
易错点
1.使用的是C/C++,需要手动删除结点,负责会占用很大的内存,使用Python或Java不需要手动改管理内存
2.遍历链表的过程一定要使用while而不是if,使用if只会遍历一次,如果用两个或以上相同的数据if无法完美处理
3.习惯用LeetCode刷题的小伙伴应该习惯了LeetCode自动初始化的链表,但是我们依旧需要掌握如何初始化链表,方法如下;
//单链表
struct ListNode{
int val;//节点上存储的元素
ListNode* next;//指向下一个节点的指针
ListNode(int x):val(x),next(NULL){}
};
C++也可以默认生成一个构造函数,但是这个构造函数不会初始化任何成员变量
//1.通过自己定义构造函数初始化节点
ListNode* head = new ListNode(5)
//2.使用默认构造函数初始化节点
ListNode* head new ListNode();
head->val = 5;
总结
删除链表是最基础的数据结构,但是平时写题目对其的书写反而最少,通过书写这道题目,让我重新认识了链表以及温习了链表的一些基本性质
题目描述:
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化 MyLinkedList
对象。int get(int index)
获取链表中下标为 index
的节点的值。如果下标无效,则返回 -1
。void addAtHead(int val)
将一个值为 val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为 val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为 val
的节点插入到链表中下标为 index
的节点之前。如果 index
等于链表的长度,那么该节点会被追加到链表的末尾。如果 index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为 index
的节点。示例:
输入 ["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"] [[], [1], [3], [1, 2], [1], [1], [1]] 输出 [null, null, null, null, 2, null, 3] 解释 MyLinkedList myLinkedList = new MyLinkedList(); myLinkedList.addAtHead(1); myLinkedList.addAtTail(3); myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3 myLinkedList.get(1); // 返回 2 myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3 myLinkedList.get(1); // 返回 3
解题思路:
·因为需要进行多次操作,直接对链表操作易出现事物以及混乱,所以均使用虚拟头结点进行操作
代码如下:
class MyLinkedList {
public:
struct LinkedNode{//定义链表
int val;
LinkedNode* next;
LinkedNode(int val):val(val),next(nullptr){}
};
MyLinkedList() {//初始化链表
_pummyHead = new LinkedNode(0);
_size = 0;
}
int get(int index) {//获取到第index给节点个数值,如果index为非法数据返回-1,因为index是从0开始,所以第0给节点是头结点
if(index > (_size-1) || index < 0) return -1;
LinkedNode* cur = _pummyHead->next;
while(index--){//一定是index--如果是--index会陷入死循环
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {//在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _pummyHead->next;
_pummyHead->next = newNode;
_size++;
}
void addAtTail(int val) {//链表最后添加一个节点
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _pummyHead;
while(cur->next != NULL){
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 = _pummyHead;
while(index--){
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
void deleteAtIndex(int index) {//删除第index个节点,如果index大于等于链表的长度,直接return,注意index是从0开始的
if(index >= _size || index < 0) return;
LinkedNode* cur = _pummyHead;
while(index--){
cur = cur->next;
}
LinkedNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;
temp == nullptr;
_size--;
}
//一定不要忘记加上这个
private:
int _size;
LinkedNode* _pummyHead;
};
·时间复杂度:涉及index的相关操作作为O(index),其余为O(1)
·空间复杂度:O(n)
难点:
需要明白链表的基本操作,可以使用纸笔,手动操作一遍,就会明白很多
总结:
这到题虽说不是特别难,但是也是在题解的帮助下,多次尝试才完成,多次反复练习,才融会贯通,这道题已经覆盖了链表的常见操作,是练习链表操作非常好的一道题
题目描述:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2] 输出:[2,1]
示例 3:
输入:head = [] 输出:[]
解题思路:
·重新定义一个链表,将第一个链表中的元素取出后,放入第二个链表中的,即可实现
·考虑使用双指针,但是对双指针的理解并不透彻
·在观看题解后,知道了还能使用递归的方法进行求解
双指针法
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;
}
};
·时间复杂度:O(n)
空间复杂度:O(1)
递归法
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);
}
};
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;
}
};
·时间复杂度:O(n)
·空间复杂度:O(n)
难点
1.双指针法的使用:
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点
2.递归法相对抽象,但是和双指针是一样的逻辑,建议先理解双指针法后再尝试理解递归法
总结:对双指针的掌握依旧不是很牢固,而且有些场景也不知道应不应该,能不能使用双指针,可能是因为题做的较少吧,还没用锻炼出题感,亦或者是对双指针的理解还没有那么透彻。