while (n1 || n2 || carry) {
int sum = carry;
if (n1) { sum += n1->val; n1 = n1->next; }
if (n2) { sum += n2->val; n2 = n2->next; }
carry = sum / 10;
dgi* p = new dgi(sum % 10);
result->addNode(p); // 头插,结果低位在前
}
图解
建表输入 999(高→低读),头插后链表为个位在前:
9
→
9
→
9
→
∅
n1 表头=个位
建表输入 100(高→低读),头插后:代表 1->0->0 即个位 0
0
→
0
→
1
→
∅
n2 表头=个位
第1位9+0+carry0=9?此例 999+1 见演进;按 example 999+100 演示进位:9+0=9,无进位
9
+
0
+
c0
=
9
本位9 carry0
第2位9+0+0=9,本位9 进位0
9
→
9
+
0
=
9
结果头插 9→9
第3位9+1+0=10,本位0 进位1
9
→
9
→
9
+
1
=
10
本位0 carry1
收尾n1、n2 都空但 carry=1:sum=1,本位1,正是 while 条件含 carry 的作用(避免丢最高位)
∅
+
∅
+
1
=
1
carry 进位成新最高位
结果结果同样头插(低位在前),从表头输出得 1099 → 即 999+100=1099
9
→
9
→
0
→
1
→
∅
表头=个位9,输出 1 0 9 9
复杂度与易错
时间 O(max(m, n)),m、n 为两数位数
空间 O(max(m, n)),新建结果链表所需空间
循环条件必须包含 carry,否则 999+1=1000 这类最高位进位会丢失
进位与本位顺序不能写反:carry=sum/10、本位=sum%10
头插法使链表低位在前,必须理解存储方向,否则会误以为相加错位
两链表长度不同时,短链遍历完后指针为空,需用 if(n1)/if(n2) 跳过,禁止解引用空指针
new 出来的结果节点需交由 intList 析构统一 delete,防止内存泄漏
复用技巧
头插法建表使输入高位在前自动转为个位在前while 用 n1||n2||carry 三条件统一处理长度不等与末位进位结果同样头插,输出顺序自然与输入对齐ios::sync_with_stdio(false)+cin.tie(nullptr) 加速读入析构函数遍历释放链表防止内存泄漏friend 友元函数访问私有 head
3029
单链表就地逆序(链表逆置)
基础
头插法就地逆序(不申请新结点,仅改指针)带头结点单链表的头插法就地逆序
带头结点的单链表尾指针tail
核心思想
读入 n 个整数用「带头结点 + 尾指针 tail」建表,每次尾插都是 O(1),保持输入顺序。逆序时不另开新链:把原首结点 begin 固定为逆序后的尾结点,不断把 begin 后面的结点摘下、用头插法挂到头结点之后,后摘下的排得越靠前,整体顺序就被翻转。每步只动常数个指针,故为就地 O(n),最后别忘了把 tail 更新为 begin。
关键代码
void revList() {
intNode* begin = head->next;
if (begin == nullptr) return; // 空表直接返回
intNode* end = begin->next;
while (end) { // 把end头插到表头之后
begin->next = end->next; // 从原位置摘下end
end->next = head->next; // end指向当前首结点
head->next = end; // end成为新的首结点
end = begin->next; // 取begin后的下一个待处理结点
}
tail = begin; // 原首结点现在是尾结点
}
对称差集是只在 A 或只在 B、但不同时出现的元素。两表都已按非递减排好,用双指针 i、j 同时从前往后扫描比较 A[i] 与 B[j]:相等是交集,两边等于该值的重复项一并跳过且不入结果;A[i] 小则只在 A,入结果并跳过 A 中相同值;A[i] 大则对称处理 B。某表扫完后,另一表的剩余元素只属于自己,去重后直接加入。一遍线性扫描即可,无需再排序或查找。
关键代码
while (i < n && j < m) {
if (A[i] == B[j]) { // 交集,两边都跳过,不入结果
int x = A[i];
while (i < n && A[i] == x) i++;
while (j < m && B[j] == x) j++;
} else if (A[i] < B[j]) { // 只在A中
int x = A[i]; C[cSize++] = x;
while (i < n && A[i] == x) i++;
} else { // 只在B中
int x = B[j]; C[cSize++] = x;
while (j < m && B[j] == x) j++;
}
}
// 余下:尾部剩余段各自去重后入C
while (i < n) { int x=A[i]; C[cSize++]=x; while(i<n && A[i]==x) i++; }
while (j < m) { int x=B[j]; C[cSize++]=x; while(j<m && B[j]==x) j++; }
图解
A
1
2
2
4
B
2
3
4
例:A=[1,2,2,4],B=[2,3,4],双指针 i、j 比较 A[i] 与 B[j]
第1步
1
2
2
4
↔
2
3
4
A[0]=1 < B[0]=2 → 1 只在 A,入 C,跳过 A 中的 1,i=1。 C=[1]
第2步
1
2
2
4
↔
2
3
4
A[1]=2 == B[0]=2 → 交集!成段跳过 A 中所有 2(i→3)与 B 中所有 2(j→1),不入 C
给定入栈顺序 inSeq 和目标出栈顺序,贪心模拟:依次取出目标里要弹出的 need,只要栈顶不是 need 且入栈序列还有剩余,就不断按序压栈,直到栈顶恰好等于 need 时弹出。若把所有元素都压完仍凑不出 need,则该出栈序列非法。因为出栈序列唯一决定每一步动作,没有别的选择,所以贪心模拟是充要判定。
关键代码
int top = 0, nextIn = 1; bool ok = true;
for (int i = 1; i <= n; i++) {
int need; cin >> need;
if (ok) {
// 栈顶不是要的数就继续按序压栈
while ((top == 0 || st[top] != need) && nextIn <= n)
st[++top] = inSeq[nextIn++];
if (top > 0 && st[top] == need) top--; // 匹配则弹出
else ok = false; // 凑不出则非法
}
}
cout << (ok ? "YES" : "NO") << "\n";
图解
输入入栈序列 inSeq =
1
→
2
→
3
;目标出栈序列 =
3
1
2
第1步need=3:栈空,按序压入 1、2、3,栈顶=3 匹配 → 弹出
压栈过程(栈底→栈顶):
1
→
2
→
3
top栈顶=3=need,pop
弹出后栈:
1
→
2
topnextIn 已越过 3(入栈耗尽)
第2步need=1:栈顶=2≠1,且 nextIn>n 无法再压栈
while 退出后检查栈:
1
→
2
topst[top]=2≠1 → 置 ok=false
第3步need=2:ok 已为 false,仅读入不判断
最终结果:
NO
出栈序列 [3,1,2] 非法
复杂度与易错
时间 O(n),每个元素至多入栈一次、出栈一次
空间 O(n),存储入栈序列与模拟栈
while 条件需同时判断 top==0(空栈)与 nextIn<=n(入栈耗尽),否则会访问 st[0] 或越界
表面是循环队列模拟(每秒队首买一张,没买完回队尾,直到第 k 人买完)。但总耗时恰等于"第 k 人买完最后一张前所有人累计买到的票数之和"。设 target=tickets[k]:排在 k 前面或本人(i≤k)每轮都先于他买票,最多买 target 张;排在后面的人(i>k)在决定性的最后一轮还没轮到就结束,最多买 target-1 张。各人再与自身票数取 min 后累加,O(n) 直接算出,无需真实维护队列。
关键代码
int target = tickets[k];
int answer = 0;
for (int i = 0; i < n; ++i) {
if (i <= k) // 在 k 前面或就是 k,最多买 target 张
answer += smaller(tickets[i], target);
else // 在 k 后面,最后一轮没轮到,最多买 target-1 张
answer += smaller(tickets[i], target - 1);
}
return answer;
一排同速移动的魔像,正数向右、负数向左,只有"左正右负"才会相撞。从左到右扫描,用栈保存当前可能存活的魔像,栈底到栈顶恰好对应长廊从左到右的顺序。正数直接入栈;遇到负数时用 while 不断与栈顶最近的右行魔像比战斗力:栈顶小则弹出继续向左撞,相等则同归于尽,栈顶大则当前被粉碎。循环结束后若当前仍存活就入栈,最终栈中元素即为答案。
关键代码
bool alive = true;
while (alive && !survivors.empty() && survivors.top() > 0 && cur < 0) {
long long topPower = absoluteValue(survivors.top());
long long curPower = absoluteValue(cur);
if (topPower < curPower) survivors.pop(); // 栈顶被粉碎,继续向左
else if (topPower == curPower){ survivors.pop(); alive=false; } // 同归于尽
else alive = false; // 当前被粉碎
}
if (alive) survivors.push(cur); // 幸存则入栈
图解
第1步
1
读入 1(正),直接入栈 → 栈底=1
第2步
1
→
2
读入 2(正),直接入栈(栈底到栈顶即左到右)
第3步
1
→
2
→
3
读入 3(正),直接入栈 → 栈=[1,2,3]
第4步
1
→
2
→
3
栈顶3>0←
-10
读入 -10(负):触发相向碰撞,cur 自右向左连撞栈顶
碰撞
1
→
2
→
∅
|3|<|-10| 弹3
碰撞
1
→
∅
|2|<|-10| 弹2
碰撞
∅
|1|<|-10| 弹1,栈空循环结束,alive 仍为真
第5步
-10
幸存的 -10 入栈 → 最终栈=[-10],输出 -10
复杂度与易错
时间 O(n),每个魔像最多入栈一次、出栈一次
空间 O(n),栈最多存放 n 个魔像
碰撞判定必须同时满足"栈非空、栈顶为正、当前为负"三条,缺一不可
战斗力相等是同归于尽:既要弹栈顶又要置 alive=false 让当前不入栈,两边都不能漏
栈顶小被粉碎后当前魔像要继续向左比较下一个栈顶,不能只比一次(用 while 而非 if)
正数无论如何直接入栈,不要误判它会和左边碰撞
栈空时输出 Empty,不要漏掉空结果的处理;战斗力较大时用 long long 更稳妥
复用技巧
栈底到栈顶天然保持左到右顺序,避免额外排序或反转用 while 循环处理一个负数引发的连续碰撞碰撞只发生在 栈顶>0 且 cur<0 这一唯一相向条件用 alive 标志区分当前魔像是被撞毁还是幸存入栈手写顺序栈与绝对值,体现栈基本操作
不显式建树,而是把"孩子关系"反向记成父指针数组 parent,使任一结点都能沿 parent 上行到根。先从 x 上行到根,把沿途结点全部标记到 on_path;再让 y 上行,遇到的第一个被标记结点就是两条到根路径的最早交汇点,即最近公共祖先(LCA)。这样把 LCA 转化为"两条到根路径求第一个公共点",无需求深度、无需递归。
关键代码
// 1) 孩子关系反存为父指针
for (int i = 1; i <= n; i++) {
cin >> a >> b;
if (a) parent[a] = i; // a 为 0 表示无左孩子
if (b) parent[b] = i;
}
// 2) 标记 x 到根的整条路径
for (int p = x; p; p = parent[p])
on_path[p] = true;
// 3) y 向上走,第一个落在 x 路径上的点即 LCA
int ans = y;
while (!on_path[ans]) ans = parent[ans];
cout << ans << endl;
把二叉树原地改成一条只沿 right 走的链表,顺序恰为先序(根-左-右),且不用栈或递归只花 O(1) 额外空间。对每个有左子树的结点 cur,找到其左子树的最右结点 pre(左子树先序的最后一个),把 cur 原来的右子树挂到 pre.right,再把整棵左子树搬到 cur 的右边并置空 cur 的左指针,然后沿 right 继续。
关键代码
while (cur != 0) {
if (tree[cur].left != 0) {
int pre = tree[cur].left;
while (tree[pre].right != 0) // 找左子树最右结点
pre = tree[pre].right;
tree[pre].right = tree[cur].right; // 原右子树挂到前驱后
tree[cur].right = tree[cur].left; // 左子树搬到右边
tree[cur].left = 0; // 置空左指针
}
cur = tree[cur].right; // 沿右链前进
}
用一次非递归先序遍历同时完成遍历与改链:手写顺序栈代替递归(n 可达 10 万,退化成链时递归会爆栈),处理当前结点 cur 时先压右孩子再压左孩子保证下轮先弹左,符合"根左右"。改链用 prev 记录先序中上一个结点,每访问 cur 就令 prev.left=0、prev.right=cur,逐步串成只走 right 的单链表。
关键代码
nodes.push(root);
int prev = 0;
while (!nodes.empty()) {
int cur = nodes.pop();
int L = tree[cur].left, R = tree[cur].right; // 先暂存原孩子
if (prev != 0) { tree[prev].left = 0; tree[prev].right = cur; }
if (R != 0) nodes.push(R); // 先压右
if (L != 0) nodes.push(L); // 再压左,下一轮先弹左
prev = cur;
}
if (prev != 0) { tree[prev].left = 0; tree[prev].right = 0; } // 收尾
给定有向图,判断从起点 s 能否到达终点 t。从 s 出发做一次 BFS 遍历,凡能被遍历到的点都是 s 可达的,所以只要遍历途中碰到 t 就说明可达。用队列保存待扩展节点,用 visited 数组防止重复入队,出队时若节点等于 t 立刻输出 True,队列空了仍没遇到则输出 False。只判可达性,BFS/DFS 皆可。
关键代码
visited[s] = true;
q.push(s);
while (!q.empty()) {
int u = q.front(); q.pop();
if (u == t) { cout << "True\n"; return 0; }
for (int v : graph[u])
if (!visited[v]) {
visited[v] = true; // 入队即标记
q.push(v);
}
}
cout << "False\n";