2.2 归并排序(分类,合并)排序

核心思想:将一个数组进行排序,可以先(递归)将他分成两半分别排序,然后把结果合并起来.若将两个有序表合并成一个有序表,称为二路归并
即:将数组分成两部分,左边部分,和右边部分分别递归再次分成两部分知道每个数组里面只有一个元素.然后调用合并函数,将两个数组进行排好序.每次排序的时候会使用同一个临时缓存,储存排好序的元素.最后的排序结果是 依次把 左半边的 数组 两两排序合并,递归再把合并的数组继续两两合并,最后左边的数组全部合并;接下来对右半部分的数组两两排序合并,重复左边的类似动作,把右边的数组排序成一个数组;最后进行左右两个有序数组合并,组成最终有序数组.
归并排序的时间复杂度是NlogN,缺点是所需要的额外辅助空间和N成正比(需要分配一个与N长度一致的临时存储空间).

2.2.1原地归并的抽象方法:

因为考虑到直接归并算法每次合并需使用一个临时空间,所以考虑在原始数组上直接进行排序,先对前半部分排序,在对后半部分排序.但是相对二路归并的复杂度高.

2.2.2二路归并具体的算法思路:(是一种自顶向下的mergesort)

1.判断需要进入递归操作的边界值.(一般 left<right)
2.把数组分成两半. Mid =(left+right)/2
3.进行左半边数组递归调用.直到 left=right ,不在递归.此时提取出数组的第一个元素
4.进行右半边数组递归调用.
5.调用merge方法,把两个有序数组合并.合并的时候会使用一个临时数组.合并的思路是从两个有序的数组第一个元素开始,依次比较,把数据按从小到大的顺序存放到临时temp;遇到某一个数组完成了,剩下一个数组还没比较完,则把剩下的数组复制到temp尾部.最后把temp数组读取,替换实际数组中.
步骤如下图:
要把a[]分成a[lo...hi]两部分,进行分别对 a[lo...mid] ,a[mid+1...hi]进行递归调用,直到调用的数组lo和hi相同时跳出,继而进行和右边部分合并排序.


image.png

代码如下:

public class Mergesort{
    public static int [] temp;
    public void sort(int[] arr){
    if(arr==null||arr.lenth==0){
         return;}
        temp=new int[arr.lenth];
         mergesort(arr,0,arr.lenth-1);
    }
        private void mergsort(int[] arr,int low,int high){
        if(low>=high){return;}
       int mid=(low+high)/2;
       mergesort(arr,low,mid);
      mergesort(mid+1,high);
     merge(arr,low,mid,high);
 }
 private void merge(int[] arr,int low,int mid,int high){
    int leftIndex=low;
    int  rightIndex=mid+1;
    int tempIndex=0;
    while(leftIndex<=mid&&rightIndex<=high){
     if(arr[leftIndex]<arr[rightIndex]){
        temp[tempIndex++]=arr[leftIndex++];
         }else{
        temp[tempIndex++]=arr[rightIndex++]; 
    }
//已经比较完了,接下来如果发现左边 或者右边还有没到最后的部分,则把那一部分直接添加到temp尾部
    while(leftIndex<=mid){
    temp[tempIndex++]=arr[leftIndex++];
    }   
        while(rightIndex<=high){
    temp[tempIndex++]=arr[rightIndex++];
    }   
//把temp 合并到arr中
    int t=0;
    while(low<=high){
        arr[low++]=temp[t++];
    }
}
 }
}
时间复杂度计算:

因为递归算法的时间复杂度可以表示为T(n)=2T(n/2)+O(n).
这是一个每次分一半的递归,总共执行排序的步骤可以画成一棵类似的树.

image.png

因为每一层的子问题时间代价为cn,一共有log2N+1 层,所以所有的时间=cn(lgN+1)=cnlnN+cn 时间复杂度
O(NlgN).
命题F:
对于长度为N的任意数组,自顶向下归并排序的需要 ½NlgN ≤O≤ NlgN次比较.
命题G:
对于长度为N的任意数组,自顶向下归并排序最多要访问数组6NlgN次.
证明:
每次归并最多需要访问数组6N次(2N 复制,2N用来将排好序的数组复制过去,2N次进行合并),根据命题F,可知需要有NlgN次比较,所以访问数组为6NlgN.

2.2.3对自定向下的归并排序的优化

1.对小规模的数组使用插入排序.(具体设置的阀值7) 一般插入排序处理小规模排序(例如15)可以将归并缩短时间10%~15%
2.测试数组是否已经有序.在merge之前添加一个方法判断arr[mid]≤arr[mid+1],如果是的话说明这个从lo ...high的数组已经是有序了
3.在merge的时候不进行多次复制.(不把数组复制到临时数组,然后又进行把临时数组的数据copy到src中,这里面至少要经过2次重复操作)优化对临时数组的空间上面还是保持不变,主要在与减少时间.
代码如下:

public class MergeSort{
    private static final int OFF_CUT=7;
    public void sort(int[] src){
        int[] org=src.clone();
        mergeSort(org,src,0,org.length-1);
    }
    private void mergeSort(int[] src,int[] dst,int lo,int hi){
     //进行阀值判断
        if(hi<lo+OFF_CUT){
            insertSort(dst,lo,hi);
            return ;
        }
        int mid=(lo+hi)/2;
        mergeSort(dst,src,lo,mid);
        mergeSort(dst,src,mid+1,hi);
        //如果src已经有序,那么直接复制到dst即可
        if(src[mid+1]>=src[mid]){
            System.arrayCopy(src,lo,dst,lo,hi-lo+1);
        }
        merge(src,dst,lo,mid,hi);
    }
    private void insertSort(int[] arr,int lo,int hi){

        for(int i=lo+1;i<=hi;i++){
            int temp=arr[i];
            int j=i;
            while(j>lo&&temp<arr[j-1];j--){
                arr[j]=arr[j-1];
            }
            arr[j]=temp;
        }
    }

    private void merge(int[] src,int[] dst,int low,int mid,int hi){
        int lowIndex=low;
        int hiIndex=mid+1;
        for(int i=low;i<=hi;i++){
            if(lowIndex>mid){
                dst[i]=src[hiIndex++];
            }else if(hiIndex>hi){
                dst[i]=src[lowIndex++];
            }else if (src[lowIndex]<src[hiIndex]) {
                dst[i]=src[lowIndex++];
            }else{
                dst[i]=src[hiIndex++];
            }
        }
    }
}
2.2.4自底向上的归并排序

使用归并排序的另一种方法是 先归并那些微型数组,然后在归并得到的子数组,反复;直到我们得到整个数组并归并一起.这样比标准递归方法需要的代码更少.
[手摇算法,又称三次反转]
用于反转字符串包括里面有不服顺序改变,可以将空间复杂度降低至O(1).
具体做法.例子:
题目要求部分反转数组。比如说1,2,3,4,5 翻转后是3,4,5,1,2

class Reverse{
private void reverse(int[] a, int low, int hi) {
    while (low < hi) {
        swap(a, low, hi);
        low++;
        hi--;
    }
}

//这里也用到内存反转3次 进行交换两个数 ,采用 ^操作 
private void swap(int[] a, int left, int right) {
    a[left] ^= a[right];
    a[right] ^= a[left];
    a[left] ^= a[right];
}
public static void main(String[] args) {
    int[] test = {1, 2, 3, 4, 5};
    Reversel rev=new Reverse();
    rev.reverse(test, 0, 1);
    rev.reverse(test, 2, test.length - 1);
    rev.reverse(test, 0, test.length - 1);
    for (int i = 0; i < test.length; i++) {
        System.out.print(test[i]);
    }
}
}
自底向上,的归并排序的思路是
  • 先分割成前半部分为1,后半部分的数组, 划分成 N/2 段.
  • 然后接着以分成前半部分为2,后半部分为2的数组.
  • 然后接着以分成前半部分为3,后半部分为3的数组.
  • 直到每部分的步长接近N 或者超过N.
/**
 * Created by leon on 18-1-24.
 * 这里是采用自底向上的算法
 */
public class MergeSortBottom2Up {
    public void sort(int[] arr) {
        System.out.println("自底向上递归前:" + Arrays.toString(arr));
        int num = arr.length;
        //翻倍递增
        for (int i = 1; i < num; i = i + i) {
            for (int j = 0; j < num; j++) {
                int low = Math.min(j, num - 1);
                j = j + i + i - 1;/*往后移动步长*/
                int high = Math.min(j, num - 1);
                int mid = (low + high) / 2;
                merge(arr, low, mid, high);
            }
        }
        System.out.println("自底向上递归后:" + Arrays.toString(arr));
    }
    /**
     * 进行在一个数组里面for循环,发现lowIndex下标超过mid 或者 highIndex 超过high进行切换
     * 比较的思路:
     * 1.lowIndex=lo,midIndex,highIndex=mid+1
     * 1.从lowIndex 开始 ++,比较src[lowIndex] 和 src[highIndex]的值,直到    src[lowIndex]>src[highIndex]停止
     * 2.用midIndex 定位到highIndex
     * 3.从highIndex++,直到src[highIdex]>src[lowIndex],停
     * 此刻说明从 midIndex~highIndex-1的数比lowIndex的小,需要插入到lowIndex之前.
     * 利用内存3次反转(手摇技术)反转内存,达到把midIndex~highIndex-1部分插入到lowIndex之前
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     * `^``````````^`^``````
     *  l            h    =>初始 l,m,h=m+1
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     * ```^````````^`^````
     *    l          h   =>移动l,arr[l]>arr[h]
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     * ```l``````````m`h``` =>把m->h位置,h++,直到arr[h]>arr[l]
     *此时说明 arr[m]~arr[h-1]的数小于 arr[l],需要进行3次内存反转,
     *达到把arr[m]~arr[h-1]排到arr[lowIndex]前面
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     *
     * ____________________
     * |1|10|9|5|4|3|2|6|7|==> 反转左边
          ^          ^ ^
     * ____________________
     * |1|10|9|5|4|3|2|6|7|==>反转右边 只有一个 2
     *    ^          ^ ^
     * ____________________
     * |1|2|3|4|5|9|10|6|7|==>反转两部分
     *    ^          ^ ^
     *完成一次排序,
     * lowIndex=lowIndex+i+i-1;继续下一次排序
     *
     * @param src
     * @param lo
     * @param mid
     * @param hi
     */
    private void merge(int[] src, int lo, int mid, int hi) {
        int lowIndex = lo;
        int highIndex = mid + 1;
        //当 lowIndex 与highIndex 重叠时.或者highIndex超出了最外层结束
        while (lowIndex < highIndex && highIndex <= hi) {
            while (lowIndex < highIndex && src[lowIndex] <= src[highIndex]) {
                lowIndex++;
            }
            int midIndex = highIndex;
            //这里的边界值很关键
            while (highIndex <= hi && src[highIndex] < src[lowIndex]) {
                highIndex++;
            }
            Convert(src, lowIndex, midIndex - 1, highIndex - 1);
            //完成一次排序
            lowIndex += highIndex - midIndex;
        }
    }
    void Convert(int a[], int low, int M, int high) {
        //反转 low...middle
        reverse(a, low, M);
        //反转middle+1 high
        reverse(a, M + 1, high);
        //反转low...high
        reverse(a, low, high);
    }
    private void reverse(int[] a, int low, int hi) {
        while (low < hi) {
            swap(a, low, hi);
            low++;
            hi--;
        }
    }
    private void swap(int[] a, int left, int right) {
        a[left] ^= a[right];
        a[right] ^= a[left];
        a[left] ^= a[right];
    }
}

2.2.5 排序算法的复杂度

研究复杂度的第一步是建立模型.一般来说寻找与问题相关的最简单的模型.排序算法,是基于主键的比较决定的.
命题I:
没有任何基于比较的算法能够保证使用少于lg(N!)~NlogN次比较将长度为N的数组排序.

这个结论告诉我们在设计排序时能达到的最佳效果.
命题J:
归并排序是一种渐进最优的基于比较的排序算法.更准确的说.归并排序在最坏情况下的比较次数和其他基于比较的任意算法的最少时间都是~NlogN.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • 搞懂基本排序算法 上篇文章写了关于 Java 内部类的基本知识,感兴趣的朋友可以去看一下:搞懂 JAVA 内部类;...
    醒着的码者阅读 1,160评论 3 4
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,225评论 0 2
  • 1 初级排序算法 排序算法关注的主要是重新排列数组元素,其中每个元素都有一个主键。排序算法是将所有元素主键按某种方...
    深度沉迷学习阅读 1,374评论 0 1
  • 总结一下常见的排序算法。 排序分内排序和外排序。内排序:指在排序期间数据对象全部存放在内存的排序。外排序:指在排序...
    jiangliang阅读 1,310评论 0 1
  • 坐上历史的车轮,思绪回到2017年头,回想2017这一整年,我还是欣慰的,毕竟年底实现了年头的期盼是一件值得...
    Anna小鱼阅读 328评论 0 0