LeetCode-912:排序数组,手写快排和归并排序,面试必须能默写的两个模板

本题在线练习LeetCode 912. 排序数组 — 在线练习(免费 · 无需登录 · AI 辅助)

配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。

题目概述

给定一个整数数组 nums,将其升序排列后返回。

要求时间复杂度为 O(n log n),不能使用内置排序函数

例如:

  • 输入:nums = [5,2,3,1] → 输出:[1,2,3,5]
  • 输入:nums = [5,1,1,2,0,0] → 输出:[0,0,1,1,2,5]

这道题看起来简单,但它是手写排序算法的标准考察题。字节面试中常考手写 partition,要求非常熟练。面试官通常要求你现场写出快速排序或归并排序,而不是调用 sort()

核心思路

这道题有两种主流解法:

  1. 快速排序(Quicksort):选一个基准值(pivot),把数组分成"比 pivot 小"和"比 pivot 大"两部分,然后递归排序。平均 O(n log n),但最坏情况 O(n^2),需要随机化 pivot 来避免。

  2. 归并排序(Mergesort):把数组从中间切成两半,分别排序,然后合并两个有序数组。稳定保证 O(n log n),但需要额外空间。

两种方法都需要掌握。下面分别实现。


解法一:快速排序(随机化 pivot)

为什么需要随机化 pivot?

如果每次都选固定位置(比如第一个或最后一个元素)作为 pivot,当输入数组已经有序或接近有序时,partition 每次只能切掉一个元素,递归深度退化为 O(n),总时间退化为 O(n^2)。

随机选择 pivot,期望情况下每次都能把数组切成大致相等的两半,保证平均 O(n log n)。

代码

import random
from typing import List


class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        def quicksort(left: int, right: int) -> None:
            if left >= right:
                return

            # 随机选 pivot,避免最坏情况
            pivot_idx = random.randint(left, right)
            nums[pivot_idx], nums[right] = nums[right], nums[pivot_idx]
            pivot = nums[right]

            # Lomuto partition
            store = left
            for i in range(left, right):
                if nums[i] < pivot:
                    nums[store], nums[i] = nums[i], nums[store]
                    store += 1
            nums[store], nums[right] = nums[right], nums[store]

            quicksort(left, store - 1)
            quicksort(store + 1, right)

        quicksort(0, len(nums) - 1)
        return nums

逐行拆解

递归出口

if left >= right:
    return

只剩 0 个或 1 个元素时,不需要排序。

随机选 pivot 并放到末尾

pivot_idx = random.randint(left, right)
nums[pivot_idx], nums[right] = nums[right], nums[pivot_idx]
pivot = nums[right]

随机选一个位置作为 pivot,把它交换到最右边。这样 partition 的逻辑可以统一:扫描 [left, right-1],pivot 固定在 right 位置。

Lomuto partition

store = left
for i in range(left, right):
    if nums[i] < pivot:
        nums[store], nums[i] = nums[i], nums[store]
        store += 1
nums[store], nums[right] = nums[right], nums[store]

store 指向"下一个应该放小于 pivot 元素"的位置。扫描过程中:

  • 如果 nums[i] < pivot,把它换到 store 位置,store 右移
  • 扫描完后,nums[store] 的位置就是 pivot 的最终归宿,把 pivot 换回来

partition 完成后:

  • [left, store-1] 的元素都 < pivot
  • nums[store] == pivot
  • [store+1, right] 的元素都 >= pivot

递归排序两侧

quicksort(left, store - 1)
quicksort(store + 1, right)

pivot 已经在正确位置,不需要再动。只递归处理左右两部分。

手动模拟

nums = [5, 2, 3, 1] 为例:

第 1 轮left=0, right=3

假设随机到 pivot_idx=2pivot=3。交换后数组变为 [5, 2, 1, 3](pivot 3 在末尾)。

Lomuto partition(store=0,扫描 i=0,1,2):

i nums[i] < pivot(3)? 操作 store 数组状态
0 5 不动 0 [5, 2, 1, 3]
1 2 swap(0,1) 1 [2, 5, 1, 3]
2 1 swap(1,2) 2 [2, 1, 5, 3]

把 pivot 放回:swap(store=2, right=3) → [2, 1, 3, 5]

pivot 3 在下标 2,位置正确。

递归左半 [2, 1]:排序后 → [1, 2]

递归右半 [5]:只有一个元素,不需要排序。

最终结果:[1, 2, 3, 5]


解法二:归并排序(稳定 O(n log n))

思路

归并排序的核心是"分治":

  1. :把数组从中间切成两半
  2. :递归排序左半部分和右半部分
  3. :把两个有序数组合并成一个有序数组

合并过程用双指针,依次比较两个有序数组的头部元素,把较小的放入结果。

代码

from typing import List


class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        def mergesort(arr: List[int]) -> List[int]:
            if len(arr) <= 1:
                return arr

            mid = len(arr) // 2
            left = mergesort(arr[:mid])
            right = mergesort(arr[mid:])

            return merge(left, right)

        def merge(a: List[int], b: List[int]) -> List[int]:
            result = []
            i = j = 0
            while i < len(a) and j < len(b):
                if a[i] <= b[j]:
                    result.append(a[i])
                    i += 1
                else:
                    result.append(b[j])
                    j += 1
            result.extend(a[i:])
            result.extend(b[j:])
            return result

        return mergesort(nums)

逐行拆解

递归出口

if len(arr) <= 1:
    return arr

空数组或只有一个元素的数组,天然有序,直接返回。

分割

mid = len(arr) // 2
left = mergesort(arr[:mid])
right = mergesort(arr[mid:])

从中间切开,递归排序左右两半。切片 arr[:mid]arr[mid:] 创建了新数组,所以归并排序不是原地排序。

合并两个有序数组

i = j = 0
while i < len(a) and j < len(b):
    if a[i] <= b[j]:
        result.append(a[i])
        i += 1
    else:
        result.append(b[j])
        j += 1
result.extend(a[i:])
result.extend(b[j:])

双指针同时扫描两个有序数组,每次取较小的放入结果。某一边先扫完后,另一边剩余元素直接追加(它们本身已经有序且都比 result 中已有的元素大)。

注意用 <= 而不是 <,这保证了归并排序的稳定性——相等元素保持原来的先后顺序。

手动模拟

nums = [5, 2, 3, 1] 为例:

              [5, 2, 3, 1]
             /            \
        [5, 2]          [3, 1]
        /    \          /    \
      [5]   [2]      [3]   [1]
        \    /          \    /
       [2, 5]          [1, 3]
             \        /
          [1, 2, 3, 5]

合并 [5] 和 [2]:比较 5 和 2,2 小 → [2];剩余 [5] 追加 → [2, 5]

合并 [3] 和 [1]:比较 3 和 1,1 小 → [1];剩余 [3] 追加 → [1, 3]

合并 [2, 5] 和 [1, 3]

步骤 a[i] b[j] 取谁 result
1 2 1 b[0]=1 [1]
2 2 3 a[0]=2 [1, 2]
3 5 3 b[1]=3 [1, 2, 3]

a 剩余 [5] 追加 → [1, 2, 3, 5]


快速排序 vs 归并排序:怎么选?

对比维度 快速排序 归并排序
平均时间 O(n log n) O(n log n)
最坏时间 O(n^2)(随机化后极少发生) O(n log n)(稳定保证)
空间 O(log n)(递归栈) O(n)(合并时需要额外数组)
稳定性 不稳定 稳定
实际速度 通常更快(常数小,缓存友好) 常数稍大

面试中的建议:

  • 如果面试官要求 O(n log n) 且不能最坏退化,写归并排序更安全
  • 如果面试官问"怎么优化快排",答案是随机化 pivot
  • 字节面试常考手写 partition,需要非常熟练。把 Lomuto partition 的模板背下来,面试时直接写

复杂度分析

快速排序

  • 平均时间复杂度:O(n log n)
  • 最坏时间复杂度:O(n^2)(随机化后概率极低)
  • 空间复杂度:O(log n)(递归栈深度,平均情况)

归并排序

  • 时间复杂度:O(n log n)(最好、最坏、平均都一样)
  • 空间复杂度:O(n)(合并时需要创建临时数组)

总结

这道题是排序算法的基础考察,面试中几乎必考。两种解法都要掌握:

快速排序的关键点

  • 随机选 pivot,避免有序输入导致 O(n^2) 退化
  • Lomuto partition 模板:store 指针记录分界线,扫描一遍完成划分
  • partition 完成后 pivot 在最终位置,递归处理两侧

归并排序的关键点

  • 先递归拆分到最小,再两两合并
  • 合并用双指针,<= 保证稳定性
  • 不是原地排序,需要额外 O(n) 空间

面试时建议先问清面试官想看哪种排序。如果没有特殊要求,快速排序(带随机化)是最常见的选择;如果强调稳定性或最坏复杂度,就写归并排序。

posted @ 2026-04-20 00:21  孤飞  阅读(112)  评论(0)    收藏  举报