归并排序(Merge Sort)

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

基本思想:

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

初始关键字 49  38  65  97  76  13  27
第一趟归并 (38 49) (65 97) (13 76) 27
第二趟归并 (38 49  65  97) (13 27  76)
第三趟归并 (13 27  38  49  65  76  97)

将待排序序列R[0...n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。

综上可知,归并排序其实要做两件事:

(1)“分解”——将序列每次折半划分。

(2)“合并”——将划分后的序列段两两合并后排序。

我们先来考虑第二步,如何合并

在每次合并过程中,都是对两个有序的序列段进行合并,然后排序。这两个有序序列段分别为 R[low, mid] 和 R[mid+1, high]。先将他们合并到一个局部的暂存数组R2中,带合并完成后再将R2复制回R中。

为了方便描述,我们称 R[low, mid] 第一段,R[mid+1, high] 为第二段。每次从两个段中取出一个记录进行关键字的比较,将较小者放入R2中。最后将各段中余下的部分直接复制到R2中。经过这样的过程,R2已经是一个有序的序列,再将其复制回R中,一次合并排序就完成了。

// 合并代码
void Merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) {
    int i = startIndex;  // i是第一段序列的下标
    int j = midIndex + 1;  // j是第二段序列的下标
    int k = startIndex;  // k是临时存放合并序列的下标
    while(i <= midIndex && j <= endIndex) {
        // 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
        if(sourceArr[i] >= sourceArr[j]) {
            tempArr[k++] = sourceArr[j++];
        } else {
            tempArr[k++] = sourceArr[i++];
        }
    }
    
    // 若第一段序列还没扫描完,将其全部复制到合并序列
    while(i <= midIndex) {
        tempArr[k++] = sourceArr[i++];
    }
    
    // 若第二段序列还没扫描完,将其全部复制到合并序列
    while(j <= endIndex) {
        tempArr[k++] = sourceArr[j++];
    }
    
    // 将合并序列复制到原始序列中
    for(i = startIndex; i <= endIndex; i++) {
        sourceArr[i] = tempArr[i];
    }
}

掌握了合并的方法,接下来,让我们来了解如何分解

在某趟归并中,设各子表的长度为gap,则归并前R[0...n-1]中共有n/gap个有序的子表:R[0...gap-1], R[gap...2gap-1], ... , R[(n/gap)gap ... n-1]。

调用Merge将相邻的子表归并时,必须对表的特殊情况进行特殊处理。

若子表个数为奇数,则最后一个子表无须和其他子表归并(即本趟处理轮空):若子表个数为偶数,则要注意到最后一对子表中后一个子表区间的上限为n-1。

void MergePass(int sourceArr[], int tempArr[], int gap, int length) {
    int low = 0;
    
    // 归并gap长度的两个相邻子表
    for (low = 0; low + 2 * gap - 1 < length; low = low + 2 * gap) {
        Merge(sourceArr, tempArr, low, low + gap - 1, low + 2 * gap - 1);
    }
    
    // 余下两个子表,后者长度小于gap
    if (low + gap - 1 < length) {
        Merge(sourceArr, tempArr, low, low + gap - 1, length - 1);
    }
}

void MergeSort(int sourceArr[], int tempArr[], int length) {
    for (int gap = 1; gap < length; gap = 2 * gap) {
        MergePass(sourceArr, tempArr, gap, length);
    }
}

算法的实现(非递归实现版本):

// 输出数组内容
void print(int array[], int length) {
    for (int j = 0; j < length; j++) {
        printf(" %d ", array[j]);
    }
    printf("\n");
}

// 合并-将划分后的序列段两两合并后排序
void Merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) {
    int i = startIndex;  // i是第一段序列的下标
    int j = midIndex + 1;  // j是第二段序列的下标
    int k = startIndex;  // k是临时存放合并序列的下标
    while(i <= midIndex && j <= endIndex) {
        // 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
        if(sourceArr[i] >= sourceArr[j]) {
            tempArr[k++] = sourceArr[j++];
        } else {
            tempArr[k++] = sourceArr[i++];
        }
    }
    
    // 若第一段序列还没扫描完,将其全部复制到合并序列
    while(i <= midIndex) {
        tempArr[k++] = sourceArr[i++];
    }
    
    // 若第二段序列还没扫描完,将其全部复制到合并序列
    while(j <= endIndex) {
        tempArr[k++] = sourceArr[j++];
    }
    
    // 将合并序列复制到原始序列中
    for(i = startIndex; i <= endIndex; i++) {
        sourceArr[i] = tempArr[i];
    }
}

void MergeSort(int sourceArr[], int tempArr[], int length) {
    for (int gap = 1, low = 0; gap < length; gap = 2 * gap) {
        // 归并gap长度的两个相邻子表
        for (low = 0; low + 2 * gap - 1 < length; low = low + 2 * gap) {
            Merge(sourceArr, tempArr, low, low + gap - 1, low + 2 * gap - 1);
        }
        
        // 余下两个子表,后者长度小于gap
        if (low + gap - 1 < length) {
            Merge(sourceArr, tempArr, low, low + gap - 1, length - 1);
        }
    }
}

int main(int argc, const char * argv[]) {
    int sourceArr[7] = { 49,38,65,97,76,13,27 };
    int tempArr[7];
    MergeSort(sourceArr, tempArr, 7);
    print(sourceArr, 7);
    
    return 0;
}

算法的实现(递归实现的版本):

// 输出数组内容
void print(int array[], int length) {
    for (int j = 0; j < length; j++) {
        printf(" %d ", array[j]);
    }
    printf("\n");
}

// 合并-将划分后的序列段两两合并后排序
void Merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) {
    int i = startIndex;  // i是第一段序列的下标
    int j = midIndex + 1;  // j是第二段序列的下标
    int k = startIndex;  // k是临时存放合并序列的下标
    while(i <= midIndex && j <= endIndex) {
        // 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
        if(sourceArr[i] >= sourceArr[j]) {
            tempArr[k++] = sourceArr[j++];
        } else {
            tempArr[k++] = sourceArr[i++];
        }
    }
    
    // 若第一段序列还没扫描完,将其全部复制到合并序列
    while(i <= midIndex) {
        tempArr[k++] = sourceArr[i++];
    }
    
    // 若第二段序列还没扫描完,将其全部复制到合并序列
    while(j <= endIndex) {
        tempArr[k++] = sourceArr[j++];
    }
    
    // 将合并序列复制到原始序列中
    for(i = startIndex; i <= endIndex; i++) {
        sourceArr[i] = tempArr[i];
    }
}

// 二路归并排序(Merge Sort)
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex) {
    int midIndex;
    if(startIndex < endIndex) { // 是if,不是while,且不含等号,否则死循环
        midIndex = (startIndex + endIndex) / 2;
        MergeSort(sourceArr, tempArr, startIndex, midIndex);
        MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
        Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
    }
}

int main(int argc, const char * argv[]) {
    int sourceArr[7] = { 49,38,65,97,76,13,27 };
    int tempArr[7];
    MergeSort(sourceArr, tempArr, 0, 6);
    print(sourceArr, 7);
    
    return 0;
}

总结

若从空间复杂度来考虑:首选堆排序,其次是快速排序,最后是归并排序。

若从稳定性来考虑,应选取归并排序,因为堆排序和快速排序都是不稳定的。

若从平均情况下的排序速度考虑,应该选择快速排序。

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

推荐阅读更多精彩内容

  • 声明:算法和数据结构的文章均是作者从github上翻译过来,为方便大家阅读。如果英语阅读能力强的朋友,可以直接到s...
    UnsanYL阅读 1,570评论 0 2
  • Ba la la la ~ 读者朋友们,你们好啊,又到了冷锋时间,话不多说,发车! 1.冒泡排序(Bub...
    王饱饱阅读 1,788评论 0 7
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,239评论 0 2
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,164评论 0 52
  • 概述排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的...
    Luc_阅读 2,255评论 0 35