912. 排序数组


912. 排序数组

难度中等

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]

输出:[1,2,3,5]

示例 2:

输入:nums = [5,1,1,2,0,0]

输出:[0,0,1,1,2,5]

选择排序

每一轮选择最小元素交换到未排定部分的开头。ok

    // 选择排序:每一轮选择最小元素交换到未排定部分的开头

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 循环不变量:[0, i) 有序,且该区间里所有元素就是最终排定的样子
        for (int i = 0; i < len - 1; i++) &#123;
            // 选择区间 [i, len - 1] 里最小的元素的索引,交换到下标 i
            int minIndex = i;
            for (int j = i + 1; j < len; j++) &#123;
                if (nums[j] < nums[minIndex]) &#123;
                    minIndex = j;//
                &#125;
            &#125;
            swap(nums, i, minIndex);
        &#125;
        return nums;
    &#125;

    private void swap(int[] nums, int index1, int index2) &#123;
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    &#125;

####


冒泡排序

比较两个相邻的元素,将值大的元素交换到右边【超时】。

    // 冒泡排序:比较两个相邻的元素,将值大的元素交换到右边。

    public int[] sortArray(int[] nums) &#123;
        int len = nums.length;
        for (int i = 0; i < len -1 ; i++) &#123; // [5,2,3,1] i索引到3即length-2时 j比较的是3,1 length-2,length-1.
            // 先默认数组是有序的,只要发生一次交换,就必须进行下一轮比较,
            // 如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
            boolean sorted = true;
            for (int j = 0; j < len-i-1 ; j++) &#123;
                if (nums[j] > nums[j+1]) &#123;
                    swap(nums, j, j+1);
                    sorted = false;
                &#125;
            &#125;
            if(sorted)&#123;
               break;
            &#125;
        &#125;
        return nums;
    &#125;

堆排序

public class Solution &#123;

    public int[] sortArray(int[] nums) &#123;
        int len = nums.length;
        // 将数组整理成堆
        heapify(nums);

        // 循环不变量:区间 [0, i] 堆有序
        for (int i = len - 1; i >= 1; ) &#123;
            // 把堆顶元素(当前最大)交换到数组末尾
            swap(nums, 0, i);
            // 逐步减少堆有序的部分
            i--;
            // 下标 0 位置下沉操作,使得区间 [0, i] 堆有序
            siftDown(nums, 0, i);
        &#125;
        return nums;
    &#125;

    /**
     * 将数组整理成堆(堆有序)
     *
     * @param nums
     */
    private void heapify(int[] nums) &#123;
        int len = nums.length;
        // 只需要从 i = (len - 1) / 2 这个位置开始逐层下移
        for (int i = (len - 1) / 2; i >= 0; i--) &#123;
            siftDown(nums, i, len - 1);
        &#125;
    &#125;

    /**
     * @param nums
     * @param k    当前下沉元素的下标
     * @param end  [0, end] 是 nums 的有效部分
     */
    private void siftDown(int[] nums, int k, int end) &#123;
        while (2 * k + 1 <= end) &#123;
            int j = 2 * k + 1;
            if (j + 1 <= end && nums[j + 1] > nums[j]) &#123;
                j++;
            &#125;
            if (nums[j] > nums[k]) &#123;
                swap(nums, j, k);
            &#125; else &#123;
                break;
            &#125;
            k = j;
        &#125;
    &#125;

    private void swap(int[] nums, int index1, int index2) &#123;
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    &#125;
&#125;

快排

     // 快速排序 1:基本快速排序

    /**
     * 列表大小等于或小于该大小,将优先于 quickSort 使用插入排序
     */
    private static final int INSERTION_SORT_THRESHOLD = 7;

    private static final Random RANDOM = new Random();


    public int[] sortArray(int[] nums) &#123;
        int len = nums.length;
        quickSort(nums, 0, len - 1);
        return nums;
    &#125;

    private void quickSort(int[] nums, int left, int right) &#123;
        // 小区间使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) &#123;
            insertionSort(nums, left, right);
            return;
        &#125;

        int pIndex = partition(nums, left, right);
        quickSort(nums, left, pIndex - 1);
        quickSort(nums, pIndex + 1, right);
    &#125;

    /**
     * 对数组 nums 的子区间 [left, right] 使用插入排序
     *
     * @param nums  给定数组
     * @param left  左边界,能取到
     * @param right 右边界,能取到
     */
    private void insertionSort(int[] nums, int left, int right) &#123;
        for (int i = left + 1; i <= right; i++) &#123;
            int temp = nums[i];
            int j = i;
            while (j > left && nums[j - 1] > temp) &#123;
                nums[j] = nums[j - 1];
                j--;
            &#125;
            nums[j] = temp;
        &#125;
    &#125;

    private int partition(int[] nums, int left, int right) &#123;
        int randomIndex = RANDOM.nextInt(right - left + 1) + left;
        swap(nums, left, randomIndex);

        // 基准值
        int pivot = nums[left];
        int lt = left;
        // 循环不变量:
        // all in [left + 1, lt] < pivot
        // all in [lt + 1, i) >= pivot
        for (int i = left + 1; i <= right; i++) &#123;
            if (nums[i] < pivot) &#123;
                lt++;
                swap(nums, i, lt);
            &#125;
        &#125;
        swap(nums, left, lt);
        return lt;
    &#125;

    private void swap(int[] nums, int index1, int index2) &#123;
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    &#125;

基本思路:快速排序每一次都排定一个元素(这个元素呆在了它最终应该呆的位置),然后递归地去排它左边的部分和右边的部分,依次进行下去,直到数组有序;

算法思想:分而治之(分治思想),与「归并排序」不同,「快速排序」在「分」这件事情上不想「归并排序」无脑地一分为二,而是采用了 partition 的方法(书上,和网上都有介绍,就不展开了),因此就没有「合」的过程。

实现细节(注意事项):(针对特殊测试用例:顺序数组或者逆序数组)一定要随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」);

复杂度分析:

时间复杂度:O(NlogN),这里 N 是数组的长度;

空间复杂度:O(logN),这里占用的空间主要来自递归函数的栈空间。


归并排序

    // 归并排序

    /**
     * 列表大小等于或小于该大小,将优先于 mergeSort 使用插入排序
     */
    private static final int INSERTION_SORT_THRESHOLD = 7;

    public int[] sortArray(int[] nums) &#123;
        int len = nums.length;
        int[] temp = new int[len];
        mergeSort(nums, 0, len - 1, temp);
        return nums;
    &#125;

    /**
     * 对数组 nums 的子区间 [left, right] 进行归并排序
     *
     * @param nums
     * @param left
     * @param right
     * @param temp  用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁
     */
    private void mergeSort(int[] nums, int left, int right, int[] temp) &#123;
        // 小区间使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) &#123;
            insertionSort(nums, left, right);
            return;
        &#125;

        int mid = left + (right - left) / 2;
        // Java 里有更优的写法,在 left 和 right 都是大整数时,即使溢出,结论依然正确
        // int mid = (left + right) >>> 1;

        mergeSort(nums, left, mid, temp);
        mergeSort(nums, mid + 1, right, temp);
        // 如果数组的这个子区间本身有序,无需合并
        if (nums[mid] <= nums[mid + 1]) &#123;
            return;
        &#125;
        mergeOfTwoSortedArray(nums, left, mid, right, temp);
    &#125;

    /**
     * 对数组 arr 的子区间 [left, right] 使用插入排序
     *
     * @param arr   给定数组
     * @param left  左边界,能取到
     * @param right 右边界,能取到
     */
    private void insertionSort(int[] arr, int left, int right) &#123;
        for (int i = left + 1; i <= right; i++) &#123;
            int temp = arr[i];
            int j = i;
            while (j > left && arr[j - 1] > temp) &#123;
                arr[j] = arr[j - 1];
                j--;
            &#125;
            arr[j] = temp;
        &#125;
    &#125;

    /**
     * 合并两个有序数组:先把值复制到临时数组,再合并回去
     *
     * @param nums
     * @param left
     * @param mid   [left, mid] 有序,[mid + 1, right] 有序
     * @param right
     * @param temp  全局使用的临时数组
     */
    private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) &#123;
        System.arraycopy(nums, left, temp, left, right + 1 - left);

        int i = left;
        int j = mid + 1;

        for (int k = left; k <= right; k++) &#123;
            if (i == mid + 1) &#123;
                nums[k] = temp[j];
                j++;
            &#125; else if (j == right + 1) &#123;
                nums[k] = temp[i];
                i++;
            &#125; else if (temp[i] <= temp[j]) &#123;
                // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
                nums[k] = temp[i];
                i++;
            &#125; else &#123;
                // temp[i] > temp[j]
                nums[k] = temp[j];
                j++;
            &#125;
        &#125;
    &#125;

基本思路:借助额外空间,合并两个有序数组,得到更长的有序数组。例如:「力扣」第 88 题:合并两个有序数组。

算法思想:分而治之(分治思想)。「分而治之」思想的形象理解是「曹冲称象」、MapReduce,在一定情况下可以并行化。

个人建议:「归并排序」是理解「递归思想」的非常好的学习材料,大家可以通过理解:递归完成以后,合并两个有序数组的这一步骤,想清楚程序的执行流程。即「递归函数执行完成以后,我们还可以做点事情」。因此,「归并排序」我个人觉得非常重要,一定要掌握。


插入排序

    // 插入排序:稳定排序,在接近有序的情况下,表现优异

    public int[] sortArray(int[] nums) &#123;
        int len = nums.length;
        // 循环不变量:将 nums[i] 插入到区间 [0, i) 使之成为有序数组
        for (int i = 1; i < len; i++) &#123;
            // 先暂存这个元素,然后之前元素逐个后移,留出空位
            int temp = nums[i];
            int j = i;
            // 注意边界 j > 0
            while (j > 0 && nums[j - 1] > temp) &#123;
                nums[j] = nums[j - 1];
                j--;
            &#125;
            nums[j] = temp;
        &#125;
        return nums;
    &#125;

优化:「将一个数字插入一个有序的数组」这一步,可以不使用逐步交换,使用先赋值给「临时变量」,然后「适当的元素」后移,空出一个位置,最后把「临时变量」赋值给这个空位的策略(就是上面那张图的意思)。编码的时候如果不小心,可能会把数组的值修改,建议多调试;

特点:「插入排序」可以提前终止内层循环(体现在 nums[j - 1] > temp 不满足时),在数组「几乎有序」的前提下,「插入排序」的时间复杂度可以达到 O(N);

由于「插入排序」在「几乎有序」的数组上表现良好,特别地,在「短数组」上的表现也很好。因为「短数组」的特点是:每个元素离它最终排定的位置都不会太远。为此,在小区间内执行排序任务的时候,可以转向使用「插入排序」。


几大基础排序

https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/


文章作者:   future
版权声明:   本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 future !
 上一篇
199. 二叉树的右视图 199. 二叉树的右视图
199. 二叉树的右视图难度中等 416 给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 示例: 输入: [1,2,3,null,5,null,4] 输出: [1, 3, 4] 解释: 1    
下一篇 
103. 二叉树的锯齿形层序遍历--🀄️ 103. 二叉树的锯齿形层序遍历--🀄️
103. 二叉树的锯齿形层序遍历难度中等 402 给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 例如: 给定二叉树 [3,9,20,null,null,15,7],
  目录