理论文章:链表part1
leetcode 203 删除节点:leetcode 203
讲解文章:leetcode203
leetcode 707 设计链表:leetcode707
讲解文章:leetcode707
leetcode206 反转链表leetcode206
讲解文章:leetcode206
视频讲解:206讲解视频
目录
1,删除节点:leetcode 203
1,熟悉的题目,陌生的手感
2,安全的写法
2,设计链表:leetcode707
3,反转链表:leetcode206
4,递归写法:
总结
数据结构课上链表是最爱考的题目之一,所以对于链表的简单操作都还是记得,但是实际在写题目的时候,也报了好几次错。这道题的想法和技巧都不难,想法就是,检查下一个节点的值,删除就是把这个节点的next连到下下个节点上去:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* pointer = new ListNode();
ListNode* result = new ListNode();
result->next = head;
pointer = result;
while(pointer != nullptr&&pointer->next!=nullptr){
if(pointer->next->val == val){
pointer->next = pointer->next->next;
}else{
pointer = pointer->next;
}
}
return result->next;
}
};
这道题卡了我几次的地方是里面的else部分。我最开始的写法是,if出来后每一次都pointer=pointer->next, 看似是没啥为题,但是报了错后仔细想了一下才发现,if执行后,实际上pointer->next的位置没有检查,如果直接pointer=pointer->next,会漏掉一个应该检查的节点。因此把他放到else里面可以保证每个节点都能被检查到
先看卡哥的示范代码:
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;
}
};
对比我的代码可以发现一些问题:
1,dummyhead创建后,实际上只需要一个扫描的指针指向每个节点即可,因为dummyhead的空间不存在,需要new,而pointer所在的空间已经存在了,再分配一个空间会造成浪费。
2,我的代码中,申请的堆资源没有及时的释放,应该删除的节点也没有及时释放,在离开函数体后,这些空间就失去管理了。这是必须要赶紧的地方:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* result = new ListNode();
result->next = head;
ListNode* pointer = result;
while(pointer->next!=nullptr){
if(pointer->next->val == val){
ListNode* onDelete = pointer->next;
pointer->next = pointer->next->next;
delete onDelete;
}else{
pointer = pointer->next;
}
}
head = result->next;
delete result;
return head;
}
};
最后一点要注意,head原来所在的空间可能被释放掉,必须给head重新赋值。
仔细看下来,这道题并没有那么复杂,具体拆解一下:
1,getIndex,应该是需要从头一个一个遍历找
2,addAtHead,经典链表头插,有一个指向头节点的指针即可。
3,addAtTail,尾插,可以设一个指向末尾节点的指针就不需要每次插入一个一个遍历过去了
4,addAtIndex,链表中间插入,也不是很复杂,核心是别数错位置就行。注意是查到index前面,就是改index-1的next
5,deleteAtIndex,也是从链表中间删除
看上去,整体上,从方便的角度讲,可以设一个头节点,一个尾节点,一个length变量存一下链表长度。但是,如果设置了尾节点,那每次删除后可能需要重新定位一下尾节点位置。因此不多找这个麻烦。
class MyLinkedList {
private:
struct Node {
int val;
Node* next;
Node(int _val) : val(_val), next(nullptr) {}
};
Node* head;
int size;
public:
MyLinkedList() {
head = nullptr;
size = 0;
}
int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
Node* current = head;
for (int i = 0; i < index; i++) {
current = current->next;
}
return current->val;
}
void addAtHead(int val) {
Node* newNode = new Node(val);
newNode->next = head;
head = newNode;
size++;
}
void addAtTail(int val) {
Node* newNode = new Node(val);
if (!head) {
head = newNode;
} else {
Node* current = head;
while (current->next) {
current = current->next;
}
current->next = newNode;
}
size++;
}
void addAtIndex(int index, int val) {
if (index < 0 || index > size) {
return;
}
if (index == 0) {
addAtHead(val);
} else if (index == size) {
addAtTail(val);
} else {
Node* newNode = new Node(val);
Node* current = head;
for (int i = 0; i < index - 1; i++) {
current = current->next;
}
newNode->next = current->next;
current->next = newNode;
size++;
}
}
void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
if (index == 0) {
Node* temp = head;
head = head->next;
delete temp;
} else {
Node* current = head;
for (int i = 0; i < index - 1; i++) {
current = current->next;
}
Node* temp = current->next;
current->next = temp->next;
delete temp;
}
size--;
}
};
显然这是一个基于单链表实现的方法。
这道题有一个需要注意的地方,就是每次数数到几的问题,get是找index的位置的数,我就数到了index,而deleteAtIndex和addAtIndex都是需要对index-1的节点进行操作,因此得数到index-1的位置上,被这个问题坑了,标记一下。
想了半天,不原地的做法太简单了,头插法一个一个遍历放进去就行。所以得想一下,原地的做法:
1,原地的做法,一定是针对指针操作的。
2,思考三个节点,
a->b->c
组成的链表。
目前看来,至少是需要两个指针才能完成一个->的翻转,
3,首先,pointer1,pointer2都指向a
4,pointer2先动,指向b, .......走不下去了,pointer2指向pointer1好说,但是链接没了,pointer2怎么继续往下走呢?
反转链表讲解
看完视频后,双指针的思路清晰的多了:
1,首先,我遇到的问题,需要第三个指针temp来解决,思路就是,在current->next = pre之前,让那个temp保存current->next,这样就可以让current接着往下走了
2,pre和current保持一个的距离,current初始化为head,pre初始化为nullptr,这样往下走的时候就是正常的。
3,移动的逻辑就是,temp=current->next; current->next = pre;
pre = current;
current = temp;
4,先保存temp=current->next,再转current->next,再让pre动,最后current动,这样可以保证一个一个往下走。
5,终止条件是什么?应该是current!=nullptr,因为current走到最后一个的时候,仍然需要翻转一次,因此不能是current->next= nullptr
代码如下:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* current = head;
ListNode* pre = nullptr;
ListNode* temp;
while(current!=nullptr){
temp = current->next;
current->next = pre;
pre = current;
current = temp;
}
return pre;
}
};
果然,思路清晰是最重要的。
看了视频的进度条后,意识到这个过程确实可以递归:
递归无非两个问题,递归过程,终止条件。
递归过程,就是双指针中while循环之中的部分
终止条件,同样也是current!=nullptr;
先试着写一些,不纠结递归的具体过程:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
head = reverse(nullptr,head);
return head;
}
ListNode* reverse(ListNode* pre, ListNode* current){
if(current == nullptr){
return pre;
}
ListNode* temp = current->next;
current->next = pre;
pre = current;
current = temp;
return reverse(pre,current);
}
};
对比卡哥的递归代码可以发现,其实我这里是循环硬改的递归,多了两步:
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);
}
};
在第二步反转后,其实已经知道了下一次迭代的pre和current,向reverse中传递值就已经相当于是赋值了,因此不需要再额外写赋值语句。
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;
}
};
卡哥给出的第二种递归略微有点抽象,想不明白了,之后有时间再想一想吧
链表的数据结构并不难,难的是怎么用指针正确的完成操作。尤其是第三题,反转链表的过程涉及到很细节的链表指针的移动,移动过程,先temp,再current->next,再pre,最后current的过程也需要再理解消化一下。