- 2. 两数相加 中等
- 19. 删除链表的倒数第 N 个结点 中等
- 21. 合并两个有序链表 简单
- 160. 相交链表 简单
- 141. 环形链表 简单
- 206. 反转链表 简单
- 234. 回文链表 简单
- 148. 排序链表 中等
- 406. 根据身高重建队列 中等
2. 两数相加
记忆要点:
- 两个链表节点逐个相加
- 定义一个进位的变量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 个结点
解法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. 合并两个有序链表
比较插入法:
记忆要点:
- 定义一个新链表,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. 相交链表
解法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 中的全部节点
哈希表法:
记忆要点:
- 定义一个新的哈希表
- 将链表里面的节点逐个节点放入哈希表里
- 每到达一个节点,检查哈希表,查看是否有该节点
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. 反转链表
迭代法:
- 双指针: 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 层。
解法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)
解法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(NlogN), 其中 N 是链表的长度
- 空间复杂度:O(logN), 其中 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(NlogN), 其中 N 是链表的长度
- 空间复杂度:O(1), 其中 N 是链表的长度。空间复杂度主要取决于递归调用的栈空间
406. 根据身高重建队列
解法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),即为排序需要使用的栈空间