• 数据结构刷题:第七天


    目录

    一,环形链表

     1,哈希表

    思路及算法

    复杂度分析

    2,快慢指针

    思路及算法

    细节

    复杂度分析

    二,合并两个有序链表

     1,递归

    算法

    复杂度分析

    2,迭代

    思路

    算法

    复杂度分析

    三,移除链表元素

    1,递归

    复杂度分析

    2,迭代

    复杂度分析

    一,环形链表

    141. 环形链表 - 力扣(LeetCode)https://leetcode.cn/problems/linked-list-cycle/?plan=data-structures&plan_progress=ggfacv7

     1,哈希表

    思路及算法

    最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。

    具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。

    1. class Solution {
    2. public:
    3. bool hasCycle(ListNode *head) {
    4. unordered_set<ListNode*> seen;
    5. while (head != nullptr) {
    6. if (seen.count(head)) {
    7. return true;
    8. }
    9. seen.insert(head);
    10. head = head->next;
    11. }
    12. return false;
    13. }
    14. };

    复杂度分析

    时间复杂度:O(N),其中 N 是链表中的节点数。最坏情况下我们需要遍历每个节点一次。

    空间复杂度:O(N),其中 N 是链表中的节点数。主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。

    2,快慢指针

    思路及算法

    本方法需要读者对「Floyd 判圈算法」(又称龟兔赛跑算法)有所了解。

    假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。

    我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

    细节

    为什么我们要规定初始时慢指针在位置 head,快指针在位置 head.next,而不是两个指针都在位置 head(即与「乌龟」和「兔子」中的叙述相同)?

    观察下面的代码,我们使用的是 while 循环,循环条件先于循环体。由于循环条件一定是判断快慢指针是否重合,如果我们将两个指针初始都置于 head,那么 while 循环就不会执行。因此,我们可以假想一个在 head 之前的虚拟节点,慢指针从虚拟节点移动一步到达 head,快指针从虚拟节点移动两步到达 head.next,这样我们就可以使用 while 循环了。

    当然,我们也可以使用 do-while 循环。此时,我们就可以把快慢指针的初始值都置为 head。

    1. class Solution {
    2. public:
    3. bool hasCycle(ListNode* head) {
    4. if (head == nullptr || head->next == nullptr) {
    5. return false;
    6. }
    7. ListNode* slow = head;
    8. ListNode* fast = head->next;
    9. while (slow != fast) {
    10. if (fast == nullptr || fast->next == nullptr) {
    11. return false;
    12. }
    13. slow = slow->next;
    14. fast = fast->next->next;
    15. }
    16. return true;
    17. }
    18. };

    复杂度分析

    时间复杂度:O(N),其中 N 是链表中的节点数。

    当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。

    当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮。

    空间复杂度:O(1)。我们只使用了两个指针的额外空间。

    二,合并两个有序链表

    21. 合并两个有序链表 - 力扣(LeetCode)https://leetcode.cn/problems/merge-two-sorted-lists/?plan=data-structures&plan_progress=ggfacv7

     1,递归

    我们可以如下递归地定义两个链表里的merge 操作
    (忽略边界情况,比如空链表等) :


    list1[0] + merge(list1[1 :],list2)     list1[0] < list2[0]
    list2[0] + merge(list1, list2[1 :)    otherwise
    也就是说,两个链表头部值较小的一个节点与剩下元素的merge 操作结果合并。

    算法


    我们直接将以上递归过程建模,同时需要考虑边界情况。
    如果11或者12 一开始就是空链表,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断11和12哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
     

    1. class Solution {
    2. public:
    3. ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    4. if (l1 == nullptr) {
    5. return l2;
    6. } else if (l2 == nullptr) {
    7. return l1;
    8. } else if (l1->val < l2->val) {
    9. l1->next = mergeTwoLists(l1->next, l2);
    10. return l1;
    11. } else {
    12. l2->next = mergeTwoLists(l1, l2->next);
    13. return l2;
    14. }
    15. }
    16. };

    复杂度分析


    ●时间复杂度:O(n+m),中n和m分别为两个链表的长度。因为每次调用递归都会去掉11
    或者12 的头节点(直到至少有一个链表为空),函数mergeTwoList多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即O(n + m)。
    ●空间复杂度: O(n+n),中n和1分别为两个链表的长度。递归调用mergeTwoLists函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时mergeTwoLists 函
    数最多调用n+m次,因此空间复杂度为O(n + m)。
     

    2,迭代

    思路

    我们可以用迭代的方法来实现上述算法。当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。

    算法

    首先,我们设定一个哨兵节点 prehead ,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 prev 指针,我们需要做的是调整它的 next 指针。然后,我们重复以下过程,直到 l1 或者 l2 指向了 null :如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 prev 节点的后面同时将 l1 指针往后移一位。否则,我们对 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 prev 向后移一位。

    在循环终止的时候, l1 和 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

    下图展示了 1->4->5 和 1->2->3->6 两个链表迭代合并的过程:

    1. class Solution {
    2. public:
    3. ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    4. ListNode* preHead = new ListNode(-1);
    5. ListNode* prev = preHead;
    6. while (l1 != nullptr && l2 != nullptr) {
    7. if (l1->val < l2->val) {
    8. prev->next = l1;
    9. l1 = l1->next;
    10. } else {
    11. prev->next = l2;
    12. l2 = l2->next;
    13. }
    14. prev = prev->next;
    15. }
    16. // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
    17. prev->next = l1 == nullptr ? l2 : l1;
    18. return preHead->next;
    19. }
    20. };

    复杂度分析

    时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。

    空间复杂度:O(1)。我们只需要常数的空间存放若干变量。

    三,移除链表元素

    203. 移除链表元素 - 力扣(LeetCode)https://leetcode.cn/problems/remove-linked-list-elements/?plan=data-structures&plan_progress=ggfacv7

    1,递归

    链表的定义具有递归的性质,因此链表题目常可以用递归的方法求解。这道题要求删除链表中所有节点值等于特定值的节点,可以用递归实现。

    对于给定的链表,首先对除了头节点 head 以外的节点进行删除操作,然后判断 head 的节点值是否等于给定的 val。如果 head 的节点值等于val,则 head 需要被删除,因此删除操作后的头节点为 head.next;如果 head 的节点值不等于 val,则head 保留,因此删除操作后的头节点还是 head。上述过程是一个递归的过程。

    递归的终止条件是 head 为空,此时直接返回 head。当head 不为空时,递归地进行删除操作,然后判断head 的节点值是否等于 val 并决定是否要删除head。

    1. class Solution {
    2. public:
    3. ListNode* removeElements(ListNode* head, int val) {
    4. if (head == nullptr) {
    5. return head;
    6. }
    7. head->next = removeElements(head->next, val);
    8. return head->val == val ? head->next : head;
    9. }
    10. };

    复杂度分析

    时间复杂度:O(n),其中 n 是链表的长度。递归过程中需要遍历链表一次。

    空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用栈,最多不会超过 n 层。

    2,迭代

    也可以用迭代的方法删除链表中所有节点值等于特定值的节点。
    用temp表示当前节点。如果temp的下一个节点不为空且下一个节点的节点值等于给定的val,则
    需要删除下一个节点。删除下一个节点可以通过而

    以下做法实现:
    temp.next = temp.next. next
    如果temp 的下一个节点的节点值不等于给定的val,则保留下一个节点,将temp移动到下一个
    节点即可。当temp的下一个节点为空时,链表遍历结束,此时所有节点值等于val的节点都被删除。具体实现方面,于链表的头节点head有可能需要被删除,因此创建哑节点dummyHead,令
    dummyHead.next= head,初 始化temp =dummyHead,然后遍历链表进行删除操作。最终
    返回dummyHead.next 即为删除操作后的头节点。

     

    1. class Solution {
    2. public:
    3. ListNode* removeElements(ListNode* head, int val) {
    4. struct ListNode* dummyHead = new ListNode(0, head);
    5. struct ListNode* temp = dummyHead;
    6. while (temp->next != NULL) {
    7. if (temp->next->val == val) {
    8. temp->next = temp->next->next;
    9. } else {
    10. temp = temp->next;
    11. }
    12. }
    13. return dummyHead->next;
    14. }
    15. };

    复杂度分析

    • 时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。

    • 空间复杂度:O(1)。

  • 相关阅读:
    macOS Outlook 查看邮件的源码 HTML源码
    Netty概述及Hello word入门
    代码随想录刷题|LeetCode 62.不同路径 63. 不同路径 II
    【鸟哥杂谈】十分钟搭建自己的本地 Node-Red可拖拽图形化物联网
    基于B/S架构的合同信息管理系统(Java+Web+MySQL)
    丢掉乱七八糟的软件,留下这4个,让你告别索然无味
    LeetCode每日一题(756. Pyramid Transition Matrix)
    运动想象 (MI) 迁移学习系列 (9) : 数据对齐(EA)
    MySQL基础篇-基本sql语句
    制作一个简单HTML公司官网网页设计(HTML+CSS)
  • 原文地址:https://blog.csdn.net/m0_63309778/article/details/126743391