一.链表基本知识
1.链表
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针)。
链表的入口节点称为链表的头结点也就是head。
2.分类
单链表,双链表,循环链表。
3.存储方式
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
4.链表的定义
c / c++:
struct linkNode {
int info;
linkNode* next;
linkNode(int val): info(val), next(nullptr) {} //自定义构造函数
//采用自定义构造函数可以这样定义节点:
linkNode* p = new linkNode(1); //可以直接赋值
//如果不采用自定义构造函数也可以,系统有默认的构造函数,但是不能直接赋值
linkNode* p = new linkNode();
p -> info = 1;
}
5.链表的删除、添加
这里不过多赘述,只是需要注意的是:在c / c++ 中,系统没有提供内存回收机制,所以需要我们手动释放。(这里在讲707.设计链表时会详细描述!)
二.移除链表元素
1.题目描述
题意:删除链表中等于给定值 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 输出:[]
2.思路
分为两种情况:删除头结点,删除链表中间的结点。
这就涉及到 两种方法:
a.直接在原链表上操作
如果是头结点,在判断头结点不为空时,直接将头指针赋为null,然后删除头结点即可。
如果是中间结点(包括尾结点),让该节点的前一个节点中指向下一个节点的指针指向该节点的下一个节点,然后删除该节点即可。
附代码:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//头节点
ListNode* tem;
while (head != nullptr && head -> val ==val) {
tem = head;
head = head -> next;
delete tem;
}
//中间节点
ListNode* cur = head;
while (cur && cur->next) {
if (cur->next->val == val) {
ListNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;
}
else {
cur = cur->next;
}
}
return head;
}
};
注意:
(1)在头节点中:
tem 只是一个临时存储head 节点的变量,它的作用是指向原来的head 节点,在原 head 节点被移除后,释放 head 节点的内存,所以不能用它进行操作。其次,每一次的循环中,tem 都要指向每次新链表的头节点 head ,所以对于 tem 的赋值要在循环里。
为什么要使用 while 而不是 if ?
因为如果这个链表是这样的: 1 1 1 1 1 1 要删除的节点数值也是1
那按理说删除后这就是个空表了,但如果使用 if ,他只会进行一次删除,答案自然就不对了。
(2)在中间节点中:
因为这样的删除要通过该节点的前一个节点进行,如果令 cur 直接指向要被删除的节点,那么该节点的前一个结点将无法求解,所以我们要保证 cur 指向的是被删除节点的 前一个结点。
其次,要保证被删除节点存在,则要保证 cur 所指向的该节点的前一个结点不为空且该节点不为空,在此情况下,如果该节点的数值==val,则进行删除操作,否则进行下一个节点的遍历。
最后,temp 的作用和上述 tem 的作用一样。
要记得使用 c / c++ 要手动释放被删除节点的内存。
(3)return
这里直接返回 head ,因为两种情况下,head 均被保存,所以直接返回即可。
b.采用虚拟头节点
由于上述方法中,每次还要判断删除的是不是头节点,过于麻烦,所以我们采用虚拟头节点的办法,将方法统一,代码简化。
虚拟头节点,顾名思义,就是在原来头节点前面再加上一个节点,让其作为链表的头节点,但本身她并不存在,所以称为虚拟。
附代码:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//先创建出来虚拟节点,让指针 dummyHead 指向它
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head; //连接到原链表中
ListNode* cur = dummyHead;
while (cur && cur->next) {
if (cur->next->val == val) {
ListNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;
}
else {
cur = cur->next;
}
}
return dummyHead->next;
}
};
注意:
(1)return
这里要返回 dummyHead -> next, 因为在操作过程中,head 节点有可能已经被删除了(因为此时的head 就相当于是链表中的节点了),但 dummyHead -> next 却永远指向的是原链表中的第一给节点,就相当于是新链表的头节点。所以这里切记不能写成head,否则可能就会出现 “ heap-use-after-free” 的错误,意思就是使用了已经被释放过的指针。
(2)为什么一定要使用临时指针进行释放?
因为返回值要新表的头指针,如果直接拿原表进行修改释放,头指针就会被修改或释放,就无法提供返回值了。
三.设计链表
1.题目描述
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
2.思路
附代码:
struct LinkNode{
int val;
LinkNode* next;
LinkNode(int val):val(val), next(nullptr) {}
};
class MyLinkedList {
private:
int size; //链表长度
LinkNode* dummyHead;
public:
MyLinkedList() {
size = 0;
dummyHead = new LinkNode(0); //虚拟头节点
}
int get(int index) {
if (index > (size - 1) || index < 0 ) {
return -1;
}
LinkNode* cur = dummyHead->next;
while (index--) {
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
LinkNode* newNode = new LinkNode(val);
newNode->next = dummyHead->next;
dummyHead->next = newNode;
size++;
}
void addAtTail(int val) {
LinkNode* newNode = new LinkNode(val);
LinkNode* cur = dummyHead;
while (cur->next) {
cur = cur->next;
}
cur->next = newNode;
newNode->next = nullptr;
size++;
}
void addAtIndex(int index, int val) {
LinkNode* newNode = new LinkNode(val);
LinkNode* cur = dummyHead;
if (index < 0) {
index = 0;
}
if (index > size) {
return;
}
if (index == size) {
while (cur->next) {
cur = cur->next;
}
cur->next = newNode;
newNode->next = nullptr;
}
else {
while (index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
}
size++;
}
void deleteAtIndex(int index) {
LinkNode* cur = dummyHead;
if (index >= 0 && index < size) {
while (index--) {
cur = cur->next;
}
LinkNode* temp = cur->next;
cur->next = cur->next->next;
size--;
delete temp;
}
//size--;
}
//size--;
};
/**
* 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);
*/
注意:
(1)题目中没有结构体的要自己定义结构体。(结构体最后记得+ ;)
(2)上下使用两个指针的要注意名称不能相同,否则有可能使用被释放的指针。
(3)定义 dummyHead 只能定义成指针类型,否则下面一步:dummyHead = new linkNode(0);
就会报错:“candidate function (the implicit copy assignment operator) not viable:
no known conversion from 'linkNode *' to 'const linkNode' for 1st argument; dereference the argument with *
第1行:字符9:注意:候选函数(隐式移动赋值运算符)不可行:第一个参数没有从“linkNode*”到“linkNode”的已知转换;”
(4)get 函数中,linkNode* cur = dummyHead->next ;如果写成 dummyHead 那么在遍历时,就会把虚拟头节点当成第 0 个节点,而题目要求第0 个节点是从原链表的第一个开始的(此时就相当于原链表 0 下标的节点变成了 1 下标)所以遍历就会出错。(具体看题目要求)
(5)因为题目中对于index 的判断要使用 链表的长度 size ,所以再进行每一步操作时要判断size是否要改变,也要记得去改变size的值。
(6)这里有个点没搞懂: while (index--) 不能写成 --index ,代码随想录中写的是这样会造成死循环,但我想了很久也没明白怎么会造成死循环呢?我觉得顶多就是打印出问题...
所以拜托有能看到这篇文章的且刚好知道问题答案的小伙伴在评论区告诉我一下(跪谢!!
四. 反转链表
1.题目描述
题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
2.思路
a.利用指针直接在原链表上操作
附代码:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* cur, * pre, * temp;
cur = head;
pre = nullptr;
while (cur) {
temp = cur->next;
cur-> next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};
注意:
(1)循环的条件不能是 cur —>next 。
(2) return的返回值。
(3)赋值顺序。
这篇博客问题还蛮多的,之后搞懂了再补充吧!(拖了两天才写完的博客...