从零单排代码随想录 | Day3 链表第一题、第二题、第三题

今天开始链表

链表的定义

接下来说一说链表的定义。
链表节点的定义,很多同学在面试的时候都写不好。
这是因为平时在刷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作为递归式子中的主要部分。

关于递归的进一步理解还是任重而道远哇,所谓的借用了的思想,先进后出,这个还要慢慢去理解…

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值