一、链表理论基础
链表的类型:
①单链表
②双链表
③循环链表
链表的存储方式:
前两天学习的数组,数组在内存中是连续分布的,但是链表在内存中并不是连续分布的,链表在内存中是通过指针域的指针链接各个节点。
而链表的定义,这一步定义,采用了结构体的形式进行定义,同时结构体中采用了节点的构造函数。在C++中,它会默认生成一个构造函数,但是默认生成的构造函数不会初始化任何的成员变量,但是在结构体中进行函数的构造就可以手动进行初始化。
例如:
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
二、相关的链表操作
删除节点
在删除节点的时候需要注意:更改了节点的指针指向后,其实只是让特定的节点不在存储在链表里而已,C++中需要手动释放节点从而释放空间,但Java,Python有自己的内存回收机制,所以无需手动释放。
添加节点
三、题目解析
(一)203.移除链表元素
题目链接: 203. 移除链表元素 - 力扣(LeetCode)
操作链表删除有两个方式:
①使用原链表删除元素 ②设置一个虚拟头节点
(1)使用原链表来删除元素
利用这一种方法在移除元素的时候,就需要分开进行处理,需要考虑当前要移除的元素是否是头节点,因为头节点的前面是没有节点的,所以需要考虑这一点。因此也就有了后续会提到的第二种方法:设置一个虚拟头节点。
那么,头节点究竟如何进行移除呢?头节点的移除,其实就只需要将头节点后移一位,让没有任何指针指向头节点,也就让本来的头节点不在链表中了,也就是head = head->next; 但在这之前,还是需要定义一个临时变量用来存储原本的“头节点”,以便于进行节点的释放,节省空间。
使用原链表代码
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
while(head != NULL && head->val == val) {
ListNode* t;
t = head;
head = head->next;
}
ListNode *current = head;
while (current != NULL && current->next != NULL) {
//注意,删除最后一个节点这里也能实现,注意看if里的条件
if (current->next->val == val) {
ListNode *t = current->next;
current->next = current->next->next;
delete t;
}
else {
current = current->next;
}
}
return head;
}
};
(2)设置一个虚拟头节点来移除元素
这个方法的意思就是,在原本的头节点前面再增加一个“临时头节点”,这样就能统一删除的方法,不需要再考虑当前的“节点”还是不是“头节点”,但是这里面存在一个易错点,就是当我们完成元素的移除后,我们习惯性的可能会返回head,但是原本的head在一定概率上会被删除,也就是原本的head其实不一定存在了,但是与此同时,我们又该如何返回一个新链表?以及删除、释放掉我们最开始所设置的虚拟头节点?
其实不难想到,我们要返回的是虚拟头节点的next,但是如果直接以这种形式返回的话,就没办法做到释放虚拟头节点,在这种情况下的解决办法就是将虚拟头节点的next赋值给head,然后进行释放节点,释放完以后便可以采用return head来返回新的链表。
设置虚拟头节点代码
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* p = new ListNode(0);
p->next = head;
ListNode *t = p;
while(t->next != NULL) {
if(t->next->val == val) {
t->next = t->next->next;
}
else {
t = t->next;
}
}
head = p->next;
delete p;
return head;
}
};
这一题的代码在写的时候还是出现了一些问题,但是还是出现了一些问题,例如,我需要先设定一个新的变量才存储所设置的临时节点,以便于后续的删除,还有一个则是进行更新遍历的变量。
(3)其他
面对这一题,还有一种方法,应该也有不少人想到过,也就是递归。在创建树,创建节点等等很多情况下都会运用到递归函数,但是个人认为它比较消耗时间,所以针对本题并没有深入地去自己写,但可以参考如下代码。
递归函数写法:
来源:代码随想录代码随想录 (programmercarl.com)
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 基础情况:空链表
if (head == nullptr) {
return nullptr;
}
// 递归处理
if (head->val == val) {
ListNode* newHead = removeElements(head->next, val);
delete head;
return newHead;
} else {
head->next = removeElements(head->next, val);
return head;
}
}
};
(二)707.设计链表
这一题整体的代码就略微多一点,但是涉及到了链表的大部分操作,显得也就比较重要。
思路描述及代码块
在类中,主要实现的功能是,初始化对象,获取特定下标值的节点的值,在头节点前插入一个节点,在尾节点后插入一个节点,在第n个节点之前插入一个节点,删除下标为特定值的节点。
①初始化对象
MyLinkedList() {
dum = new LinkedNode(0);
size = 0;
}
②获取链表下标为index的节点的值
int get(int index) {
if (index > size -1 || index < 0) {
return -1;
}
else {
LinkedNode *cur = dum->next;
while(index--) {
cur = cur->next;
}
return cur->val;
}
}
③将一个值为val的节点插入到链表中的第一个元素之前
④将一个值为val的节点作为该链表的最后一个节点
⑤将一个值为val的节点插入到链表下标为index的节点之前
⑥删除下标为index的节点
这一题的代码我在力扣上没提交,因为测试中一直报错emmm后续再研究研究
(三)206.反转链表
文章链接:代码随想录 (programmercarl.com)
视频链接:
这一题的难点在于,反转的思路及其节点之间的搭建过程。节点的反转,就是将头节点变成尾节点,而尾节点变成头节点,我认为最难的就是处理节点之间的联系以及边界的处理。同时,这一题仿佛又用到了前两天所学到的“双指针”,这一题的双指针也和前两天的又有所不同了。
这一题的双指针,主要是用在了一前一后的指向,以便于改变链表之间的连接关系,还有一个就是代码的循环中代码怎么写。
图中
①为先断掉cur的next,但需要提前将原cur->next存储起来
②为更改cur的next
③④为移动指针
该思路代码如下:
while(cur) {
t = cur->next;
cur->next = pre;
pre = cur;
cur = t;
}
提交通过代码:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *cur = head,*pre = NULL ,*t;
while(cur) {
t = cur->next;
cur->next = pre;
pre = cur;
cur = t;
}
return pre;
}
};