Files
codetutor/backend/data/questions/merge-sorted-array.yaml
2025-05-25 12:43:25 +01:00

200 lines
9.4 KiB
YAML

title: Merge Sorted Array
slug: merge-sorted-array
difficulty: easy
leetcode_id: 88
leetcode_url: https://leetcode.com/problems/merge-sorted-array/
categories:
- arrays
- two-pointers
- sorting
patterns:
- two-pointers
description: |
You are given two integer arrays `nums1` and `nums2`, sorted in **non-decreasing order**, and two integers `m` and `n`, representing the number of elements in `nums1` and `nums2` respectively.
**Merge** `nums1` and `nums2` into a single array sorted in **non-decreasing order**.
The final sorted array should not be returned by the function, but instead be *stored inside the array* `nums1`. To accommodate this, `nums1` has a length of `m + n`, where the first `m` elements denote the elements that should be merged, and the last `n` elements are set to `0` and should be ignored. `nums2` has a length of `n`.
constraints: |
- `nums1.length == m + n`
- `nums2.length == n`
- `0 <= m, n <= 200`
- `1 <= m + n <= 200`
- `-10^9 <= nums1[i], nums2[j] <= 10^9`
examples:
- input: "nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3"
output: "[1,2,2,3,5,6]"
explanation: "The arrays we are merging are [1,2,3] and [2,5,6]. The result of the merge is [1,2,2,3,5,6]."
- input: "nums1 = [1], m = 1, nums2 = [], n = 0"
output: "[1]"
explanation: "The arrays we are merging are [1] and []. The result of the merge is [1]."
- input: "nums1 = [0], m = 0, nums2 = [1], n = 1"
output: "[1]"
explanation: "The arrays we are merging are [] and [1]. The result of the merge is [1]. Because m = 0, there are no elements in nums1. The 0 is only there to ensure the merge result can fit in nums1."
explanation:
intuition: |
Imagine you have two sorted stacks of numbered cards that you need to combine into one sorted stack, but you must place the result in the first stack's holder which already has empty slots at the end.
The key insight is to **fill from the back**. Since `nums1` has extra space at the end (the `n` zeros), we can place elements starting from position `m + n - 1` and work backwards. By comparing the *largest* remaining elements from both arrays and placing the larger one at the current end position, we avoid overwriting any elements we still need.
Think of it like this: if you start from the front, you'd overwrite elements in `nums1` before you've had a chance to place them. But starting from the back is safe because those positions are just placeholder zeros.
This backwards approach lets us merge in-place without needing any extra space.
approach: |
We solve this using the **Three Pointers (Merge from End)** approach:
**Step 1: Initialise three pointers**
- `p1`: Points to the last valid element in `nums1` (index `m - 1`)
- `p2`: Points to the last element in `nums2` (index `n - 1`)
- `p`: Points to the last position in `nums1` (index `m + n - 1`) — where we'll place the next largest element
&nbsp;
**Step 2: Compare and place elements from back to front**
- While both `p1` and `p2` are valid (>= 0):
- Compare `nums1[p1]` and `nums2[p2]`
- Place the **larger** value at `nums1[p]`
- Decrement the pointer of whichever array we took from
- Decrement `p`
&nbsp;
**Step 3: Handle remaining elements in nums2**
- If `p2 >= 0`, copy remaining elements from `nums2` to `nums1`
- We don't need to handle remaining `nums1` elements — they're already in place!
&nbsp;
This approach works because we're always placing the largest unplaced element at the rightmost unfilled position.
common_pitfalls:
- title: Starting from the Front
description: |
A natural instinct is to merge from the beginning, like in the classic merge step of merge sort.
However, if you start placing elements at `nums1[0]`, you'll overwrite elements in `nums1` that you haven't processed yet. For example, with `nums1 = [1,2,3,0,0,0]` and `nums2 = [2,5,6]`, placing `1` at index 0 is fine, but then where do you put `nums1`'s original `1`? It gets overwritten.
Starting from the back avoids this because the back positions are just placeholder zeros.
wrong_approach: "Merge from the front (index 0)"
correct_approach: "Merge from the back (index m + n - 1)"
- title: Forgetting to Copy Remaining nums2 Elements
description: |
After the main loop, if `p2 >= 0`, there are still elements in `nums2` that need to be copied. For example, if `nums1 = [4,5,6,0,0,0]` and `nums2 = [1,2,3]`, all of `nums2` needs to go at the front.
You don't need to worry about leftover `nums1` elements — they're already in their correct positions since we're modifying `nums1` in-place.
wrong_approach: "Only running the comparison loop"
correct_approach: "Copy remaining nums2 elements after the main loop"
- title: Using Extra Space
description: |
Some solutions create a new array to hold the merged result, then copy it back to `nums1`. While this works, it uses O(m + n) extra space.
The problem specifically mentions `nums1` has extra space at the end — this hint suggests an in-place solution is expected. The three-pointer approach achieves O(1) extra space.
key_takeaways:
- "**Merge from the end**: When merging into an array with trailing space, work backwards to avoid overwriting elements you still need"
- "**Three-pointer technique**: Use separate pointers for each input and the output position for clean, in-place merging"
- "**Foundation for merge sort**: This is the merge step used in merge sort — understanding it helps with divide-and-conquer algorithms"
- "**In-place modification**: When a problem says 'modify in-place', look for ways to avoid extra space by clever pointer manipulation"
time_complexity: "O(m + n). Each element from both arrays is visited exactly once."
space_complexity: "O(1). We only use three pointer variables regardless of input size."
solutions:
- approach_name: Three Pointers (Merge from End)
is_optimal: true
code: |
def merge(nums1: list[int], m: int, nums2: list[int], n: int) -> None:
# Start from the end of both arrays
p1 = m - 1 # Last valid element in nums1
p2 = n - 1 # Last element in nums2
p = m + n - 1 # Position to place next element
# Compare and place larger element at the end
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[p] = nums1[p1]
p1 -= 1
else:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
# Copy remaining elements from nums2 (if any)
# No need to copy remaining nums1 elements - they're already in place
while p2 >= 0:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
explanation: |
**Time Complexity:** O(m + n) — We process each element from both arrays exactly once.
**Space Complexity:** O(1) — Only three pointer variables used, regardless of input size.
By filling from the back, we guarantee that we never overwrite an element we still need. The larger of the two current elements gets placed at the rightmost unfilled position, and we work our way left until all elements are merged.
- approach_name: Copy and Sort
is_optimal: false
code: |
def merge(nums1: list[int], m: int, nums2: list[int], n: int) -> None:
# Copy nums2 into the empty slots of nums1
for i in range(n):
nums1[m + i] = nums2[i]
# Sort the entire array
nums1.sort()
explanation: |
**Time Complexity:** O((m + n) log(m + n)) — Dominated by the sorting step.
**Space Complexity:** O(log(m + n)) — Space used by the sorting algorithm.
This approach ignores the fact that both arrays are already sorted and just combines then sorts. While simple to implement, it's less efficient than the optimal O(m + n) solution. It's included here to show why leveraging the sorted property matters.
- approach_name: Extra Array
is_optimal: false
code: |
def merge(nums1: list[int], m: int, nums2: list[int], n: int) -> None:
# Create a copy of the valid elements in nums1
nums1_copy = nums1[:m]
# Two pointers for merging
p1 = 0 # Pointer for nums1_copy
p2 = 0 # Pointer for nums2
p = 0 # Pointer for placement in nums1
# Merge while both arrays have elements
while p1 < m and p2 < n:
if nums1_copy[p1] <= nums2[p2]:
nums1[p] = nums1_copy[p1]
p1 += 1
else:
nums1[p] = nums2[p2]
p2 += 1
p += 1
# Copy remaining elements
while p1 < m:
nums1[p] = nums1_copy[p1]
p1 += 1
p += 1
while p2 < n:
nums1[p] = nums2[p2]
p2 += 1
p += 1
explanation: |
**Time Complexity:** O(m + n) — We process each element once.
**Space Complexity:** O(m) — We create a copy of the first m elements.
This is the classic merge approach from merge sort, but it requires extra space to avoid overwriting elements. By copying `nums1`'s valid elements first, we can safely merge from the front. While this achieves O(m + n) time, the extra space makes it suboptimal compared to the three-pointer approach.