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

203.移除链表元素

题目描述:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例一:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例二:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

列表中的节点数目在范围 [0, 104] 内
1 <= Node.val <= 50
0 <= val <= 50

解题思路:

  1. 关键词提取:链表、头节点、删除指定节点
  2. 暴力解法:
    1. 新建一个链表
    2. 遍历头节点,不符合条件,就将值赋值给新链表的新节点,符合条件就忽略,不做处理

代码如下:

/**
 * 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* dummyHead = new ListNode(0);
        dummyHead->next = NULL;
        // 从虚拟头节点开始遍历,可以将需要遍历的链表的头节点包含在内
        ListNode* curNode = dummyHead;
        while(head != NULL)
        {
            // 不符合条件,可以存放到新的链表中
            if(head->val != val)
            {
                // 申请一个新的节点
                ListNode* tmp = new ListNode(0);
                // 对新节点进行赋值
                tmp->val = head->val;
                tmp->next = NULL;
                // 将新节点挂载到新的链表中
                curNode->next = tmp;
                // 更新新链表的遍历节点指针
                curNode = curNode->next;
            }
            head = head->next;
        } 
        // 返回新链表的头节点
        return dummyHead->next;
    }
};

总结:

  1. 第一次做的时候,仅仅会暴力解法,不按照题目的建议去做,而是采取一种新的方式,缺点是空间复杂度为o(n)。
    1)第一次接触链表的知识,采取的是新建链表的方式,还是容易弄错的,如怎样将新的节点挂载到新的头节点后方。
    2)虚拟头节点的方式是题解中会提到的,第一次做题时不理解,只是记住模板。

  2. 跟着代码随想录刷题的时候,找到需要特别注意的细节。
    1)虚拟头节点的含义和用法。carl哥都讲解得很清晰。详见视频

    [https://www.bilibili.com/video/BV18B4y1s7R9/]:

    2)不得不说,每次做链表的题目,都把自己绕晕了,暂时还没找到一个很清晰的路径。经常是比对代码,发现代码基本上一模一样,但是就是无法AC,就很离谱,多次折磨之下,还是没找到原因,只知道,按照题解代码,重新码一遍就好了。
    3)其实,还是得去打印出来这个链表,才能更清晰一些,不能说放弃就放弃。最终发现,是自己对头节点的删除考虑不充分,这个头节点删除以后,会再次更新,还是头节点,因此,不能只判断一次,而是得找到下一个不需要删除的节点才算结束。

707.设计链表

题目描述:
你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例一:

输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3

提示:

0 <= index, val <= 1000
请不要使用内置的 LinkedList 库。
调用 get、addAtHead、addAtTail、addAtIndex 和 deleteAtIndex 的次数不超过 2000 

解题思路:

  1. 关键词提取:
  2. 解析:从零构建一个链表,需要对链表的各个操作都要做一个基础性掌握,一点点尝试。

代码如下:

class MyLinkedList {
public:
    // 结构体,对链表节点做一个声明
    struct LinkedList{
        int val;
        LinkedList* next;
        // new 节点的时候,可以方便的做初始化
        LinkedList(int val):val(val),next(nullptr){}
    };
    // 对声明做初始化
    MyLinkedList() {
        dummyHead = new LinkedList(0);
        size = 0;
    }
    // 获取指定下标的值
    int get(int index) {
        // 初始化时,对链表大小,有做记录,不需要层层遍历,直接可以比对出大小,解决临界情况
        if(index > size - 1 || index < 0){
            return -1;
        }
        // 指针指向虚拟头节点的下一个值
        LinkedList* curNode = dummyHead->next;
        // 检索下标,当下标指向0时,即是要找的那个元素
        while(index != 0)
        {
            index--;
            curNode = curNode->next;
        }
        return curNode->val;
    }
    // 增加头节点
    void addAtHead(int val) {
        // 申请一个新的节点,并且插入到虚拟头节点后
        LinkedList* tmp = new LinkedList(val);
        tmp->next = dummyHead->next;
        dummyHead->next = tmp;
        size++;
    }
    // 增加尾节点
    void addAtTail(int val) {
        LinkedList* tmp = new LinkedList(val);
        LinkedList* curNode = dummyHead;
        // 从虚拟节点开始,看下一个节点是否是空节点,解决临界状态(头节点为空)
        while(curNode->next != NULL)
        {
            curNode = curNode->next;
        }
        curNode->next = tmp;
        size++;
    }
    // 指定下标插入节点
    void addAtIndex(int index, int val) {
        if(index > size){
            return;
        }
        if(index < 0){
            index = 0;
        }
        // 指向虚拟头节点
        LinkedList* curNode = dummyHead;
        // 找到需要插入的下标的前一个元素
        while(index != 0)
        {
            index--;
            curNode = curNode->next;
        }
        LinkedList* tmp = new LinkedList(val);
        tmp->next = curNode->next;
        // 在下标的前一个元素后面,插入新节点
        curNode->next = tmp;
        size++;
    }
    
    void deleteAtIndex(int index) {
        if(index < 0 || index >= size){
            return;
        }   
        // 指向虚拟头节点
        LinkedList* curNode = dummyHead;
        // 找到需要插入的下标的前一个元素
        while(index != 0)
        {
            index--;
            curNode = curNode->next;
        }
        // 删除指定的节点
        LinkedList* tmp = curNode->next;
        curNode->next = curNode->next->next;
        delete(tmp);
        tmp = nullptr;
        size--;
    }
    // 声明
    private:
    int size;
    LinkedList* dummyHead;
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList* obj = new MyLinkedList();
 * int param_1 = obj->get(index);
 * obj->addAtHead(val);
 * obj->addAtTail(val);
 * obj->addAtIndex(index,val);
 * obj->deleteAtIndex(index);
 */

总结:

  1. 第一次做的时候,会采用暴力解法。
    1)按照定义,无论是头插还是尾插,都需要对链表进行遍历。

    2)对于头节点是否为空,需要操作的下标是否超出链表长度,都需要做特殊处理。

  2. 跟着代码随想录刷题的时候,发现特殊处理都可以统一处理,不用搞得代码那么复杂。
    1)采用虚拟头节点的方式。
    2)在链表定义的时候,同时申请一个链表长度的变量,每次新增节点的时候,都做计数,因此,可以不需要遍历就完成与链表下标有关的临界条件处理,是时间复杂度的优化。

206.反转链表

题目描述:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例一:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例二:

输入:head = [1,2]
输出:[2,1]

提示:

链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000

解题思路:

  1. 关键词提取:头节点、反转
  2. 解法一:新建一个链表,可以用头插法,达到链表反转的目标。
  3. 解法二:递归法,改变next指针的方向

代码如下:

/**
 * 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* dummyHead = new ListNode(0);
        ListNode* curNode = dummyHead;
        // 开始遍历原有链表,由前往后遍历
        while(head != nullptr) {
            // 新建节点
            ListNode* newNode = new ListNode(head->val);
            // 采用头插法,将新节点插入到虚拟头节点后面
            newNode->next = curNode->next;
            curNode->next = newNode;
            // 原有链表头指针移动
            head = head->next;
        }
        return dummyHead->next;
    }
};

总结:

  1. 第一次做的时候,和上一题一样,新建一个链表的方式是最容易想到的,就跟数组一样。
    1)其实,这也是一种懒惰,不愿意在原链表上进行操作,而且对于链表的遍历和指针敬而远之,经常会报错指针指向空,然后花大力气去查哪个位置条件没写对。

    2)链表的操作相对来说难以理解一些,即使画图也很难理解,跟数组的区别还是比较大的。

    3)最终还是会采取打印出链表的方式去立体化自己的想法,看得到的会让人印象深刻一些,在不确定是否正确的时候,就增加打印日志吧。

  2. 跟着代码随想录刷题的时候,会发现还有两种解法,一种是双指针法,一种是从后往前的递归法,膜拜。
    1)双指针法相对而言,容易理解一些,跟递归法一致,就是在反转链表的时候,先将前后节点的地址都保存起来,再按照指定的顺序重新分配next地址。
    2)从后往前的解法,我一开始想到过,第一次做的时候,我是新建一个链表,然后将原链表进行倒序遍历(实际上就是遍历n!次,每次遍历到最后一个节点),然后将对应的值,顺序放到新的链表中。也是反复折腾,多次打印链表,多次调试才实现。这个思路用递归去实现,还是比较模糊,不够清晰,可能要三刷才能有感觉。

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值