今天开始链表
链表的定义
接下来说一说链表的定义。
链表节点的定义,很多同学在面试的时候都写不好。
这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。
而在面试的时候,一旦要自己手写链表,就写的错漏百出。
这里我给出C/C++的定义链表节点方式,如下所示:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。
但是这个构造函数不会初始化任何成员变量,下面我来举两个例子:
通过自己定义构造函数初始化节点:
ListNode* head = new ListNode(5);
使用默认构造函数初始化节点:
ListNode* head = new ListNode();
head->val = 5;
所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!
补充一些对于这个C++中对链表的设置的语法
1.在 C++ 中,struct 是一种自定义数据类型的声明方式。在这里,我们定义了一个名为 ListNode 的结构体。
ListNode 结构体包含两个成员变量:
int val:表示节点上存储的元素的值,它是一个整型变量。
ListNode *next:表示指向下一个节点的指针,它是一个指向 ListNode 类型的指针。
除此之外,这个结构体还有一个构造函数。构造函数的作用是创建一个新的 ListNode 节点并初始化它的成员变量。在这个构造函数中,x 是传入的参数,它被用来初始化节点的 val 成员变量,而 next 成员变量则被初始化为 NULL,表示当前节点没有下一个节点。
//
是的,这里在定义 ListNode 结构体时直接调用了它的构造函数,这是合法的。这种做法称为“在定义结构体时初始化成员变量”,也称为“成员初始化列表”。
在 C++ 中,可以在定义结构体或类类型时,通过成员初始化列表的方式来初始化成员变量。上述代码中,在 ListNode(int x) 函数的参数列表后面使用“冒号”(:)并跟随一组成员初始化列表,来完成对 val 和 next 成员变量的初始化。
因此,在创建 ListNode 类型的对象时,可以直接调用该结构体的构造函数,并为其传入一个整数参数作为节点的值,从而对 val 成员变量进行初始化,同时将 next 指针成员变量初始化为 NULL。
203.移除链表元素
区分首元节点和头节点,在此处是首元节点
链表里的元素(Listnode),它有自己的val值,也有自己的下一个节点,但是他没有名字,需要你自己去定义一个temp名字,然后再删了他,就删掉了他的内存空间。
方法一:不设虚拟头节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
//感谢力扣帮忙定义单链表
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){//cur!=NULL是啥情况,是提防到链表末尾吗;在第一个循环结束的时候,此时头节点没被删的话,他必不是val值;所以cur是被查过的,他一定不是val,但他有可能已经空了(在开头删除头结点的时候就被删完了),所以有前面的判断条件;在这边查的都是cur->next的val值并删除他们。
if(cur->next->val==val){
ListNode* tmp=cur->next;
cur->next=cur->next->next;
delete tmp;
}
else{//这边不是动cur->next的位置,让cur向后位移一次,从而下次查新cur->next的值
cur=cur->next;
}
}
return head; //cur是去查阅值的,在不断移动,停留在最后一个Val值节点前面的那个点;头结点在初始删完后仍然是head
}
};
方法二:设置虚拟头节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* head_temp=new ListNode (0);
head_temp->next=head;
ListNode* cur=head_temp;
while (cur->next->!=NULL){
if(cur->next->val ==val){
ListNode* temp=cur->next;
cur->next=cur->next->next;
delete temp;
cur=cur->next; //就是这一行写的有问题,不能这么写。前面判断后面链表节点是要删除的,所以要再向后一位变成了next,此时cur的位置不需要变动。只有节点不删除时,才移动到下一个节点。
}
}
head=head_temp->next;
delete head_temp;
return head;
//再复习了一下,cur是移动扫描的,temp是虚拟头结点,最后要删的
}
};
添加一个else就好啦
707. 设计链表
熟悉链表的增改删查,传统艺能啦!
第一段的LinkedNode(int val):val(val),next(nullptr){}
中这段代码是一个链表节点的构造函数,它接受一个整数类型的参数val,并将该值赋给当前节点的val成员变量。同时,它还将当前节点的next成员变量初始化为nullptr,表示当前节点的下一个节点为空指针。
通常情况下,链表的每个节点都包含一个值和一个指向下一个节点的指针(如果是双向链表则还有一个指向前一个节点的指针)。这个构造函数就是用来初始化链表节点中的这两个成员变量的。
nullptr是C++11中引入的一种空指针常量,用于表示一个空指针。
在早期版本的C++中,通常使用NULL宏来表示空指针。但是NULL实际上是一个整数类型的常量,而不是指针类型的常量,在某些情况下可能会导致类型转换问题。为了避免这个问题,C++11引入了一个新的关键字nullptr,专门用来表示指针类型的空值。
使用nullptr表示空指针可以提高代码的可读性和安全性。如果尝试将一个指针赋值为nullptr,那么就不会发生任何意外的类型转换,从而避免了潜在的编程错误。
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->val;
}
关于为什么不能使用whie(--x)
?
如果使用 --index,那么每次执行完循环体内的代码后,while 循环的条件语句 index-- 会将 index 的值减 1,因此 while 循环的终止条件就无法被满足,导致程序陷入死循环。 因为这个执行语句一直可以执行,所以一直会work。
相反,使用 index-- 可以正确地遍历链表,因为它先使用原始值,然后再将 index 减 1。这样当 index 减到 0 时,while 循环的终止条件就会达成,从而结束循环。
dummyhead的作用
Dummy head(哑节点)是一种在链表数据结构中常见的技巧,它通常是指在链表头部添加一个虚拟节点,目的是为了方便链表操作。比如,在删除链表中某个节点时,如果要删除的节点是链表的第一个节点,那么需要对链表头进行特殊处理,这会导致代码实现变得复杂。但是,如果在链表头部添加一个dummy head作为哨兵节点,那么无论哪个节点被删除,链表头的处理都可以和其他节点的处理方法一致,不需要特殊考虑。
所以cur一开始设定的时候就是dummyhead的下一个。
void addAtHead(int val) {
LinkedNode* Newnode= new LinkedNode(val);
Newnode->next=_DummyHead->next;
_DummyHead->next=Newnode;
_size++; //别忘了在加入玩头结点后size++!
}
void addAtTail(int val) {
LinkedNode* Newnode= new LinkedNode(val);
//LinkedNode* cur = _DummyHead->next; //这里也有问题,初始应该只是指向虚拟头结点
LinkedNode* cur = _DummyHead; //感觉这个也是害怕给了个空链表,那对自己一检索就是野指针了。
while(cur->next!=nullptr){
cur=cur->next;
}
// Newnode=cur->next; //这一行我写错了,此时的cur_next是没有值的,是需要被赋予新的尾节点,所以此时需要额外的加上Newnode。应是Newnode赋予给cur->next。
cur->next=Newnode;
_size++;
}
void addAtIndex(int index, int val) {
if(index>_size)return;
if(index<0)index=0;
LinkedNode* Newnode= new LinkedNode(val);
LinkedNode* cur=_DummyHead; //注意这边因为是加在index的节点之前,所以cur扫到index前一位。和前面的查找区分开来。
while(index--){
cur=cur->next;
}
Newnode->next=cur->next;
cur->next=Newnode;
_size++;
}
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp=nullptr;
_size--;
}
private:
int _size;
LinkedNode * _dummyHead;
};
在链表中定义私有变量(private)时,也是参考以下格式:
class MyLinkedList {
public:
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val) : val(val), next(nullptr) {}
};
MyLinkedList() {
_DummyHead = new LinkedNode(0);
_tail = _DummyHead; // 初始化尾节点为虚拟头节点
_size = 0;
}
// other methods for manipulating the linked list
private:
LinkedNode* _DummyHead;
LinkedNode* _tail; // 定义私有变量
int _size;
};
需要重新声明结构体class的名称哦!
206.反转链表
方法一:双指针
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* tmp;//需要一个临时指针先保存cur的后一位,从而保证cur在改变方向后可以向后一位。
ListNode* cur= head;
ListNode* pre=NULL;
while(cur){
tmp=cur->next;
cur->next=pre;
pre=cur;
cur=tmp;
}
return pre;
}
};
在理解完思路后,本题目其实并不难,更深刻的理解了地址和地址->next的地址的含义…
方法二:递归法
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre; //这是递归的终止条件,其实是和前面的While循环里的条件相对应的。
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);
}
};
在使用递归的时候,找到递归中重复的元素(这里是temp和cur)和递归的终止条件是非常重要的。
递归中参与的元素,往往都是中间变量和第一个变量,对应高考数学的an和an+1作为递归式子中的主要部分。
关于递归的进一步理解还是任重而道远哇,所谓的借用了栈的思想,先进后出,这个还要慢慢去理解…