Java笔记18——七大排序

目录

1. 概念

1.1 排序

1.2 稳定性(重要)

1.3关于内部排序和外部排序

基础排序算法O(n^2)

1. 选择排序

⒉双向选择排序

​编辑

3. 冒泡排序

4. 插入排序

5.折半插入排序

6.希尔排序

7.归并排序(重要)

归并排序的衍生问题

8. 归并排序(非递归写法)(了解即可)

9. 快速排序

10:非递归实现快速排序(了解)

11.堆排序

面试题:海量数据处理——用到外部存储器

测试程序实现


知识回故:

数据结构:关心的是如何高效的管理数据 ——》 CURD 增删改查
 

我们之前学的:链表,数组,二叉树,哈希表 都属于数据结构内容。

算法:如何利用好数据
比如:

排序算法: 如何按照的一定的规则、方法将给定的一组数据进行由大到小的排序。
 

注释:排序属于算法的概念

1. 概念

1.1 排序

1. 排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

2. 平时的上下文中,如果提到排序,通常指的是排升序(非降序)。

 升序和降序:

严格升序(不包含重复元素)︰数字有小到大依次排列i1<i2

非降序:前一个元素 <= 后一个元素 依次类推
在指定的集合中若发现  i1 > i2,则这个集合—定不是非降序集合

严格降序(不包含重复元素):数字由大到小依次排列  i1 > i2

非升序: 前一个元素  >=  后一个元素

3. 通常意义上的排序,都是指的原地排序(in place sort)。

原地排序:不开辟额外的数组空间进行的排序
注释:(排序问题基本必问)

1.2 稳定性(重要)

两个相等的数据,如果经过排序后,排序算法能保证其相对位置(先后顺序)不发生变化,则我们称该算法是具备稳定性的排序算法。

注释:a,b只是为了区分相同的数据之间的不同。 

在选择排序算法时,除了时间复杂度和空间复杂度之外,某些场景下,排序的稳定性也是我们考量的因素之一。
 

应用场景:

京东商城的订单系统每个订单的属性
单号,金额,下单时间默认按照单号在排序。
选择稳定性的排序算法,按照金额排序之后,彼此的下单顺序没有改变。
假设我现在要按照订单金额由小到大进行订单的排序,最希望的是按照订单金额有小到大排序之后,时间顺序不要发生改变。

 

1.3关于内部排序和外部排序

本章节讲的七大排序都是内部排序(待排序的数据都存放在内存中
 

都是基于元素的比较来进行的存储的元素有明确的大小关系
比如:排序i1和i2时就是比较i1和i2的大小关系

i1 = 10;  i2 = 20;

七大排序(内部排序)

 最常用的就两个:快速排序 和 归并排序

外部排序:

外部排序(数据存储在硬盘中,每次排序都需要从硬盘读取一部分内容到内存中,把这部分数据排序之后写回硬盘)

外部排序比较常见了三大:桶排序,基数排序,计数排序 时间复杂度都是:O(n)

虽然看起来很快,但并不见得就比上面的七大排序快!

这三个算法不具备普遍性,每个算法都需要特殊的数据场景下才能使用。

---------------------------------------------------------------------------------------------------------------------------------
 

基础排序算法O(n^2)
 

不要看不起这些基础的排序算法

基础的排序算法往往作为高阶排序算法的优化手段,比如:插入排序经常用在高阶排序的优化中。

写排序时的核心思想:—定要定义好待排序数组区间已经排序好的数组区间

1. 选择排序

规则:每次从无序区间选择一个最大或最小值的一个元素,放在无序区间的最后或者最前,直到待排序的所有元素排序完毕。
 

比如:我每次遍历循环找到该数组中最大值的索引,然后把它和最后一位交换。然后继续循环遍历找最大值索引进行交换。

注释:每安排好一个最大值的位置,寻找的数组内容就可以少一位了。

注释:堆排序就是把最大值放在最后。

有序区间和无序区间:

代码实现:

    /**
     * 选择排序——直接选择排序
     */
    public static void selectionSort(int[] arr){
        //最开始的无序区间 [i....n)
        //有效区间[]为空
        //最外层的for循环指的循环走的趟数,每走一趟外层循环,就有一个最小值放在了正确的位置
        for (int i = 0; i < arr.length - 1; i++) {
            //min指的是最小元素的下标
            int min = i;
            //内存循环在查找当前无序区间的最小值索引
            for (int j = i + 1 ; j < arr.length; j++) {
                if(arr[min] > arr[j]){ //找到了 j对应的元素比当前最小值还小
                    min = j;
                }
            }
            //min变量此刻一定保存了当前无序区间的最小值索引
            // 有序区间[0..i) + 1
            // 无序区间[i..n) - 1
            swap(arr,i,min);
        }

    }

注释:选择排序在排序过程中无法保证相同元素的先后顺序的

          选择排序不是一个稳定性的算法

⒉双向选择排序


任何算法都有优化的空间~~,学无止境,探索不能停止

之前选择排序—次只能选择一个最小值或者最大值,一次只选一个元素放在正确位置。


我现在—次选俩,每次从无序区间中选出最小值和最大值,存放在无序区间的最开始和最后面位置,重复上述过程~~

   /**
     * 双向选择排序
     * @param arr
     */
    public static void selectionSortOP(int[] arr) {
        int low = 0;
        int high = arr.length - 1;
        // low == high => 无序区间只剩下最后一个元素,其实整个数组已经有序了。
        while (low < high) {
            int min = low;
            int max = low;
            for (int i = low + 1; i <= high; i++) {
                if (arr[i] < arr[min]) {
                    min = i;
                }
                if (arr[i] > arr[max]) {
                    max = i;
                }
            }
            // 此时min对应了最小值索引,交换到无序区间的最前面
            swap(arr,min,low);
            // 边界条件 low == max
            if (max == low) {
                max = min;
            }
            swap(arr,max,high);
            low ++;
            high --;
        }
    }

3. 冒泡排序

 两两比较,大的交换到后面,然后再依次比较,大的交换到后面,遍历下来后,最大值就在最后一位上面了。

 /**
     * 冒泡排序
     * @param arr
     */
    public static void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            boolean isSwaped = false;
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr,j,j + 1);
                    isSwaped = true;
                }
            }
            if (!isSwaped) {
                // 内层没有元素交换,此时整个数组已经有序
                break;
            }
        }
    }

4. 插入排序

打牌码牌的过程

直接插入排序: 将待排序的集合看做两部分,已排序的区间[0..i);    待排序的区间[i...n);
 

每次选择无序区间的第一个元素插入到有序区间的合适位置,直到整个数组有序。

 代码实现:

    /**
     * 直接插入排序
     * 已排序区间[0..i)  => 默认第一个元素就是已经排好序的区间
     * 待排序区间[i...n)
     * @param arr
     */
    public static void insertionSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            // 已排序区间[0...1)
            // 待排序区间[i ..n)
            // 选择无序区间的第一个元素,不断向前看
            // 注意看内层循环的终止条件 j >= 1而不是 j >= 0 ?
            // 因为此时arr[j] 不断向前看一个元素  j - 1 要合法 j - 1 >= 0
            for (int j = i; j >= 1 && arr[j] < arr[j - 1]; j--) {
                swap(arr,j,j - 1);
                //把判断条件加到循环条件里面
//                // 边界
//                if (arr[j] > arr[j - 1]) {
//                    // arr[i] 恰好是有序区间的后一个元素,无序区间的第一个元素
//                    // 当前无序区间的第一个元素 > 有序区间最后一个元素
//                    break;
//                }else {
//                    swap(arr,j,j - 1);
//                }
            }
        }
    }

 注释:如果给的数据近乎有序,那么插入排序贼快。比堆排序还快。

插入排序和选择排序相比到底优在哪?
 

 和选择排序最大的区别,当已经排序的集合的最后元素 < 当前无序区间的第一个元素,内层循环可以直接退出。大大降低了时间

极端情况∶若待排序的数组就是一个完全升序数组,插入排序就会进化为O(n)=>内层循环一次也不走最好情况时间复杂度
 

 

5.折半插入排序

    /**
     * 二分插入排序
     * @param arr
     */
    public static void insertionSortBS(int[] arr) {
        // 有序区间[0..i)
        // 无序区间[i..n)
        for (int i = 0; i < arr.length; i++) {
            int val = arr[i];
            // 有序区间[left...right)
            int left = 0;
            int right = i;
            while (left < right) {
                int mid = (left + right) / 2;
                if (val < arr[mid]) {
                    right = mid;
                }else {
                    // val >= arr[mid]
                    left = mid + 1;
                }
            }
            // 搬移[left..i)的元素
            for (int j = i; j > left ; j--) {
                arr[j] = arr[j - 1];
            }
            // left就是待插入的位置
            arr[left] = val;
        }
    }

6.希尔排序

希尔排序又叫:缩小增量排序  时间复杂度:O(n^ 1.2 - n ^ 1.3)
 

希尔排序就是插入排序的优化

方法:不断将小数组调整的近乎有序,整个大数组就接近有序状态,这个时候使用插入排序效率很高的
 

我们发现当数组近乎有序时,插入排序的效率非常高

核心思路:

step1:先选定一个整数(gap),将待排序的数据分成gap组,所有距离为gap的为同一组。对每一个小数组先进行插入排序。
 

step2:然后对gap进行:gap = gap / 2(3),重复step1

注释:为什么除以 2 或 3 是大量实验数据得来,这样效率会比较高
 

当 gap == 1:说明整个数组已经被调整的近乎有序,此时针对整个数组进行一次插入排序即可,效率一定是比较高的。
 

注释:分而治之的思想,元素个数越小就越有序,插入就越快。

 

    /**
     * 希尔排序 - 缩小增量排序,按照 gap 将原数组分为gap子数组,子数组内容先排序,不断缩小gap值,直到 gap = 1
     * 当 gap = 1时,整个数组已经近乎有序,只需要最后再来一次插入排序即可
     * @param arr
     */
    public static void shellSort(int[] arr){
        int gap = arr.length >> 1;//因为位运算就最快的效率

        while(gap >= 1){
            //按照gap分组后,组内进行插入排序。
            insertionSortByGap(arr, gap);
            gap = gap >> 1;
        }

    }

    /**  希尔排序的辅助方法
     * 按照 gap进行分组,然后插入排序
     * 可以想象挤兑情况,假设此时 gap = 1 ,那就跟插入排序一模一样
     * @param arr
     * @param gap
     */
    private static void insertionSortByGap(int[] arr, int gap) {

        //i++ : 新元素比有序数组最大值要小时,才需要进行插入排序操作~
        for (int i = gap; i < arr.length; i++) {                 //距离为gap的元素才算是一组内的
            for (int j = i; j - gap >= 0 && arr[j] < arr[j - gap] ; j -= gap) {
                    swap(arr, j, j - gap);
            }
        }
    }

7.归并排序(重要)

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子 序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归:不断将原数组拆分为子数组(一分为二),直到每个子数组只剩下一个元素  =》  归过程结束
 

并:不断合并相邻的两个子数组为一个大的子数组,合并的过程就是将两个已经有序的子数组合并为一个大的有序子数组,直到合并到整个数组。
 

并过程采用的方法类似于之前写的 合并两个有序链表,两个指针~

 代码实现:这是没有优化的状态

注释:这里arr数组 和 aux数组存在 l 个单位的偏移量

    /**
     * 归并排序
     */
    public static void mergeSort(int[] arr){
        mergeSortInternal(arr, 0, arr.length - 1);
    }

    /**
     * 在arr[l....r] 上进行归并排序
     */
    private static void mergeSortInternal(int[] arr, int l, int r) {
        if(l >= r){
            return;
        }

        int min = l + ((r - l) >> 1);//防止数据过大溢出,把多余部分除以 2就是一半的长度
        mergeSortInternal(arr,l, min);
        mergeSortInternal(arr,min + 1, r);
        //此时数组arr【l..min) 和 arr【min + 1 .... r)已经分别有序 只剩下合并两个数组
        // 就用合并链表的操作
        merge(arr, l, min, r);
    }

    /**
     * 将有序子数组arr[l..mid] 和 有序arr[mid + 1...r] 合并为一个大的有序数组arr[l..r]
     * 将两个有序的子数组合并成一个大的有效数组
     */
    private static void merge(int[] arr, int l, int min, int r) {
        int[] aux = new int[r - l + 1];//创建一个l。。r范围等长的数组
        // l = 2; r = 4
        //arr[2, 3, 4]
        //aux[0, 1, 2] 在aux中下标差了 l 的偏移量
        for (int i = 0; i < aux.length; i++) {
            aux[i] = arr[i + l];
            // aux的索引下标0...arr.length - 1
            // arr的下标l...r
        }//循环完后把arr中 l到r范围的元素复制完毕   //注释:之所以要复制,是因为不复制而进行合并的话,
                                            //可能在过程中会覆盖掉还没进行比较的元素
        //第一个数组的下标,左数组
        int i = l;                 //这两个变量是记录左右数组走到哪了
        //第二个数组的下标,右数组
        int j = min + 1;
        for (int k = l; k <= r; k++) {
            if(i > min){
                //第一个数组已经遍历完毕
                arr[k] = aux[j - l];
                j ++;
            }else if(j > r){
                //第二个数组已经遍历完毕
                arr[k] = aux[i - l];
                i ++;
            }else if(aux[i - l] < aux[j - l]){ //这里要 减去 偏移量 l才是aux数组的索引
                //左数组此刻元素 小于 右数组此刻元素
                arr[k] = aux[i - l];
                i ++;
            }else{
                //左数组此刻元素 大于 右数组此刻元素
                arr[k] = aux[j - l];
                j ++;
            }

        }

    }

优化一:

 优化二:

注意偏移量的细节 

 需要优化的代码:

    /**
     * 在arr[l....r] 上进行归并排序
     */
    private static void mergeSortInternal(int[] arr, int l, int r) {

//        if(l >= r){
//            return;
//        }
        if(r - l <= 15){
            insertionSort(arr,l,r);//15是实验得来数据,小于15的数组插入排序更快
            return;
        }


        int min = l + ((r - l) >> 1);//防止数据过大溢出,把多余部分除以 2就是一半的长度
        mergeSortInternal(arr,l, min);
        mergeSortInternal(arr,min + 1, r);
        //此时数组arr【l..min) 和 arr【min + 1 .... r)已经分别有序 只剩下合并两个数组

       // 就用合并链表的操作
      //  merge(arr, l, min, r);
      // 1.到底什么时候才需要合并 arr[mid] < arr[mid + 1] 说明?arr[mid] 数组1的最大值 arr[mid + 1]数组2的最小值
      // 整个数组已经有序了,那你还合并个der
        if(arr[min] > arr[min + 1]){
            merge(arr, l, min, r);
        }

    }

    /**
     * 在arr[l..r]上进行插入排序
     */
    private static void insertionSort(int[] arr, int l, int r) {
        for (int i = l + 1; i <= r; i++) {
            for (int j = i ; j >= l + 1 && arr[j] < arr[j - 1] ; j--) {
                    swap(arr,j, j - 1);
            }
        }
    }

时间复杂度:那么那块部分是 logn 哪块部分是 n 呢? 

 注释:归并排序是一个非常稳定的 nlogn 级别

空间复杂度:因为遍历合并的数组是新开辟里面找的,所以是 O(n)

归并排序的衍生问题

剑指 Offer 51. 数组中的逆序对 - 力扣(LeetCode)

148. 排序链表 - 力扣(LeetCode)

注释:归并排序非常适合对 链式结构 进行排序,链表,二叉树等

8. 归并排序(非递归写法)(了解即可)

    /**
     * 迭代实现归并排序(了解)
     * @param arr
     */
    public static void mergeSortNonRecursion(int[] arr) {
        // 最外层循环表示每次合并的子数组的元素个数
        // 子数组为1个元素,第二次循环2个元素,第三次循环合并4个元素,第四次8个元素 ..
        // 直到整个数组合并完成
        for (int sz = 1;sz <= arr.length ;sz += sz) {
            // 内存循环变量i就是每次合并操作的开始索引l
            // [8,6,2,3,1,5,7,4]  => [2,3,6,8]  [1,4,5,7]
            //                                             i
            for (int i = 0; i + sz < arr.length; i += sz + sz) {
                // 边界i + sz + sz - 1 > arr.length
                merge(arr,i,i + sz - 1,Math.min(i + sz + sz - 1,arr.length - 1));
            }
        }
    }

大体思路就是从小到大,从内到外。之前递归的归并排序,整个数组切一半,一半,再一半直到元素个数为 一。 现在迭代写法就是先让 size 从 一开始,到二,到 整个数组的一半即完成。

9. 快速排序

快速排序:20世纪最伟大的算法之一
 

核心思路:分区
 

分区值∶默认选择最左侧元素pivot

从无序区间选择一个值作为分界点,pivot开始扫描原集合,将数组中所有小于该pivot的元素放在分界点左则,>=该元素的值放在分区点的右队。经过本轮交换,,pivt放在了最终位置,pivot的左侧都是小于该值的元素,pivot的右侧都是大于该值的元素,在这两个子区间继续重复上述过程,直到整个集合有序。

分区方法1:<<算法导论>>中的分区思想
 

步骤:第一次遍历

 红色的V就是我们选择的最左侧比较元素。

arr[l + 1] .....arr[ j ] 橙色部分全都是小于 v 的元素。

arr[ j + 1 ] ...... arr[ i  -  1 ] 紫色部分 是大于等于 v 的元素 

注释: j 是小于v 的最后一个元素索引

arr[ i ] 是当前正在扫描的元素 e

1. 若此时 arr[ i ] >= v

此时只需要 i ++ 继续处理下一个元素  arr [ j+ 1..i )  >=  v

2. 若此时 arr[ i ] < v

 

与元素 j+1 交换 然后 j ++ 即可。

 因为 j 是小于 v 的最后一个元素索引

 注释:上面右区间子类部分里面应该是 小于等于 v,写错了 

 

 

 

然后在橙色部分和紫色部分继续重复上述过程即可
 

当元素为空,或者只剩下一个元素的时候就停止循环 

代码实现:这是未优化的快排

    /**
     *  快速排序
     */
    public static void quickSort(int[] arr){
        quickSortInternal(arr, 0, arr.length - 1);
    }

    /**
     *  递归实现快速排序
     */
    private static void quickSortInternal(int[] arr, int l, int r) {
        //终止条件:当元素为空,或者只剩下一个元素的时候就停止循环
        if(l >= r){
            return;
        }
        int p = partition(arr, l, r);

        quickSortInternal(arr, l, p - 1);
        quickSortInternal(arr, p + 1, r);
    }

    /**
     * 快速排序的辅助工具: 对整个数组进行分区,小于 v在左,大在右,并返回中间索引
     */
    private static int partition(int[] arr, int l, int r) {
        //选择默认值
        int v = arr[l];
        //因为小于v的索引是 l + 1到 j
        //最开始小于v数组应该为空 [ ]
        int j = l;
        for (int i = l + 1; i <= r ; i++) {
            if(arr[i] < v){
                swap(arr, j + 1, i);
                j ++;//扩大小于v的范围
            }
        }
        //最后再交换 l 和 j元素的位置就完成分区了
        swap(arr, l, j);
        return j;
    }

时间复杂度:

 这递归部分,每次都是拆一半进行递归,折半递归。

递归调用次数平均情况下就是一个二叉树的高度 logn

所以很明显是个 logn 级别

 这一步是遍历整个数组进行分区过程,要遍历整个数组才能完成分区,O(n) 级别

 所以这两步加起来就是 O(nlogn)

空间复杂度:递归调用次数 O(logn)

归并排序无论数据长啥样子,都是无脑的一分为二,保证递归次数一定logn级别,非常稳定的nlogn的算法


快速排序的性能含严格受制于初始数据的情况而定
 

比如:我们上面写的快排代码,当我们生产一个近乎有序数组时,快排的性能会严重下降。

甚至比 O(n^2) 的插入排序还慢 

如果有序数组数据太大甚至还会 栈溢出

 原因:单分支,深度接近于 n

关于分区点选择问题:极端情况下,数组就是一个完全有序的数组

 

此时当原数组近乎有序时,按照最左侧元素进行分区的时候,造成左右两颗递归树严格不平衡甚至极端情况下退化为链表  O(logn) - > O(n)  ,整体从 nlogn  =>  n ^ 2
这些方法快就快在折半完成数据的快排。失去就优势就不行了

接近办法:分区值选择不能武断的就选择最左侧或者最右侧
 

常见的方法:

1 .三数取中=》最左侧,最右侧,中间值=》每次循环都随机选择其中之一

2 .每次递归时选择数组中任意一个元素作为分区点。(推荐)
 

第二种方法:

步骤:1

 步骤:2

就这两步,就能解决快排之前留下的隐患,重回巅峰!

关于分区点的选择。使用随机数随机取一个数组索引的元素作为分区点,基本上不可能出现单支树的情况,避免近乎有序数组上快排退化问题
 

优化2:

 

第二种分区方法:市面上教科书包括校招的笔试题选择题常用分区方法  Hoare挖坑法

核心步骤:

第一步:我们先选择第一个元素为 v 值, i 和 j 是区间范围

 第二步:j一直向前走,找到第一个小于 v 值的,把arr【i】 = arr【j】

再让 i 向前走,找第一个大于 v 值的然后进行赋值。

 第三步: 

 左边都是小于 25的元素, 右边都是大于 25 的元素

 为什么会普遍使用这样的方法?

没有元素交换都是直接赋值,理论上会减少因为换带来的时间损耗
 

    /**
     *  快速排序,使用挖坑分区法
     * @param arr
     */
    public static void quickSortHoare(int[] arr){
        //其他步骤都跟正常的快排没区别,就是在分区有差别
        quickSortInternalHoare(arr, 0, arr.length - 1);
    }

    /**
     * 挖坑法--快排递归
     */
    private static void quickSortInternalHoare(int[] arr, int l, int r) {
        if(r - l <= 15){
            insertionSort(arr, l, r);
            return;
        }
        int p = partitionHoare(arr, l, r);
        quickSortInternalHoare(arr, l, p - 1);
        quickSortInternalHoare(arr, p + 1, r);

    }

    /**
     * 挖坑法实现快排分区
     */
    private static int partitionHoare(int[] arr, int l, int r) {
        int randomIndex = random.nextInt(l, r);//随机数
        swap(arr, l, randomIndex);

        int v = arr[l];
        int i = l;
        int j = r;

        while(i < j){
            // 先让j从后向前扫描到第一个 < v的元素停止
            while(i < j && arr[j] >= v){
                j --;
            }
            arr[i] = arr[j];

            // 再让i从前向后扫描到第一个 > v的元素停止
            while(i < j && arr[i] <= v){
                i ++;
            }
            arr[j] =  arr[i];

        }//分区完后 i == j
        arr[i] = v;
        return i;
    }

 

10:非递归实现快速排序(了解)

非递归快排:借助 栈 这个结构

    /**
     * 快速排序的迭代写法
     * @param arr
     */
    public static void quickSortNonRecursion(int[] arr) {
        Deque<Integer> stack = new ArrayDeque<>();
        // r
        stack.push(arr.length - 1);
        // l
        stack.push(0);
        // 每次从栈中取出两个元素,这辆个元素就是待排序区间的l..r
        while (!stack.isEmpty()) {
            int l = stack.pop();
            int r = stack.pop();
            if (l >= r) {
                // 当前子数组已经处理完毕
                continue;
            }
            int p = partition(arr,l,r);
            // 继续入栈两个子区间
            stack.push(p - 1);
            stack.push(l);

            stack.push(r);
            stack.push(p + 1);
        }
    }

迭代写法和递归写法的时间空间复杂度都一样的。

递归方法就是递归的调用次数
迭代写法就是栈的深度–和递归的调用次数相同O(logn)

迭代写法:就是每次给栈中传入你当前要排序数组的范围,因为是先入后出的关系,所以先传入右范围再左范围。

你可以发现上面就是一直在往栈里面传入 l 和 r,至于排序过程还是交给自己写的partition方法

 

11.堆排序

    //原地排序
    public static void heapSort(int[] arr){
        //1.先将任意数组进行heapify调整为最大堆
        for(int i = (arr.length - 1 - 1) / 2 ; i >= 0; i--){
            //不断进行着 siftDown 操作
            siftDown(arr, i, arr.length);
        }
        //2 不断交换堆顶元素到数组末尾,每交换一个元素,就有一个元落在了最终位置
        for (int i = arr.length - 1; i > 0 ; i--) {
            // arr[i] 就是未排序数组的最大值,交换末尾
            swap(arr,0, i);
            siftDown(arr,0, i);//这里数组长度必须传 i 因为每成功一次相当于
            //末尾位置确定了,就不需要被下层操作的影响。逻辑上相当于数组没有了这个元素位置。
        }

    } //注释:arr. length - 1是最后一个元素的索引
     //而 (arr.length - 1 - 1) / 2 是找最后一个元素的父类索引

    /**
     * 交换两个元素的位置
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    /**
     *
     * @param arr
     * @param i  //当前哪个位置的元素需要跟树顶换位置,进行下层操作
     * @param length //左子树大于总长度就不用下层了
     */
    private static void siftDown(int[] arr, int i, int length) {
        //终止条件,不存在左子树索引就结束
        while(2 * i + 1 < length){
            int j = 2 * i + 1;
            if(j + 1 < length && arr[j + 1] > arr[j] ){
                j = j + 1;
            }
            //此时 j索引就是左右子树中最大值的索引
            if(arr[i] > arr[j]){
                break;
            }else{
                swap(arr, i,j);
                i = j;
            }

        }
    }
}

面试题:海量数据处理——用到外部存储器

这就用到了归并排序的并操作。 

把大数据拆分成若干个小数据进行排序,然后200份数据进行合并链表即可。

测试程序实现

注意点:如果我想一个数组大小为100w的近乎升序的数组,在100w各元素中,大部分元素都已经有序只有极个别的部分元素乱序 如何实现?

测试程序代码实现:

package sort;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 测试七大排序的性能以及产生辅助的测试数组
 */
public class SortHelper {
    // 生成随机数的一个类
    private static final ThreadLocalRandom random = ThreadLocalRandom.current();

    /**
     * 1.产生一个大小为n的随机整数数组,数组的取值范围为[l..r)
     * @param n 数组的元素个数
     * @param l 数组的取值最小值
     * @param r 数组元素最大值
     */
    public static int[] genraRandomArray(int n, int l, int r){
        int[] data = new int[n];
        for (int i = 0; i < n; i++) {
            // 生成一个[l..r)的随机数
            data[i] = random.nextInt(l,r);
        }
        return data;
    }

    /**
     * 2.生成一个大小为n的近乎有序的数组
     * @param n 元素个数
     * @param swapTimes 交换的次数,这个参数越大,数组越无序
     */
    public static int[] generateSortedArray(int n,int swapTimes) {
        int[] data = new int[n];
        for (int i = 0; i < n; i++) {
            data[i] = i;
        }
        for (int i = 0; i < swapTimes; i++) {
            //生成一个【 0....n) 随机数
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            int temp = data[a];
            data[a] = data[b];
            data[b] = temp;
        }
        return data;
    }

    /**
     * 深拷贝原数组
     * @param arr
     */
    public static int[] arrCopy(int[] arr) {
        return Arrays.copyOf(arr,arr.length);
    }

    /**
     * 在指定的数组arr上测试排序名称为sortName的排序算法性能
     * @param arr
     * @param sortName
     */  //运用了反射的知识
    public static void testSort(int[] arr,String sortName) {
        Class<SevenSort> cls = SevenSort.class;
        try {
            Method method = cls.getDeclaredMethod(sortName,int[].class);
            long start = System.nanoTime();
            method.invoke(null,arr);
            long end = System.nanoTime();
            if (isSorted(arr)) {
                System.out.println(sortName + "排序完成,共耗时:" + (end - start) / 1000000.0 + "ms");
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 检查当前的arr数组是否是一个非降序数组,前一个元素 <= 后一个元素
     * @param arr
     * @return
     */
    private static boolean isSorted(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i] > arr[i + 1]) {
                // 反例
                System.err.println("排序算法有误!");
                return false;
            }
        }
        return true;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值