代码随想录算法训练营第三天| 203.移除链表元素 、707.设计链表 、206.反转链表

题目链接:leetcode 203、移除链表元素

文章讲解:代码随想录 203、移除链表元素讲解

视频讲解:手把手带你学会操作链表 | LeetCode:203.移除链表元素

自己看到题目的第一想法

题目: 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
提示:
列表中的节点数目在范围 [ 0 , 1 0 4 ] [0, 10^4] [0,104]
1 < = N o d e . v a l < = 50 1 <= Node.val <= 50 1<=Node.val<=50
0 < = v a l < = 50 0 <= val <= 50 0<=val<=50
想法: 这个题目比较基础,因为之前自己学习数据结构的时候也了解过,所以写出题目比较顺利。大致记录一下自己的思路。关键点就两个,一个是删除链表节点要哪些操作,另一个就是考虑特殊情况,比如第一个节点就是val值怎么办。

删除链表节点需要delete当前访问的节点,然后把上一个节点链接到下一个节点,所以需要两个节点指针,一个指向当前访问的节点,另一个指向上一个节点,在循环中不停更新,然后删除节点时,要先给上一个节点的next赋值,然后再delete当前节点,否则就找不到下一个节点的地址了。

为了解决特殊情况,就有了虚拟头节点,因为有了虚拟头节点,原本的开头的节点地位就和其他节点一样了,不需要特别关照。我定义虚拟头节点一开始是建了一个节点对象,后来改成了指针,这样做可以与形参统一,防止写出乱子,初始化虚拟头节点时考虑了一下要赋什么val值,看了一下提示信息写了val的范围,不过还是担心出问题,后来发现不需要赋值val,因为根本不会访问,只要给next指针赋值原来的头节点地址就可以了。

最后大循环就是访问当前节点,当前节点不是nullptr就进行访问,判断是否需要删除。

时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)

自己写的代码:

/**
 * 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(new ListNode);
        HEAD -> next = head;
        ListNode* prev = HEAD;
        ListNode* curr = head;
        while (curr != nullptr) {
            if (curr -> val == val) {
                prev -> next = curr -> next;
                delete curr;
                curr = prev -> next;
            } else {
                curr = curr -> next;
                prev = prev -> next;
            }
        }
        return HEAD -> next;
    }
};

看完代码随想录和大家探讨之后的想法

讲解给的代码:

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、这里访问当前节点只用了一个节点指针,同时访问next和下一个next节点,我自己思考的时候这样想容易写错,所以还是保持自己的写法。
2、最后删除了虚拟头节点的指针,我自己写的没有回收这个内存。

题目链接:leetcode 707、设计链表

文章讲解:代码随想录 707、设计链表讲解

视频讲解:帮你把链表操作学个通透!LeetCode:707.设计链表

自己看到题目的第一想法

题目: 不粘贴了,就是设计一个链表类,实现常用功能。
想法: 这个题目非常常见了,然后自己直接开始写,思路比较清晰,但是由于自己不熟悉,所以还是出现了问题。

这里贴一下自己写的有bug代码:

class MyLinkedList {
public:
    int val;
    MyLinkedList* next;
    int length;
    MyLinkedList() {
        val = -1;
        next = nullptr;
        length = 1;
    }

    MyLinkedList(int n) {
        val = n;
        next = nullptr;
        length = 1;
    }
    
    int get(int index) {
        MyLinkedList* curr = this;
        for (int i = 0; i < index; i++) {
            curr = curr -> next;
        }
        if (curr != nullptr) return curr -> val;
        return -1;
    }
    
    void addAtHead(int val) {
        MyLinkedList* head = new MyLinkedList(val);
        head -> next = this;
        //可以这样写吗? this不能改怎么办?
        this = head;
        length++;
    }
    
    void addAtTail(int val) {
        MyLinkedList* curr = this;
        while (curr -> next != nullptr) {
            curr = curr -> next;
        }
        MyLinkedList* tail = new MyLinkedList(val);
        curr -> next = tail;
        length++;
    }
    
    void addAtIndex(int index, int val) {
        if (index > length) {
            return ;
        }
        if (index == length) {
            addAtTail(val);
            return ;
        }
        MyLinkedList* HEAD = new MyLinkedList();
        HEAD -> next = this;
        MyLinkedList* addPos = HEAD;
        for (int i = 0; i < index; i++) {
            addPos = addPos -> next;
        }
        MyLinkedList* add = new MyLinkedList(val);
        MyLinkedList* temp = addPos -> next;
        addPos -> next = add;
        add -> next = temp;
        length++;
        return ;
    }
    
    void deleteAtIndex(int index) {
        if (index >= length) return ;
        MyLinkedList* HEAD = new MyLinkedList();
        HEAD -> next = this;
        MyLinkedList* deletePos = HEAD;
        for (int i = 0; i < index; i++) {
            deletePos = deletePos -> next;
        }
        //把deletePos的next删掉
        MyLinkedList* temp = deletePos -> next -> next;
        delete deletePos -> next;
        deletePos -> next = temp;
        length--;
        return ;
    }
};

自己写出bug问题原因有以下几点:
1、没有贯彻使用虚拟头节点,反而造成了实现增删功能的时候考虑的东西过于复杂,以后链表题不思考过多,直接都使用虚拟头节点,让自己养成习惯。
2、和虚拟头节点相关,我自己在实现添加头部节点时产生了一个问题,题目该功能函数返回值是void,我怎么让当前类持有最新的头节点,于是我写的bug里就出现了修改this指针这种情况,其实我知道不对,但是不知道怎么实现,另外就是由于是在做题,就处处想着判题的程序能否识别我添加的其他成员变量,其实在类里添加虚拟头节点,改变虚拟头节点的next指针就可以了,自己写的时候过于畏手畏脚了,这也是平常自己写代码的时候一个不好的习惯。
3、最重要的一点是,我的思维有点僵化了,讲解给的代码是在链表类中又写了节点的结构体,链表类维护成员变量:长度_size和虚拟头节点_dummyHead。这个想法太关键了,我自己没有想多写这些代码,造成了很多困难,这个其实和上面第2个问题是一致的。

看完代码随想录和大家探讨之后的想法

没什么好说的
正确代码:

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;
    }

    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
    void addAtHead(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(cur->next != nullptr){
            cur = cur->next;
        }
        cur->next = newNode;
        _size++;
    }

    // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果index大于链表的长度,则返回空
    // 如果index小于0,则在头部插入节点
    void addAtIndex(int index, int val) {

        if(index > _size) return;
        if(index < 0) index = 0;        
        LinkedNode* newNode = new LinkedNode(val);
        //注意初始值,在index前加节点需要访问上一个节点,所以从虚拟头节点开始
        LinkedNode* cur = _dummyHead;
        while(index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }

    // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
    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--;
    }

    // 打印链表
    void printLinkedList() {
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int _size;
    LinkedNode* _dummyHead;

};

几点细节提醒自己以后注意:
1、注意题目中index的意义,是从0开始的下标还是链表中第几个元素,本题目中是下标。
2、学到一个新的遍历链表的小技巧,就是while循环条件的设置。如果要访问尾节点,就是while(curr -> next !=nullptr),如果是访问某个index,就while(index–),循环体里curr = curr -> next;
3、注意在index处节点前添加节点和删除index处节点时,需要访问上一个节点,因为还需要把链表连上,所以遍历的初始值设置为虚拟头节点。在尾部添加节点也需要访问上一个节点,因为原本尾部没有节点,是访问不到的,所以初始也是虚拟头节点。而获取index处节点的值,方便的想法是直接访问index处节点,所以遍历初始值设置为虚拟头节点的next。
4、注意构造函数的写法,自己写的时候忘了C++11特性:初始化列表,虚拟头节点的val赋值无所谓,不用多想,长度一开始设置为0。
5、注意链表类和节点结构体的关系,记住这样的写法。
6、注意添加节点时,next指针的赋值顺序,我自己写的总用临时变量存,其实没必要。

题目链接:leetcode 206、反转链表

文章讲解:代码随想录 206、反转链表讲解

视频讲解:[帮你拿下反转链表 | LeetCode:206.反转链表(https://www.bilibili.com/video/BV1nB4y1i7eL)

自己看到题目的第一想法

题目: 给你单链表的头节点 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* reverseList(ListNode* head) {
        //双指针法
        ListNode* pre = nullptr;
        ListNode* curr = head;
        ListNode* temp;
        while (curr != nullptr) {
            temp = curr -> next;
            curr -> next = pre;
            pre = curr;
            curr = temp;
        }
        return pre;
    }
};

// class Solution {
// public:
//     ListNode* reverse(ListNode* pre, ListNode* curr) {
//         if (curr == NULL) return pre;
//         ListNode* temp = curr -> next;
//         curr -> next = pre;
//         return reverse(curr, temp);
//     }

//     ListNode* reverseList(ListNode* head) {
//         //递归法
//         return reverse(NULL, head);
//     }
// };

两个一起贴出来了,看完讲解以后自己又写了一遍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值