打卡hot100
238. 除自身以外数组的乘积 - 力扣(LeetCode)
class Solution {
public:vector<int> productExceptSelf(vector<int>& nums) {int n = nums.size();vector<int> ans(n,1);for(int i = 1; i < n; i++){ // 从左到右遍历ans[i] = nums[i - 1] * ans[i-1]; }int temp = 1;for(int i = n- 2; i >= 0;i--){ // 从右到左遍历temp *= nums[i+1]; // 用temp进行维护右边的数组ans[i] = ans[i] * temp;}return ans;}
};
你的实现是标准且高效的 “左前缀 × 右后缀” 做法,时间 (O(n))、额外空间 (O(1))(不计输出),且不使用除法。为什么对——看两个循环分别保证了什么不变式。
一、第一趟(左→右):把左侧前缀积放进 ans[i]
vector<int> ans(n, 1);
for (int i = 1; i < n; ++i) {ans[i] = nums[i - 1] * ans[i - 1];
}
-
令 (L[i] = \prod_{k=0}^{i-1} nums[k])(空积 (L[0]=1))。
-
循环不变式:执行到第
i
次时,ans[i] == L[i]
。-
初始
ans[0]=1
,即 (L[0]); -
递推:
ans[i] = ans[i-1] * nums[i-1]
,等于 (L[i-1]\cdot nums[i-1] = L[i])。
-
-
结束后:整行成立——
ans[i]
正好存了 左侧乘积 (L[i])。
边界正确性:从
i=1
开始,保证nums[i-1]
不越界;同时保留ans[0]=1
(空积)。
二、第二趟(右→左):用滚动变量乘上右侧后缀积
int temp = 1; // 表示当前 i 的右侧乘积 R[i]
for (int i = n - 2; i >= 0; --i) {temp *= nums[i + 1]; // 令 temp = R[i] = ∏_{k=i+1}^{n-1} nums[k]ans[i] *= temp; // ans[i] = L[i] * R[i]
}
-
令 (R[i] = \prod_{k=i+1}^{n-1} nums[k])(空积 (R[n-1]=1))。
-
循环不变式:进入下标
i
时,temp == R[i]
。-
初始在
i = n-2
处,temp
先乘nums[n-1]
,得到R[n-2]
; -
一般地,
temp *= nums[i+1]
后正是 (R[i]),于是ans[i] *= temp
得到 (L[i]\cdot R[i])。
-
-
i=n-1
不需要处理:此时 (R[n-1]=1),而ans[n-1]
已是 (L[n-1]=\prod_{k=0}^{n-2} nums[k])。
边界正确性:从
i=n-2
开始,保证nums[i+1]
不越界,并最终覆盖到i=0
。
三、为何自然处理 0
-
若数组里 无 0:每个
ans[i] = L[i]*R[i]
即“总乘积除去自身”,无需除法。 -
若 恰有 1 个 0:只有 0 所在位置
k
的ans[k] =
(其它元素之积),其余位置因 (L[i]) 或 (R[i]) 含 0 而为 0。上述两趟正好得出这个结果。 -
若 ≥2 个 0:任意位置的左或右乘积都含 0,结果全为 0。仍然被上述乘法自然覆盖。
四、复杂度与实现细节
-
时间:两趟线性扫描 (O(n));空间:只用到
ans
和一个临时标量temp
,额外 (O(1))。 -
初值选
1
:ans
初始化为 1、temp
初始为 1,都是利用“乘法单位元”方便原地乘。 -
溢出:如担心中间值暂时超出
int
,可把中间乘子改为long long
,最后再转回int
(LeetCode 的用例通常安全)。
五、快速走例
nums = [1,2,3,4]
-
左扫后
ans = [1, 1, 2, 6]
(即 (L)) -
右扫:
temp=1→4→12→24
,得到ans=[24,12,8,6]
✅
nums = [0,1,2,3]
-
左扫后
ans = [1,0,0,0]
-
右扫:最终
ans=[6,0,0,0]
(只有 0 的位置非 0)✅
结论:你的代码严格维护了“第一趟写 (L[i])、第二趟乘 (R[i])”这两个不变式,因此对每个 i
,ans[i]=L[i]*R[i]
,即“除自己之外的所有数的乘积”。
148. 排序链表 - 力扣(LeetCode)
/*** 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 {
private:ListNode* merge(ListNode* left, ListNode* right){ListNode* dummyhead = new ListNode(0);ListNode* cur = dummyhead;// 比较left, right 的大小while(left && right){if(left->val < right->val){cur->next = left;left = left->next;}else{cur->next = right;right = right->next;}cur = cur->next;}if(left){cur->next = left;}else{cur->next = right;}return dummyhead->next;}
public:ListNode* sortList(ListNode* head) {if(head == nullptr || head->next == nullptr) return head;ListNode * slow = head;ListNode * fast = head->next;while(fast && fast->next){slow = slow->next;fast = fast->next->next;}ListNode* temp = slow->next;slow->next = nullptr;ListNode* right = sortList(temp);ListNode* left = sortList(head);return merge(left, right);}};
这份代码优美地实现了 归并排序(Merge Sort) 算法,其背后最核心的原理是 分治(Divide and Conquer) 思想。
想象一下,让你整理一副被打乱的扑克牌。你一个人从头整理到尾会很慢。但如果使用分治思想,你会这样做:
-
分割 (Divide): 把牌堆大致分成两半。
-
解决 (Conquer): 你找两个朋友,每人给一半,让他们分别把各自那一半牌整理好。(这两个朋友也可以继续找他们的朋友来分担任务,这就是“递归”)。
-
合并 (Combine): 当你的两个朋友把两堆整理好的牌还给你时,你只需要做一个简单的“合并”动作,就能得到一副完全有序的牌。
你的代码就是用这个逻辑来排序链表的。
第一部分: sortList
函数 —— 运筹帷幄的“总管”
这个函数扮演着“总管”的角色。它的主要工作是分割任务、向下分派,以及在任务完成后调用合并专家。
C++
public:ListNode* sortList(ListNode* head) {
第1步:递归的出口 (Base Case)
C++
if(head == nullptr || head->next == nullptr) return head;
原理与原因:这是任何递归算法的灵魂,也是它的终点。如果不对递归设置出口,它就会无限进行下去,直到程序崩溃(栈溢出)。
-
为什么是这个条件? 一个空的链表或只有一个节点的链表,它本身就是“有序”的。这是我们能遇到的最简单、无需再分治的问题。因此,当任务被分割到这么小的时候,就可以直接返回结果了。
第2步:分割阶段 - 找到链表中点
C++
ListNode * slow = head;ListNode * fast = head->next;while(fast && fast->next){slow = slow->next;fast = fast->next->next;}
原理与原因:为了将链表一分为二,我们必须先找到它的中点。这里使用了经典的 快慢指针(Fast & Slow Pointers) 技巧。
-
slow
指针是“慢跑者”,一次移动一步。 -
fast
指针是“快跑者”,一次移动两步。 -
为什么这样能找到中点? 想象在一条跑道上,快跑者的速度是慢跑者的两倍。当快跑者到达终点时,慢跑者正好跑完了一半的路程。在链表中,当
fast
指针到达链表末尾(fast
或fast->next
为空)时,slow
指针就恰好位于链表的中间位置。
第3步:切断链表
C++
ListNode* temp = slow->next; // temp 是右半段链表的头节点slow->next = nullptr; // 从中间切断,形成两个独立的链表
原理与原因:找到中点 slow
之后,slow
是左半段的尾巴,而 slow->next
就是右半段的开头。
-
我们必须执行
slow->next = nullptr;
来切断左右两部分的连接。如果不切断,左半部分链表实际上仍然会连着右半部分,我们就没有成功地将问题“分割”成两个独立的子问题。
第4步:解决与合并阶段 - 递归调用
C++
ListNode* right = sortList(temp);ListNode* left = sortList(head);return merge(left, right);
原理与原因:这是分治思想的完美体现。
-
ListNode* left = sortList(head);
:总管把左半部分这个子任务,再次交给sortList
函数去处理。我们在这里有一种“递归的信念”——我们相信这个函数调用最终会返回一个排好序的左半部分链表。 -
ListNode* right = sortList(temp);
:同理,我们也让sortList
函数去处理右半部分,并相信它能返回一个排好序的右半部分链表。 -
return merge(left, right);
:当两个“下属”(递归调用)都完成了各自的任务,返回了两个有序的子链表后,总管就把这两个成果交给了merge
这个“合并专家”去进行最后的合并,然后返回最终结果。
第二部分: merge
函数 —— 高效工作的“合并专家”
这个函数的目标非常专一:接收两个已经排好序的链表,然后将它们合并成一个更大的有序链表。
C++
private:ListNode* merge(ListNode* left, ListNode* right){
第1步:哨兵节点 (Dummy Head)
C++
ListNode* dummyhead = new ListNode(0);ListNode* cur = dummyhead;
原理与原因:这是一个在链表操作中极其有用的技巧。
-
我们创建一个临时的“哑节点”或“哨兵节点”
dummyhead
。cur
指针从这个节点开始,一步步向后构建我们合并后的新链表。 -
为什么需要它? 如果没有哨兵节点,我们在插入第一个节点时,需要写额外的
if
语句来判断新链表是不是空的。有了它,我们可以统一所有节点的插入逻辑,代码更简洁,不易出错。它就像一个临时的“钩子”,我们先把东西挂在钩子上,最后返回钩子后面的东西就行了。
第2步:循环合并
C++
while(left && right){if(left->val < right->val){cur->next = left;left = left->next;}else{cur->next = right;right = right->next;}cur = cur->next;}
原理与原因:这是合并的核心。只要两个链表都还有节点:
-
比较
left
和right
当前节点的值。 -
把值较小的那个节点连接到新链表的末尾(
cur->next = ...
)。 -
哪个链表的节点被选中了,那个链表的指针就向后移动一步。
-
最后,
cur
指针也必须移动到新链表的末尾(cur = cur->next
),为下一次连接做准备。
第3步:连接剩余的“尾巴”
C++
if(left){cur->next = left;}else{cur->next = right;}
原理与原因:当 while
循环结束时,说明 left
或 right
中至少有一个已经空了。另一个链表可能还有一些剩余的节点。
-
为什么可以直接连接? 因为我们知道输入给
merge
的两个链表本身就是有序的。所以,如果一个链表有剩余,那么它剩下的所有节点一定都比我们已经合并到新链表里的所有节点都要大。因此,我们无需再逐个比较,直接把这剩下的一整条“尾巴”接到新链表的末尾即可。这是一个关键的优化。
第4步:返回最终结果
C++
return dummyhead->next;
原理与原因:dummyhead
只是我们为了方便操作而创建的临时工具,它不属于排序结果的一部分。真正排好序的链表的第一个节点是 dummyhead
的下一个节点。
总结
整个过程就像一个金字塔形的任务分派与结果回收:
-
sortList
在金字塔顶端接收任务,不断地将任务对半分割,向下传递,直到任务小到只有一个节点(金字塔底端)。 -
在金字塔底端,只有一个节点的链表被认为“已完成”,然后开始向上返回。
-
每一层的
merge
函数都将下层返回的两个有序结果合并成一个更大的有序结果,再继续向上返回。 -
最终,在金字塔顶端,最后一次
merge
操作完成了整个链表的排序。
23. 合并 K 个升序链表 - 力扣(LeetCode)
/*** 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 {
private:struct CMP{bool operator()(ListNode* a, ListNode* b){return a->val > b->val;}};
public:ListNode* mergeKLists(vector<ListNode*>& lists) {priority_queue<ListNode*, vector<ListNode*>, CMP> pq;for(auto list : lists){if(list){pq.push(list);}}ListNode* dummyhead = new ListNode(0);ListNode* cur = dummyhead;while(!pq.empty()){auto small = pq.top();pq.pop();cur->next = small;cur = cur->next;if(small->next){pq.push(small->next);}}return dummyhead->next;} };