代码随想录算法训练营day:
今天学习链表,训练营进度有点快感觉跟不太上了
学习内容:
链表的基础的概念:
这里只写了单链表。
C++创建链表的方式:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 构造函数,指向下一个节点的指针,初始化为nullptr
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
ListNode就是一个由结构体声明的节点类型,val就是链表当前节点的储存值,*next是一个指针指向下一个节点的地址(也是ListNode型)。因为普通对象(非指针)在栈上分配内存,其生命周期由作用域控制。当函数返回时,栈上的局部变量会被销毁。链表节点需要在函数调用之间保持其状态,动态内存分配使得节点可以在堆上分配并在整个程序运行期间保持有效。所以使用指针定义下一个节点的地址,并且用new创建节点来在堆上分配内存,不会自动删除。
使用时可以这样:
ListNode* head = new ListNode(5);
因为用了new,在程序结束时要用delete手动释放内存。
Python创建链表的方式:
class ListNode:
def __init__(self, val, next=None):
self.val = val
self.next = next
Python理解起来就简单很多了,ListNode的self具有val和next两个属性,分别对应储存值和下一个节点。
下面是今日习题
学习产出:
203:
虚拟头节点的使用,可以使整个删除过程统一(不然需要特殊处理头节点,要体会这个思想)。删除节点时要顾前顾尾衔接好。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummynode = new ListNode(0,head);
ListNode* temp=dummynode;
while(temp->next != nullptr){
if(temp->next->val==val){
ListNode* cur = temp->next;
temp->next=temp->next->next;
delete cur;
}else{
temp=temp->next;
}
}
head=dummynode->next;
delete dummynode;
return head;
}
};
注意private里的size和虚拟头节点的设置,方便了内部函数调用链表的长度和虚拟节点,也避免了外部破坏内部链表的信息。链表和数组的最关键区别也许就是需要手动实现一些内存和信息的调用。MyLinkedList初始化为一个具有虚拟节点的空列表,需要添加节点来使用各种功能。
class MyLinkedList {
public:
struct ListNode {
int val;
ListNode* next;
ListNode(int val):val(val), next(nullptr){}
};
MyLinkedList() {
dummyLineNode=new ListNode(0);
size=0;
}
int get(int index) {
if(index<0||index>=size){
return -1;
}else{
ListNode *cur=dummyLineNode->next;
for(int i=0;i<index;++i){
cur=cur->next;
}
return cur->val;
}
}
void addAtHead(int val) {
ListNode *newhead = new ListNode(val);
newhead->next=dummyLineNode->next;
dummyLineNode->next=newhead;
size++;
}
void addAtTail(int val) {
ListNode* newtail=new ListNode(val);
ListNode* cur=dummyLineNode;
for(int i=0;i<size;++i){
cur=cur->next;
}
cur->next=newtail;
size++;
}
void addAtIndex(int index, int val) {
if(index<0||index>size){
return;
}else if(index<size){
ListNode* newnode=new ListNode(val);
ListNode* cur=dummyLineNode;
for(int i=0;i<index;++i){
cur=cur->next;
}
newnode->next=cur->next;
cur->next=newnode;
size++;
}else {addAtTail(val);}
}
void deleteAtIndex(int index) {
if(index<0||index>=size){
return;
}else{
ListNode* cur=dummyLineNode;
for(int i=0;i<index;++i){
cur=cur->next;
}
ListNode *temp=cur->next;
cur->next=cur->next->next;
delete temp;
size--;
}
}
private:
int size;
ListNode* dummyLineNode;
};
206:
一开始想创建一个列表(向量)记录所有节点的值,再倒序填入新节点,不出意外地内存超了。
双指针法实现正向箭头变逆向箭头:
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;
}
};
递归法:
递归函数的工作原理就是先递归到底,遇到终止条件后开始回溯。每次递归调用后,当前函数的执行会暂停,直到递归调用返回。回溯过程中,逐层返回并执行每一层的剩余代码。所以在你给出的反转链表的递归函数中,递归会先一路递归到底,然后从最深层开始逐层返回并执行代码,完成链表的反转。
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;
}
};