美文网首页计算机知识大讲堂
09.排序:快排和归并排序

09.排序:快排和归并排序

作者: 学海一乌鸦 | 来源:发表于2020-05-24 20:09 被阅读0次

    1.归并排序

    image.png

    大体思路 归并排序核心思想:分治思想(大问题化分为小问题去解决,小的问题解决了,大问题自然也能解决掉)
    其中分治是一种解决问题的思想,而递归是一种编程技巧;
    递归二要素:
    递推公式:merge_sort(p,r)=merge(merge_sort(p,q),merge_sort(q+1,r));其中q=(p+q)/2
    截止条件:p>=r,结束递归
    中间的merge函数:申请一个新的数组,对两个子数组分别进行比较,按照从小到大的顺序排列好
    临界条件:
    写代码时一定要小心,费了半天劲终于分析清楚了,不要在细节上被绊倒

    
    public class MergeSort {
        public static void  mergeSort(int[] arr) {
            // 非空校验
            if (arr == null || arr.length <= 0) {
                return ;
            }
             merge_sort(0,arr.length-1,arr);
        }
        /**
         * 
         * @param p 元素开始的位置
         * @param r 元素结束的位置
         * @param arr 待合并的数组
         */
        private static void merge_sort(int p,int r,int []arr){
            if(p>=r){
                return ;
            }
            //防止越界
            int q=p+(r-p)/2;
           merge_sort(p,q,arr);
           merge_sort(q+1, r, arr);
           mergeArrBySentry(p,q,r,arr);
        }
        
        /**
         * 
         * @param arr1
         * @param arr2
         * @return
         */
        private static void mergeArr(int p, int q, int r, int[] arr) {
            // 申请一个新数组
            int[] merge = new int[r - p + 1];
            // 初始化 ijk
            int i = p;
            int j = q + 1;
            int k = 0;
            while (i <= q && j <= r) {
                // 这里的等号很重要,保证元素排序的稳定性
                if (arr[i] <= arr[j]) {
                    merge[k++] = arr[i++];
                } else {
                    merge[k++] = arr[j++];
                }
            }
            // 判断哪个子数组子数组中有剩余元素
            if (i > q) {
                while (j <= r) {
                    merge[k++] = arr[j++];
                }
    
            }
            if (j > r) {
                while (i <= q) {
                    merge[k++] = arr[i++];
                }
            }
            // 将tmp中的数组拷贝回a[p...r]
            for (i = 0; i <= r - p; ++i) {
                arr[p + i] = merge[i];
            }
        }
    
        /**
         * 
         * @param p
         * @param q
         * @param r
         * @param arr
         */
        private static void mergeArrBySentry(int p, int q, int r, int[] arr) {
           // 比原定的数组大一个,用来储存哨兵
           int[] leftArr = new int[q - p + 2];
           int[] rightArr = new int[r - q + 1];
           // 给新数组赋值
           for (int i = 0; i < q - p + 1; i++) {
               leftArr[i] = arr[i + p];
           }
           leftArr[q - p + 1] = Integer.MAX_VALUE;
           // 给新数组赋值
           for (int i = 0; i < r - q; i++) {
               rightArr[i] = arr[i + q + 1];
           }
           rightArr[r - q] = Integer.MAX_VALUE;
           int k = p;
           int i = 0;
           int j = 0;
           while (k <= r) {
               // 当左边数组到达哨兵值时,i不再增加,直到右边数组读取完剩余值,同理右边数组也一样
               if (leftArr[i] <= rightArr[j]) {
                   arr[k++] = leftArr[i++];
               } else {
                   arr[k++] = rightArr[j++];
               }
           }
       }
    
        public static void main(String[] args) {
            MergeSort mergeSort=new MergeSort();
            int []arr={8,7,6,5,4,3,3,2};
            mergeSort.mergeSort(arr);
            ArrUtils.printAll(arr);
        }
    }
    

    归并排序的复杂度分析

    1.是否是稳定的排序算法?
    这个主要取决于mergeArr的实现,对于两个待合并的数组,如果相同,先选择左边的数组,那么就是稳定的排序算法。
    2.时间复杂度?
    归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。
    3.空间复杂度?
    空间复杂度是 O(n)。
    递归代码的空间复杂度并不能像时间复杂度那样累加。尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小。

    2.快排

    大体思路:采用自上而下的分治思想 ,如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。然后将数据分为小于分区点的数据和大于分区点的数据。

    1. 递归二要素
      quick_sort(arr,s,e)=quick_sort(arr,s,q)+quick_sort(arr,q+1,e);
      if(s>=e) return;
    2. 分区方法
      选择最后一个元素作为target
    3. 分区过程


      image.png
    public class QuickSort {
        public static void quickSort(int[] arr) {
            if (arr == null || arr.length <= 0) {
                return;
            }
            quick_sort(arr, 0, arr.length - 1);
        }
    
        /**
         * 进行快排
         * 
         * @param arr
         * @param start
         * @param end
         */
        private static void quick_sort(int[] arr, int start, int end) {
            // 递归终止条件
            if (start >= end) {
                return;
            }
            int q = partition(arr, start, end);
            quick_sort(arr, start, q-1);
            quick_sort(arr, q + 1, end);
        }
    
        /**
         * 对数组arr从[start,end]进行排序,并返回临界点
         * 
         * @param arr
         * @param start
         * @param end
         * @return
         */
        private static int partition(int[] arr, int start, int end) {
            // 默认选择最后一个元素
            int target = arr[end];
            // 变量i用来遍历每一个元素;变量j是大于target和小于target的临界点,在此之前的值都是小于target
            int j = start;
            int i = start;
            for (; i < end; i++) {
                //如果小于target,需要移动到j前面
                if (arr[i] < target) {
                    if(i!=j){
                        swap(arr, i, j);
                    }
                    j++;
                }
            }
            // 最后把target元素放在中间
            swap(arr, end, j);
            return j;
        }
    
        private static void swap(int[] arr, int m, int n) {
            int tmp = arr[m];
            arr[m] = arr[n];
            arr[n] = tmp;
        }
    
        public static void main(String[] args) {
            int []arr={1,2,4,8,6,5,7};
            quickSort(arr);
            ArrUtils.printAll(arr);
        }
    }
    

    2.2与归并排序的区别与联系

    image.png

    归并排序:自下而上。先递归在比较;
    快速排序:自上而下。先 比较再递归;
    归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法

    2.3 性能分析

    1. 原地排序算法?
      是的,不需要额外的空间
    2. 稳定排序?
      不是
    3. 时间复杂度
      T(n) 在大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n2)。
      一个比较极端的例子。如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n/2 个元素,这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n2)。

    3.排序优化

    插入排序:

    • 原地排序
    • 稳定排序
    • 时间复杂度:对于已经排好序的序列,O(n),常规情况O(n2)
      快速排序
    • 原地排序
    • 不稳定的排序
    • 时间复杂度:平均O(logn),极端情况O(n2)

    优化:
    小于一定规模的数,采用插入排序;
    对于快速排序,每次随机选取一个数字作为分区点

    public class TheBestSort {
        private static int DEFAULT_LENGTH = 10;
    
        public static void theBestSort(int[] arr) {
            if (arr == null || arr.length <= 0) {
                return;
            }
            // 插入排序
            if (arr.length <= DEFAULT_LENGTH) {
                InsertSort(arr);
                return;
            }
            // 否则使用快排
            quickSort(arr, 0, arr.length - 1);
        }
    
        // 快速排序
        private static void quickSort(int[] arr, int l, int r) {
            // 递归的终止条件
            if (r - l <= DEFAULT_LENGTH) {
                InsertSort(arr);
                return;
            }
            int p = partition(arr, l, r);
            quickSort(arr, l, p - 1);
            quickSort(arr, p + 1, r);
        }
    
        // 分区方法
        private static int partition(int[] arr, int l, int r) {
            // 随机取数法
            int m = (int) ((Math.random() * (r - l + 1)) + l);
            swap(arr, m, r);
            int target = arr[r];
            // 计数器
            int i = l;
            // 分界区[0,j-1] <target [j,r] >=target
            int j = l;
            // 最后的r不用比较
            for (; i < r; i++) {
                if (arr[i] < target) {
                    // 简单优化一,当i=j的时候,不用交换了
                    if (i != j) {
                        swap(arr, i, j);
                    }
                    j++;
                }
            }
            swap(arr, r, j);
            return j;
        }
    
        // 插入排序
        private static void InsertSort(int[] arr) {
            int n = arr.length;
            for (int i = 1; i < n; i++) {
                int val = arr[i];
                int j = i - 1;
                for (; j >= 0; j--) {
                    // 不加==。保证稳定排序
                    if (arr[j] > val) {
                        swap(arr, j, j + 1);
                    } else {
                        // 一个不满足,结束循环
                        break;
                    }
                }
                arr[j + 1] = val;
            }
        }
    
        // 交换
        private static void swap(int[] arr, int n, int m) {
            int tmp = arr[m];
            arr[m] = arr[n];
            arr[n] = tmp;
        }
    
        public static void main(String[] args) {
            int arr[] = { 5, 6, 4, 3, 6, 2, 6, 2, 6, 7, 5, 8, 4, 33, 22, 66, 788, 33, 22 };
            theBestSort(arr);
            ArrUtils.printAll(arr);
        }
    }
    

    相关文章

      网友评论

        本文标题:09.排序:快排和归并排序

        本文链接:https://www.haomeiwen.com/subject/qiljahtx.html