[算法]——归并排序(Merge Sort)

归并排序(Merge Sort)与快速排序思想类似:将待排序数据分成两部分,继续将两个子部分进行递归的归并排序;然后将已经有序的两个子部分进行合并,最终完成排序。其时间复杂度与快速排序均为O(nlogn),但是归并排序除了递归调用间接使用了辅助空间栈,还需要额外的O(n)空间进行临时存储。从此角度归并排序略逊于快速排序,但是归并排序是一种稳定的排序算法,快速排序则不然。

所谓稳定排序,表示对于具有相同值的多个元素,其间的先后顺序保持不变。对于基本数据类型而言,一个排序算法是否稳定,影响很小,但是对于结构体数组,稳定排序就十分重要。例如对于student结构体按照关键字score进行非降序排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// A structure data definition
typedef struct __Student
{
    char name[16];
    int score;
}Student;
// Array of students
name :  A      B     C     D
score:  80     70    75    70
 
Stable sort in ascending order:
name :  B      D     C     A
score:  70     70    75    80
 
Unstable sort in ascending order:
name :  D      B     C     A
score:  70     70    75    80

其中稳定排序可以保证B始终在D之前;而非稳定排序,则无法保证。

1)数组的归并排序

归并排序的思想实际上是一种分治法,即将待排序数据分成两部分,分别对两部分排序,然后将这两部分合并。下面以非降序排序为例:

1
2
3
4
5
6
7
8
9
10
11
// Split arr[] into two parts [begin,mid), [mid,end)
// and using merge_core to merge this two parts
// Total Time O(nlogn)
void merge_sort(int arr[], int begin, int end)
{
    if (end-begin < 2) return;
    int mid = (begin+end)>>1;
    merge_sort(arr,begin,mid);
    merge_sort(arr,mid,end);
    merge_core(arr,begin,mid,end);
}   // Time O(logn)

其中arr[]为待排序数组,对于一个长度为N的数组,直接调用merge_sort(arr,0,N);则可以排序。

归并排序总体分为两步,首先分成两部分,然后对每个部分进行排序,最后合并。当然也可以分成三部分或其他,然而通常是分成两部分,因此又称为二路归并。merge_core可以将两个有序数组合并成一个,具体操作如图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* merge core: combine two parts which are sorted in ascending order
 * arr[]: ..., 1, 4, 8, 2, 3, 7, 9, ...
 *      begin__|        |__mid      |__end
 * part1: 1, 4, 8       part2: 2, 3, 7, 9
 *
 * combination:
 * part1:[1]            [4]       [8]
 * part2: |   [2]  [3]   |   [7]   |   [9]
 *        |    |    |    |    |    |    |
 * tmp  :[1]  [2]  [3]  [4]  [7]  [8]  [9]
 *
 * at last, copyback tmp to arr[begin,end)
 * */

合并的前提是,两个数组已经是有序的。其代码为:

1
2
3
4
5
6
7
8
9
10
void merge_core(int arr[], int begin, int mid, int end)
{
    int i=begin, j=mid, k=0;
    int *tmp = (int*)malloc(sizeof(int)*(end-begin));
    for(; i<mid && j<end; tmp[k++]=(arr[i]<arr[j]?arr[i++]:arr[j++]));
    for(; i<mid; tmp[k++]=arr[i++]);
    for(; j<end; tmp[k++]=arr[j++]);
    for(i=begin, k=0; i<end; arr[i++]=tmp[k++]);
    free(tmp);
}   // Time O(n), Space O(n)

其中第6,7两行,将剩余的部分追加到tmp[]中,然后将tmp[]写回到arr[]。因此,对于数组使用归并排序,需要辅助空间O(n)。由于是尾部调用merge_core,当然可以将其写入到merge_sort尾部,这里为了思路清晰,将其分成两部分书写。

2)链表的归并排序

事实上,归并排序更适合对链表排序,因为在合并两个链表时,不需要额外的辅助空间存储,而且也不需要对数据拷贝,直接移动指针即可。唯一的不便是:需要每次寻找到链表的中间节点,然后以此将该链表分割成两部分。寻找中间节点,可以定义两个指针fast和Mid,fast每次移动两步,mid每次移动一步,当fast到链表尾部时,mid此时处于链表中间(不用考虑奇偶情况):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Merge sort for single list as ascending order
// single list node define
typedef struct __ListNode
{
    int val;
    struct __ListNode *next;
}ListNode;
 
// Merge sort for single list without head node
ListNode *merge_sort(ListNode *head)
{
    if (head==NULL || head->next==NULL) return head;
    ListNode *fast, *mid, H;
    // find mid node between head and end
    for (H.next=head, fast=mid=&H; fast && fast->next;){
        mid = mid->next;
        fast = fast->next->next;
    }
    fast = mid->next;
    mid->next = NULL;    // cut down mid part from head list
    mid = fast;
 
    head = merge_sort(head);
    mid = merge_sort(mid);
    return merge_core(head,mid);
}

注意,找到链表的中间节点后,务必将其指向NULL,以保证确实将链表分成两部分。然后将两个链表head与mid进行合并。由于合并后可能会修改链表头结点,因此要返回新的链表头结点。下面是合并操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// merge single list without head node (ascending order)
ListNode *merge_core(ListNode *i, ListNode *j)
{
    ListNode H, *p;
    for (p=&H; i && j; p=p->next){
        if (i->val < j->val){
            p->next = i;
            i = i->next;
        }
        else{
            p->next = j;
            j = j->next;
        }
    }
    p->next = (i ? i:j);
    return H.next;
}

链表合并时,不需要像数组那样,直接可以将链表尾部p->next指向剩余的i或j,即可完成合并。可以看出,归并排序更适合于对链表排序,而快速排序适合于数组排序。

 注:本文涉及的源码:merge sort : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/sort/mergesort.c

posted @   eudiwffe  阅读(28695)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示