从相邻元素对还原数组 图解题解
这道题到底在问什么
- 输入
- adjacentPairs = [[2,1],[3,4],[3,2]]
- 输出
- [1,2,3,4] (相邻关系:1-2、2-3、3-4)
- 输入
- adjacentPairs = [[4,-2],[1,4],[-3,1]]
- 输出
- [-3,1,4,-2] (含负数,反过来 [-2,4,1,-3] 也算对)
最优解:为什么这么做
一句话答案:LeetCode 1743 从相邻元素对还原数组:把打乱的相邻对当无向边建图,链两端的度恰好是 1,从端点带着 prev 沿边走一遍就还原出整个顺序,时间 O(n)。
adjacentPairs 只给了相邻关系,要还原成什么
给你一个二维数组 adjacentPairs,长度 n 减 1,每一项 [u, v] 只说明 u 和 v 在原数组 nums 里挨着,但谁左谁右不定、各对也乱序。nums 里的数互不相同、可能为负、绝对值到十万,要把这些散乱的相邻关系拼回 nums。题面 adjacentPairs = [[2,1],[3,4],[3,2]] 还原成 [1,2,3,4];带负数的例子答案 [-3,1,4,-2],倒着写也对。
把相邻对排序首尾接起来,为什么接不成链
顺手容易想到把相邻对排序再首尾相接拼成链。可 [u, v] 谁前谁后不定、各对排列也乱,排序无从下手,拼接也不知该接哪端——[2,1] 摆成 …2,1… 还是 …1,2…?排序拼不出唯一顺序。真正稳的信息不是先后,而是「谁和谁相邻」这层关系,得换种结构装它。
相邻对看成边之后,链的两头藏着什么信号
把每一对 [u, v] 看成一条连 u 和 v 的无向边,n 个数就是 n 个点、n 减 1 条边的图。原数组本是一条线:最左的数只有右邻居、最右的只有左邻居,首尾各只连一条边,度,也就是邻居个数,恰好是 1;中间每个数左右各一个邻居,度是 2。两端于是有了精准标记——全图里度为 1 的,只会是链的两端点。
认准度为 1 的端点,带着 prev 把链走通
实现上先用哈希表 g 建邻接表:遍历每对 a、b,往 g[a] 塞 b、g[b] 塞 a,互相登记。建完扫一遍挑出只有一个邻居的键当端点,取较小的作起点 start。再从 start 沿边走:prev 记上一步、cur 记当前点,每到一处先把 cur 填进答案,再选不等于 prev 的那个邻居当下一站 nxt。中间点两个邻居总有一个是 prev,跳过它才不原地折返;走到另一端点时下一站为空,链到头。
顺着 [[2,1],[3,4],[3,2]] 从头建图走一遍
就用题面 adjacentPairs = [[2,1],[3,4],[3,2]]。建图:[2,1] 让 g[2]=[1]、g[1]=[2];[3,4] 让 g[3]=[4]、g[4]=[3];[3,2] 再往 g[3] 添 2、g[2] 添 3。最终 g[1]=[2]、g[2]=[1,3]、g[3]=[4,2]、g[4]=[3]。度为 1 的是 1 和 4,取较小的 1 当起点。开走:cur=1 填答案,邻居只有 2,去 2;到 2,邻居 [1,3] 里 1 是 prev 跳过,去 3;到 3,邻居 [4,2] 里 2 是 prev 跳过,去 4;到 4,邻居只剩 prev,下一站为空。答案 [1,2,3,4],对上题面。
从中间点起步会乱套,负数和 n=2 要不要另写
复杂度很干净:建图、扫端点、沿链各点访问一次,没排序没嵌套循环,时间 O(n);邻接表条目约为边数两倍,空间 O(n)。几处易走偏:从中间点起步会乱套,它两向都通、一次定不下序,得认准度为 1 的端点再出发;走中间点忘带 prev,会朝来路折回、两点间反复横跳;还有别拿数值直接当下标,值到正负十万会越界。边界上,n 等于 2 时两点都是端点、随便起步都行,含负数照常建图,多解正倒都对。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢三件事:相邻对就是边、度为 1 的是端点、走链时跳过来时的邻居。下面一帧帧套它。
- 4先把出现过的 5 个数摆成 5 个点:15、40、27、33、58。此刻它们之间一条边都没有,右边的邻接表也是空的。接下来把 adjacentPairs 里的每一对,都连成一条无向边。
- 5看第 0 对相邻元素 [27, 40],它说明 27 和 40 在原数组里紧挨着。先把这两个点点亮,准备在它们之间连一条无向边。
- 6连上 27 和 40 这条边。因为是无向的,两边都要记:27 的邻居里加上 40、40 的邻居里加上 27。看右边面板,27 现在的度是 1、40 的度是 1。继续下一对。
- 7看第 1 对相邻元素 [33, 58],它说明 33 和 58 在原数组里紧挨着。先把这两个点点亮,准备在它们之间连一条无向边。
- 8连上 33 和 58 这条边。因为是无向的,两边都要记:33 的邻居里加上 58、58 的邻居里加上 33。看右边面板,33 现在的度是 1、58 的度是 1。继续下一对。
- 9看第 2 对相邻元素 [15, 40],它说明 15 和 40 在原数组里紧挨着。先把这两个点点亮,准备在它们之间连一条无向边。
- 10连上 15 和 40 这条边。因为是无向的,两边都要记:15 的邻居里加上 40、40 的邻居里加上 15。看右边面板,15 现在的度是 1、40 的度是 2。继续下一对。
- 11看第 3 对相邻元素 [27, 33],它说明 27 和 33 在原数组里紧挨着。先把这两个点点亮,准备在它们之间连一条无向边。
- 12连上 27 和 33 这条边。因为是无向的,两边都要记:27 的邻居里加上 33、33 的邻居里加上 27。看右边面板,27 现在的度是 2、33 的度是 2。四条边全连完,图就建好了。
- 13图建好了,扫一遍每个点的度。度为 2 的 40、27、33 都是被夹在中间的元素,而度为 1 的只有 15 和 58 这两个点,它们各自只有一个邻居,正是这条链的两个端头。
- 14从哪一端起步都能得到合法答案。参考解统一从较小的那个端点出发,也就是 15。把它标成起点,接下来沿着边一个点一个点往前走,同时把走过的值填进还原数组。
- 15从起点 15 看起。它是端点,邻居只有 40 一个,方向没有悬念,下一步就走向 40。
- 16把起点 15 填进还原数组的第 0 位。目前 nums 是 [15]。
- 17走到 40。它有两个邻居:27 和 15。其中 15 是刚才来的那个,跳过它;另一个 27 就是要去的下一站,把这条边点亮。
- 18把 40 接到还原数组第 1 位。灰色的问号是还没填的位置,继续往后走。
- 19走到 27。它有两个邻居:40 和 33。其中 40 是刚才来的那个,跳过它;另一个 33 就是要去的下一站,把这条边点亮。
- 20把 27 接到还原数组第 2 位。灰色的问号是还没填的位置,继续往后走。
- 21走到 33。它有两个邻居:58 和 27。其中 27 是刚才来的那个,跳过它;另一个 58 就是要去的下一站,把这条边点亮。
- 22把 33 接到还原数组第 3 位。灰色的问号是还没填的位置,继续往后走。
- 23走到 58。它是另一个端点,唯一的邻居 33 正是上一步来的地方,没有新的可走,链到此为止,全部还原完成。
- 24把 58 接到还原数组第 4 位。它是链尾,数组填满了。
- 25整条链走完,还原数组是 [15, 40, 27, 33, 58]。你可以核对一下:相邻的 15-40、40-27、27-33、33-58 恰好就是题目给的四对,一个不多一个不少,还原正确。反着写成 [58, 33, 27, 40, 15] 也是合法答案。
⚠️ 容易写错的地方
✗ 错:想着给相邻对排序、拼接来还原
✓ 对:把相邻对当无向边建图
相邻对顺序被打乱、左右也不定,直接排序拼不出链;建成图后「链」的结构一目了然
✗ 错:从任意点或中间点开始走
✓ 对:必须从度为 1 的端点出发
中间点有两个邻居、两个方向都能走,无法一次定序;端点只有一个方向,能一次走通整条链
✗ 错:走中间点时忘了记来时的邻居
✓ 对:带着 prev,每步跳过 prev 取另一个邻居
中间点有两个邻居,不排除 prev 就会原地折返、走不出去
✗ 错:用数组下标当图的键
✓ 对:用哈希表建图
值可能是负数、可到正负十万,拿值当下标会越界,必须用哈希表映射邻居
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
from string import *
from operator import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def restoreArray(self, adjacentPairs: List[List[int]]) -> List[int]:
g = defaultdict(list)
for a, b in adjacentPairs:
g[a].append(b)
g[b].append(a)
start = min(x for x, ns in g.items() if len(ns) == 1)
ans, prev, cur = [], None, start
while cur is not None:
ans.append(cur)
nxt = None
for v in g[cur]:
if v != prev:
nxt = v
break
prev, cur = cur, nxt
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
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:
vector<int> restoreArray(vector<vector<int>>& adjacentPairs) {
unordered_map<int, vector<int>> g;
for (auto& e : adjacentPairs) {
g[e[0]].push_back(e[1]);
g[e[1]].push_back(e[0]);
}
int start = INT_MAX;
for (auto& [x, ns] : g) if (ns.size() == 1) start = min(start, x);
vector<int> ans;
int prev = INT_MIN, cur = start;
while (true) {
ans.push_back(cur);
int nxt = INT_MIN;
for (int v : g[cur]) if (v != prev) { nxt = v; break; }
if (nxt == INT_MIN) break;
prev = cur;
cur = nxt;
}
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int[] restoreArray(int[][] adjacentPairs) {
Map<Integer, List<Integer>> g = new HashMap<>();
for (int[] e : adjacentPairs) {
g.computeIfAbsent(e[0], k -> new ArrayList<>()).add(e[1]);
g.computeIfAbsent(e[1], k -> new ArrayList<>()).add(e[0]);
}
int start = Integer.MAX_VALUE;
for (Map.Entry<Integer, List<Integer>> e : g.entrySet()) if (e.getValue().size() == 1) start = Math.min(start, e.getKey());
int[] ans = new int[g.size()];
int prev = Integer.MIN_VALUE, cur = start;
for (int i = 0; i < ans.length; i++) {
ans[i] = cur;
int next = Integer.MIN_VALUE;
for (int v : g.get(cur)) if (v != prev) { next = v; break; }
prev = cur;
cur = next;
}
return ans;
}
}复杂度
时间
O(n)
建图遍历 n 减 1 个相邻对是 O(n);扫一遍找度为 1 的端点是 O(n);沿链走每个点恰好访问一次也是 O(n),合起来线性
空间
O(n)
邻接表要为每个值存它的邻居,总条目数是边数的两倍即 2 乘 n 减 1,峰值与 n 同阶;输出数组也是 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 从相邻元素对还原数组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么两个端点的度一定是 1、中间元素一定是 2?+
因为原数组是一条线性排列。最左边的数只有右边一个邻居、左边空着,最右边的数只有左边一个邻居,首尾各自只贡献一条边,度就是 1;中间每个数左右各有一个邻居,各贡献两条边,度是 2。正是这个规律,让「度为 1」成了精准定位两端的判据,不用再猜哪个是头哪个是尾。
能不能不建图,直接把相邻对排序拼一拼?+
拼不出来。相邻对里 u、v 的左右不定,各对之间也是乱序,排序既没有可比的键,拼接时又不知道该从哪一端接、接哪个数,结果不唯一。换成图之后,「相邻」这层关系被完整存下,链的结构就清楚了,从度为 1 的端点顺着走就是唯一顺序(首尾对调那种整体反转除外)。
沿链走时那个 prev,到底防的是什么?+
防原地折返。中间点有两个邻居,一个是刚走过来的上一站,另一个才是要去的下一站。如果不记住上一站,程序会把「回头路」也当成合法的下一步,于是在相邻两点间来回横跳,永远走不到链尾。带上 prev,每步只挑那个不等于 prev 的邻居,方向就唯一了,一条链一次走通。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 从相邻元素对还原数组 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。