questions S-W

This commit is contained in:
2025-05-30 19:18:33 +01:00
parent 68699f35ec
commit f7e491f1e8
46 changed files with 9696 additions and 0 deletions

View File

@@ -0,0 +1,266 @@
title: Sort an Array
slug: sort-an-array
difficulty: medium
leetcode_id: 912
leetcode_url: https://leetcode.com/problems/sort-an-array/
categories:
- arrays
- sorting
- recursion
patterns:
- dynamic-programming
description: |
Given an array of integers `nums`, sort the array in ascending order and return it.
You must solve the problem **without using any built-in** functions in `O(n log n)` time complexity and with the smallest space complexity possible.
constraints: |
- `1 <= nums.length <= 5 * 10^4`
- `-5 * 10^4 <= nums[i] <= 5 * 10^4`
examples:
- input: "nums = [5,2,3,1]"
output: "[1,2,3,5]"
explanation: "After sorting the array, the positions of some numbers are not changed (for example, 2 and 3), while the positions of other numbers are changed (for example, 1 and 5)."
- input: "nums = [5,1,1,2,0,0]"
output: "[0,0,1,1,2,5]"
explanation: "Note that the values of nums are not necessarily unique."
explanation:
intuition: |
Imagine you have a messy deck of cards and need to sort them. One natural approach is **divide and conquer**: split the deck in half, sort each half separately, and then merge them back together.
This is the essence of **Merge Sort**. The key insight is that merging two *already sorted* arrays is easy and efficient - you just compare the front elements of each array and pick the smaller one, repeating until both arrays are exhausted.
Think of it like this: sorting a huge pile of papers is overwhelming, but sorting two small piles and then combining them? That's manageable. By recursively applying this principle, we break the problem into trivially small pieces (single elements, which are inherently sorted) and build up the solution.
The constraint requiring `O(n log n)` time rules out simple algorithms like Bubble Sort or Insertion Sort (which are `O(n^2)`). Merge Sort guarantees `O(n log n)` in all cases - worst, average, and best - making it a reliable choice.
approach: |
We solve this using **Merge Sort**, a classic divide-and-conquer algorithm:
**Step 1: Base case**
- If the array has 0 or 1 elements, it's already sorted - return it as-is
- This is the termination condition for our recursion
&nbsp;
**Step 2: Divide the array**
- Find the middle index: `mid = len(nums) // 2`
- Split into two halves: `left = nums[:mid]` and `right = nums[mid:]`
&nbsp;
**Step 3: Recursively sort each half**
- Call merge sort on the left half
- Call merge sort on the right half
- Trust the recursion - each half will come back sorted
&nbsp;
**Step 4: Merge the sorted halves**
- Create a result array
- Use two pointers, one for each sorted half
- Compare elements at both pointers, add the smaller one to result
- Advance the pointer of the array from which we took the element
- When one array is exhausted, append all remaining elements from the other
&nbsp;
**Step 5: Return the merged result**
- The merged array is now fully sorted
- This bubbles up through the recursion to produce the final sorted array
common_pitfalls:
- title: Using O(n^2) Sorting Algorithms
description: |
Simple algorithms like Bubble Sort, Selection Sort, or Insertion Sort have `O(n^2)` time complexity.
With `n = 5 * 10^4`, this means up to 2.5 billion operations - far too slow and will result in **Time Limit Exceeded (TLE)**.
The problem explicitly requires `O(n log n)`, so you must use Merge Sort, Heap Sort, or Quick Sort (with proper pivot selection).
wrong_approach: "Nested loops comparing/swapping adjacent elements"
correct_approach: "Divide and conquer with O(n log n) guarantee"
- title: Quick Sort Worst Case
description: |
While Quick Sort is often fast in practice, its worst case is `O(n^2)` - which happens with already sorted arrays or when the pivot is always the smallest/largest element.
LeetCode tests often include edge cases that trigger this worst case. Merge Sort avoids this entirely with guaranteed `O(n log n)` performance.
If using Quick Sort, randomize pivot selection or use median-of-three to avoid worst case.
wrong_approach: "Quick Sort with first/last element as pivot"
correct_approach: "Merge Sort or Quick Sort with randomized pivot"
- title: Inefficient Merging
description: |
A common mistake is using inefficient operations during the merge step, like repeatedly inserting at the beginning of a list (`O(n)` per insert) or using `pop(0)` in Python.
Always append to the end of your result array and use index pointers to track position in the source arrays.
wrong_approach: "Using pop(0) or insert(0) during merge"
correct_approach: "Use index pointers and append to result"
- title: Not Handling Remaining Elements
description: |
After the main merge loop, one of the two halves may still have elements remaining.
Forgetting to append these leftover elements results in missing data in the output. Always extend the result with any remaining elements from both halves.
wrong_approach: "Stopping when one array is exhausted"
correct_approach: "Append remaining elements from both arrays after loop"
key_takeaways:
- "**Divide and conquer**: Break large problems into smaller subproblems, solve them, and combine results"
- "**Merge Sort guarantees**: Unlike Quick Sort, Merge Sort is `O(n log n)` in all cases - no worst-case degradation"
- "**Space tradeoff**: Merge Sort uses `O(n)` extra space for the merge step, but this is acceptable for guaranteed performance"
- "**Foundation for advanced algorithms**: The merge technique appears in external sorting, merge intervals, and many other problems"
time_complexity: "O(n log n). We divide the array log n times, and each level of recursion does O(n) work to merge."
space_complexity: "O(n). We need auxiliary space for the temporary arrays during merging. The recursion stack adds O(log n), but O(n) dominates."
solutions:
- approach_name: Merge Sort
is_optimal: true
code: |
def sortArray(nums: list[int]) -> list[int]:
# Base case: single element or empty array is already sorted
if len(nums) <= 1:
return nums
# Divide: find the middle and split
mid = len(nums) // 2
left = nums[:mid]
right = nums[mid:]
# Conquer: recursively sort both halves
sorted_left = sortArray(left)
sorted_right = sortArray(right)
# Combine: merge the two sorted halves
return merge(sorted_left, sorted_right)
def merge(left: list[int], right: list[int]) -> list[int]:
result = []
i = j = 0
# Compare elements from both arrays and add smaller one
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# Add any remaining elements from left array
while i < len(left):
result.append(left[i])
i += 1
# Add any remaining elements from right array
while j < len(right):
result.append(right[j])
j += 1
return result
explanation: |
**Time Complexity:** O(n log n) - We divide the array into halves log n times, and merging at each level takes O(n) total.
**Space Complexity:** O(n) - We need extra space for the temporary arrays created during slicing and merging.
This is the classic Merge Sort implementation. It recursively divides the array until we have single elements, then merges them back in sorted order. The `<=` comparison in merge ensures stability (equal elements maintain their relative order).
- approach_name: Heap Sort
is_optimal: true
code: |
def sortArray(nums: list[int]) -> list[int]:
n = len(nums)
# Build max heap - start from last non-leaf node
for i in range(n // 2 - 1, -1, -1):
heapify(nums, n, i)
# Extract elements from heap one by one
for i in range(n - 1, 0, -1):
# Move current root (max) to end
nums[0], nums[i] = nums[i], nums[0]
# Heapify the reduced heap
heapify(nums, i, 0)
return nums
def heapify(nums: list[int], n: int, i: int) -> None:
largest = i # Initialize largest as root
left = 2 * i + 1 # Left child index
right = 2 * i + 2 # Right child index
# Check if left child exists and is greater than root
if left < n and nums[left] > nums[largest]:
largest = left
# Check if right child exists and is greater than current largest
if right < n and nums[right] > nums[largest]:
largest = right
# If largest is not root, swap and continue heapifying
if largest != i:
nums[i], nums[largest] = nums[largest], nums[i]
heapify(nums, n, largest)
explanation: |
**Time Complexity:** O(n log n) - Building the heap takes O(n), and we perform n extractions each taking O(log n).
**Space Complexity:** O(1) - Heap Sort is in-place, using only the input array (ignoring recursion stack for heapify, which can be made iterative).
Heap Sort first builds a max heap from the array. Then it repeatedly extracts the maximum element (root), places it at the end, and restores the heap property. This achieves O(n log n) with O(1) extra space, though it's not stable.
- approach_name: Quick Sort (Randomized)
is_optimal: false
code: |
import random
def sortArray(nums: list[int]) -> list[int]:
quick_sort(nums, 0, len(nums) - 1)
return nums
def quick_sort(nums: list[int], low: int, high: int) -> None:
if low < high:
# Randomize pivot to avoid worst case
pivot_idx = random.randint(low, high)
nums[pivot_idx], nums[high] = nums[high], nums[pivot_idx]
# Partition and get pivot position
pivot_pos = partition(nums, low, high)
# Recursively sort elements before and after partition
quick_sort(nums, low, pivot_pos - 1)
quick_sort(nums, pivot_pos + 1, high)
def partition(nums: list[int], low: int, high: int) -> int:
pivot = nums[high] # Pivot is at the end
i = low - 1 # Index of smaller element
for j in range(low, high):
# If current element is smaller than or equal to pivot
if nums[j] <= pivot:
i += 1
nums[i], nums[j] = nums[j], nums[i]
# Place pivot in correct position
nums[i + 1], nums[high] = nums[high], nums[i + 1]
return i + 1
explanation: |
**Time Complexity:** O(n log n) average, O(n^2) worst case - Randomization makes worst case extremely unlikely.
**Space Complexity:** O(log n) average for recursion stack, O(n) worst case.
Quick Sort partitions the array around a pivot, placing smaller elements before it and larger elements after. Randomizing the pivot selection prevents adversarial inputs from triggering the O(n^2) worst case. While fast in practice, Merge Sort is preferred when guaranteed O(n log n) is required.