Merge Sorted Arrays Without Extra Space
Given two sorted arrays arr1 of size and arr2 of size , the goal is to merge their elements such that the first smallest elements reside in arr1 in sorted order, and the remaining elements reside in arr2 in sorted order. The re-arrangement must be achieved in-place without allocating any external array space ( auxiliary space).
1. Problem Breakdown & Re-arrangement Logic
When given two sorted arrays, merging them typically implies creating a single combined sequence. However, under the constraint of no extra space, we must rearrange the elements directly inside the original arrays.
Example Scenario
-
Input:
arr1[] = {1, 3, 5, 7},arr2[] = {0, 2, 6, 8, 9},
-
Virtual Merged State (if combined):
{0, 1, 2, 3, 5, 6, 7, 8, 9} -
Expected Output (partitioned back into
arr1andarr2):arr1[] = {0, 1, 2, 3}(First elements)arr2[] = {5, 6, 7, 8, 9}(Remaining elements)
2. Conceptual Mental Model
To solve this without extra storage, consider the structural placement of elements across the boundary separating arr1 and arr2:
- Core Invariant: All elements in
arr1must be smaller than or equal to all elements inarr2. - Target Condition: Once every element in
arr1is every element inarr2, sorting each array independently restores global sorted order across both arrays without requiring an auxiliary data structure.
3. Naive Approach: Using an Auxiliary Array
The naive intuition utilizes two pointers to merge both sorted arrays into a temporary array arr3 of size , and then copies the elements back into arr1 and arr2.
Execution Walkthrough
Vertical Pointer Flow
4. Code & Complexity Analysis: Naive Approach
C++ Implementation
#include <vector>
#include <iostream>
void mergeNaive(std::vector<int>& arr1, std::vector<int>& arr2, int n, int m) {
std::vector<int> arr3(n + m);
int left = 0;
int right = 0;
int index = 0;
// Step 1: Merge elements into arr3 using two pointers
while (left < n && right < m) {
if (arr1[left] <= arr2[right]) {
arr3[index++] = arr1[left++];
} else {
arr3[index++] = arr2[right++];
}
}
// Copy remaining elements from arr1, if any
while (left < n) {
arr3[index++] = arr1[left++];
}
// Copy remaining elements from arr2, if any
while (right < m) {
arr3[index++] = arr2[right++];
}
// Step 2: Write back elements to arr1 and arr2
for (int i = 0; i < n + m; i++) {
if (i < n) {
arr1[i] = arr3[i];
} else {
arr2[i - n] = arr3[i]; // Mapping index to second array
}
}
}
Complexity
- Time Complexity:
- Traversing both arrays to fill
arr3takes time. - Copying elements back takes time.
- Traversing both arrays to fill
- Space Complexity: auxiliary space due to
arr3.
5. Optimal Solution 1: Left-Right Two-Pointer Swap & Sort
To eliminate extra space, swap misallocated elements directly between arrays using the boundary invariant.
Conceptual Insight
- The largest elements of
arr1reside near its end (n - 1). - The smallest elements of
arr2reside near its beginning (0). - If
arr1[left](starting atn - 1) is greater thanarr2[right](starting at0), they are on the wrong sides of the partition boundary and must be swapped.
arr1: [1, 3, 5, 7] arr2: [0, 2, 6, 8, 9]
^ ^
left = n-1 right = 0
(Largest) (Smallest)
Step-by-Step Algorithm
- Initialize
left = n - 1(last element ofarr1) andright = 0(first element ofarr2). - Compare
arr1[left]andarr2[right]:- If
arr1[left] > arr2[right]: Swap them, decrementleft--, incrementright++. - If
arr1[left] <= arr2[right]: Break early. Since both arrays are individually sorted, all elements to the left ofleftinarr1are smaller thanarr1[left], and all elements to the right ofrightinarr2are larger thanarr2[right].
- If
- Sort
arr1andarr2individually to restore sorted order inside each partition.
6. Step-by-Step Simulation: Optimal Solution 1
Using arr1 = {1, 3, 5, 7} and arr2 = {0, 2, 6, 8, 9}:
Implementation (C++)
#include <vector>
#include <algorithm>
void mergeOptimal1(std::vector<int>& arr1, std::vector<int>& arr2, int n, int m) {
int left = n - 1;
int right = 0;
// Swap elements if they belong to the other partition
while (left >= 0 && right < m) {
if (arr1[left] > arr2[right]) {
std::swap(arr1[left], arr2[right]);
left--;
right++;
} else {
// Early break because arrays are sorted
break;
}
}
// Re-sort individual arrays to maintain intra-array order
std::sort(arr1.begin(), arr1.end());
std::sort(arr2.begin(), arr2.end());
}
Complexity
- Time Complexity:
- Swapping loop runs at most times.
- Sorting
arr1takes andarr2takes .
- Space Complexity: auxiliary space.
7. Optimal Solution 2: The Gap Method (Shell Sort Intuition)
The second optimal solution uses the Gap Method, derived from the Shell Sort algorithm. It works by comparing and swapping elements that are a fixed distance ("gap") apart, gradually reducing the gap until it reaches 1.
Derivation of Gap Formula
- Total elements = .
- Initial
gap= . - Integer representation of ceiling division:
- In each iteration, after processing all pairs at the current gap distance, reduce the gap:
- The process terminates after the pass with
gap = 1finishes.
8. Index Mapping Across Two Virtual Arrays
Since arr1 and arr2 are physically distinct memory blocks, we treat them as a single logical array of length using conditional index translation:
Logical Array Indexing:
0 .............. n-1 | n .............. n+m-1
[ arr1 ] | [ arr2 ]
Pointer Traversal Rules
When given a pointer at logical index idx:
idx < n: Refers toarr1[idx].idx >= n: Refers toarr2[idx - n].
This gives three index interaction cases for pointers left and right = left + gap:
Case 1: Both in arr1
left < n AND right < n
Compare: arr1[left] vs arr1[right]
Case 2: left in arr1, right in arr2
left < n AND right >= n
Compare: arr1[left] vs arr2[right - n]
Case 3: Both in arr2
left >= n AND right >= n
Compare: arr2[left - n] vs arr2[right - n]
9. Step-by-Step Simulation of the Gap Method
arr1 = {1, 3, 5, 7}()arr2 = {0, 2, 6, 8, 9}()- Total Length .
Pass 1: gap = ceil(9 / 2) = 5
left = 0,right = 5arr1[0](1) vsarr2[1](2) , no swap.
left = 1,right = 6arr1[1](3) vsarr2[2](6) , no swap.
left = 2,right = 7arr1[2](5) vsarr2[3](8) , no swap.
left = 3,right = 8arr1[3](7) vsarr2[4](9) , no swap.
rightreaches index 9 (out of bounds). Pass ends.
Pass 2: gap = ceil(5 / 2) = 3
left = 0,right = 3arr1[0](1) vsarr1[3](7) , no swap.
left = 1,right = 4arr1[1](3) vsarr2[0](0) , SWAP!arr1becomes{1, 0, 5, 7},arr2becomes{3, 2, 6, 8, 9}.
left = 2,right = 5arr1[2](5) vsarr2[1](2) , SWAP!arr1becomes{1, 0, 2, 7},arr2becomes{3, 5, 6, 8, 9}.
left = 3,right = 6arr1[3](7) vsarr2[2](6) , SWAP!arr1becomes{1, 0, 2, 6},arr2becomes{3, 5, 7, 8, 9}.
- Subsequent comparisons for
gap = 3require no further swaps.
Pass 3: gap = ceil(3 / 2) = 2
Swaps correct smaller localized inversions.
Pass 4: gap = ceil(2 / 2) = 1
Runs adjacent comparisons across the virtual array. Replaces all remaining inversion pairs.
Final Result:
arr1 = {0, 1, 2, 3}arr2 = {5, 6, 7, 8, 9}
10. Code Implementation: Gap Method
#include <vector>
#include <algorithm>
// Helper function to swap elements if left_val > right_val
void swapIfGreater(std::vector<int>& arr1, std::vector<int>& arr2, int ind1, int ind2, int n) {
// Resolve pointer ind1
int val1 = (ind1 < n) ? arr1[ind1] : arr2[ind1 - n];
// Resolve pointer ind2
int val2 = (ind2 < n) ? arr1[ind2] : arr2[ind2 - n];
if (val1 > val2) {
if (ind1 < n && ind2 < n) {
std::swap(arr1[ind1], arr1[ind2]);
} else if (ind1 < n && ind2 >= n) {
std::swap(arr1[ind1], arr2[ind2 - n]);
} else {
std::swap(arr2[ind1 - n], arr2[ind2 - n]);
}
}
}
void mergeOptimal2_GapMethod(std::vector<int>& arr1, std::vector<int>& arr2, int n, int m) {
int len = n + m;
int gap = (len / 2) + (len % 2); // Equivalent to ceil(len / 2.0)
while (gap > 0) {
int left = 0;
int right = left + gap;
while (right < len) {
swapIfGreater(arr1, arr2, left, right, n);
left++;
right++;
}
if (gap == 1) break; // Terminate after completing gap = 1
gap = (gap / 2) + (gap % 2); // Calculate next ceiling gap
}
}
Python Implementation
def merge_optimal2_gap_method(arr1: list[int], arr2: list[int], n: int, m: int) -> None:
def swap_if_greater(ind1: int, ind2: int) -> None:
# Determine actual references
val1 = arr1[ind1] if ind1 < n else arr2[ind1 - n]
val2 = arr1[ind2] if ind2 < n else arr2[ind2 - n]
if val1 > val2:
if ind1 < n and ind2 < n:
arr1[ind1], arr1[ind2] = arr1[ind2], arr1[ind1]
elif ind1 < n and ind2 >= n:
arr1[ind1], arr2[ind2 - n] = arr2[ind2 - n], arr1[ind1]
else:
arr2[ind1 - n], arr2[ind2 - n] = arr2[ind2 - n], arr2[ind1 - n]
length = n + m
gap = (length // 2) + (length % 2)
while gap > 0:
left = 0
right = left + gap
while right < length:
swap_if_greater(left, right)
left += 1
right += 1
if gap == 1:
break
gap = (gap // 2) + (gap % 2)
11. Comparative Analysis
12. Edge Cases & Debugging Pitfalls
Note: Pay close attention to these scenarios during technical interviews:
-
gap = 1Infinite Loop Bug:- Issue:
ceil(1 / 2) = 1. If you updategap = (gap / 2) + (gap % 2)without an explicit break condition, the gap remains 1 forever. - Fix: Always check
if (gap == 1) break;immediately after completing thegap = 1pass, or break whengapattempts to update past 1.
- Issue:
-
Index Mapping Off-By-One Errors:
- When
right >= n, accessingarr2requiresright - n. Usingrightdirectly will throw an out-of-bounds exception.
- When
-
Disjoint Arrays:
- Example:
arr1 = {1, 2, 3},arr2 = {4, 5, 6}. - Both optimal algorithms should correctly perform zero swaps and leave the input unchanged without performance overhead.
- Example:
Summary and Key Takeaways
- Problem Goal: Merge two sorted arrays
arr1() andarr2() in-place so thatarr1gets the smallest elements andarr2gets the remaining elements, both sorted. - Naive Solution: Merge elements into an auxiliary array of size using two pointers, then copy back. Costs time and space.
- Optimal 1 (Swap & Sort): Start
leftat (arr1) andrightat (arr2). Swap wheneverarr1[left] > arr2[right], then moveleft--andright++. Oncearr1[left] <= arr2[right], stop swapping and sort each array independently. Time: , Space: . - Optimal 2 (Gap Method): Uses Shell Sort intuition. Define
gap = ceil((n + m) / 2)and compare elements that aregappositions apart across the combined array. - Virtual Index Translation: Convert a logical index
idxin range to physical arrays: accessarr1[idx]ifidx < n, else accessarr2[idx - n]. - Gap Reduction: Update
gap = ceil(gap / 2)using integer arithmetic(gap / 2) + (gap % 2). - Termination Condition: Explicitly stop the Gap Method loop after completing the iteration where
gap == 1to prevent infinite loops. Time: , Space: .