1743. Restore the Array From Adjacent Pairs
Problem Description
You need to restore an original integer array that contains n
unique elements. You don't remember the array itself, but you have information about which elements were adjacent to each other.
Given:
- A 2D array
adjacentPairs
of sizen-1
where eachadjacentPairs[i] = [ui, vi]
tells you that elementsui
andvi
were next to each other in the original array - The pairs can be given in any order (either
[a, b]
or[b, a]
) - Every consecutive pair from the original array is represented exactly once in
adjacentPairs
Your task is to reconstruct and return the original array nums
.
For example, if the original array was [1, 2, 3, 4]
, you might receive adjacent pairs like [[2,1], [3,4], [3,2]]
which tells you:
- 2 and 1 were adjacent
- 3 and 4 were adjacent
- 3 and 2 were adjacent
From this information, you can deduce the original array was either [1, 2, 3, 4]
or [4, 3, 2, 1]
(both are valid solutions).
The key insight is that elements at the start and end of the array will only appear in one pair (they have only one neighbor), while all middle elements appear in exactly two pairs (they have two neighbors). The solution builds a graph of connections, finds a starting element (one with only one neighbor), and then traverses through the connections to rebuild the array.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves elements (nodes) and their adjacent relationships (edges). Each element in the array can be viewed as a node, and the adjacent pairs represent edges connecting these nodes.
Is it a tree?
- Yes: Since we have
n
unique elements andn-1
adjacent pairs that connect them all, this forms a tree structure (a connected acyclic graph). Additionally, the structure is linear - it's essentially a path graph where each node (except the endpoints) has exactly 2 neighbors.
DFS
- Yes: Based on the flowchart, when we have a tree structure, DFS is the recommended approach.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem.
This aligns perfectly with the solution approach:
- Build a graph from the adjacent pairs where each node tracks its neighbors
- Find a starting point (a node with only one neighbor - this must be an endpoint)
- Use DFS traversal to reconstruct the array by following the connections from one element to the next, avoiding backtracking to already visited nodes
The DFS pattern is ideal here because we need to traverse through the entire connected structure exactly once, following the path from one endpoint to the other endpoint of the original array.
Intuition
The key observation is that when we have an array and know all adjacent pairs, we're essentially given the edges of a path graph. Think of it like a chain where each element is connected to its neighbors.
In any array, there are exactly two special elements - the first and last elements. These are special because they each have only one neighbor. All other elements in the middle have two neighbors (one on each side).
This gives us our starting point: if we count how many times each element appears in the adjacent pairs, the elements that appear only once must be the endpoints of our original array.
Once we identify an endpoint, we can start rebuilding the array like following a trail:
- Start from one endpoint (it doesn't matter which one)
- Move to its only neighbor
- From there, we have two choices of where to go next, but one of them is where we just came from
- Keep moving forward, always choosing the neighbor we haven't visited yet
- Continue until we've reconstructed the entire array
For example, if we have pairs [[1,2], [2,3], [3,4]]
:
- Element 1 appears once (neighbor: 2)
- Element 2 appears twice (neighbors: 1, 3)
- Element 3 appears twice (neighbors: 2, 4)
- Element 4 appears once (neighbor: 3)
So 1 and 4 are endpoints. Starting from 1, we go to 2, then to 3 (not back to 1), then to 4, giving us [1, 2, 3, 4]
.
This is essentially a DFS traversal along a linear path, where we maintain our direction by remembering where we came from and always moving to the unvisited neighbor.
Learn more about Depth-First Search patterns.
Solution Approach
The implementation follows a graph-based approach using an adjacency list and linear traversal:
Step 1: Build the Graph
g = defaultdict(list)
for a, b in adjacentPairs:
g[a].append(b)
g[b].append(a)
We create an adjacency list where each element maps to its neighbors. Since pairs can be given in any order [a,b]
or [b,a]
, we add bidirectional edges. This gives us a graph where:
- Endpoint elements have exactly 1 neighbor
- Middle elements have exactly 2 neighbors
Step 2: Find a Starting Point
for i, v in g.items():
if len(v) == 1:
ans[0] = i
ans[1] = v[0]
break
We iterate through the graph to find any element with only one neighbor - this must be an endpoint. We place it at position 0 of our answer array and its only neighbor at position 1.
Step 3: Reconstruct the Array
for i in range(2, n):
v = g[ans[i - 1]]
ans[i] = v[0] if v[1] == ans[i - 2] else v[1]
Starting from position 2, we fill the rest of the array by:
- Looking at the previous element
ans[i-1]
and getting its neighborsv
- Since
ans[i-1]
has two neighbors (except for endpoints), one is where we came fromans[i-2]
and the other is where we need to go next - We select the neighbor that is NOT
ans[i-2]
as our next element
The algorithm has:
- Time Complexity:
O(n)
where n is the number of elements, as we process each pair once and traverse the array once - Space Complexity:
O(n)
for storing the graph and the result array
This approach elegantly uses the graph structure to perform a directed traversal without needing explicit visited tracking, since we always know our previous position and can avoid backtracking.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with adjacentPairs = [[4,3], [1,4], [3,2]]
.
Step 1: Build the Graph
We create an adjacency list from the pairs:
- From
[4,3]
: Add 3 to 4's neighbors, add 4 to 3's neighbors - From
[1,4]
: Add 4 to 1's neighbors, add 1 to 4's neighbors - From
[3,2]
: Add 2 to 3's neighbors, add 3 to 2's neighbors
Our graph becomes:
1: [4] 2: [3] 3: [4, 2] 4: [3, 1]
Step 2: Find a Starting Point
Count neighbors for each element:
- Element 1: 1 neighbor (only 4) β This is an endpoint!
- Element 2: 1 neighbor (only 3) β This is also an endpoint
- Element 3: 2 neighbors (4 and 2) β Middle element
- Element 4: 2 neighbors (3 and 1) β Middle element
We pick element 1 as our starting point. Initialize our answer array of size 4:
ans[0] = 1
(our starting endpoint)ans[1] = 4
(1's only neighbor)ans = [1, 4, ?, ?]
Step 3: Reconstruct the Array
Now we fill positions 2 and 3:
Position 2 (i=2):
- Previous element:
ans[1] = 4
- 4's neighbors:
[3, 1]
- Element two positions back:
ans[0] = 1
- Since 1 is where we came from, we choose 3
ans[2] = 3
ans = [1, 4, 3, ?]
Position 3 (i=3):
- Previous element:
ans[2] = 3
- 3's neighbors:
[4, 2]
- Element two positions back:
ans[1] = 4
- Since 4 is where we came from, we choose 2
ans[3] = 2
ans = [1, 4, 3, 2]
Final Result: [1, 4, 3, 2]
We can verify this is correct:
- 1 and 4 are adjacent β
- 4 and 3 are adjacent β
- 3 and 2 are adjacent β
Note that [2, 3, 4, 1]
would also be a valid solution (the reverse order), which we'd get if we started from element 2 instead of element 1.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def restoreArray(self, adjacentPairs: List[List[int]]) -> List[int]:
6 # Build adjacency graph where each number maps to its neighbors
7 adjacency_graph = defaultdict(list)
8 for first_num, second_num in adjacentPairs:
9 adjacency_graph[first_num].append(second_num)
10 adjacency_graph[second_num].append(first_num)
11
12 # Calculate the length of the original array
13 # (number of pairs + 1 gives us the total elements)
14 array_length = len(adjacentPairs) + 1
15 result_array = [0] * array_length
16
17 # Find a starting point: an element with only one neighbor
18 # This must be one of the endpoints of the original array
19 for number, neighbors in adjacency_graph.items():
20 if len(neighbors) == 1:
21 result_array[0] = number
22 result_array[1] = neighbors[0]
23 break
24
25 # Reconstruct the array by following the chain of neighbors
26 # At each position, choose the neighbor that isn't the previous element
27 for index in range(2, array_length):
28 current_neighbors = adjacency_graph[result_array[index - 1]]
29 # Select the neighbor that is not the previously visited element
30 if current_neighbors[1] == result_array[index - 2]:
31 result_array[index] = current_neighbors[0]
32 else:
33 result_array[index] = current_neighbors[1]
34
35 return result_array
36
1class Solution {
2 public int[] restoreArray(int[][] adjacentPairs) {
3 // Calculate the size of the original array (n pairs means n+1 elements)
4 int arraySize = adjacentPairs.length + 1;
5
6 // Build an adjacency list graph where each number maps to its neighbors
7 Map<Integer, List<Integer>> adjacencyGraph = new HashMap<>();
8 for (int[] pair : adjacentPairs) {
9 int firstElement = pair[0];
10 int secondElement = pair[1];
11
12 // Add bidirectional edges since pairs can be in any order
13 adjacencyGraph.computeIfAbsent(firstElement, k -> new ArrayList<>()).add(secondElement);
14 adjacencyGraph.computeIfAbsent(secondElement, k -> new ArrayList<>()).add(firstElement);
15 }
16
17 // Initialize the result array
18 int[] restoredArray = new int[arraySize];
19
20 // Find a starting point: an element with only one neighbor (array endpoint)
21 for (Map.Entry<Integer, List<Integer>> entry : adjacencyGraph.entrySet()) {
22 if (entry.getValue().size() == 1) {
23 // Found an endpoint of the array
24 restoredArray[0] = entry.getKey();
25 restoredArray[1] = entry.getValue().get(0);
26 break;
27 }
28 }
29
30 // Build the rest of the array by traversing the graph
31 // Each element (except endpoints) has exactly 2 neighbors
32 for (int i = 2; i < arraySize; i++) {
33 List<Integer> currentNeighbors = adjacencyGraph.get(restoredArray[i - 1]);
34
35 // Choose the neighbor that isn't the previous element to avoid going backward
36 if (currentNeighbors.get(1) == restoredArray[i - 2]) {
37 restoredArray[i] = currentNeighbors.get(0);
38 } else {
39 restoredArray[i] = currentNeighbors.get(1);
40 }
41 }
42
43 return restoredArray;
44 }
45}
46
1class Solution {
2public:
3 vector<int> restoreArray(vector<vector<int>>& adjacentPairs) {
4 // Calculate the size of the original array (n pairs means n+1 elements)
5 int arraySize = adjacentPairs.size() + 1;
6
7 // Build an adjacency list where each number maps to its neighbors
8 unordered_map<int, vector<int>> adjacencyList;
9 for (auto& pair : adjacentPairs) {
10 int firstNum = pair[0];
11 int secondNum = pair[1];
12 adjacencyList[firstNum].push_back(secondNum);
13 adjacencyList[secondNum].push_back(firstNum);
14 }
15
16 // Initialize the result array
17 vector<int> result(arraySize);
18
19 // Find a starting point (element with only one neighbor - must be an endpoint)
20 for (auto& [value, neighbors] : adjacencyList) {
21 if (neighbors.size() == 1) {
22 result[0] = value; // Set first element as the endpoint
23 result[1] = neighbors[0]; // Set second element as its only neighbor
24 break;
25 }
26 }
27
28 // Reconstruct the array by following the chain of neighbors
29 for (int i = 2; i < arraySize; ++i) {
30 // Get the neighbors of the previous element
31 vector<int> currentNeighbors = adjacencyList[result[i - 1]];
32
33 // Choose the neighbor that isn't the element we came from (i-2)
34 // Each internal element has exactly 2 neighbors
35 result[i] = (currentNeighbors[0] == result[i - 2]) ? currentNeighbors[1] : currentNeighbors[0];
36 }
37
38 return result;
39 }
40};
41
1function restoreArray(adjacentPairs: number[][]): number[] {
2 // Calculate the size of the original array (n pairs means n+1 elements)
3 const arraySize = adjacentPairs.length + 1;
4
5 // Build an adjacency list where each number maps to its neighbors
6 const adjacencyList = new Map<number, number[]>();
7
8 for (const pair of adjacentPairs) {
9 const firstNum = pair[0];
10 const secondNum = pair[1];
11
12 // Add secondNum as a neighbor of firstNum
13 if (!adjacencyList.has(firstNum)) {
14 adjacencyList.set(firstNum, []);
15 }
16 adjacencyList.get(firstNum)!.push(secondNum);
17
18 // Add firstNum as a neighbor of secondNum
19 if (!adjacencyList.has(secondNum)) {
20 adjacencyList.set(secondNum, []);
21 }
22 adjacencyList.get(secondNum)!.push(firstNum);
23 }
24
25 // Initialize the result array
26 const result: number[] = new Array(arraySize);
27
28 // Find a starting point (element with only one neighbor - must be an endpoint)
29 for (const [value, neighbors] of adjacencyList.entries()) {
30 if (neighbors.length === 1) {
31 result[0] = value; // Set first element as the endpoint
32 result[1] = neighbors[0]; // Set second element as its only neighbor
33 break;
34 }
35 }
36
37 // Reconstruct the array by following the chain of neighbors
38 for (let i = 2; i < arraySize; i++) {
39 // Get the neighbors of the previous element
40 const currentNeighbors = adjacencyList.get(result[i - 1])!;
41
42 // Choose the neighbor that isn't the element we came from (i-2)
43 // Each internal element has exactly 2 neighbors
44 result[i] = currentNeighbors[0] === result[i - 2] ? currentNeighbors[1] : currentNeighbors[0];
45 }
46
47 return result;
48}
49
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of elements in the original array (or len(adjacentPairs) + 1
).
- Building the adjacency graph takes
O(n-1)
time since we iterate through all adjacent pairs once, and there aren-1
pairs for an array of lengthn
. - Finding the starting element (with degree 1) requires iterating through the graph dictionary, which has
n
unique elements, takingO(n)
time in the worst case. - Reconstructing the array requires iterating from index 2 to
n-1
, performing constant-time operations at each step (accessing the adjacency list and comparing values), which takesO(n)
time. - Overall:
O(n-1) + O(n) + O(n) = O(n)
Space Complexity: O(n)
- The graph dictionary
g
stores all unique elements as keys, with each element appearing in exactly 2 pairs (except the endpoints which appear in 1 pair). This results inn
keys with a total of2(n-1)
values stored across all adjacency lists, giving usO(n)
space. - The answer array
ans
stores exactlyn
elements, requiringO(n)
space. - Additional variables use
O(1)
space. - Overall:
O(n) + O(n) + O(1) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Assuming Fixed Neighbor Order in Adjacency List
The most common pitfall occurs in the array reconstruction step when selecting the next element. Developers often assume a specific order of neighbors in the adjacency list:
Incorrect Assumption:
# WRONG: Assumes the unvisited neighbor is always at a specific index
for index in range(2, array_length):
current_neighbors = adjacency_graph[result_array[index - 1]]
result_array[index] = current_neighbors[0] # Incorrectly assumes first neighbor is unvisited
Why This Fails:
When we add edges bidirectionally to the graph, the order of neighbors in each list is arbitrary. For a middle element with neighbors [a, b]
, we cannot assume which one we've already visited based on their position in the list.
Correct Solution: Always explicitly check which neighbor was the previous element and select the other one:
for index in range(2, array_length):
current_neighbors = adjacency_graph[result_array[index - 1]]
# Explicitly check which neighbor is the previous element
if current_neighbors[0] == result_array[index - 2]:
result_array[index] = current_neighbors[1]
else:
result_array[index] = current_neighbors[0]
2. Not Handling Edge Cases with Small Arrays
Another pitfall is not considering arrays with only 2 elements:
Problem Scenario:
If adjacentPairs = [[1, 2]]
, the array has only 2 elements. The reconstruction loop for i in range(2, n)
won't execute at all.
Solution: The code handles this correctly by:
- Setting positions 0 and 1 before the loop
- The loop starting from index 2 only runs if
n > 2
- For a 2-element array, the initial setup is sufficient
3. Using Set Instead of List for Neighbors
Some might try to optimize by using sets for the adjacency list:
# PROBLEMATIC: Using sets loses indexed access
adjacency_graph = defaultdict(set)
Why This Causes Issues:
Sets don't support indexing (neighbors[0]
or neighbors[1]
), making it harder to implement the neighbor selection logic efficiently.
Better Approach: Stick with lists for simpler indexed access, or if using sets, convert to list when needed:
current_neighbors = list(adjacency_graph[result_array[index - 1]])
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!