2007. Find Original Array From Doubled Array
Problem Description
You are given an array called changed
that was created through a specific transformation process. The original process worked like this:
- Start with an integer array called
original
- For every element in
original
, append twice its value to create a new array - Randomly shuffle this new array to get
changed
For example, if original = [1, 3, 4]
, then after doubling we get [1, 3, 4, 2, 6, 8]
, which after shuffling might become changed = [6, 3, 8, 1, 4, 2]
.
Your task is to determine if the given array changed
could have been created through this process. If it was, return the original
array (elements can be in any order). If changed
could not have been created this way, return an empty array.
The key insight is that every element x
in the original array must have both x
and 2x
present in the changed
array. The solution sorts the array and greedily pairs each element with its double. Starting from the smallest element ensures we correctly identify which elements belong to the original array versus which are the doubled values.
The algorithm uses a counter to track available elements. For each element x
, it checks if x
is still available (hasn't been paired yet), then tries to pair it with 2x
. If 2x
isn't available, the array cannot be a valid doubled array. The bit shift operation x << 1
is used as an efficient way to calculate 2x
.
Intuition
The key observation is that if we have a valid doubled array, the smallest element in changed
must belong to the original array. Why? Because the smallest element cannot be a doubled value of any other element in the array (since doubling makes numbers larger, not smaller).
Once we identify that the smallest element belongs to original
, we know its double (2 * smallest
) must also exist somewhere in changed
. We can pair these two elements and remove them from consideration.
This suggests a greedy approach: always process elements from smallest to largest. For each unprocessed element x
, we know it must be from the original array (since all smaller elements have already been processed and paired). We then look for its double 2x
to pair with it.
Why does sorting help? Without sorting, we might mistakenly treat a doubled value as an original element. For instance, if changed = [4, 2, 8, 1]
, we might wrongly think 4
is an original element and look for 8
as its double. But actually, 4
is the double of 2
. By sorting to get [1, 2, 4, 8]
and processing from left to right, we correctly identify 1
and 2
as original elements with doubles 2
and 4
respectively.
The counting approach with a hash table allows us to efficiently track which elements are still available for pairing. When we use an element as either an original or a double, we decrement its count. If at any point we need an element that's not available (count is 0), we know the array cannot be a valid doubled array.
Solution Approach
The implementation follows these steps:
-
Sort the array: First, we sort the
changed
array in ascending order. This ensures we process elements from smallest to largest, allowing us to correctly identify which elements belong to the original array. -
Count element frequencies: We use a
Counter
(hash table) to track the frequency of each element in the sorted array. This data structure allows us to efficiently check availability and update counts as we pair elements. -
Iterate and pair elements: We traverse through the sorted array. For each element
x
:- Check if
cnt[x] > 0
. If it's 0, this element has already been used as someone's double, so we skip it withcontinue
. - Decrement
cnt[x]
by 1 to mark that we're using this element as an original value. - Check if
cnt[x << 1] > 0
(wherex << 1
is equivalent tox * 2
). If the double doesn't exist or has already been fully used, we return an empty array as this isn't a valid doubled array. - Decrement
cnt[x << 1]
by 1 to mark that we've paired this double with its original. - Add
x
to our answer array since we've confirmed it's part of the original array.
- Check if
-
Return the result: After successfully processing all elements, we return the answer array containing the original elements.
The algorithm has a time complexity of O(n log n)
due to sorting, where n
is the length of the changed
array. The space complexity is O(n)
for the counter and answer array.
The bit shift operation x << 1
is used as an optimization for multiplying by 2, as left-shifting by 1 bit position doubles the value in binary representation.
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 the solution with changed = [6, 3, 8, 1, 4, 2]
.
Step 1: Sort the array
After sorting: [1, 2, 3, 4, 6, 8]
Step 2: Create frequency counter
cnt = {1: 1, 2: 1, 3: 1, 4: 1, 6: 1, 8: 1}
Step 3: Process each element from smallest to largest
Processing 1:
- Check if
cnt[1] > 0
? Yes (count is 1) - Mark 1 as used:
cnt[1] = 0
- Look for its double:
1 << 1 = 2
- Check if
cnt[2] > 0
? Yes (count is 1) - Mark 2 as paired:
cnt[2] = 0
- Add 1 to answer:
answer = [1]
- Updated counter:
cnt = {1: 0, 2: 0, 3: 1, 4: 1, 6: 1, 8: 1}
Processing 2:
- Check if
cnt[2] > 0
? No (already used as 1's double) - Skip with
continue
Processing 3:
- Check if
cnt[3] > 0
? Yes (count is 1) - Mark 3 as used:
cnt[3] = 0
- Look for its double:
3 << 1 = 6
- Check if
cnt[6] > 0
? Yes (count is 1) - Mark 6 as paired:
cnt[6] = 0
- Add 3 to answer:
answer = [1, 3]
- Updated counter:
cnt = {1: 0, 2: 0, 3: 0, 4: 1, 6: 0, 8: 1}
Processing 4:
- Check if
cnt[4] > 0
? Yes (count is 1) - Mark 4 as used:
cnt[4] = 0
- Look for its double:
4 << 1 = 8
- Check if
cnt[8] > 0
? Yes (count is 1) - Mark 8 as paired:
cnt[8] = 0
- Add 4 to answer:
answer = [1, 3, 4]
- Updated counter:
cnt = {1: 0, 2: 0, 3: 0, 4: 0, 6: 0, 8: 0}
Processing 6:
- Check if
cnt[6] > 0
? No (already used as 3's double) - Skip with
continue
Processing 8:
- Check if
cnt[8] > 0
? No (already used as 4's double) - Skip with
continue
Step 4: Return result
Return [1, 3, 4]
- successfully reconstructed the original array!
The key insight: by processing smallest to largest, we correctly identify that 1, 3, and 4 are original elements (with doubles 2, 6, and 8 respectively), rather than mistakenly treating doubled values as originals.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def findOriginalArray(self, changed: List[int]) -> List[int]:
6 # Sort the array to process elements from smallest to largest
7 changed.sort()
8
9 # Count frequency of each element
10 frequency_counter = Counter(changed)
11
12 # Store the original array elements
13 original_array = []
14
15 # Iterate through sorted array
16 for number in changed:
17 # Skip if this number has already been used as a doubled value
18 if frequency_counter[number] == 0:
19 continue
20
21 # Use current number as an original element
22 frequency_counter[number] -= 1
23
24 # Check if its double exists and is available
25 doubled_value = number * 2 # Using multiplication instead of bit shift for clarity
26 if frequency_counter[doubled_value] <= 0:
27 # If double doesn't exist or isn't available, array is invalid
28 return []
29
30 # Mark the doubled value as used
31 frequency_counter[doubled_value] -= 1
32
33 # Add the original number to result
34 original_array.append(number)
35
36 return original_array
37
1class Solution {
2 public int[] findOriginalArray(int[] changed) {
3 int arrayLength = changed.length;
4
5 // Sort the array in ascending order
6 Arrays.sort(changed);
7
8 // Create a frequency counter array to track occurrences of each number
9 // Size is determined by the maximum value in the sorted array
10 int[] frequencyCounter = new int[changed[arrayLength - 1] + 1];
11
12 // Count the frequency of each element in the changed array
13 for (int number : changed) {
14 frequencyCounter[number]++;
15 }
16
17 // Initialize result array with half the size of input array
18 // Since doubled array has twice the elements of original
19 int[] result = new int[arrayLength / 2];
20 int resultIndex = 0;
21
22 // Process each number in the sorted array
23 for (int currentNumber : changed) {
24 // Skip if this number has already been used as a doubled value
25 if (frequencyCounter[currentNumber] == 0) {
26 continue;
27 }
28
29 // Use current number as an original value
30 frequencyCounter[currentNumber]--;
31
32 // Calculate the doubled value
33 int doubledValue = currentNumber * 2;
34
35 // Check if doubled value exists and is available
36 if (doubledValue >= frequencyCounter.length || frequencyCounter[doubledValue] <= 0) {
37 // If doubled value doesn't exist or is not available,
38 // the array cannot be a valid doubled array
39 return new int[0];
40 }
41
42 // Mark the doubled value as used
43 frequencyCounter[doubledValue]--;
44
45 // Add the original number to the result
46 result[resultIndex++] = currentNumber;
47 }
48
49 return result;
50 }
51}
52
1class Solution {
2public:
3 vector<int> findOriginalArray(vector<int>& changed) {
4 // Sort the array in ascending order to process smaller elements first
5 sort(changed.begin(), changed.end());
6
7 // Create a frequency counter array to track occurrences of each element
8 // Size is max_element + 1 to accommodate all values from 0 to max
9 vector<int> frequencyCount(changed.back() + 1);
10
11 // Count the frequency of each element in the changed array
12 for (int element : changed) {
13 ++frequencyCount[element];
14 }
15
16 // Store the original array elements
17 vector<int> originalArray;
18
19 // Process each element in sorted order
20 for (int currentElement : changed) {
21 // Skip if this element has already been used as a doubled value
22 if (frequencyCount[currentElement] == 0) {
23 continue;
24 }
25
26 // Use current element as an original value
27 --frequencyCount[currentElement];
28
29 // Calculate the doubled value
30 int doubledValue = currentElement << 1; // Equivalent to currentElement * 2
31
32 // Check if doubled value exists and is available
33 if (doubledValue >= frequencyCount.size() || frequencyCount[doubledValue] <= 0) {
34 // Invalid case: doubled value doesn't exist or already used up
35 return {};
36 }
37
38 // Mark the doubled value as used
39 --frequencyCount[doubledValue];
40
41 // Add the original element to result
42 originalArray.push_back(currentElement);
43 }
44
45 return originalArray;
46 }
47};
48
1/**
2 * Finds the original array from a doubled array.
3 * Given an array that was created by doubling every element of an original array,
4 * returns the original array. If no valid original array exists, returns empty array.
5 *
6 * @param changed - The doubled array containing original elements and their doubles
7 * @returns The original array if valid, otherwise empty array
8 */
9function findOriginalArray(changed: number[]): number[] {
10 // Sort the array in ascending order to process smaller elements first
11 changed.sort((a, b) => a - b);
12
13 // Create frequency counter array with size based on maximum element
14 const maxElement = changed[changed.length - 1];
15 const frequencyCounter: number[] = new Array(maxElement + 1).fill(0);
16
17 // Count frequency of each element in the changed array
18 for (const element of changed) {
19 frequencyCounter[element]++;
20 }
21
22 // Result array to store the original elements
23 const originalArray: number[] = [];
24
25 // Process each element in sorted order
26 for (const element of changed) {
27 // Skip if this element has already been used as a double
28 if (frequencyCounter[element] === 0) {
29 continue;
30 }
31
32 // Use current element as an original value
33 frequencyCounter[element]--;
34
35 // Calculate the double of current element
36 const doubledValue = element * 2;
37
38 // Check if doubled value exists and is available
39 if (doubledValue >= frequencyCounter.length || frequencyCounter[doubledValue] <= 0) {
40 // No valid double found, original array cannot be formed
41 return [];
42 }
43
44 // Mark the doubled value as used
45 frequencyCounter[doubledValue]--;
46
47 // Add current element to the original array
48 originalArray.push(element);
49 }
50
51 return originalArray;
52}
53
Time and Space Complexity
Time Complexity: O(n ร log n)
, where n
is the length of the array changed
.
The time complexity is dominated by the sorting operation changed.sort()
, which takes O(n ร log n)
time. After sorting, the code iterates through the array once with a for loop, performing constant-time operations for each element (Counter lookups and updates are O(1)
on average). The Counter initialization takes O(n)
time. Therefore, the overall time complexity is O(n ร log n) + O(n) + O(n) = O(n ร log n)
.
Space Complexity: O(n)
, where n
is the length of the array changed
.
The space complexity is determined by:
- The Counter object
cnt
which stores at mostn
key-value pairs:O(n)
- The answer list
ans
which can contain at mostn/2
elements:O(n)
- The sorting operation may use
O(log n)
space for the recursion stack (if using an in-place sorting algorithm like quicksort) orO(n)
space (if using mergesort)
The dominant space usage is O(n)
, resulting in an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Zero Values
The most critical pitfall in this solution is the incorrect handling of zeros. When the original array contains 0, its double is also 0 (since 0 ร 2 = 0). This means each zero in the original array requires TWO zeros in the changed array.
Problem Example:
- Input:
changed = [0, 0, 0, 0]
- Expected:
original = [0, 0]
(two zeros doubled gives four zeros) - Buggy behavior: The algorithm tries to pair each 0 with itself as its double, potentially returning
[0, 0, 0, 0]
as the original array
Solution: Add special handling for zeros:
for number in changed: if frequency_counter[number] == 0: continue frequency_counter[number] -= 1 doubled_value = number * 2 # Special case: when number is 0, we need another 0 available # (not the same one we just decremented) if frequency_counter[doubled_value] <= 0: return [] frequency_counter[doubled_value] -= 1 original_array.append(number)
2. Odd Length Array Check
If the input array has an odd number of elements, it cannot be a valid doubled array since the doubled array must have exactly twice the number of elements as the original.
Solution: Add a length check at the beginning:
def findOriginalArray(self, changed: List[int]) -> List[int]:
# Early return for odd-length arrays
if len(changed) % 2 != 0:
return []
# Rest of the implementation...
3. Negative Numbers Handling
While the problem typically involves non-negative integers, if negative numbers are allowed, the sorting and pairing logic becomes more complex. For negative numbers, their doubles are "more negative" (e.g., -3 doubled is -6).
Solution: Sort by absolute value if negatives are allowed:
# Sort by absolute value for correct pairing with negatives
changed.sort(key=abs)
4. Integer Overflow with Bit Shifting
While x << 1
is efficient, it can cause issues with very large numbers near the integer limit. Using x * 2
is safer and more readable.
Complete Corrected Solution:
def findOriginalArray(self, changed: List[int]) -> List[int]:
# Check for odd length
if len(changed) % 2 != 0:
return []
changed.sort()
frequency_counter = Counter(changed)
original_array = []
for number in changed:
if frequency_counter[number] == 0:
continue
frequency_counter[number] -= 1
doubled_value = number * 2
if frequency_counter[doubled_value] <= 0:
return []
frequency_counter[doubled_value] -= 1
original_array.append(number)
return original_array
Which of the following array represent a max heap?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Donโt Miss This!