一文助你搞定所有排序

1、排序的概念

1.1 排序的概念

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

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

 

内部排序 :数据元素全部放在内存中的排序。
外部排序 :数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 常见的排序 

        在编程语言中常见的排序算法有以下几种:

2、常见排序算法的实现 

2.1 插入排序

2.1.1 基本思想

        直接插入排序是一种简单的插入排序法,其基本思想是: 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。 实际中我们玩扑克牌时,就用了插入排序的思想

2.1.2 直接插入排序

        当插入第 i (i >= 1)个元素的时候,i 前面的元素已经排好序,此时用arr[i]与前面的元素进行比较,找到适合的位置插入即可,如果arr[i]的元素小于原来位置上的元素,原来位置上的元素则需要向后移。当arr[i]大于原来元素元素时说明找到了arr[i]该插入的位置,直接将arr[i]插在当前位置的后一个元素即可。

        如图,要将当前数组的元素直接插入排序:

        我们需要先定义两个下标i = 1, j = i - 1,再定义一个临时变量tmp = arr[i]。

        然后再将下标为 j 的元素与tmp进行对比,如果array[j] > tmp,则要把下标为 j 的位置让出来,即 array[j + 1] = array[j],同时 j--,继续对比。 

        直到 j < 0结束对比,此时 j + 1下标的位置就是当前tmp元素的位置,再将tmp的值赋给array[j + 1]就ok了,即array[j + 1] = tmp。

        然后 i 继续向后走,j 继续赋值为 i - 1。

        比较array[j] > tmp,则给tmp让出位置(注意,让出位置不能在排序过程中损失任何一个元素),array[j + 1] = array[j] ,j-- 。

        继续比较array[j] 仍然大于 tmp 继续让位置 array[j + 1] = array[j],j-- 。

        当 j < 0的时候,将tmp赋给索引为 j + 1的位置 。

         代码、时间、空间复杂度以及适用场景如下:

//默认从小到大排序
    public static void insertSort(int[] arr){
        //直接插入排序
        //时间复杂度:最好情况:数据完全有序,O(n) 最坏情况:数据完全逆序,O(n^2)
        //结论:当给的数据越有序排序越快
        //如果有一组基本有序的数据要排序————》这个快
        //空间复杂度:O(1)
        //稳定性:稳定的排序
        //一个本身稳定的排序 是可以实现为不稳定的
        //但是相反 一个本身就不稳定的排序 是不可能实现为稳定的
        for (int i = 1;i < arr.length;i++){
            int tmp = arr[i];
            int j = i - 1;
            for (; j >= 0 ; j--) {
                if(arr[j] > tmp){//说明tmp在当前j的前面
                    //让j下标的元素往前面盖
                    arr[j+1] = arr[j];
                }else {
                    break;
                }
            }
            arr[j+1] = tmp;
        }
    }

2.1.3 希尔排序

        希尔排序又称缩小增量法。其基本思想是:先选定一个整数,把待排序数组中所有元素分成多个组(距离为那个整数的元素分在同一组内),并对每一组的元素进行排序,然后,再取一个整数,重复上述分组和排序的工作。当取的值达到1的时候,所有的记录在同一组内排好序。(其实也就是分组进行直接插入排序)。

        如下图,有一组数据,其初始状态如下:

        取整数gap为5,然后将互相距离为5的元素分为一组,然后进行排序:         再取整数gap为2,然后再将互相距离为2的元素分为一组,即4 2 5 8 5一组,1 3 9 6 7一组,然后组内进行排序:

        最后,后再取整数gap为1,此时距离为1,即所有的元素都在一组,然后再进行分组,组内排序: 

        由上面的内容,我们可以知道希尔排序其实是对直接插入排序的优化gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。

        希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定:

         下面是希尔排序的代码、时间复杂度的范围、空间复杂度和稳定性:

//希尔排序
    //采用分组:每组进行插入排序     跳跃式的分组可能会将更小的元素 尽可能地往前放
    //时间复杂度:???n^1.3~n^1.5
    //空间:O(1)
    //稳定性:不稳定
    public  static void shellSort(int[] arr){
        int gap = arr.length;
        while(gap > 1){
            gap /= 2;
            shell(arr,gap);
        }
    }
    private  static  void shell(int[] arr,int gap){
        for (int i = gap; i < arr.length; i++) {
            //最后都会看成一组进行排序所以++/+gap不影响
            int tmp = arr[i];
            int j = i-gap;
            for (; j >= 0; j-=gap) {
                if(arr[j] > tmp){
                    //如果大于就换位
                    arr[j+gap] = arr[j];
                }else{
                    //不大于就放回去,并且跳出循环
                    // arr[j+1] = tmp;
                    break;
                }
            }
            arr[j+gap] = tmp;
        }

2.2 选择排序

2.2.1 基本思想

        每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到排序的数据元素排完。

2.2.2 直接选择排序

  1. 初始状态:整个数组分为有序区和无序区,初始时有序区为空,无序区包含数组的所有元素。
  2. 第 1 轮选择:在无序区中找到最小的元素,将它与无序区的第一个元素交换位置,此时有序区包含 1 个元素,无序区包含 n - 1 个元素。
  3. 第 2 轮选择:在剩余的无序区中找到最小的元素,将它与无序区的第一个元素交换位置,此时有序区包含 2 个元素,无序区包含 n - 2 个元素。
  4. 重复上述步骤,直到无序区为空,此时整个数组就有序了。

        如下图,如何将其以直接选择排序的方式进行排序。         定义下标i , j , minIndex。        然后将 j 逐步向后加加,比较arr[j] 与 arr[minIndex]的大小,如果arr[j] < arr[minIndex],则交换对应的值一直 j 遍历完整个数组后。        进行交换 

        i++,继续寻找下一个最小的元素。最终使得数组称为升序。

 

   //直接选择排序
    //[j]<[minIndex]——>更新
    //时间复杂度:不管最好最坏   都是O(n^2)
    //空间复杂度:O(1)
    //稳定性:不稳定
    public static  void selectSort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            int minIndex = i;
            int j = i+1;
            for (; j < arr.length; j++) {
               if (arr[j] < arr[minIndex]){
                   minIndex = j;
               }
            }
           swap(arr,minIndex,i);
        }
    }
    private static void swap(int[] arr,int i,int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

        直接选择排序还有一种优化的方法:定义minindex和maxindex,设置一个i遍历数组,如果arr[i] < arr[minIndex] 则把 i 赋给minIndex,如果arr[i] > arr[maxIndex],则把 i 赋给maxIndex,这样 i 遍历一次之后,就能知道最大和最小的值了,然后然后将对应最大和最小的值分别放在索引为right 和 left 的位置,然后left++,right--。

 private static void swap(int[] arr,int i,int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    public static void selectSort2(int[] arr){
        int left = 0;
        int right = arr.length-1;
        while(left < right){
            int minIndex = left;
            int maxIndex = left;
            for (int i = left+1; i <= right ; i++) {
                //更新最大最小值下标
                if(arr[i] < arr[minIndex]){
                    minIndex = i;
                }
                if(arr[i] > arr[maxIndex]){
                    maxIndex = i;
                }
            }
            swap(arr,left,minIndex);
            //最大值正好在最小值地的位置交换到了minIndex
            if(maxIndex == left){
                maxIndex = minIndex;
            }
            swap(arr,right,maxIndex);
            left++;
            right--;
        }
    }

2.2.3 堆排序

        堆排序(Heapsort) 是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆( 也就是上篇文章讲过的优先级队列 )来进行选择数据。 需要注意的是排升序要建大堆,排降序建小堆。
        我们直接看题目:将下图数组中的数据进行排序,使得其按照从小到大的顺序排列。

        如果要得到一个从小到大排序的数组,则建立一个大根堆。         将堆顶元素和最后一个元素进行交换。

         交换后,usedSize-- 然后将剩余的元素再进行调整,使其为大根堆。

        继续进行将第一个元素与堆顶元素进行交换。 

        然后再继续调整为大根堆。 

        如此就实现了从小到大的排序,代码如下: 

//堆排序
    //时间复杂度:O(n*logN)
    //空间复杂度:O(1)
    //稳定性:不稳定的
    //数据量大的时候堆排一定比希尔快
    public static void heapSort(int[] arr){
        creatBigHeap(arr);
        int end = arr.length-1;
        while(end > 0) {
            swap(arr, 0, end);
            shiftDown(arr, 0, end);
            end--;
        }
    }
    private static void creatBigHeap(int[] arr){
        for (int parent = (arr.length-1-1)/2;parent >= 0 ; parent--) {
            shiftDown(arr,parent,arr.length);
        }
    }

    private static void shiftDown(int[]arr,int parent,int end) {
        int child = 2*parent+1;
        while(child < end){
            if(child + 1 < end && arr[child] < arr[child+1]){
                child++;
            }
            if(arr[child] > arr[parent]){
                swap(arr,child,parent);
                parent = child;
                child = 2*parent+1;
            }else{
                break;
            }
        }
    }
     private static void swap(int[] arr,int i,int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

2.3 交换排序 

2.3.1 冒泡排序

        由于在之前的文章已经讲解过,这里就不做过多赘述,这是冒泡排序文章的链接:冒泡排序(Bubble Sort)_逆向冒泡排序-优快云博客

        代码如下:

//冒泡排序
    //时间复杂度:O(n^2)  如果加了优化  最好情况O(n)
    //空间复杂度:O(1)
    //稳定性:稳定
    public  static void bubleSort(int[] arr){
        for (int i = 0; i < arr.length-1; i++) {//比较的趟数
            boolean flg = false;
            for (int j = 0; j < arr.length-1-i; j++) {
                //每次比较都比上一次少一次
                if(arr[j] > arr[j+1]){
                    swap(arr,j,j+1);
                    flg = true;
                }
            }
            if(!flg){
                return;
            }
        }
    }
private static void swap(int[] arr,int i,int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

 2.3.2 快速排序 

        快速排序是Hoare 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
         这是快排的大致框架:
2.3.2.1 Hoare法 

        如下图数组:

        定义一个left,一个right 

        Key对应的元素是6,然后先从后面right开始,找比6小的元素,再从前面left开始,找比6大的元素。        找到后,交换值。 

        直到left和right相遇之后。         将相遇的这个索引的数值和6进行交换。

        换完之后,会发现,6左边的元素都是比6小的值,6右边的元素都是比6大的值

此时称6为privot(基准)。

        然后再以上面的方法,分而治之,即再在6的左边,和6的右边进行上述操作

        例如:在左边,Key为3。

        再先从right开始,找比3小的元素,找到2,再从左边left出发找比3大的元素,发现会相遇。

         在拿相遇时候的值和3进行交换。

        此时3又是新的基准。 

        再对3的左边进行操作,有新的left和right。        先从right找比2小的--1,再从left找比2大的,与right相遇。

        相遇的值与2进行交换。         此时2有序了,左侧只有一个元素,1也有序了。

        再对3的右树进行操作:操作过后,4,5交换位置,这样6的左树就全部有序了。       快排的递归可以类比二叉树的递归过程:

        第一次找到privot6:

        对6的左树进行递归: 

        再对3的左树递归: 

         再逐层向下递归:

         然后再递归3的右树,再递归6的右树等等,所以快速排序重点在于:如何确定基准privot的位置。Hoare法只是其中找基准的方法之一。

        完整代码:

   public static void quickSort(int[] arr){
        quick(arr,0,arr.length-1);
    }
    private static void quick(int[] arr,int start,int end){
        if(start >= end) return;//左边是一个节点 或者 一个节点也没有了
        //取基准
        int priot = parttionHoare(arr,start,end);
        //类比遍历二叉树
        quick(arr,start,priot-1);
        quick(arr,priot+1,end);
    }
   private static int parttionHoare(int[] arr,int left,int right){
        int k = arr[left];
        int i = left;
        while(left < right){
            //如果left先走的话,left和right相遇的地方一定是比k大的
            while(left < right && arr[right] >= k){//这里为什么取等
                //如果不取可能会死循环
                right--;
            }//right    下标一定是比k小的数据
            while(left < right && arr[left] <= k){//这里为什么取等
                left++;
            }//left     下标一定是比k大的数据
            swap(arr,left,right);
        }
        //相遇的位置和i交换
        swap(arr, left, i);
        return left;
    }
private static void swap(int[] arr,int i,int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
2.3.2.2 挖坑法

        挖坑法,顾名思义,是会挖出一个一个空的,会首先将key的值单独存起来(相当于挖了一个坑),然后从right开始找比key小的元素,找到后,把该索引的值填入给key的下标索引中 

        如图:先将6存起来 

        然后right找到比6小的元素5。         把5覆盖到6的位置,同时之前5的位置相当于又留了一个坑。

        left向前走,找到比6大的元素的下标索引。

        把7填到刚刚5留下的坑的位置,同时7也留下了新的坑。        right继续向前找到比6小的元素。

        把4填到刚刚7留下的坑,同时4也留下了新的坑。        left继续向后找到比6大的元素,9把刚刚4留下的坑填入,留下新的坑。

        right继续向前找到比6小的元素,3把刚刚9留下的坑填入,留下新的坑。        left向后走,和right相遇,相遇位置是空的坑,将最开始记录下的6填入。

        完成一次排序,返回基准值,然后进行递归,挖坑法的大致流程和Hoare法相同,但在细节和每一次的排序结果中有所不同。 

        代码:

  public static void quickSort(int[] arr){
        quick(arr,0,arr.length-1);
    }
    private static void quick(int[] arr,int start,int end){
        if(start >= end) return;//左边是一个节点 或者 一个节点也没有了
        //取基准
        int priot = parttion(arr,start,end);
        //类比遍历二叉树
        quick(arr,start,priot-1);
        quick(arr,priot+1,end);
    }
    private static int parttion(int[] arr,int left,int right){
        int k = arr[left];
        int i = left;
        while(left < right){
            //如果left先走的话,left和right相遇的地方一定是比k大的
            while(left < right && arr[right] >= k){//这里为什么取等
                //如果不取可能会死循环
                right--;
            }//right    下标一定是比k小的数据
            arr[left] = arr[right];
            while(left < right && arr[left] <= k){//这里为什么取等
                left++;
            }//left     下标一定是比k大的数据
            arr[right] = arr[left];
        }
        //相遇的位置和i交换
        arr[left] = k;
        return left;
    }
2.3.2.3 前后指针法

        起始时,prev指针指向序列的开头,cur指针指向prev指针的后一个位置。

     使用代码来理解这种方法:

        示例与代码结合,示例满足273行循环,进入循环,274行中array[cur] < array[left],示例中array[cur] 等于1,array[left] 等于6, 满足条件,再看array[++prev] != array[cur] array[++prev]等于1,array[cur] 等于1,两个值相等,不满足if条件,不进入循环,cur++。

         cur++后,cur仍小于等于right,继续进入循环,仍不满足if的第二个条件,cur++。

        继续进入循环,此时情况不满足if的第一个条件,直接cur++,注意此时因为不满足第一个if条件,就不会进入if的第二个条件,所以prev不会执行前置++。        继续进入循环,此时情况不满足if的第一个条件,直接cur++,prev仍然不会执行前置++。

        此时进入循环,if的两个条件都满足,且在第二个条件的时候,prev的位置也进行了变化。         进入if循环,交换cur和prev的值。

        然后cur++。         继续向后推进,if的两个条件都满足,在满足第二个条件的时候,prev的位置继续进行变化。

        进入if条件,进行swap交换。         cur++。

        进入循环,满足if的两个条件,prev先前置++。         再swap交换。

       cur++。 

        再进入循环,不满足if的第一个条件,prev不前置++,不交换,cur++         此时cur 等于 right 还可以进入循环,不满足if的第一个条件,prev不前置++,不交换。

        此时cur > right 无法进入循环,再交换left和prev的值。        prev的位置作为基准进行返回,这就是一次前后指针的过程。

        前后指针方法的特点是:使用单指针进行扫描:cur指针从左到右扫描数组,同时利用prev指针标记小于基准元素区域的边界,通过条件判断和交换操作完成分区。 

  public static void quickSort(int[] arr){
        quick(arr,0,arr.length-1);
    }
    private static void quick(int[] arr,int start,int end){
        if(start >= end) return;//左边是一个节点 或者 一个节点也没有了
        //取基准
        int priot = partition(arr,start,end);
        //类比遍历二叉树
        quick(arr,start,priot-1);
        quick(arr,priot+1,end);
    }
    //前后指针法
   private static int partition(int[] array, int left, int right) {
        int prev = left ;
        int cur = left+1;
        while (cur <= right) {
            if(array[cur] < array[left] && array[++prev] != array[cur]) {
                swap(array,cur,prev);
            }
            cur++;
        }
        swap(array,prev,left);
        return prev;
    }

 2.3.3 快速排序优化

        在刚刚提到的快速排序中,无论是Hoare法、挖坑法还是前后指针法,都会出现最坏情况:即单分支树的情况,导致栈溢出异常,如何解决呢?

        栈溢出,就是因为树的高度太多了,想办法优化,就是降低需要的空间,就是要降低树的高度,即尽量使得单分支树变为满 / 完全二叉树。

2.3.3.1 三数取中法选key 

        如下图数据,如果直接进行快速排序,会形成单分支树。

        可以对下标索引进行操作,找到位于 left 和 right 的中间位置索引mid。         对比left mid right 三个下标索引对应的元素值 5   7   9, 比较这三个值的大小,判断哪一个的大小位于这三个值的中间位置,即7是5 7 9 三个元素的中间,然后7的位置和left进行交换。

        这样7就称为了新的key值,在每次递归前,先进行上面操作,这样进行快速排序的时候,不会出现单分支树的情况了。

        下面是实现三数取中法的代码:

 private static int midOfTree(int[] arr, int left, int right) {
        int mid = (left+right)/2;
        if(arr[left] < arr[right]){
            if(arr[mid] < arr[left]){
                return left;
            } else if(arr[mid] > arr[right]){
                return right;
            } else{
                return mid;
            }
        }else {
                if(arr[mid] > arr[left]){
                    return left;
                } else if (arr[mid] < arr[right]) {
                    return right;
                }else {
                    return mid;
                }
        }

    }

        在快排方法中使用: 

2.3.3.2 递归到比较小的子区间的时候,可以考虑使用插入排序 

        对于一棵满  / 完全二叉树来说,最后两层的结点的总数,一般是最多的,占了整棵树的 2 / 3。

        在递归创建的时候,最后两层所占用的空间也是最多的。在快速排序中,是利用树的递归思想不断在进行排序,越递归到后面,即递归到比较小的子区间的时候,其实是到最后已经大致有序了,我们知道,直接插入排序方法在对大致有序的数组进行排序时候,其效率较高

        所以,我们可以在进行快速排序的过程中,当递归到比较小的子区间的时候,使用插入排序。

        当 end - start + 1 <= 15 代表当前子数组中包含的元素总数,当子数组中包含的元素总数小于15的时候,就可以使用直接插入排序方法,减少递归的次数。

        下面是快速排序的完整代码:


//快速排序
    //时间复杂度:最好情况:完全二叉树 O(N*logN)              最坏情况:O(N^2)单分支
    //空间复杂度:最好情况:完全二叉树 O(logN)  最坏情况:O(N)单分支
    //稳定性:不稳定
    //优化方法:三数取中法降低树的高度      小的有序的区间用插入排序    减少递归次数
    //1、挖坑法     2、hor法      3、前后指针法
 public static void quickSort(int[] arr){
        quick(arr,0,arr.length-1);
    }
    private static void quick(int[] arr,int start,int end){
        if(start >= end) return;//左边是一个节点 或者 一个节点也没有了
        if(end-start+1 <= 15){
        //插入排序
            insertSortRange(arr,start,end);
            return;
        }

        //三数取中
        int index =  midOfTree(arr,start,end);
        swap(arr,index,start);//此时交换完之后  一定能保证start下标是中间大的数字
        //取基准
        int priot = parttion(arr,start,end);
        //类比遍历二叉树
        quick(arr,start,priot-1);
        quick(arr,priot+1,end);
    }private static int midOfTree(int[] arr, int left, int right) {
        int mid = (left+right)/2;
        if(arr[left] < arr[right]){
            if(arr[mid] < arr[left]){
                return left;
            } else if(arr[mid] > arr[right]){
                return right;
            } else{
                return mid;
            }
        }else {
                if(arr[mid] > arr[left]){
                    return left;
                } else if (arr[mid] < arr[right]) {
                    return right;
                }else {
                    return mid;
                }
        }

    }
    private static void insertSortRange(int[] arr,int begin,int end){
        for (int i = begin+1; i <= end; i++) {
            int tmp = arr[i];
            int j = i-1;
            for (; j >= begin; j--) {
                if(arr[j] > tmp){
                    //如果大于就换位
                    arr[j+1] = arr[j];
                }else{
                    //不大于就放回去,并且跳出循环
                    // arr[j+1] = tmp;
                    break;
                }
            }
            arr[j+1] = tmp;
        }
    } 
 private static int parttion(int[] arr,int left,int right){
        int k = arr[left];
        int i = left;
        while(left < right){
            //如果left先走的话,left和right相遇的地方一定是比k大的
            while(left < right && arr[right] >= k){//这里为什么取等
                //如果不取可能会死循环
                right--;
            }//right    下标一定是比k小的数据
            arr[left] = arr[right];
            while(left < right && arr[left] <= k){//这里为什么取等
                left++;
            }//left     下标一定是比k大的数据
            arr[right] = arr[left];
        }
        //相遇的位置和i交换
        arr[left] = k;
        return left;
    }

2.3.4 非递归实现快速排序

        在新的基准操作的时候,入栈的时候先入left再入right,操作的时候,仍然先操作右边,再操作左边。

 

  //非递归实现快排
    public static  void quickSortNor(int[] arr){
        Stack<Integer> stack = new Stack<>();
        int left = 0;
        int right = arr.length-1;
        int piovt = partition(arr,left,right);
        if(piovt-1 > left){//说明左边一定有两个元素
            stack.push(left);
            stack.push(piovt-1);
        }
        if(piovt+1 < right){//说明右边一定有两个元素
            stack.push(piovt+1);
            stack.push(right);
        }
        while(!stack.isEmpty()){
            right = stack.pop();
            left = stack.pop();
             piovt = partition(arr,left,right);
            if(piovt-1 > left){//说明左边一定有两个元素
                stack.push(left);
                stack.push(piovt-1);
            }
            if(piovt+1 < right){//说明右边一定有两个元素
                stack.push(piovt+1);
                stack.push(right);
            }
        }
    }

2.4 归并排序

2.4.1 基本思想

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

        分步讲解: 

        对其先进行分解(先分解左边 再 分解右边 最后合并),第一次分解 以中间的m的索引 作为新的数组的r进行分解。

        再向下进行分解。

        继续分解,直到r == l的时候分解完毕 。         分解完毕后,再进行合并。

        分解很简单,就一直向下递归即可。

        如何将两个序列段进行合并呢?

        以如下图的两个有序段(6,10 和 1, 7)举例,对两个序列段分别定义s 和 e,来表示第一个和第二个序列端的开始和合并。

         因为要合并到一个新的数组,所以应该计算两个序列段的元素个数,即 3 - 0 + 1 = 4,申请一个长度为4的数组来存放合并后的序列。然后每次让s1和s2进行比较,谁小,谁放入数组中,对应的s++,e是一个结束的位置,如果有一个数组的s逐渐向后递归,直到其有一个数组没有元素了,则说明另一个数组的值都大于另一个数组的最大的值,直接将剩下的数组全部按顺序放入即可。

2.4.2 递归实现归并排序

    //归并排序  分解左边 分解右边 合并
    //最常用的外部排序
    //空间复杂度:O(n)
    //时间复杂度:不管有无序都是 N*logN
    //稳定的排序:插入  冒泡  归并
    public static void mergeSort(int[] arr){
        mergrSortFunc(arr,0,arr.length-1);
    }
    private static void mergrSortFunc(int[] arr,int left,int right){
        if(left >= right) return;;
        int mid = (left+right)/2;
        //分解左边
        mergrSortFunc(arr,left,mid);
        //分解右边
        mergrSortFunc(arr,mid+1,right);
        //合并
        merge(arr,left,right,mid);
    }

    private static void merge(int[] arr, int left, int right, int mid) {
        int s1 = left;
        int s2 = mid+1;
        int[] tmpArr = new int[right-left+1];
        int k = 0;
        //证明两个区间    都同时有数据的
        while(s1 <= mid && s2<= right){
            if(arr[s1] <= arr[s2]){
                tmpArr[k++] = arr[s1++];
            }else{
                tmpArr[k++] = arr[s2++];
            }
        }
        //把剩余的元素放入tmpArr中
        while(s1 <= mid){
            tmpArr[k++] = arr[s1++];
        }
        while(s2 <= right){
            tmpArr[k++] = arr[s2++];
        }
        //tmpArr   里面一定是有序的
        for (int i = 0; i < tmpArr.length; i++) {
            arr[i+left] = tmpArr[i];
        }
    }

2.4.3 非递归实现归并排序

 //非递归实现归并
    public static void mergeSort2(int[] arr){
        int gap = 1;
        while(gap < arr.length){
            for (int i = 0; i < arr.length; i += 2*gap) {
                int left = i;
                int mid = left+gap-1;
                int right = mid+gap;
                if(mid >= arr.length){
                    mid = arr.length-1;
                } if(right>= arr.length){
                    right = arr.length-1;
                }
                merge(arr,left,right,mid);
            }
            gap *= 2;
        }
    }

2.4.4 海量数据排序问题 

        外部排序:排序过程需要在磁盘等等外部存储的排序。

        前提:内存只有 1G,需要排序的数据有 100G。

        因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序 。 
1. 先把文件切分成 200 份,每个 512 M。
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以。
3. 进行 2 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了。
        过程如下图:

3、排序算法复杂度及稳定性分析总结 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值