• 归并排序和快速排序


    1、归并排序

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

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

    public class Solution {
        // 归并排序
    
        /**
         * 列表大小等于或小于该大小,将优先于 mergeSort 使用插入排序
         */
        private static final int INSERTION_SORT_THRESHOLD = 7;
    
        public int[] sortArray(int[] nums) {
            int len = nums.length;
            int[] temp = new int[len];
            mergeSort(nums, 0, len - 1, temp);
            return nums;
        }
    
        /**
         * 对数组 nums 的子区间 [left, right] 进行归并排序
         *
         * @param nums
         * @param left
         * @param right
         * @param temp  用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁
         */
        private void mergeSort(int[] nums, int left, int right, int[] temp) {
            // 小区间使用插入排序
            if (right - left <= INSERTION_SORT_THRESHOLD) {
                insertionSort(nums, left, right);
                return;
            }
    //如果不使用快排,应该加上递归结束条件
         //if(left == right){
    // return;
    //}
    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]) { return; } mergeOfTwoSortedArray(nums, left, mid, right, temp); } /** * 对数组 arr 的子区间 [left, right] 使用插入排序 * * @param arr 给定数组 * @param left 左边界,能取到 * @param right 右边界,能取到 */ private void insertionSort(int[] arr, int left, int right) { for (int i = left + 1; i <= right; i++) { int temp = arr[i]; int j = i; while (j > left && arr[j - 1] > temp) { arr[j] = arr[j - 1]; j--; } arr[j] = temp; } } /** * 合并两个有序数组:先把值复制到临时数组,再合并回去 * * @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) { System.arraycopy(nums, left, temp, left, right + 1 - left); int i = left; int j = mid + 1; for (int k = left; k <= right; k++) { if (i == mid + 1) { nums[k] = temp[j]; j++; } else if (j == right + 1) { nums[k] = temp[i]; i++; } else if (temp[i] <= temp[j]) { // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前) nums[k] = temp[i]; i++; } else { // temp[i] > temp[j] nums[k] = temp[j]; j++; } } } }

      优化 1:在「小区间」里转向使用「插入排序」,Java 源码里面也有类似这种操作,「小区间」的长度是个超参数,需要测试决定,我这里参考了 JDK 源码;

      优化 2: 在「两个数组」本身就是有序的情况下,无需合并;

      优化 3:全程使用一份临时数组进行「合并两个有序数组」的操作,避免创建临时数组和销毁的消耗,避免计算下标偏移量。

      注意:实现归并排序的时候,要特别注意,不要把这个算法实现成非稳定排序,区别就在 <= 和 < ,已在代码中注明。

      「归并排序」比「快速排序」好的一点是,它借助了额外空间,可以实现「稳定排序」,Java 里对于「对象数组」的排序任务,就是使用归并排序(的升级版 TimSort,在这里就不多做介绍)。

    复杂度分析:

      时间复杂度:O(N log N)O(NlogN),这里 NN 是数组的长度;
      空间复杂度:O(N)O(N),辅助数组与输入数组规模相当。
      「归并排序」也有「原地归并排序」和「不使用递归」的归并排序,但是我个人觉得不常用,编码、调试都有一定难度。递归、分治处理问题的思想在基础算法领域是非常常见的,建议多练习编写「归并排序」学习递归思想,了解递归的细节,熟悉分治的思想。

    经典问题:

      《剑指 Offer》第 51 题:数组中的逆序对,照着归并排序的思路就能写出来。
      「力扣」第 315 题:计算右侧小于当前元素的个数,它们是一个问题。

    未优化版归并排序:

    public static void main(String[] args) {
            int[] arrays = {9, 2, 5, 1, 3, 2, 9, 5, 2, 1, 8};
            sort(arrays, 0, arrays.length - 1);
    
            for (Integer i : arrays){
                System.out.print(i + "-");
            }
        }
    
        private static void sort(int[] array, int left,int right){
            if(left == right){
                return;
            }
            int mid = (left + right) / 2;
    
            sort(array,left,mid);
            sort(array,mid+1,right);
            merge(array,left,mid+1,right);
        }
    
        private static void merge(int[] arrays,int L,int M,int R){
            //左边的数组的大小
            int[] leftArray = new int[M - L];
    
            //右边的数组大小
            int[] rightArray = new int[R - M + 1];
    
            //往这两个数组填充数据
            for (int i = L; i < M; i++) {
                leftArray[i - L] = arrays[i];
            }
            for (int i = M; i <= R; i++) {
                rightArray[i - M] = arrays[i];
            }
    
            int i = 0, j = 0;
            // arrays数组的第一个元素
            int  k = L;
    
            //比较这两个数组的值,哪个小,就往数组上放
            while (i < leftArray.length && j < rightArray.length) {
    
                //谁比较小,谁将元素放入大数组中,移动指针,继续比较下一个
                if (leftArray[i] < rightArray[j]) {
                    arrays[k] = leftArray[i];
    
                    i++;
                    k++;
                } else {
                    arrays[k] = rightArray[j];
                    j++;
                    k++;
                }
            }
    
            //如果左边的数组还没比较完,右边的数都已经完了,那么将左边的数抄到大数组中(剩下的都是大数字)
            while (i < leftArray.length) {
                arrays[k] = leftArray[i];
    
                i++;
                k++;
            }
            //如果右边的数组还没比较完,左边的数都已经完了,那么将右边的数抄到大数组中(剩下的都是大数字)
            while (j < rightArray.length) {
                arrays[k] = rightArray[j];
    
                k++;
                j++;
            }
        }

    2、快速排序 

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

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

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

      以下是针对特殊测试用例(有很多重复元素的输入数组)有 3 种版本的***:

        版本 1:基本***:把等于切分元素的所有元素分到了数组的同一侧,可能会造成递归树倾斜;
        版本 2:双指针***:把等于切分元素的所有元素等概率地分到了数组的两侧,避免了递归树倾斜,递归树相对平衡;
        版本 3:三指针***:把等于切分元素的所有元素挤到了数组的中间,在有很多元素和切分元素相等的情况下,递归区间大大减少。
      这里有一个经验的总结:之所以***有这些优化,起因都是来自「递归树」的高度。关于「树」的算法的优化,绝大部分都是在和树的「高度」较劲。类似的通过减少树高度、使得树更平衡的数据结构还有「二叉搜索树」优化成「AVL 树」或者「红黑树」、「并查集」的「按秩合并」与「路径压缩」。

      写对「快速排序」的技巧:保持「循环不变量」,即定义的变量在循环开始前、循环过程中、循环结束以后,都保持不变的性质,这个性质是人为根据问题特点定义的。
      「循环不变量」的内容在《算法导论》这本书里有介绍。我个人觉得非常有用。「循环不变量」是证明算法有效性的基础,更是写对代码的保证,遵守循环不变量,是不是该写等于号,先交换还是先 ++ ,就会特别清楚,绝对不会写错,我在编码的时候,会将遵守的「循环不变量」作为注释写在代码中。
      快速排序丢失了稳定性,如果需要稳定的快速排序,需要具体定义比较函数,这个过程叫「稳定化」,在这里就不展开了。

    使用「快速排序」解决的经典问题(非常重要)

      TopK 问题:「力扣」第 215 题:数组中的第 K 个最大元素;
      荷兰国旗问题:「力扣」第 75 题:颜色分类。

    版本1:

    import java.util.Random;
    
    public class Solution {
    
        // 快速排序 1:基本快速排序
    
        /**
         * 列表大小等于或小于该大小,将优先于 quickSort 使用插入排序
         */
        private static final int INSERTION_SORT_THRESHOLD = 7;
    
        private static final Random RANDOM = new Random();
    
    
        public int[] sortArray(int[] nums) {
            int len = nums.length;
            quickSort(nums, 0, len - 1);
            return nums;
        }
    
        private void quickSort(int[] nums, int left, int right) {
            // 小区间使用插入排序
            if (right - left <= INSERTION_SORT_THRESHOLD) {
                insertionSort(nums, left, right);
                return;
            }
    
            int pIndex = partition(nums, left, right);
            quickSort(nums, left, pIndex - 1);
            quickSort(nums, pIndex + 1, right);
        }
    
        /**
         * 对数组 nums 的子区间 [left, right] 使用插入排序
         *
         * @param nums  给定数组
         * @param left  左边界,能取到
         * @param right 右边界,能取到
         */
        private void insertionSort(int[] nums, int left, int right) {
            for (int i = left + 1; i <= right; i++) {
                int temp = nums[i];
                int j = i;
                while (j > left && nums[j - 1] > temp) {
                    nums[j] = nums[j - 1];
                    j--;
                }
                nums[j] = temp;
            }
        }
    
        private int partition(int[] nums, int left, int right) {
            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++) {
                if (nums[i] < pivot) {
                    lt++;
                    swap(nums, i, lt);
                }
            }
            swap(nums, left, lt);
            return lt;
        }
    
        private void swap(int[] nums, int index1, int index2) {
            int temp = nums[index1];
            nums[index1] = nums[index2];
            nums[index2] = temp;
        }
    }

    版本2:

    import java.util.Random;
    
    public class Solution {
    
        // 快速排序 2:双指针(指针对撞)快速排序
    
        /**
         * 列表大小等于或小于该大小,将优先于 quickSort 使用插入排序
         */
        private static final int INSERTION_SORT_THRESHOLD = 7;
    
        private static final Random RANDOM = new Random();
    
        public int[] sortArray(int[] nums) {
            int len = nums.length;
            quickSort(nums, 0, len - 1);
            return nums;
        }
    
        private void quickSort(int[] nums, int left, int right) {
            // 小区间使用插入排序
            if (right - left <= INSERTION_SORT_THRESHOLD) {
                insertionSort(nums, left, right);
                return;
            }
    
            int pIndex = partition(nums, left, right);
            quickSort(nums, left, pIndex - 1);
            quickSort(nums, pIndex + 1, right);
        }
    
        /**
         * 对数组 nums 的子区间 [left, right] 使用插入排序
         *
         * @param nums  给定数组
         * @param left  左边界,能取到
         * @param right 右边界,能取到
         */
        private void insertionSort(int[] nums, int left, int right) {
            for (int i = left + 1; i <= right; i++) {
                int temp = nums[i];
                int j = i;
                while (j > left && nums[j - 1] > temp) {
                    nums[j] = nums[j - 1];
                    j--;
                }
                nums[j] = temp;
            }
        }
    
        private int partition(int[] nums, int left, int right) {
            int randomIndex = left + RANDOM.nextInt(right - left + 1);
            swap(nums, randomIndex, left);
    
            int pivot = nums[left];
            int lt = left + 1;
            int gt = right;
    
            // 循环不变量:
            // all in [left + 1, lt) <= pivot
            // all in (gt, right] >= pivot
            while (true) {
                while (lt <= right && nums[lt] < pivot) {
                    lt++;
                }
    
                while (gt > left && nums[gt] > pivot) {
                    gt--;
                }
    
                if (lt >= gt) {
                    break;
                }
    
                // 细节:相等的元素通过交换,等概率分到数组的两边
                swap(nums, lt, gt);
                lt++;
                gt--;
            }
            swap(nums, left, gt);
            return gt;
        }
    
        private void swap(int[] nums, int index1, int index2) {
            int temp = nums[index1];
            nums[index1] = nums[index2];
            nums[index2] = temp;
        }
    }

    版本3:

    import java.util.Random;
    
    public class Solution {
    
        // 快速排序 3:三指针快速排序
    
        /**
         * 列表大小等于或小于该大小,将优先于 quickSort 使用插入排序
         */
        private static final int INSERTION_SORT_THRESHOLD = 7;
    
        private static final Random RANDOM = new Random();
    
        public int[] sortArray(int[] nums) {
            int len = nums.length;
            quickSort(nums, 0, len - 1);
            return nums;
        }
    
        private void quickSort(int[] nums, int left, int right) {
            // 小区间使用插入排序
            if (right - left <= INSERTION_SORT_THRESHOLD) {
                insertionSort(nums, left, right);
                return;
            }
    
            int randomIndex = left + RANDOM.nextInt(right - left + 1);
            swap(nums, randomIndex, left);
    
            // 循环不变量:
            // all in [left + 1, lt] < pivot
            // all in [lt + 1, i) = pivot
            // all in [gt, right] > pivot
            int pivot = nums[left];
            int lt = left;
            int gt = right + 1;
    
            int i = left + 1;
            while (i < gt) {
                if (nums[i] < pivot) {
                    lt++;
                    swap(nums, i, lt);  //这一步交换是因为相等的情况下会出现lt++后不等于i
                    i++;
                } else if (nums[i] == pivot) {
                    i++;
                } else {
                    gt--;
                    swap(nums, i, gt);
                }
            }
            swap(nums, left, lt);
            // 注意这里,大大减少了两侧分治的区间
            quickSort(nums, left, lt - 1);
            quickSort(nums, gt, right);
        }
    
        /**
         * 对数组 nums 的子区间 [left, right] 使用插入排序
         *
         * @param nums  给定数组
         * @param left  左边界,能取到
         * @param right 右边界,能取到
         */
        private void insertionSort(int[] nums, int left, int right) {
            for (int i = left + 1; i <= right; i++) {
                int temp = nums[i];
                int j = i;
                while (j > left && nums[j - 1] > temp) {
                    nums[j] = nums[j - 1];
                    j--;
                }
                nums[j] = temp;
            }
        }
    
        private void swap(int[] nums, int index1, int index2) {
            int temp = nums[index1];
            nums[index1] = nums[index2];
            nums[index2] = temp;
        }
    }

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

  • 相关阅读:
    git知识点总结
    自动化进阶
    unittest单元测试框架
    自动化测试模型
    webdriver
    python文件处理
    uva 11077 置换
    poj 1066 Treasure Hunt
    poj 2661 Factstone Benchmark
    hdu 4180
  • 原文地址:https://www.cnblogs.com/jing-yi/p/13204160.html
Copyright © 2020-2023  润新知