代码随想录算法训练营第3天

代码随想录算法训练营第3天 | 链表理论基础,203.移除链表元素,707.设计链表,206.反转链表

一、刷题部分

1.1 链表理论基础

链表是由一个个节点串联而成的,节点包含数据域和指针域,数据域用来存放数据,而指针域实现了节点之间的串联。

链表中有单链表、双链表、循环链表:

链表的物理空间是不连续的,通过指针存储下一节点的物理地址。

链表的简单定义需要能够熟练会写:

// 单链表节点
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(NULL) {}
    ListNode(int x, ListNode* p) : val(x), next(p) {}
}

有时候只需节点定义就可以初始化链表了,但也可以完善一下把链表类封装好:

// 单链表
class LinkList {
public:
    struct ListNode {
        int val;
    	ListNode* next;
    	ListNode(int x) : val(x), next(NULL) {}
    	ListNode(int x, ListNode* p) : val(x), next(p) {}
    }
    ListNode(){}
private:
	ListNode *head = new ListNode();
}

链表的删除,插入等操作都是对节点的,需要注意处理指针的关系,很多时候需要额外定义临时指针来辅助暂存一些地址。

链表可以低成本地增删节点,但是链表的查询成本就比数组高,这个特点决定链表适合于查询操作较少而增删操作较多的情景。

1.2 203.移除链表元素

1.2.1 题目描述

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

示例 1:

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

示例 2:

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

示例 3:

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

提示:

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

1.2.2 初见想法

删除链表 val 需要先把开头的 val 节点全部删掉才可以开始后面的工作。代码逻辑如下:

/**
 * 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 *p = nullptr;
        ListNode *temp = nullptr;
        //将链表开头所有val结点删除
        while(head != nullptr && head -> val == val){
            temp = head;
            head = head -> next;
            delete temp;
        }
        //开头一删发现全空了,直接返回
        if(head == nullptr) return head;
        p = head;
        while(p -> next != nullptr){
            if(p -> next -> val == val){
                temp = p -> next;
                p -> next = p -> next -> next;
                delete temp;
                continue;
            }
            p = p -> next;
        }
        return head;
    }
};

1.2.3 看录后想法

如果题目定义的链表结构使用实头结点,那么可以自行定义一个虚拟头结点 DummyHead 来统一操作方式。

/**
 * 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(-1, head);
        ListNode* p = DummyHead;
        while(p -> next != nullptr){
            if(p -> next -> val == val){
                ListNode* temp = p -> next;
                p-> next = p-> next -> next;
                delete temp;
                continue;
            }
            p = p-> next;
        }
        head = DummyHead -> next;
        delete DummyHead;
        return head;
    }
};

1.2.4 遇到的困难

没有想到虚拟头节点的方法,所以直接写的时候总是感觉不太清晰,操作很繁杂。

1.3 707.设计链表

1.3.1 题目描述

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

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

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

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • `void addAtHe
  • ad(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 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndexdeleteAtIndex 的次数不超过 2000

1.3.2 初见想法

就是一些功能函数的实现。写一下看看吧:

class MyLinkedList {
public:
    //定义链表结构体
    struct MyLinkedNode{
        int val;
        MyLinkedNode* next;
        MyLinkedNode() : val(-1), next(nullptr){}
        MyLinkedNode(int x) : val(x), next(nullptr){}
        MyLinkedNode(int x, MyLinkedNode* next) : val(x), next(next){}
    };
    MyLinkedList(){
        _DummyHead = new MyLinkedNode();
        _size = 0;
    }
    
    int get(int index) {
        //cout<<"get("<<index<<")---";
        if(index < 0 || index >= _size) return -1;
        MyLinkedNode* cur = _DummyHead -> next;
        for(int i = 0; i < index; i++){
            cur = cur -> next;
        }
        return cur -> val;
    }
    
    void addAtHead(int val) {
        //cout<<"addAtHead("<< val << ")---\n";
        MyLinkedNode* temp = new MyLinkedNode(val, _DummyHead -> next);
        _DummyHead->next = temp;
        _size++;
        //cout<<"现在的size是"<<_size<<endl;
        //printLink();
    }
    
    void addAtTail(int val) {
        //cout<<"addAtTail("<<val<<")---\n";
        MyLinkedNode* cur = _DummyHead;
        for(int i = 0; i < _size; i++){
            cur = cur -> next;
        }
        cur -> next = new MyLinkedNode(val);
        _size++;
        //cout<<"现在的size是"<<_size<<endl;
        //printLink();
    }
    
    void addAtIndex(int index, int val) {
        //cout<<"addAtIndex("<<index<<", "<<val<<")---\n";
        //先处理特殊情况
        if(index > _size || index < 0) return;
        else if(index == _size){
            addAtTail(val);
            return;
        }
        else if(index == 0){
            addAtHead(val);
            return;
        }

        //正式开始
        MyLinkedNode* cur = _DummyHead;
        for(int i = 0; i < index; i++){
            cur = cur -> next;
        }
        MyLinkedNode* temp = new MyLinkedNode(val, cur -> next);
        cur -> next = temp;
        _size++;
        //cout<<"现在的size是"<<_size<<endl;
        //printLink();
    }
    
    void deleteAtIndex(int index) {
        //cout<<"deleteAtIndex("<<index<<")---\n";
        if(index < 0 || index >= _size) return;
        MyLinkedNode* cur = _DummyHead;
        for(int i = 0; i < index; i++)
            cur = cur -> next;
        MyLinkedNode* temp = cur -> next;
        cur -> next = cur -> next -> next;
        delete temp;
        _size--;
        //cout<<"现在的size是"<<_size<<endl;
        //printLink();
    }

    void printLink(){
        cout<<"现在的链表为:[";
        MyLinkedNode* cur = _DummyHead -> next;
        for(int i = 0; i < _size; i++){
            cout<< cur->val << ", ";
        }
        cout<<"]\n";
    }
private:
    MyLinkedNode* _DummyHead;
    int _size;
};

/**
 * 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.3.3 看录后想法

这题就是基本写法的实现。

1.3.4 遇到的困难

有一些小错误不容易发现,老是编译失败,后来发现是一个条件判断写错了。花挺久时间在这上面的。

1.4 题目名称

1.4.1 题目描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

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

示例 2:

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

示例 3:

输入:head = []
输出:[]

提示:

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

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

1.4.2 初见想法

是一道指针的基本操作问题,通过修改指针从而完成这道题目。用 3 个指针pre,cur,nxt来指向3个节点,然后每次修改 cur 的 next 指针指向 pre ,从而完成这道题目。感觉最困难的点是在开始和结束的过程需要注意一些细节。写写看吧:

/**
 * 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* cur = head;
        ListNode* prv = nullptr;
        ListNode* nxt = nullptr;
        while(cur){
            nxt = cur -> next;
            cur -> next = prv;
            prv = cur;
            cur = nxt;
        }
        head = prv;
        return head;
    }
};

1.4.3 看录后想法

所谓的双指针法与我的想法有一定的相似之处的,不过视频讲解里使用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* reverseList(ListNode* head) {
        ListNode *cur = head;
        ListNode *pre = nullptr;
        while (cur != nullptr) {
            //设立临时变量保存下一个节点的地址
            ListNode *nxt = cur -> next;
            //指针更改
            cur -> next = pre;
            //向后移动
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
};

发现这个看起来和我之前写的那一版极其相似,但是我觉得我的思路会很清晰,因此这个再写一遍的代码还是挺重要的。

接下来是递归的方法:

然而本题的递归方式我是看起来极其困难的,视频讲解用的是将原来的双指针法一一对应过去,但是这样子去写一个递归还有什么意义吗?我觉得需要不用双指针法而能够写出递归这样才是真正把递归学明白了。

现在我需要找找递归地资料好好巩固一下递归思想来看看能不能解决我现在的困惑。

感觉不行,这题上来一个递归实在有点奇怪,可能是我目前确实水平达不到,不急,就先按照录里的思想,对照着双指针法来一遍递归吧。

//递归写法
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        return reverse(nullptr, head);
    }
    ListNode* reverse(ListNode* pre, ListNode* cur) {
        //递归第一步:先写好出口条件
        if (cur == nullptr) {
            return pre;
        }
        //接下来仿照双指针法,先定义临时变量存下一节点
        ListNode *nxt = cur -> next;
        //指针转向
        cur -> next = pre;
        //继续往后递归
        return reverse(cur, nxt);
    }
};

1.4.4 遇到的困难

递归的思路总是觉得奇怪,可能是这块我掌握的不好。我认为这个问题不是目前的我可以解决的,而是自己水平提高后自然就掌握的能力,因此这里就不多花费功夫了。

二、总结与回顾

今天搞了一下链表的几道题目,总体来看还是比较简单的。希望可以后面再接再厉。

学习时长:4h

posted @ 2025-01-10 16:30  xc0208  阅读(750)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示