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

发布于 2022-11-20  575 次阅读


前置知识

定义单链表

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

通过自己的构造函数初始化节点

ListNode* head = new ListNode(666);

通过默认构造函数(即不手动写构造函数)初始化节点

ListNode* head = new ListNode();
Node->val = 666;

203. 移除链表元素 - 力扣(LeetCode)

思路:这道题第一眼就想到了用快慢指针做,但是由于对链表操作不熟悉,前前后后折腾了好久。总体思路就是快指针在前面探测,慢指针在后面采集,快指针遇到val值相符的就跳过,不一样的就让慢指针->next = 快指针,同时要记得进行慢指针 = 快指针的操作,实际操作的时候要考虑几个特殊情况,首先就是链表为空,那么直接return head,其次是链表只有一个节点,那么需要判断val,相符return NULL,否则return head,最后因为第一个节点head并没有判断,所以要再次判定一下,如果head->val == val,那么要执行head = head->next 。还有一种想法就是直接处理头结点,先找到第一个非val值的结点,让它来当头结点,接下来再处理其他节点,这种想法应该会更便捷一些,不需要我前面设置的那么多特殊情况,后来又用两个标准答案分别写了一遍,现在看来快慢指针有点画蛇添足了

不要的内存要注意释放,标准答案中的delete tmp就是释放内存用的

要注意顺序,一定要先判断cur != NULL再判断cur->next != NULL,否则会发生越界

我的AC代码

//时间复杂度O(n),空间复杂度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) {
        if(head == NULL) {
            return head;
        }
        if(head->next == NULL) {
            if(head->val == val) {
                return NULL;
            }
            else {
                return head;
            }
        }
        ListNode* slow = head;
        ListNode* fast = head->next;
        while(fast != NULL) {
            while(fast != NULL && fast->val == val) {
                fast = fast->next;
            }
            slow->next = fast;
            slow = fast;
            if(fast == NULL) {
                break;
            }
            fast = fast->next;
        }
        if(head->val == val) {
            head = head->next;
        }
        return head;
    }
};

标准答案

直接使用原来的链表来进行移除节点操作:

//时间复杂度O(),空间复杂度O()
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 删除头结点
        while (head != NULL && head->val == val) { // 注意这里不是if
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }

        // 删除非头结点
        ListNode* cur = head;
        while (cur != NULL && cur->next!= NULL) {
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};

设置一个虚拟头结点在进行移除节点操作:

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

707. 设计链表 - 力扣(LeetCode)

思路:有很多需要注意的点,我的代码中没有采用虚拟头结点,导致写起来有点复杂,也没有设置一个_size来记录链表的长度,导致一些特判需要用get函数慢慢跑,有点舍近求远,下次写这种题如果采用虚拟头结点并且为链表的大小单独设置一个变量会好很多。在写的过程中头脑不清楚踩了很多坑,比如犯了最低级的错误判断相等只用了一个等于号,还有插入节点的时候用了两个->,多跑了一步,导致迟迟无法ACCEPT,花了很多时间,引以为戒!

我的AC代码

下列代码中的NULL应该改为nullptr,因为NULL具有二义性,既代表0,又代表空指针

struct Node {
    int val;
    Node* next;
    Node(): val(-1), next(NULL) {}
    Node(int x): val(x), next(NULL) {}
    Node(int x, Node* next): val(x), next(next) {}
};// 可以放在Public里面
class MyLinkedList {
public:
    Node* head;
    MyLinkedList() {
        head = new Node();
    }

    int get(int index) {
        Node* cur = head;
        int cnt = 0;
        while(cur != NULL) {
            if(cnt == index) {
                return cur->val;
            }
            else {
                ++cnt;
                cur=cur->next;
            }
        }
        return -1;
    }

    void addAtHead(int val) {
        if(head->val == -1) {
            Node* tmp = head;
            Node* f = new Node(val,head->next);
            head = f;
            delete tmp;
        }
        else {
            Node* fir = new Node(val,head);
            head = fir;
        }
    }

    void addAtTail(int val) {
        if(head->val == -1) {
            Node* tmp = head;
            Node* f = new Node(val);
            head = f;
            delete tmp;
        }
        else {
            Node* cur = head;
            Node* last = new Node(val);
            while(cur != NULL && cur->next != NULL) {
                cur = cur -> next;
            }
            cur->next = last;
        }    
    }

    void addAtIndex(int index, int val) {
        if(index <= 0) {
            addAtHead(val);
        }
        else if(get(index) == -1) {
            if(get(index - 1) != -1) {
                addAtTail(val);
            }
        }
        else {
            Node* cur = head;
            int cnt = 1;
            while(cur != NULL && cur->next != NULL) {
                if(cnt == index) {
                    Node* b = new Node(val,cur->next);
                    cur->next = b;
                    break;
                }
                else {
                    ++cnt;
                    cur = cur->next;
                }
            }
        }
    }

    void deleteAtIndex(int index) {
        if(get(index) != -1) {
            Node* cur = head;
            if(index == 0) {
                head = head ->next;
            }
            else {
                int cnt = 1;
                while(cur != NULL && cur->next != NULL) {
                    if(cnt == index) {
                        Node* tmp = cur->next;
                        cur->next = cur->next->next;
                        delete tmp;
                        break;
                    }
                    else {
                        ++cnt;
                        cur = cur->next;
                    }
                }
            }
        }
    }
};

标准答案

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,则置为0,作为链表的新头节点。
    void addAtIndex(int index, int val) {
        if (index > _size || index < 0) {
            return;
        }
        LinkedNode* newNode = new LinkedNode(val);
        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;
        _size--;
    }

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

};

206. 反转链表 - 力扣(LeetCode)

思路:用虚拟头结点秒杀,思路就是先设置一个虚拟头结点newHead,然后遍历原有的链表,每遇到一个,就生成一个新结点newHead,该结点的值为遍历到的原链表的结点的值,然后指向newHead->next,接下来再将newHead->next指向newHead,如此反复,最后返回newHead->next

但是我这个答案空间复杂度为O(n),重启了一个新链表,比较浪费,更推荐标准答案的做法

我的AC代码

//时间复杂度O(n),空间复杂度O(n)
/**
 * 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) {
        if((head == nullptr) || (head->next == nullptr)) {
            return head;
        }
        ListNode* cur = head;
        ListNode* newHead = new ListNode(0);
        while(cur != NULL) {
            ListNode* newNode = new ListNode(cur->val,newHead->next);
            newHead->next = newNode;
            cur = cur->next;
        }
        return newHead->next;
    }
};

标准答案

双指针法

//时间复杂度O(),空间复杂度O()
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp;                     // temp用来保存cur的下一个节点,temp一定不要赋值
        ListNode* cur = head;           // 或者赋空指针,否则输入为空时会出错
        ListNode* pre = NULL;
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

递归法1

class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        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);
    }

};

递归法2

这个递归结合这张图会比较好理解一些,这是leetcode官方给的解释

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 边缘条件判断
        if(head == NULL) return NULL;
        if (head->next == NULL) return head;

        // 递归调用,翻转第二个节点开始往后的链表,写head->next是为了继续推进
        // 如果传入head则会一直原地踏步,并且会无限递归循环
        ListNode *last = reverseList(head->next);
        // head 指向的那个节点应该指向 head
        head->next->next = head;
        // head 需要指向 NULL,否则会产生环
        head->next = NULL;
        // 上面这两句话的意思就是让我这个节点后面的那个节点指向我,然后我再指向NULL
        // 实际递归过程是最后一个节点开始到第一个节点,分别进行上述操作,即
        // 不断地让后面的节点指向我,我再指向NULL,这样从后往前
        // 最后就做到了反转链表并且让反转链表完成后的最后一个节点指向NULL
        return last;
    }
};