219 lines
10 KiB
YAML
219 lines
10 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:
|
|
- slug: two-pointers
|
|
is_optimal: true
|
|
|
|
function_signature: "def merge(nums1: list[int], m: int, nums2: list[int], n: int) -> list[int]:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums1: [1, 2, 3, 0, 0, 0], m: 3, nums2: [2, 5, 6], n: 3 }
|
|
expected: [1, 2, 2, 3, 5, 6]
|
|
- input: { nums1: [1], m: 1, nums2: [], n: 0 }
|
|
expected: [1]
|
|
- input: { nums1: [0], m: 0, nums2: [1], n: 1 }
|
|
expected: [1]
|
|
hidden:
|
|
- input: { nums1: [4, 5, 6, 0, 0, 0], m: 3, nums2: [1, 2, 3], n: 3 }
|
|
expected: [1, 2, 3, 4, 5, 6]
|
|
- input: { nums1: [1, 2, 3, 0, 0, 0], m: 3, nums2: [4, 5, 6], n: 3 }
|
|
expected: [1, 2, 3, 4, 5, 6]
|
|
- input: { nums1: [2, 0], m: 1, nums2: [1], n: 1 }
|
|
expected: [1, 2]
|
|
|
|
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
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**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!
|
|
|
|
|
|
|
|
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.
|