《代码随想录》学习笔记:链表合集

前言

  一刷代码随想录,今天的内容是 链表。所有题目均源自LeetCode。
  欢迎大家在评论区留言发表自己的看法,如文章有不妥之处,请批评指正。



一、练习题目

  1. 203. 移除链表元素
  2. 707. 设计链表
  3. 206. 反转链表
  4. 24. 两两交换链表中的节点
  5. 19. 删除链表的倒数第 N 个结点
  6. 面试题 02.07. 链表相交
  7. 142. 环形链表 II

二、源码剖析

1、203. 移除链表元素|★☆☆☆☆

第一版:添加虚拟头节点

时间复杂度:O(1),空间复杂度: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* removeElements(ListNode* head, int val) {
        // 设置一个虚拟头结点,即使遇到头节点是要删除的节点也不用单独操作
        ListNode* virHead = new ListNode(-1, head);
        ListNode* cur = head, *pre = virHead;

        // 遍历删除满足条件的节点
        while (cur != nullptr) {
            if (cur->val == val) {
                ListNode* temp = cur;
                cur = temp->next;
                pre->next = cur;
                delete temp;
            }else {
                pre = cur;
                cur = cur->next;
            }
        }
        return virHead->next;
    }
};

第二版:优化第一版,优化删除节点的操作

时间复杂度:O(1),空间复杂度: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* removeElements(ListNode* head, int val) {
        // 设置一个虚拟头结点,即使遇到头节点是要删除的节点也不用单独操作
        ListNode* virHead = new ListNode(-1, head);
        ListNode* cur = virHead;

        // 遍历删除满足条件的节点
        while (cur->next != nullptr) {
            if (cur->next->val == val) {
                ListNode* temp = cur->next;
                cur->next = temp->next;
                delete temp;
            }else {
                cur = cur->next;
            }
        }
        return virHead->next;
    }
};

2、707. 设计链表|★★★☆☆

第一版:半看题解,主要是LeetCode给出的代码里没有 ListNode 的定义,以为就不能使用

class MyLinkedList {
    // 利用ListNode内置单链表设计本链表,要实现双链表的操作
public:
    // 定义链表节点结构体
    struct ListNode {
        int val;
        ListNode* next;
        ListNode(int val):val(val), next(nullptr){}
    };
    
    MyLinkedList() {
        this->head = new ListNode(-1);  // 虚头节点,创建对象即构造
        this->size = 0;
    }
    
    int get(int index) {
        // 链表查询只能从头往后逐个索引
        // 索引是从0开始,因此索引的最大下标为size-1
        if (index < 0 || index >= size) {   
            return -1;
        }
        // 否则一定找得到位于index下标的节点
        ListNode *cur = head;
        for (int i = 0; i <= index; ++i) {  
            cur = cur->next;            // <= 就处理了虚头节点
        }
        return cur->val;
    }
    
    void addAtHead(int val) {
        // 插入在链表头,利用addAtIndex实现
        addAtIndex(-1,val);
    }
    
    void addAtTail(int val) {
        // 插入在链表尾,利用addAtIndex实现
        addAtIndex(size,val);
    }
    
    void addAtIndex(int index, int val) {
        // 插入在链表任意地方,因为使用的是单向链表,因此需要一个pre记录前一个节点
        if (index > size) {
            return;
        }
        ++size;
        ListNode *newNode = new ListNode(val);
        index = max(0, index);
        // 无论在哪里插入,因为有了一个虚头节点,因此都算作在中间插入,都要记录插入点的前一个节点,因此循环查找到index前一个节点即可
        ListNode *pre = head;
        for (int i = 0; i < index; ++i) {
            pre = pre->next;            // 找到第index个节点的前一个节点
        }
        // 在中间插入节点,尾部插入节点特殊在cur为nullptr
        ListNode *cur = pre->next;      // 记录第index个节点
        pre->next = newNode;            // 前一个节点指向新节点
        newNode->next = cur;            // 新节点指向原来第index个节点
    }
    
    void deleteAtIndex(int index) {
        // 删除节点就是将当前结点的前一个结点指向当前节点的后继节点,因此也需要一个pre
        if (index < 0 || index >= size) {   
            return;
        }
        --size; 
        ListNode *pre = head;
        for (int i = 0; i < index; ++i) {
            pre = pre->next;
        }
        ListNode *cur = pre->next;
        pre->next = cur->next;
        delete cur;   
    }

protected:
    int size;           // 记录链表大小
    ListNode *head;
};

/**
 * 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);
 */

3、206. 反转链表|★☆☆☆☆

第一版:递归法:看了一行题解代码,处理当前结点指向前继节点那里

时间复杂度: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 *res = reverseList(head->next);    // 记录顺序节点的尾节点,作为最后返回的头节点
        head->next->next = head;                    // 将当前结点的下一个节点指向当前结点,即反转了
        head->next = nullptr;                       // 将当前节点指向nullptr
        return res;
    }
};

第二版:双指针法,迭代法

时间复杂度: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) {
        // 双指针,迭代法
        // 利用pre和cur,顺序遍历的时候就将方向反转
        ListNode *cur = head, *pre = nullptr;
        ListNode *temp;
        while (cur != nullptr) {
            temp = cur->next;       // 记录当前节点的后继节点
            cur->next = pre;        // 反转
            pre = cur;
            cur = temp;
        }
        return pre;				   // 此时cur已经是nullptr,因此要返回pre才是原链表尾节点
    }
};

4、24. 两两交换链表中的节点|★☆☆☆☆

第一版:

  1. 单向链表只有next,因此交换即需要将第二个节点的next指向第一个节点,而第一个节点的next需要指向原第二个节点的后继节点
  2. 因此需要一个变量cur指向原交换节点中第一个节点的前一个结点,一个变量pre指向原交换节点中的第一个节点,一个变量nex指向原交换节点中的第二个节点的后继节点
  3. 然后画一个图,就能很好的理解其中的指向关系

时间复杂度: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* swapPairs(ListNode* head) {
        // 单向链表只有next,因此交换即需要将第二个节点的next指向第一个节点,而第一个节点的next需要指向原第二个节点的后继节点
        // 因此需要一个变量cur指向原交换节点中第一个节点的前一个结点,一个变量pre指向原交换节点中的第一个节点,一个变量nex指向原交换节点中的第二个节点的后继节点
        // 然后画一个图,就能很好的理解其中的指向关系
        ListNode* dummyHead = new ListNode(-1);   // 虚头节点
        dummyHead->next = head;
        ListNode* cur = dummyHead;                // 当前节点直线即将交换节点的前一个节点
        while (cur->next != nullptr && cur->next->next != nullptr) {
            ListNode* pre = cur->next;      // 记录需要交换节点中的第一个节点
            ListNode* nex = cur->next->next->next;// 记录原第二个节点的后一个节点
            cur->next = cur->next->next;    // 将需要交换节点中第二个节点放在前面
            cur->next->next = pre;          // 将需要交换节点中第一个节点放在后面
            cur = pre;                      // 当前节点指向交换后的两个节点的末尾,即未交换节点的前一个节点
            cur->next = nex;                // 将交换后的节点指向原来未交换的节点
        }
        return dummyHead->next;
    }
};

5、19. 删除链表的倒数第 N 个结点|★☆☆☆☆

第一版:两次遍历

时间复杂度:O(L),空间复杂度: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* removeNthFromEnd(ListNode* head, int n) {
        // 方法一:两次遍历
        // 首先增加一个虚头节点,以便好删除正数第一个节点
        // 第一次遍历从虚拟头节点开始,得到节点数目 sz,则需要删除的节点为原链表正数第 sz-n+1 个节点,如果虚头节点下表为0,则删除节点下标为 sz-n,删除节点迁移节点下标为 sz-n-1
        // 第二次遍历,使用cur指向需要删除节点的前一个结点,即cur遍历到下标为 sz-n-1 的节点停止,再将 cur->next = cur->next->next 即可
        ListNode* dummyHead = new ListNode(-1);
        dummyHead->next = head;
        int sz = 0;
        ListNode* cur = dummyHead;
        while (cur->next != nullptr) {
            ++sz;
            cur = cur->next;
        }
        cur = dummyHead;
        for (int i = 0; i < sz-n; ++i) {
            cur = cur->next;
        }
        cur->next = cur->next->next;
        return dummyHead->next;
    }
};

第二版:双指针,快慢指针,一次遍历

时间复杂度:O(L),空间复杂度: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* removeNthFromEnd(ListNode* head, int n) {
        // 方法二:一次遍历,双指针
        // 首先增加一个虚头节点
        // 然后使用双指针分别指向两个节点,first 和 last = first+n,两个指针间的距离固定为n
        // 两个指针同时向后移动,当 first->next == nullptr 时,last 刚好指向删除节点的前一个结点
        // 最后last->next = last->next->next 即可
        ListNode* dummyHead = new ListNode(-1, head);
        ListNode* first = dummyHead, *last = dummyHead;
        for (int i = 0; i < n; ++i) {
            first = first->next;
        }
        while (first->next != nullptr) {
            first = first->next;
            last = last->next;
        }
        // 删除节点
        ListNode* temp = last->next;
        last->next = last->next->next;
        delete(temp);
        return dummyHead->next;
    }
};

6、面试题 02.07. 链表相交|★☆☆☆☆

第一版:快慢指针,双指针

时间复杂度:O(n+m),空间复杂度:O(1)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        // 双指针,快慢指针
        // ptrA和ptrB两个指针分别指向两个链表的头节点,两个指针同时向后移动
        // 假设ptrA指向短链表,则ptrA肯定比ptrB先走到链表尾,此时就将ptrA指向headB,然后两个指针继续向后移动
        // 当ptrB走到链表尾,则将ptrB指向headA,此时ptrA和ptrB到两链表相交的起始节点距离相同,继续向后移动指针
        // 如果两链表相交,则一定最多遍历两次就会找到所求点,此时ptrA == ptrB; 如果没有相交,遍历两次后ptrA和ptrB都指向nullptr
        // 因此循环条件可以为 ptrA->next == ptrB->next 则停止,但需要额外考虑两个链表中有>=1个为空的情况,和链表第一个节点就是相交起点的情况
        // 因此可以考虑各自增加一个虚头节点,就不用单独考虑特殊情况了
        ListNode* dummyHeadA = new ListNode(-1, headA);
        ListNode* dummyHeadB = new ListNode(-2, headB);
        ListNode* ptrA = dummyHeadA, *ptrB = dummyHeadB;
        while (ptrA->next != ptrB->next) {
            ptrA = ptrA->next == nullptr ? dummyHeadA : ptrA->next;
            ptrB = ptrB->next == nullptr ? dummyHeadB : ptrB->next;
        }
        return ptrA->next;
    }
};

第二版:改进第一版,不使用虚头节点,终止条件改为 ptrA==ptrB

时间复杂度:O(n+m),空间复杂度:O(1)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        // 双指针,快慢指针
        // ptrA和ptrB两个指针分别指向两个链表的头节点,两个指针同时向后移动
        // 假设ptrA指向短链表,则ptrA肯定比ptrB先走到链表尾,此时就将ptrA指向headB,然后两个指针继续向后移动
        // 当ptrB走到链表尾,则将ptrB指向headA,此时ptrA和ptrB到两链表相交的起始节点距离相同,继续向后移动指针
        // 如果两链表相交,则一定最多遍历两次就会找到所求点,此时ptrA == ptrB; 如果没有相交,遍历两次后ptrA和ptrB都指向nullptr
        // 但需要额外考虑两个链表中有>=1个为空的情况
        if (headA == nullptr || headB == nullptr) {
            return nullptr;
        }
        ListNode* ptrA = headA, *ptrB = headB;
        while (ptrA != ptrB) {
            ptrA = ptrA == nullptr ? headA : ptrA->next;
            ptrB = ptrB == nullptr ? headB : ptrB->next;
        }
        return ptrA;
    }
};

7、142. 环形链表 II|★★☆☆☆

第一版:哈希表

时间复杂度:O(n),空间复杂度:O(n)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        // 使用哈希表
        unordered_set<ListNode*> set;
        ListNode* cur = head;
        while (cur != nullptr) {
            if (set.find(cur) != set.end()) return cur;
            set.insert(cur);
            cur = cur->next;
        }
        return nullptr;
    }
};

第二版:快慢指针

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

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        // 快慢指针
        // first每次移动2格,last每次移动1格,如果存在环,一定会相遇,且相遇点在环内,如果不存在环,则first一定先指向NULL
        // 设头节点到环入口点长度a,环长度b,环入口点到相遇点的长度为s,相遇点到环入口点长度为b-s
        // 则存在 2*(a+s) = a+s+n*b,即 a+s = n*b, a = n*b-s = (n-1)*b + b-s
        // 即从头节点到环入口点的长度 = 相遇点到环入口点长度
        // 根据上述理论,就可以先使 first 和 last 第一次相遇后,last不动,first指向head,两者再同时移动,每次都移动1个节点,再次相遇时就是还入口点
        ListNode *first = head, *last = head;
        while (first != nullptr) {
            if (first->next == nullptr) {   // 无环
                return nullptr;     
            }
            first = first->next->next;
            last = last->next;
            if (first == last) {            // 有环,重置first,first和last每次移动一个节点直到相遇
                first = head;
                while (first != last) {
                    first = first->next;
                    last = last->next;
                }
                return last;
            }
        }
        return nullptr;
    }
};

总结

   ( 1 ) (1) (1) 删除、交换、反转节点等操作,先添加一个虚头节点 dummyHead,就能 不用再单独考虑链表只有0、1个节点的情况。
   ( 2 ) (2) (2) 只是需要找到某个节点,即相当于索引,则可以不必添加虚头节点。
   ( 3 ) (3) (3) 虚头节点和快慢指针 是链表类型题的AC小助手。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值