字节 LeetCode CodeTop 912.排序数组

前置题目:可先学习相对更简单的快速选择算法,重点掌握划分(partition)的原理,题目见hot100 215.数组中的第K个最大元素

思路:手写快排。

1.快速排序的核心思想:

(1)定义一个递归函数sort(left,right),表示把子数组[left,right]升序排列。

(2)划分子数组[left,right],划分方法同hot100 215.数组中的第K个最大元素。设划分后,基准元素pivot的下标为i。

(a)在pivot左侧的元素,继续递归排序,即递归调用sort(left,i - 1)。

(b)在pivot右侧的元素,继续递归排序,即递归调用sort(i + 1,right)。

(3)递归边界:如果left >= right,说明子数组为空或者只剩一个元素,此时无需排序,直接返回。

(4)递归入口:sort(0,n - 1)。

2.优化:如果子数组已是升序数组,直接返回。

3.疑问:

(1)如果不随机选择基准元素pivot,会发生什么?

答:

如果子数组是降序的,且我们每次都选子数组的第一个(或者最后一个)元素作为pivot,那么按照算法,j不变或者移动到最左边,划分是最不均匀的,算法会退化至O(n^2)。随机选择pivot的目的是能使划分在期望意义上是均匀的(j移动到子数组的中间),保证算法的期望时间复杂度为O(nlogn)。

(2)代码中的nums[i] < pivot和nums[i] > pivot,能否改成nums[i] <= pivot和nums[i] >= pivot?

答:

这个做法会在子数组所有元素相同时,划分后的j是子数组的最后一个元素的下标,是最不均匀的划分,算法也会退化至O(n^2)。

(3)代码中的i <= j能否改成i < j?

答:

这样的话会算错。来看一个例子nums = [2,1,3],pivot = 2。

(a)左指针i = 1移动到i = 2,右指针j = 2因为不满足i < j的条件,无法移动。此时我们交换2和nums[j] = 3,得到[3,1,2],返回j = 2。然而j = 2的左侧有大于pivot = 2的元素,划分失败。

(b)如果写成i <= j,那么最终i = 2,j = 1。此时我们交换2和nums[j] = 1,得到[1,2,3],返回j = 1。这样的划分就是正确的。

4.复杂度分析:

(1)期望时间复杂度:O(nlogn),其中n是nums的长度。在平均情况下,可以视作均匀划分(partition)。递归深度为O(logn),每一层都会涉及O(n)个元素的比较和交换,所以时间复杂度等于一个高为O(logn),底边长为O(n)的矩形的面积,即O(nlogn)。

(2)期望空间复杂度:O(logn)。递归需要期望O(logn)的栈开销。

5.总结:

附代码:

java 复制代码
class Solution {
    private static final Random rand = new Random();
    public int[] sortArray(int[] nums) {
        quickSort(nums,0,nums.length - 1);
        return nums;
    }
    // 快速排序子数组[left,right]
    private void quickSort(int[] nums,int left,int right){
        // 优化:如果子数组已经升序,直接返回
        boolean ordered = true;
        for(int i = left;i < right;i++){
            if(nums[i] > nums[i + 1]){
                ordered = false;
                break;
            }
        }
        if(ordered){
            return;
        }
        // 接收pivot最终的位置,满足pivot前面的元素都小于pivot,pivot后面的元素都大于pivot
        int i = partition(nums,left,right);
        quickSort(nums,left,i - 1);  // 排序在 pivot 左侧的元素
        quickSort(nums,i + 1,right); // 排序在 pivot 右侧的元素
    }

    // 在子数组[left,right]中随机选择一个基准元素pivot
    // 根据pivot重新排列子数组[left,right]
    // 重新排列后,<= pivot的元素都在pivot的左侧,>= pivot的元素都在pivot的右侧
    // 返回pivot在重新排列后的nums中的下标
    // 特别地,如果子数组的所有元素都等于pivot,我们会返回子数组的中心下标,避免退化
    private int partition(int[] nums,int left,int right){
        // 1.在子数组[left,right]中随机选择一个基准元素pivot
        // rand.nextInt(int bound)表示生成一个范围在[0,bound)之间的随机整数
        int i = left + rand.nextInt(right - left + 1);
        int pivot = nums[i];
        // 把pivot与子数组第一个元素交换,避免pivot干扰后续划分,从而简化实现逻辑
        swap(nums,i,left);

        // 2. 相向双指针遍历子数组 [left + 1, right]
        // 循环不变量:在循环过程中,子数组的数据分布始终如下图
        // [ pivot | <=pivot | 尚未遍历 | >=pivot ]
        //   ^                 ^     ^         ^
        //   left              i     j         right

        i = left + 1;
        int j = right;

        // 每走一步都会:i++或者j--
        // 数组长度有限,因此也不会无限循环下去
        while(true){
            while(i <= j && nums[i] < pivot){
                i++;
            }
            // 此时nums[i] >= pivot
            while(i <= j && nums[j] > pivot){
                j--;
            }
            // 此时nums[j] <= pivot
            if(i >= j){
                break;
            }
            // 维持循环不变量
            swap(nums,i,j);
            i++;
            j--;
        }
        // 循环结束后
        // [ pivot | <=pivot | >=pivot ]
        //   ^             ^   ^     ^
        //   left          j   i     right

        // 3. 把 pivot 与 nums[j] 交换,完成划分(partition)
        // 为什么与 j 交换?
        // 如果与 i 交换,可能会出现 i = right + 1 的情况,已经下标越界了,无法交换
        // 另一个原因是如果 nums[i] > pivot,交换会导致一个大于 pivot 的数出现在子数组最左边,不是有效划分
        // 与 j 交换,即使 j = left,交换也不会出错
        swap(nums,left,j);
        // 交换后
        // [ <=pivot | pivot | >=pivot ]
        //               ^
        //               j

        // 返回 pivot 的下标
        return j;
    }
    // 交换 nums[i] 与 nums[j],即索引位置对应的元素值
    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

ACM模式:

java 复制代码
import java.util.Random;
import java.util.Scanner;

class Solution {
    private static final Random rand = new Random();

    public int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

    // 快速排序子数组[left,right]
    private void quickSort(int[] nums, int left, int right) {
        // 优化:如果子数组已经升序,直接返回
        boolean ordered = true;
        for (int i = left; i < right; i++) {
            if (nums[i] > nums[i + 1]) {
                ordered = false;
                break;
            }
        }
        if (ordered) {
            return;
        }
        // 接收pivot最终的位置,满足pivot前面的元素都小于pivot,pivot后面的元素都大于pivot
        int i = partition(nums, left, right);
        quickSort(nums, left, i - 1);
        quickSort(nums, i + 1, right);
    }

    // 在子数组[left,right]中随机选择一个基准元素pivot
    // 根据pivot重新排列子数组[left,right]
    // 重新排列后,<= pivot的元素都在pivot的左侧,>= pivot的元素都在pivot的右侧
    // 返回pivot在重新排列后的nums中的下标
    private int partition(int[] nums, int left, int right) {
        // 1.在子数组[left,right]中随机选择一个基准元素pivot
        int i = left + rand.nextInt(right - left + 1);
        int pivot = nums[i];
        // 把pivot与子数组第一个元素交换
        swap(nums, i, left);

        // 2. 相向双指针遍历子数组 [left + 1, right]
        i = left + 1;
        int j = right;

        while (true) {
            while (i <= j && nums[i] < pivot) {
                i++;
            }
            while (i <= j && nums[j] > pivot) {
                j--;
            }
            if (i >= j) {
                break;
            }
            swap(nums, i, j);
            i++;
            j--;
        }

        // 3. 把 pivot 与 nums[j] 交换,完成划分
        swap(nums, left, j);
        return j;
    }

    // 交换 nums[i] 与 nums[j]
    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 读取数组长度
        int n = scanner.nextInt();

        // 读取数组元素
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = scanner.nextInt();
        }

        // 排序数组
        Solution solution = new Solution();
        int[] result = solution.sortArray(nums);

        // 输出排序后的数组
        for (int i = 0; i < result.length; i++) {
            System.out.print(result[i]);
            if (i < result.length - 1) {
                System.out.print(" ");
            }
        }

        scanner.close();
    }
}
相关推荐
Liangwei Lin1 小时前
LeetCode 48. 旋转图像
算法
AGV算法笔记1 小时前
【具身智能研究进展】RoboBrain 2.5:让机器人真正理解“空间”和“时间”的大脑模型
算法·3d·机器人·具身智能·感知算法
love在水一方1 小时前
【InternNav】 工程详细分析
人工智能·算法·机器学习
合兴软件@2 小时前
合兴软件重磅推出高性能HSM固件 国密算法赋能汽车信息安全新防线
网络·算法·网络安全·汽车·信息与通信
wearegogog1232 小时前
基于遗传算法的阵列天线方向图优化MATLAB实现
算法·matlab
Controller-Inversion2 小时前
312. 戳气球
算法
图码2 小时前
最大子数组和问题:从暴力到Kadane算法的优雅蜕变
数据结构·算法·动态规划·柔性数组
Raink老师2 小时前
用100道题拿下你的算法面试(链表篇-5):删除链表的倒数第 N 个节点
算法·链表·面试
qq_296553272 小时前
[特殊字符] 数组中的递增三元组:O(n) 时间高效查找,面试必考!
数据结构·算法·面试·职场和发展·组合模式·柔性数组