Leecode Hot100 刷题笔记本-链表-9题(C++版)

130 阅读9分钟
  1. 2. 两数相加 中等
  2. 19. 删除链表的倒数第 N 个结点 中等
  3. 21. 合并两个有序链表 简单
  4. 160. 相交链表 简单
  5. 141. 环形链表 简单
  6. 206. 反转链表 简单
  7. 234. 回文链表 简单
  8. 148. 排序链表 中等
  9. 406. 根据身高重建队列 中等

2. 两数相加

Screen Shot 2023-07-25 at 10.43.22 AM.png 记忆要点:

  • 两个链表节点逐个相加
  • 定义一个进位的变量carry
  • 当前的节点值是(n1+n2+carry)%10
  • 进位的值是(n1+n2+carry)/10
  • 遍历结束后如果carry > 0, 还要附加一个节点
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *head = nullptr, *tail = nullptr;
        int carry = 0;
        while (l1 || l2) {
            int n1 = l1 ? l1->val: 0;
            int n2 = l2 ? l2->val: 0;
            int sum = n1 + n2 + carry;
            if (!head) {
                head = tail = new ListNode(sum % 10);
            } else {
                tail->next = new ListNode(sum % 10);
                tail = tail->next;
            }
            carry = sum / 10;
            if (l1) {
                l1 = l1->next;
            }
            if (l2) {
                l2 = l2->next;
            }
        }
        if (carry > 0) {
            tail->next = new ListNode(carry);
        }
        return head;
    }
};
  • 时间复杂度:O(max(m, n)),其中 m 和 n 分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要 O(1) 的时间
  • 空间复杂度:O(1),注意返回值不计入空间复杂度

19. 删除链表的倒数第 N 个结点

Screen Shot 2023-07-27 at 4.57.25 PM.png

解法1: 计算链表长度:

记忆要点:

  • 定义哑节点dummy, dummy->next = head
  • 首先获得链表的长度L, 需要删的节点位于L-n+1
class Solution {
public:
    int getLength(ListNode* head) {
        int length = 0;
        while (head) {
            ++length;
            head = head->next;
        }
        return length;
    }

    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        int length = getLength(head);
        ListNode* cur = dummy;
        for (int i = 1; i < length - n + 1; ++i) {
            cur = cur->next;
        }
        cur->next = cur->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

解法2: 栈

记忆要点:

  • 遍历链表节点依次入栈, 根据栈先进后出的原则
  • 弹出栈第N个节点就是需要删除的节点
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        stack<ListNode*> stk;
        ListNode* cur = dummy;
        while (cur) {
            stk.push(cur);
            cur = cur->next;
        }
        for (int i = 0; i < n; ++i) {
            stk.pop();
        }
        ListNode* prev = stk.top();
        prev->next = prev->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};
  • 时间复杂度:O(N)
  • 空间复杂度:O(N), 栈的开销.

解法3: 快慢指针

记忆要点:

  • 定义哑节点dummy, dummy->next = head
  • 定义两个指针, 快指针比慢指针领先n个节点
  • 快指针到达尾节点时, 慢指针恰好位于倒数第n个节点
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        ListNode* first = head;
        ListNode* second = dummy;
        for (int i = 0; i < n; ++i) {
            first = first->next;
        }
        while (first) {
            first = first->next;
            second = second->next;
        }
        second->next = second->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};

  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

21. 合并两个有序链表

Screen Shot 2023-07-24 at 9.21.14 AM.png

比较插入法:

记忆要点:

  • 定义一个新链表,shuangzhizhen
  • 两个移动的指针指向两个输入的链表头部
  • 选取值小的节点直接加入新链表,并将其指针向后移动
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (!l1)
            return l2;
        if (!l2)
            return l1;
        ListNode* ihead = new ListNode(0);
        ListNode* node = ihead;
        while (l1 && l2)
        {    
            if (l1->val > l2->val)    swap(l1, l2);
            node->next = l1;
            node = node->next;
            l1 = l1->next;
        }
        node->next = l1 ? l1 : l2;
        return ihead->next;
    }
};

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

递归法:

记忆要点:

  • 不用定义新的链表
  • 递归比较两个链表头节点的值, 通过交换顺序, 使小的为l1
  • l1的next为mergeTwoLists的返回值
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 && l2)
        {
            if(l1->val > l2->val)   swap(l1,l2);            
            l1->next = mergeTwoLists(l1->next, l2);
        }
        return l1 ? l1 : l2;
    }
};
  • 时间复杂度:O(n+m)
  • 空间复杂度:O(n+m)

160. 相交链表

Screen Shot 2023-07-24 at 9.25.31 AM.png

解法1: 双指针法

记忆要点:

  • 双指针
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if (headA == nullptr || headB == nullptr) 
        {
            return nullptr;
        }
        ListNode *pA = headA, *pB = headB;
        while (pA != pB) 
        {
            pA = pA == nullptr ? headB : pA->next;
            pB = pB == nullptr ? headA : pB->next;
        }
        return pA;
    }
};
  • 时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。 需要遍历两个链表各一次
  • 时间复杂度:O(1)

解法2: 哈希表法

记忆要点:

  • 定义一个新的哈希表
  • 首先遍历链表A, 将A的每个节点加入哈希表中
  • 遍历链表B, 对于遍历到的节点, 判断节点是否在哈希集合里
  • 如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        unordered_set<ListNode *> visited;
        ListNode *temp = headA;
        while (temp != nullptr) {
            visited.insert(temp);
            temp = temp->next;
        }
        temp = headB;
        while (temp != nullptr) {
            if (visited.count(temp)) {
                return temp;
            }
            temp = temp->next;
        }
        return nullptr;
    }
};
  • 时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。 需要遍历两个链表各一次
  • 时间复杂度:O(m),其中 m 是链表 headA 的长度。需要使用哈希集合存储链表 headA 中的全部节点

141. 环形链表

Screen Shot 2023-07-25 at 8.36.58 AM.png

哈希表法:

记忆要点:

  • 定义一个新的哈希表
  • 将链表里面的节点逐个节点放入哈希表里
  • 每到达一个节点,检查哈希表,查看是否有该节点
class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head != nullptr) {
            if (seen.count(head)) {
                return true;
            }
            seen.insert(head);
            head = head->next;
        }
        return false;
    }
};
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

快慢指针法:

记忆要点:

  • 定义一快一慢两个指针
  • 慢指针位于head, 快指针位于head->next
  • 慢指针一次走一步,快指针一次走两步
  • 快指针反过来追上慢指针说明存在环形
class Solution {
public:
    bool hasCycle(ListNode* head) {
        if (head == nullptr || head->next == nullptr) {
            return false;
        }
        ListNode* slow = head;
        ListNode* fast = head->next;
        while (slow != fast) {
            if (fast == nullptr || fast->next == nullptr) {
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }
        return true;
    }
};
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

206. 反转链表

Screen Shot 2023-07-25 at 9.10.18 AM.png

迭代法:

  • 双指针: pre和cur
  • cur->next = pre
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr) {
            ListNode* next = curr->next;
            curr->next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
};
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

递归法:

记忆要点:

  • 假设链表的其余部分已经被反转,现在应该如何反转它前面的部分?
  • n1→…→nk−1→nk→nk+1←…←nm
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head || !head->next) {
            return head;
        }
        ListNode* newHead = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return newHead;
    }
};
  • 时间复杂度:O(N),其中 n 是链表的长度。需要对链表的每个节点进行反转操作。
  • 空间复杂度:O(N),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为 n 层。

234. 回文链表

Screen Shot 2023-07-25 at 9.49.23 AM.png

解法1: 数组迭代器法:

记忆要点:

  • 定义一个List
  • 一个迭代器在头,一个迭代器在尾
  • 迭代器相遇说明检查完了
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vals;
        while (head != nullptr) {
            vals.emplace_back(head->val);
            head = head->next;
        }
        for (int i = 0, j = (int)vals.size() - 1; i < j; ++i, --j) {
            if (vals[i] != vals[j]) {
                return false;
            }
        }
        return true;
    }
};
  • 时间复杂度:O(N),其中N指的是链表的元素个数
  • 空间复杂度:O(N),其中N指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值

解法2: 双指针法:

记忆要点:

  • 双指针
  • 先找到中间节点, 再翻转中间之后的链表
  • 逐个对比
  • 也可以用快慢指针, 快指针一次两个节点,慢指针一次一个节点,用慢指针将链表分成两部分再翻转.
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (head == nullptr) {
            return true;
        }

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode* firstHalfEnd = endOfFirstHalf(head);
        ListNode* secondHalfStart = reverseList(firstHalfEnd->next);

        // 判断是否回文
        ListNode* p1 = head;
        ListNode* p2 = secondHalfStart;
        bool result = true;
        while (result && p2 != nullptr) {
            if (p1->val != p2->val) {
                result = false;
            }
            p1 = p1->next;
            p2 = p2->next;
        }        

        // 还原链表并返回结果
        firstHalfEnd->next = reverseList(secondHalfStart);
        return result;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr != nullptr) {
            ListNode* nextTemp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    ListNode* endOfFirstHalf(ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};
  • 时间复杂度:O(N),其中 N 指的是链表的大小
  • 空间复杂度:O(1),我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)

148. 排序链表

Screen Shot 2023-07-25 at 11.03.20 AM.png

解法1: 自顶向下归并排序

记忆要点:

  • 通过快慢指针找到链表中点, 将链表分成两个子链表
  • 对两个子链表进行排序
  • 将排序好的子链表合并成一个链表(参考21)
  • 递归的终止条件是链表的节点个数小于或等于 1,即当链表为空或者链表只包含 1 个节点时,不需要对链表进行拆分和排序
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        return sortList(head, nullptr);
    }

    ListNode* sortList(ListNode* head, ListNode* tail) {
        if (head == nullptr) {
            return head;
        }
        if (head->next == tail) {
            head->next = nullptr;
            return head;
        }
        ListNode* slow = head, *fast = head;
        while (fast != tail) {
            slow = slow->next;
            fast = fast->next;
            if (fast != tail) {
                fast = fast->next;
            }
        }
        ListNode* mid = slow;
        return merge(sortList(head, mid), sortList(mid, tail));
    }

    ListNode* merge(ListNode* head1, ListNode* head2) {
        ListNode* dummyHead = new ListNode(0);
        ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
        while (temp1 != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
            temp = temp->next;
        }
        if (temp1 != nullptr) {
            temp->next = temp1;
        } else if (temp2 != nullptr) {
            temp->next = temp2;
        }
        return dummyHead->next;
    }
};
  • 时间复杂度:O(Nlog⁡N), 其中 N 是链表的长度
  • 空间复杂度:O(log⁡N), 其中 N 是链表的长度。空间复杂度主要取决于递归调用的栈空间

解法2: 自底向上归并排序

记忆要点:

  • 用 subLength 表示每次需要排序的子链表的长度, 初始时subLength = 1
  • 每次将链表拆分成若干个长度为 subLength 的子链表,(最后一个子链表的长度可以小于 subLength), 按照每两个子链表一组进行合并,合并后即可得到若干个长度为 subLength×2 的有序子链表
  • 将 subLength 的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于 length,整个链表排序完毕
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        int length = 0;
        ListNode* node = head;
        while (node != nullptr) {
            length++;
            node = node->next;
        }
        ListNode* dummyHead = new ListNode(0, head);
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode* prev = dummyHead, *curr = dummyHead->next;
            while (curr != nullptr) {
                ListNode* head1 = curr;
                for (int i = 1; i < subLength && curr->next != nullptr; i++) {
                    curr = curr->next;
                }
                ListNode* head2 = curr->next;
                curr->next = nullptr;
                curr = head2;
                for (int i = 1; i < subLength && curr != nullptr && curr->next != nullptr; i++) {
                    curr = curr->next;
                }
                ListNode* next = nullptr;
                if (curr != nullptr) {
                    next = curr->next;
                    curr->next = nullptr;
                }
                ListNode* merged = merge(head1, head2);
                prev->next = merged;
                while (prev->next != nullptr) {
                    prev = prev->next;
                }
                curr = next;
            }
        }
        return dummyHead->next;
    }

    ListNode* merge(ListNode* head1, ListNode* head2) {
        ListNode* dummyHead = new ListNode(0);
        ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
        while (temp1 != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
            temp = temp->next;
        }
        if (temp1 != nullptr) {
            temp->next = temp1;
        } else if (temp2 != nullptr) {
            temp->next = temp2;
        }
        return dummyHead->next;
    }
};
  • 时间复杂度:O(Nlog⁡N), 其中 N 是链表的长度
  • 空间复杂度:O(1), 其中 N 是链表的长度。空间复杂度主要取决于递归调用的栈空间

406. 根据身高重建队列

Screen Shot 2023-07-27 at 5.31.59 PM.png

解法1: 从低到高考虑

记忆要点:

  • 先将所有人身高从低到高排序
  • 定义一个新队列
  • 根据Ki依次将旧队列里的元素放到新队列里
class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(), [](const vector<int>& u, const vector<int>& v) {
            return u[0] < v[0] || (u[0] == v[0] && u[1] > v[1]);
        });
        int n = people.size();
        vector<vector<int>> ans(n);
        for (const vector<int>& person: people) {
            int spaces = person[1] + 1;
            for (int i = 0; i < n; ++i) {
                if (ans[i].empty()) {
                    --spaces;
                    if (!spaces) {
                        ans[i] = person;
                        break;
                    }
                }
            }
        }
        return ans;
    }
};
  • 时间复杂度:O(n2),其中 n 是数组 people 的长度, 我们需要 O(nlogn) 的时间进行排序, 随后需要 O(n2) 的时间遍历每一个人并将他们放入队列中, 由于前者在渐近意义下小于后者,因此总时间复杂度为 O(n2)
  • 空间复杂度:O(logn),即为排序需要使用的栈空间

解法2: 从高到低考虑

记忆要点:

  • 按照 hi为第一关键字降序,ki 为第二关键字升序进行排序
  • 定义一个新队列
  • 根据Ki依次将旧队列里的元素放到新队列里
class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(), [](const vector<int>& u, const vector<int>& v) {
            return u[0] > v[0] || (u[0] == v[0] && u[1] < v[1]);
        });
        vector<vector<int>> ans;
        for (const vector<int>& person: people) {
            ans.insert(ans.begin() + person[1], person);
        }
        return ans;
    }
};
  • 时间复杂度:O(n2),其中 n 是数组 people 的长度, 我们需要 O(nlogn) 的时间进行排序, 随后需要 O(n2) 的时间遍历每一个人并将他们放入队列中, 由于前者在渐近意义下小于后者,因此总时间复杂度为 O(n2)
  • 空间复杂度:O(logn),即为排序需要使用的栈空间