欢迎光临
我们一直在努力

代码随想录算法训练营第三天 | 203.移除链表元素、707.设计链表、206.反转链表

链表

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

在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了。但自己也要会写出来:

// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(nullptr) {} // 节点的构造函数
};

如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!因此尽量自己把构造函数加上。构造函数使用 C++ 中的构造函数初始化列表语法,可以理解为:“创建一个ListNode对象,用x初始化val,用nullptr初始化next”。别忘了最后的{},后面没有分号。

链表的增添和删除都是O(1)操作,也不会影响到其他节点。但是要注意,要是删除最后个节点,需要从头节点查找到倒数第二个节点,通过next指针进行删除操作,查找的时间复杂度是O(n)。因为链表节点在内存里不连续,每个节点只知道“下一个是谁”(单链表)。

NULL 和 nullptr 都表示“空指针”,但在 C++ 里推荐用 nullptr。nullptr 是 C++11 引入的专门的空指针字面量,类型是std::nullptr_t,只会转换成任意指针类型(或指向成员的指针),不会当成普通整数用,因此更安全。

203.移除链表元素

题目链接:https://leetcode.cn/problems/remove-linked-list-elements/ 文章讲解:代码随想录 视频讲解:手把手带你学会操作链表 | LeetCode:203.移除链表元素

在单链表中移除头结点和移除其他节点的操作方式是不一样。因此可以通过设置一个虚拟头结点的方式,这样原链表的所有节点就都可以按照统一的方式进行移除了。

class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
ListNode* cur = dummyHead;//cur用来遍历链表,此处将cur初始指向虚拟头节点
while (cur->next != NULL) { //注意是while
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;
}
};

【注】 1、注意cur初始指向dummyHead还是dummyHead->next,此处将cur初始指向虚拟头节点dummyHead,因为在链表中想要删除某一元素,必须要知道它的前一个元素是什么。 2、为何最后要head = dummyHead->next? 因为在删除过程中你可能把原来的第一个节点也删掉了(比如要删的值就在头部),此时原来的 head 已经不可靠了,而 dummyHead->next 永远指向“当前删除后链表的第一个有效节点”。 3、注意temp和dummyHead的释放

707.设计链表

题目链接:https://leetcode.cn/problems/design-linked-list/description/ 文章讲解:代码随想录 视频讲解:帮你把链表操作学个通透!LeetCode:707.设计链表

这道题目设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目。还是采用设置一个虚拟头结点再进行操作的方式,更为简便。

注意

class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
}; //注意分号别忘了!!!

// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0; //初始化size为0,此时链表未添加任何元素
}

// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
//这里是获取节点值,不用考虑把dummyhead->next赋为head的事情
int get(int index) {
if (index > (_size 1) || index < 0) //因为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++; //记得size的更新
}

// 在链表最后面添加一个节点,因为要找到链表最后一个元素,因此需要遍历变量cur
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
//注意此处不可以是dummyhead->next,因为若没有元素,dummyhead->next是nullptr
//此时cur == nullptr,紧接着访问cur->next就会空指针解引用(崩溃)
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++; //记得size的更新
}

// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return; //注意这里是index>size,因为题目中说明index等于链表的长度,那么该节点会被追加到链表的末尾
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;
}
//跳出循环时,cur此时指向要删除元素的前一个元素(画简图易得)
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;

};

【注】 1、注意索引从0开始,因此在获取到第index个节点数值的函数get中,index > (_size – 1)则返回错误。 2、插入和删除确定赋值关系时,可以画个图更清晰 3、记得size的更新增加时size++,删除时size– 4、由于链表是通过指针连接,因此想要找到第index个或最后一个元素,就需要定义一个cur进行遍历。 5、判断cur是指向dummyhead还是dummyhead->next,可以画个图,根据while里的终止条件判断。 6、先判断,再new 7、被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针,如果之后的程序不小心使用了tmp,会指向难以预想的内存空间 8、构造函数中用的dummyhead 和 size要在在类中进行成员变量声明,声明成 private 的核心原因:封装(encapsulation)——它们是链表的“内部实现细节”,不应该让外部随便改,否则很容易把链表状态搞坏。

206.反转链表

题目链接:https://leetcode.cn/problems/reverse-linked-list/description/ 文章讲解:代码随想录 视频讲解:帮你拿下反转链表 | LeetCode:206.反转链表 | 双指针法 | 递归法

双指针法

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null(翻转前pre是cur的前一个节点,翻转后是cur的后一个节点)。

class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) { //当cur遍历到末尾空指针,则循环结束
temp = cur->next; // 保存一下cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作,指定cur->next之前要用temp保存当前cur的下一个元素,否则链表就断连了
// 更新pre和cur指针,一定要先移动pre后移动cur
pre = cur;
cur = temp;
}
return pre;
}
};

递归法

递归法直接写很难写,要对照着双指针写比较好。 递归写法就是:函数自己调用自己,把一个大问题拆成“更小规模的同类问题”,直到遇到“最小情况(终止条件)”为止,然后一层层返回结果。

递归的三要素:

函数定义:这个函数要解决什么子问题 终止条件:什么时候不再递归(否则会无限递归) 递归关系:当前问题怎么转成更小的问题

下面采用递归的思想,把双指针法的while变成“函数自己调用自己”。此题中递归的行为就是每一次pre和cur向右移动

class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法:reverse(cur,temp),其实就是做了这两步
// pre = cur; //cur替换pre
// cur = temp;//remp替换cur,即原本reverse(pre,cur)变为reverse(cur,temp)
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}

};

递归体逐行解释: (1) temp = cur->next 先把下一步要处理的节点保存下来(否则下一行把指针改了就可能找不到后面了)。 temp 相当于迭代里的“缓存 next”

(2) cur->next = pre 把当前节点 cur 的 next 指向 pre,实现“局部反转”: 当前节点被接到已反转链表的前面

(3) return reverse(cur, temp) 推进状态(非常关键): 新的 pre 应该变成当前节点 cur(因为 cur 已经加入反转链表了) 新的 cur 应该变成 temp(继续处理下一节点)

总结

第三天学习了链表相关操作以及双指针操作和递归法。

赞(0)
未经允许不得转载:171主机测评 » 代码随想录算法训练营第三天 | 203.移除链表元素、707.设计链表、206.反转链表
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址