2130. Maximum Twin Sum of a Linked List
題目 / Problem
中文:給定一個長度為偶數 n 的鏈結串列(linked list)的頭節點 head。第 i 個節點(從 0 開始編號)的「孿生節點」(twin)是第 (n-1-i) 個節點,其中 0 <= i <= n/2 - 1。也就是第一個節點配最後一個、第二個配倒數第二個,依此類推。一對孿生節點的值相加稱為「孿生和」(twin sum)。請回傳所有孿生和當中的最大值。
English: You are given the head of a linked list with even length n. Node i (0-indexed) and node n-1-i are "twins" — the first node pairs with the last, the second with the second-to-last, and so on. The "twin sum" is the sum of a node and its twin. Return the maximum twin sum across all pairs.
Constraints / 限制
- 節點數量是偶數,範圍 [2, 10^5] / The number of nodes is even, in range [2, 10^5].
- 1 <= Node.val <= 10^5.
Worked Example / 範例
Input: head = [5,4,2,1]
Output: 6
配對:node0+node3 = 5+1 = 6,node1+node2 = 4+2 = 6。最大值是 6。 Pairs: 5+1=6 and 4+2=6, so the maximum twin sum is 6.
名詞解釋 / Glossary
- 鏈結串列 / linked list:一種資料結構,由一連串「節點」組成,每個節點存一個值
val和一個指向下一個節點的指標next。你只能從頭開始一個一個往後走,不能像陣列那樣用索引直接跳到中間。 - 節點 / node:鏈結串列的基本單位,包含資料
val和指標next(next指向下一個節點,最後一個節點的next是NULL)。 - 指標 / pointer:一個變數,存的是另一個物件(這裡是節點)的「記憶體位址」。
p->val表示「順著指標 p 找到那個節點,取它的 val」。 - 雙指針 / two pointers:用兩個指標同時掃描資料的技巧。本題讓一個指標從前半起點走、另一個從後半起點走,同時前進來湊出每一對孿生和。
- 反轉鏈結串列 / reversing a linked list:把箭頭方向掉頭,讓原本
A->B->C變成C->B->A。常用「三指標」逐步把每個節點的next改成指向前一個節點。 - 快慢指針 / slow-fast pointers:找中點的技巧。慢指標一次走一步、快指標一次走兩步;當快指標到尾端時,慢指標剛好在中間。
- 原地修改 / in-place:不另外開一個大陣列,直接在原本的結構上動手腳,藉此把額外空間降到 O(1)。
- 時間/空間複雜度 / time & space complexity:用 O(...) 描述「資料量變大時,花的時間/記憶體大致怎麼成長」。
n在這裡指節點數量。
思路
最直覺的暴力法(brute force)是:先把整條鏈結串列走一遍,把每個節點的值依序存進一個陣列。因為鏈結串列不能像陣列那樣用索引直接跳到第 n-1-i 個節點,所以先「攤平」成陣列後,孿生和就很好算:用兩個索引 left = 0 和 right = n-1,把 arr[left] + arr[right] 算出來、更新最大值,然後 left++、right--,直到兩者相遇。這個做法完全正確,時間是 O(n),但需要一個額外的陣列,空間是 O(n)。對於 n 高達 10^5 的情況雖然能過,但我們可以做得更省。
更省空間的關鍵觀察是:孿生節點其實就是「前半段的第 i 個」和「後半段倒過來的第 i 個」。所以我們不需要整個陣列,只要能讓「後半段」也能從前往後走就好——這就是反轉的用處。具體三步:第一步用快慢指針找到中點(慢指標每次一步、快指標每次兩步,快指標到尾端時慢指標剛好在中間);第二步把後半段就地反轉,讓它的箭頭掉頭;第三步用兩個指標,一個從原本的頭、一個從反轉後的後半段頭,同時往後走,每一步 first->val + second->val 就是一對孿生和,取最大值即可。為什麼這樣對?因為反轉後,後半段的第一個節點正好是原本的最後一個節點,也就是頭節點的孿生;兩指標同步前進,每一步都精確對上一對孿生。這個做法時間 O(n)、空間 O(1)。
The brute-force idea is simple: walk the whole list once and copy every value into an array. We do this because a linked list can't jump directly to index n-1-i the way an array can — you can only step forward node by node. Once flattened into an array, twin sums are easy: set left = 0, right = n-1, compute arr[left] + arr[right], track the max, then move left++ and right-- until they meet. This is correct and runs in O(n) time, but it uses an O(n) array.
To save space, notice that a twin is just "the i-th node from the front" paired with "the i-th node of the reversed back half." So instead of storing everything, we make the back half traversable front-to-front by reversing it. Three steps: (1) find the middle with slow/fast pointers — slow moves one node at a time, fast two, so when fast reaches the end slow sits at the midpoint; (2) reverse the second half in place by flipping each node's next to point backward; (3) walk one pointer from the original head and another from the reversed second half simultaneously — at each step first->val + second->val is exactly one twin sum, and we keep the maximum. It works because after reversal the first node of the back half is the original last node, i.e. the head's twin, and stepping both pointers in lockstep lines up every twin pair. This runs in O(n) time and O(1) extra space.
逐步走查 / Walkthrough
以 head = [5,4,2,1](n = 4)為例 / Using head = [5,4,2,1] (n = 4):
Step 1 — 找中點 / Find middle (slow/fast):
| 動作 / action | slow 位置 | fast 位置 |
|---|---|---|
| 起始 / start | 5 | 5 |
| 走一輪 / iter 1 | 4 | 2 |
| 走一輪 / iter 2 | 2 | NULL (停止) |
fast 變成 NULL,迴圈停止,slow 指向 2,這就是後半段的起點。/ fast is NULL, loop stops; slow points at 2, the start of the back half.
Step 2 — 反轉後半段 / Reverse the second half ([2,1] → [1,2]):
| prev | cur | 動作 / action |
|---|---|---|
| NULL | 2 | 把 2->next 指向 NULL / point 2 backward |
| 2 | 1 | 把 1->next 指向 2 / point 1 backward |
| 1 | NULL | 結束,反轉頭是 1 / done, new head is 1 |
反轉後後半段是 1 -> 2。前半段仍是 5 -> 4。/ Back half is now 1 -> 2; front half is still 5 -> 4.
Step 3 — 雙指針求孿生和 / Two pointers for twin sums:
| first | second | twin sum | max |
|---|---|---|---|
| 5 | 1 | 5+1 = 6 | 6 |
| 4 | 2 | 4+2 = 6 | 6 |
| (second 到 NULL,停) | 6 |
回傳 6。/ Return 6. ✅
Solution — C
// 演算法 / Algorithm:
// 1) 快慢指針找中點 / slow-fast pointers find the middle.
// 2) 原地反轉後半段 / reverse the second half in place.
// 3) 兩指針從兩半的頭同步前進,每步取孿生和的最大值 / walk both
// halves together, keep the max of each twin sum. O(n) time, O(1) space.
/**
* Definition for singly-linked list. / 單向鏈結串列的定義(LeetCode 已提供)
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int pairSum(struct ListNode* head) {
// slow 和 fast 都從頭出發 / both pointers start at head
struct ListNode* slow = head; // 慢指標,最後會停在中點 / ends at the middle
struct ListNode* fast = head; // 快指標,速度是慢指標兩倍 / moves twice as fast
// 快指標每次走兩步,所以要確認 fast 和 fast->next 都存在才能再走
// fast moves 2 steps, so both fast and fast->next must exist to continue
while (fast != NULL && fast->next != NULL) {
slow = slow->next; // 慢指標前進一步 / slow advances 1 node
fast = fast->next->next; // 快指標前進兩步 / fast advances 2 nodes
}
// 迴圈結束時 slow 正好指向後半段的第一個節點
// when the loop ends, slow points at the first node of the back half
// --- 原地反轉後半段 / reverse the back half in place ---
struct ListNode* prev = NULL; // 反轉後新串列的「前一個」節點,起始為空 / previous node
struct ListNode* cur = slow; // 從後半段開頭開始反轉 / start reversing from the middle
while (cur != NULL) {
struct ListNode* nxt = cur->next; // 先記住下一個,否則改了指標就找不到 / save next first
cur->next = prev; // 把箭頭掉頭,指向前一個 / flip the arrow backward
prev = cur; // prev 往前挪到目前節點 / move prev up
cur = nxt; // cur 往前挪到剛存的下一個 / move cur up
}
// 反轉完成後 prev 是後半段反轉後的新頭 / prev is now the head of the reversed back half
// --- 雙指針求最大孿生和 / two pointers for max twin sum ---
struct ListNode* first = head; // 從原始頭走前半段 / front pointer
struct ListNode* second = prev; // 從反轉後的後半段頭走 / back pointer
int best = 0; // 記錄目前最大孿生和 / running maximum
while (second != NULL) { // 後半段走完代表所有孿生都配完了 / back half exhausted = done
int sum = first->val + second->val; // 一對孿生和 / one twin sum
if (sum > best) best = sum; // 比目前最大還大就更新 / update the max
first = first->next; // 前指標前進 / advance front
second = second->next; // 後指標前進 / advance back
}
return best; // 回傳最大孿生和 / return the answer
}
Solution — C++
// 演算法 / Algorithm:
// 1) 快慢指針找中點 / slow-fast pointers find the middle.
// 2) 原地反轉後半段 / reverse the second half in place.
// 3) 兩指針同步前進,取孿生和最大值 / walk both halves, keep max twin sum.
// O(n) time, O(1) extra space.
/**
* 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 {
public:
int pairSum(ListNode* head) {
// 快慢指針找中點 / find the middle with slow & fast pointers
ListNode* slow = head; // 走一步 / one step at a time
ListNode* fast = head; // 走兩步 / two steps at a time
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next; // 慢指標 +1 / slow advances by 1
fast = fast->next->next;// 快指標 +2 / fast advances by 2
}
// slow 現在指向後半段第一個節點 / slow is now at the start of the back half
// 原地反轉後半段 / reverse the back half in place
ListNode* prev = nullptr; // 反轉串列的前一節點 / previous node, starts empty
ListNode* cur = slow; // 從中點開始 / start from the middle
while (cur != nullptr) {
ListNode* nxt = cur->next; // 暫存下一個節點 / save next before we overwrite it
cur->next = prev; // 箭頭掉頭 / reverse the link
prev = cur; // prev 前進 / move prev forward
cur = nxt; // cur 前進 / move cur forward
}
// prev 是反轉後後半段的頭 / prev is the head of the reversed back half
// 雙指針求最大孿生和 / two pointers compute the max twin sum
ListNode* first = head; // 前半段指標 / front pointer
ListNode* second = prev; // 後半段指標 / back pointer
int best = 0; // 目前最大值 / running maximum
while (second != nullptr) { // 後半段走完即結束 / stop when back half ends
// std::max 回傳兩者較大的,比手寫 if 更簡潔 / std::max returns the larger of two
best = std::max(best, first->val + second->val); // 更新最大孿生和 / update max
first = first->next; // 前進 / advance front
second = second->next; // 前進 / advance back
}
return best; // 回傳答案 / return the result
}
};
複雜度 / Complexity
- Time: O(n) — 找中點走半條串列、反轉走半條、雙指針走半條,每一步都是常數時間,總共是 n 的常數倍,所以是 O(n)。
n是節點數量。/ Finding the middle, reversing, and the two-pointer pass each touch at most half the list with constant work per node, summing to a constant multiple of n.nis the number of nodes. - Space: O(1) — 只用了幾個指標變數(slow、fast、prev、cur、first、second)和一個整數,與
n無關,沒有額外配置陣列。反轉是原地進行的。/ Only a handful of pointer variables and one integer are used, independent ofn. The reversal is done in place — no extra array.
Pitfalls & Edge Cases
- 快指針的迴圈條件 / Fast-pointer loop condition:必須同時檢查
fast != NULL && fast->next != NULL。因為fast一次走兩步,若漏檢查就可能對 NULL 做->next,造成程式崩潰。/ You must check bothfastandfast->nextbefore stepping two nodes, otherwise you dereference NULL and crash. - 反轉前先存 next / Save
nextbefore reversing:cur->next = prev會覆蓋掉原本的 next,所以一定要先用nxt暫存,否則就找不到後面的節點,串列直接斷掉。/cur->next = prevoverwrites the original link, so you must stashnxtfirst or you lose the rest of the list. - 迴圈停止條件用 second / Loop on
second, notfirst:反轉後後半段的長度恰好是前半段的一半對應,用second != NULL結束最安全;因為長度保證偶數,兩半剛好等長,配對不會漏也不會多。/ Stop whensecondbecomes NULL; sincenis even the halves are equal length, so every twin is paired exactly once. - 整數溢位免擔心 / No overflow worry:每個值最多 10^5,兩個相加最多 2×10^5,遠在 32 位元
int範圍內,不需要long long。/ Two values sum to at most 200000, well withinintrange — no need for a wider type. best初值設 0 安全 / Initializingbest = 0is safe:因為Node.val >= 1,任何孿生和至少是 2,一定會比初值 0 大,所以第一次比較必定更新。/ Since values are ≥ 1, every twin sum is ≥ 2 and beats the initial 0, so the max is always set correctly.- 不要忘記回傳值的意義 / Return the max, not the last sum:題目要的是所有孿生和的最大值,不是某一對的和,所以要在迴圈裡持續比較更新。/ The answer is the maximum over all pairs, so you must compare every twin sum, not just return the last one.