链表基础:
注意链表需要初始化,力扣中已经初始化了,自己写需要加上。
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
203.移除链表元素
删除链表中等于给定值 val 的所有节点。
题目链接
视频链接
思路:
无
看完代码随想录之后的思路:
普通方法:头节点和其他节点有不同的处理方式:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
while(head!=NULL&&head->val==val)
{
ListNode* tmp=head;
head=head->next;
delete tmp;
}
ListNode* cur=head;
while(cur!=NULL&&cur->next!=NULL)
{
if(cur->next->val==val)
{
ListNode* tmp=cur->next;
cur->next=cur->next->next;
delete tmp;
}
else
{
cur=cur->next;
}
}
return head;
}
};
虚拟头节点法:在开头加一个虚拟的头节点,这样就不用区分头节点是否需要删除,主要删除的部分可以统一为一种。注意开头和结尾,需要把虚拟头节点和实际头节点相互转换:实际的头节点可能会发生变化,所以需用使用head=dummyhead->next。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyhead=new ListNode(0);
dummyhead->next=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;
}
};
遇到的困难:
普通方法:
其他节点的循环中,cur->next->val==val不能写在while中,需要写在里面的一层 if 中,否则头节点没找到val值直接就结束了。
虚拟头结点法:
虚拟头节点定义式,不能忘记 * 号
开头和结尾需要将虚拟头结点和实际头节点相互转换,且因为实际头节点可能发生变化,所以只能通过虚拟头节点的下一个来执行。
收获:
C++中链表节点的定义(ListNode* tmp=cur->next;)
链表单个元素指针的用法(->)
C++中内存的释放(delete tmp)
重点:
一定要写一个临时指针指向头节点
707.设计链表
设计链表中的增删功能
思路:
无
看完代码随想录之后的思路:
1.先判断输入参数(链表元素位置)的合法性
2.找到题目所需链表的位置,然后进行操作
class MyLinkedList {
public:
//定义链表的结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
//初始化链表,还需要末尾的私有才能使用,否则会报错:私有定义成员变量,只能类内使用
//和类名相同的函数是构造函数,用于初始化变量,若不写系统自动生成一个构造函数
MyLinkedList() {
_dummyHead=new LinkedNode(0);
_size=0;
}
int get(int index) {//这里链表的序号index从0开始取
if(index<0||index>_size-1)
return -1;
LinkedNode* cur=_dummyHead->next;
while(index--)
{
cur=cur->next;
}
return cur->val;
}
void addAtHead(int val) {
LinkedNode* newNode=new LinkedNode(val);
newNode->next=_dummyHead->next;//不是给newnode,是给newnode->next
_dummyHead->next=newNode;
_size++;
}
void addAtTail(int val) {
LinkedNode* newNode=new LinkedNode(val);
LinkedNode* cur=_dummyHead->next;//提交时必须使用_dummyHead才可以,不太懂,不应该都一样吗
while(cur->next!=nullptr)
{
cur=cur->next;
}
cur->next=newNode;
_size++;
}
void addAtIndex(int index, int val) {
if(index>_size)return;//因为是void,所以返回不了-1
if(index<0)index=0;
LinkedNode* newNode=new LinkedNode(val);
LinkedNode* cur=_dummyHead;//要在前面插入,需要知道前一个指针的地址(作为cur),所以不是dunmmyNode->next
while(index--)
{
cur=cur->next;
}
newNode->next=cur->next;//index=链表长度在末尾加入,相当于把null向后推了一位
cur->next=newNode;
_size++;
}
void deleteAtIndex(int index) {
if(index<0||index>_size-1)return;
LinkedNode* cur=_dummyHead;
while(index--)
{
cur=cur->next;
}
LinkedNode* tmp=cur->next;
cur->next=cur->next->next;//空指针也可以直接赋值,这样末尾直接删掉
delete tmp;
_size--;
}
private:
int _size;
LinkedNode* _dummyHead;
};
遇到的困难:
定义链表的结构体
使用了自引用结构体进行定义
在结构体内部引用这个结构体的名称,但是注意一定是使用指针的形式,否则这个结构体就构成了代码死循环。
struct LinkedNode { int val; //链表节点上的函数 LinkedNode* next; //链表指向下一个节点的指针(自引用结构体) //这句式构造函数,自己可以不定义,C++默认生成一个构造函数 LinkedNode(int val):val(val), next(nullptr){} };
结构体定义完成后进行初始化
通过自己定义构造函数初始化:
ListNode* head = new ListNode(5);
默认构造函数初始化:
ListNode* head = new ListNode(); //初始化 head->val = 5; //给变量赋值
公有和私有:
- 类中:变量为属性,函数为方法
- 为什么一定要用私有才能正常定义_dummyHead:私有中进行定义,这部分定义只有类内可以使用,两个优点:1.可以自己控制读写权限 2. 可以检测数据的有效性
- 和类名相同的函数是构造函数,用于初始化变量,若不写系统自动生成一个构造函数
提交时在末尾加必须使cur等于_dummyHead,才会不报错
提交时的输入:
["MyLinkedList","addAtTail","get"] [[],[1],[0]]
考虑特例:若原始链表是个空链表,那么cur定义为_dummyHead->next时,cur已经是个空指针了,没有cur->next可寻。
收获:
C++的结构体
类的属性和方法,公有和私有,构造函数
重点:
一定要注意边界的特殊条件:没有元素时、只有一个元素时
操作的第n个点,一定是cur->next,这样才能利用cur来增加或者删除。否则在增加或者删除时,会操作到n-1或者n+1。
添加节点时,一定先把后一个节点的地址写在新节点的指针上,再将新节点的地址写在前一个节点的指针上,否则顺序反了之后,后一个节点的地址就找不到了。
206.反转链表
将输入的链表翻转后输出
思路:
无
看完代码随想录之后的思路:
双指针法:
三个变量,cur、pre(原链表中的前一个)、temp(临时存放cur的变量),三个互相转换。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp;
ListNode*cur =head;
ListNode *pre=NULL;
while(cur)
{
temp=cur->next;
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;
}
};
递归方法:
只存临时地址和翻转链接方向,不做推进的步骤(交给递归做)
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur)
{
if(cur==NULL)return pre;
ListNode* temp=cur->next;
cur->next=pre;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
return reverse(NULL,head);
}
};
遇到的困难:
没遇到啥困难,递归很牛逼,用一步递归节省了两步推进操作。递归每次循环少1步操作,总体循环次数多1次,很划算。
收获:
递归:
递归就是在函数内部调用本函数,一直执行下去,自己写条件来终止——一般用 if 语句来终止。
若是没有终止命令,会因为数据存满栈而不返回或取出,导致栈溢出错误。
ListNode定义函数时使用的结构体指针。
重点:
双指针法返回的是pre,因为最后cur指向了NULL,新的head为pre
双指针循环的条件,否则容易出现空指针异常、死循环的错误
双指针写法的推进步骤,注意先把cur给pre,再把temp给cur,否则连不起来。
递归中的初始参数的赋值、递归函数中的 if 意义、循环递归中的参数赋值 不能搞混。