在问 · 算法题解

912. 排序数组

题目描述

输入整数数组 nums,其中可能有重复值。

把它按从小到大的顺序排好并返回。

不能调用任何内置的排序函数,时间复杂度要达到 O(n log(n)),额外空间越少越好。

示例 1
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

解释:重复的值也要保留。

数据范围

  • 1 <= nums.length <= 5 * 10^4
  • -5 * 10^4 <= nums[i] <= 5 * 10^4

解题思路:堆排序

堆排序:把数组本身看成一棵完全二叉树,下标 i 的孩子是 2i+1 和 2i+2。第一步建最大堆:从最后一个非叶子结点向前,对每个结点做“下沉”(和较大的孩子交换,直到不小于孩子)。第二步反复把堆顶(当前最大值)与堆末尾交换,堆缩小一格,再让新堆顶下沉。数组末尾就从大到小依次排好。

为什么正确

nums[0:end] 是最大堆,nums[end:] 已排好且每个元素都不小于堆中任何元素。

复杂度分析

时间 O(n log n),建堆 O(n);空间 O(1),原地排序。

易错点

下沉时要和“较大的孩子”交换,否则堆性质会被破坏。第二阶段下沉的范围是 [0, end),不能碰已排好的尾部。快速排序如果不随机选枢轴,在有序或大量重复的数据上会退化到 O(n²)。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        n = len(nums)
        def sift(i, size):
            while 2 * i + 1 < size:
                c = 2 * i + 1
                if c + 1 < size and nums[c + 1] > nums[c]:
                    c += 1
                if nums[i] >= nums[c]:
                    break
                nums[i], nums[c] = nums[c], nums[i]
                i = c
        for i in range(n // 2 - 1, -1, -1):
            sift(i, n)
        for end in range(n - 1, 0, -1):
            nums[0], nums[end] = nums[end], nums[0]
            sift(0, end)
        return nums

载入编辑器,自己跑一遍 →

其他解法

堆排序最坏 O(n log n) 且原地,但不稳定、访存跳跃;快速排序平均最快但只有期望保证;归并排序最坏 O(n log n) 且稳定,但要 O(n) 额外空间。

解法 2:随机快速排序(三路划分) 时间 期望 O(n log n),最坏 O(n²),空间 期望 O(log n)(递归栈)

随机选一个基准值 pivot,用三个指针一遍扫描把区间分成“小于 pivot、等于 pivot、大于 pivot”三段(荷兰国旗划分),再对左右两段递归。随机选基准避免有序输入退化,三路划分避免大量重复元素时退化。

常数小、缓存友好,实际通常是最快的比较排序;但只有期望复杂度保证,并且不稳定。如果不做随机化或不处理重复元素,在本题的卡数据下会超时。

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        def quick(lo, hi):
            if lo >= hi:
                return
            pivot = nums[random.randint(lo, hi)]
            lt, i, gt = lo, lo, hi
            while i <= gt:
                if nums[i] < pivot:
                    nums[lt], nums[i] = nums[i], nums[lt]
                    lt += 1
                    i += 1
                elif nums[i] > pivot:
                    nums[i], nums[gt] = nums[gt], nums[i]
                    gt -= 1
                else:
                    i += 1
            quick(lo, lt - 1)
            quick(gt + 1, hi)

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

解法 3:归并排序 时间 O(n log n),空间 O(n)

把数组从中间分成两半,分别递归排好,再用双指针把两个有序段合并到辅助数组,最后拷回原数组。合并时相等元素先取左半部分的,所以排序是稳定的。

最坏情况也是 O(n log n) 且稳定,还能顺便统计逆序对,也适合链表和外部排序。代价是需要 O(n) 辅助空间。

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        tmp = [0] * len(nums)

        def merge_sort(lo, hi):
            if lo >= hi:
                return
            mid = (lo + hi) // 2
            merge_sort(lo, mid)
            merge_sort(mid + 1, hi)
            i, j, k = lo, mid + 1, lo
            while i <= mid and j <= hi:
                if nums[i] <= nums[j]:
                    tmp[k] = nums[i]
                    i += 1
                else:
                    tmp[k] = nums[j]
                    j += 1
                k += 1
            while i <= mid:
                tmp[k] = nums[i]
                i += 1
                k += 1
            while j <= hi:
                tmp[k] = nums[j]
                j += 1
                k += 1
            nums[lo:hi + 1] = tmp[lo:hi + 1]

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

栈与堆 专题的其他题目

查看 栈与堆 全部题目 →