排序搜索之归并排序算法

二:归并排序算法

  归并排序算法是基于互补过程的排序算法,它的优点主要有二:它是稳定的算法,对于任何输入,它的时间复杂度均为NlgN;它顺序的访问数据,因此可以高效的对链表等数据结构排序。它的缺点是所需的空间与N成正比,虽然我们可以克服这个缺点,但这样做非常复杂且开销巨大。 

1.1 基本算法

  归并排序算法首先将数组分为两个子数组来排序,然后合并这两个有序的子数组。

mergesort
 1 /************************************
 2  函 数 名  : mergesort
 3  功能描述  : 归并排序算法
 4  输入参数  : [I/O] int a[]    待排序数组
 5              [I] int aux[]    辅助数组,
 6              [I] int count    待排序数组元素个数
 7  返 回 值  : 无
 8  备注      : 
 9  
10  修改历史      :
11   1.日    期   : 2013年1月10日
12     作    者   : leaf_yyl
13     修改内容   : 新生成函数
14 
15 ************************************/
16 void mergesort(int a[], int aux[], int count)
17 {
18     /* 递归终止条件 */
19     if(count <= 1)
20     {
21         return ;
22     }
23     
24     /* 以(count>>1)为界限将数组分为两个子数组,递归排序 */
25     mergesort(a, aux, count>>1);
26     mergesort(a + (count>>1), aux + (count>>1), count - (count>>1));
27     
28     /* 合并已排序的两个子数组 */
29     merge(a, aux, count);
30 }

合并数组,也就是上面的merge函数,是归并排序算法的核心。我们先考虑最简单的情况。mergesort中将数组a分为了两个有序的子数组al(数组a的前(count>>1)个元素)和ar(数组a的后(count - (count>>1))个元素),我们只需要将al和ar合并即可。我们使用一个循环:如果al为空,就从ar中取出一个一个元素放入aux中;如果ar为空,就从al中取出一个元素放入aux中;如果al和ar均不为空,那么将al和ar中剩余元素的最小者放入aux中。当al和ar均为空时,循环终止。然后再将辅助数组aux中的元素拷贝到主数组a中,归并完成。

merge
 1 void merge(int a[], int aux[], int count)
 2 {
 3     int cursor = 0;     /* 辅助数组游标 */
 4     int al = 0;         /* 左侧数组游标 */
 5     int ar = count>>1;  /* 右侧数组游标 */
 6     
 7     /* 合并子数组到辅助数组 */
 8     while(cursor < count)
 9     {
10         /* 和count/(count>>1)比较防止越界/合并错误 */
11         if( (count == ar)
12             || ((al < (count>>1)) 
13                 && (a[al] < a[ar])) )
14         {
15             aux[cursor++] = a[al++];
16         }
17         else
18         {
19             aux[cursor++] = a[ar++];
20         }
21     }
22 
23     /* 复制到原数组 */
24     cursor = 0;
25     while(cursor < count)
26     {
27         a[cursor] = aux[cursor];
28         cursor++;
29     }
30 }

1.2 性能和优化

  1.2.1 消除尾递归

    归并排序算法也是一个递归排序算法,消除尾递归会带来性能上的优化。我们将mergesort函数中的

/* 递归终止条件 */
if(count <= 1)
{
    return ;
}

修改为

/* 采用插入排序消除尾递归 */
if(count <= SORT_RECURSION_MIN_DEPTH + 1)    /* 见附录1 */
{
    insertion(a, 0, count - 1);              /* 见附录2 */
    return ;
}

即可。前人研究表明,SORT_RECURSION_MIN_DEPTH定义在5-20之间比较高效。

  1.2.2 消除测试操作

    如上,在merge函数的内循环里,我们包含了两个测试操作,用于确定对两个输入数组的访问是否到达了数组结尾。这两个测试通常为假,我们可以通过将第二个数组变为倒序来消除测试操作。此时数组a中最大的元素就成为了观察哨--无论它在子数组al还是ar中。由于需要确定输出数组是升序还是降序,我们需要修改一下mergesort函数,同时merge函数里也要判断升降序。虽然函数看起来大了一些,但由于while循环被简化了,实际运行效率反而更高。

mergesort(消除尾递归,使用观察哨)
 1 void mergesort(int a[], int aux[], int count, int flag)
 2 {
 3     /* 采用插入排序消除尾递归 */
 4     if(count <= SORT_RECURSION_MIN_DEPTH + 1)
 5     {
 6         insertion(a, 0, count - 1);
 7         return ;
 8     }
 9 
10     /* 将数组分为大小接近,先升后降/先降后升的两个子数组,递归排序 */
11     mergesort(a, aux, count>>1, flag);
12     mergesort(a + (count>>1), aux + (count>>1), count - (count>>1), !flag);
13 
14     /* 合并已排序的两个子数组 */
15     merge(a, aux, count, flag);
16 }
17 
18 void merge(int a[], int aux[], int count, int flag)
19 {
20     int cursor = 0;     /* 辅助数组游标 */
21     int al = 0;         /* 左侧数组游标 */
22     int ar = count - 1; /* 右侧数组游标 */
23 
24     if(flag)
25     {
26         /* 升序排列合并数组到辅助数组 */
27         while(cursor < count)
28         {
29             if(a[al] < a[ar])
30             {
31                 aux[cursor++] = a[al++];
32             }
33             else
34             {
35                 aux[cursor++] = a[ar--];
36             }
37         }
38     }
39     else
40     {
41         /* 降序排列合并数组到辅助数组 */
42         while(cursor < count)
43         {
44             if(a[al] > a[ar])
45             {
46                 aux[cursor++] = a[al++];
47             }
48             else
49             {
50                 aux[cursor++] = a[ar--];
51             }
52         }
53     }
54 
55     /* 复制到原数组 */
56     cursor = 0;
57     while(cursor < count)
58     {
59         a[cursor] = aux[cursor];
60         cursor++;
61     }
62 }

  1.2.3 消除复制操作

    在merge函数里我们使用辅助数组aux来保存已排序的元素,再将这些元素拷贝回去:这些拷贝显然是费时的。一个可行的策略是通过交换主数组a和辅助数组aux来减少这些拷贝。在将主数组a分为两个子数组时,子数组直接保存在辅助数组aux里,同时使用主数组a作为下次递归调用的辅助数组,而辅助数组aux则成为主数组。

 

mergesort(消除复制操作)
 1 void mergesort(int a[], int aux[], int count, int flag)
 2 {
 3     /* 采用插入排序消除尾递归 */
 4     if(count <= SORT_RECURSION_MIN_DEPTH + 1)
 5     {
 6         insertion(a, 0, count - 1);
 7         return ;
 8     }
 9 
10     /* 将数组分为大小接近,先升后降/先降后升的两个子数组 */
11     /* 子数组保存在数组aux中,并使用数组a做为下次递归调用的辅助数组 */
12     mergesort(aux, a, count>>1, flag);
13     mergesort(aux + (count>>1), a + (count>>1), count - (count>>1), !flag);
14 
15     /* 将aux数组中元素合并到数组a */
16     merge(aux, a, count, flag);
17 }
18 
19 void merge(int a[], int aux[], int count, int flag)
20 {
21     int cursor = 0;     /* 辅助数组游标 */
22     int al = 0;         /* 左侧数组游标 */
23     int ar = count - 1; /* 右侧数组游标 */
24 
25     if(flag)
26     {
27         /* 升序排列合并数组到辅助数组 */
28         while(cursor < count)
29         {
30             if(a[al] < a[ar])
31             {
32                 aux[cursor++] = a[al++];
33             }
34             else
35             {
36                 aux[cursor++] = a[ar--];
37             }
38         }
39     }
40     else
41     {
42         /* 降序排列合并数组到辅助数组 */
43         while(cursor < count)
44         {
45             if(a[al] > a[ar])
46             {
47                 aux[cursor++] = a[al++];
48             }
49             else
50             {
51                 aux[cursor++] = a[ar--];
52             }
53         }
54     }
55 }

 

 

 

这样,我们将原先NlgN次的复制操作减少到了N次,但需要在调用归并排序算法前将主数组a的元素拷贝到辅助数组aux中。我们可以在mergesort外再封装一层,把辅助数组aux的申请和拷贝操作封装起来,如下

 

mergesort(傻瓜版)
 1 int mergesort(int a[], int count)
 2 {
 3     int* aux = NULL;
 4 
 5     /* 申请空间 */
 6     aux = (int*)malloc(count * sizeof(int));
 7     if(NULL == aux)
 8     {
 9         SYS_DEBUG_ERROR("Memory alloc failed!");     /* 见附录1 */
10         return SYS_ERR;                              /* 见附录1 */
11     }
12 
13     /* 拷贝数组并排序 */
14     memcpy(aux, a, count * sizeof(int));
15     m_sort(a, aux, count, SORT_ASCENDING_ORDER);     /* 见附录1 */
16 
17     /* 释放内存,函数返回 */
18     Safe_Free(aux);                                  /* 见附录1 */
19     return SYS_OK;                                   /* 见附录1 */
20 }
21 
22 void m_sort(int a[], int aux[], int count, int flag)
23 {
24     /* 采用插入排序消除尾递归 */
25     if(count <= SORT_RECURSION_MIN_DEPTH + 1)
26     {
27         insertion(a, 0, count - 1);
28         return ;
29     }
30 
31     /* 将数组分为大小接近,先升后降/先降后升的两个子数组 */
32     /* 子数组保存在数组aux中,并使用数组a做为下次递归调用的辅助数组 */
33     m_sort(aux, a, count>>1, flag);
34     m_sort(aux + (count>>1), a + (count>>1), count - (count>>1), !flag);
35 
36     /* 将aux数组中元素合并到数组a */
37     merge(aux, a, count, flag);
38 }
39 
40 void merge(int a[], int aux[], int count, int flag)
41 {
42     int cursor = 0;     /* 辅助数组游标 */
43     int al = 0;         /* 左侧数组游标 */
44     int ar = count - 1; /* 右侧数组游标 */
45 
46     if(flag)
47     {
48         /* 升序排列合并数组到辅助数组 */
49         while(cursor < count)
50         {
51             if(a[al] < a[ar])
52             {
53                 aux[cursor++] = a[al++];
54             }
55             else
56             {
57                 aux[cursor++] = a[ar--];
58             }
59         }
60     }
61     else
62     {
63         /* 降序排列合并数组到辅助数组 */
64         while(cursor < count)
65         {
66             if(a[al] > a[ar])
67             {
68                 aux[cursor++] = a[al++];
69             }
70             else
71             {
72                 aux[cursor++] = a[ar--];
73             }
74         }
75     }
76 }

 

 

 

  1.2.4 原位归并

    归并排序算法使用了与N成正比的额外数组空间以保证归并的效率,有没有不使用额外的数组而进行原位归并的方法呢?一个简单的想法是在merge函数里使用插入排序,此时虽然实现了原位归并,但是时间复杂度也增加到了N2,显然不可取。如果大家有什么好的方法的话,欢迎指教~~~

1.3 归并排序算法的非递归实现和链表实现

  1.3.1 非递归实现

    我们将数组中的所有元素看做大小为1的有序子表,遍历这些有序子表进行1-1归并,产生大小为2的有序子表,然后遍历数组进行2-2归并,产生大小为4的有序子表;然后进行4-4归并产生大小为8的有序子表。以此类推,直到整个数组有序。

mergesort(非递归实现)
 1 int mergesort(int a[], int count)
 2 {
 3     int i, j;
 4     int* aux = NULL;
 5 
 6     /* 申请空间 */
 7     aux = (int*)malloc(count * sizeof(int));
 8     if(NULL == aux)
 9     {
10         SYS_DEBUG_ERROR("Memory alloc failed!");
11         return SYS_ERR;
12     }
13 
14     /* 循环合并有序子表 */
15     for(i = 1; i < count; i += i)
16         for(j = 0; j < count; j += (i << 1))
17         {
18             merge(a + j, aux + j, i, min(2*i, count - j));
19         }
20 
21     return SYS_OK;
22 }
23 
24 void merge(int a[], int aux[], int m, int count)
25 {
26     int cursor = 0;     /* 辅助数组游标 */
27     int al = 0;         /* 左侧数组游标 */
28     int ar = m;         /* 右侧数组游标 */
29 
30     /* 合并子数组到辅助数组 */
31     while(cursor < count)
32     {
33         /* 和count/(m)比较防止越界/合并错误 */
34         if( (count <= ar)
35             || ((al < m) 
36                 && (a[al] < a[ar])) )
37         {
38             aux[cursor++] = a[al++];
39         }
40         else
41         {
42             aux[cursor++] = a[ar++];
43         }
44     }
45 
46     /* 复制到原数组 */
47     cursor = 0;
48     while(cursor < count)
49     {
50         a[cursor] = aux[cursor];
51         cursor++;
52     }
53 }

  1.3.2 链表实现

    归并排序的数组实现中需要额外的内存空间,所以我们可以考虑使用链表来实现。也就是说,不使用辅助数组占用空间,而是使用链表。此时,归并排序算法没有使用额外的空间,实现了原位归并。

mergesort(双向链表上的原位归并)
 1 struct tagClistcell
 2 {
 3     struct tagClistcell* pre;
 4     struct tagClistcell* next;
 5     void* data;
 6 };
 7 typedef struct tagClistcell clistcell;
 8 
 9 typedef struct tagClist
10 {
11     clistcell* iter;
12     int     count;
13 }clist;
14 
15 void mergesort(clist* l)
16 {
17     int i, j;
18     clistcell* iter = l->iter;
19 
20     /* 循环归并链表上的有序子链 */
21     for(i = 1; i < l->count; i += i)
22     {
23         for(j = 0; j < l->count; j += 2*i)
24         {
25             /* merge函数返回下一次归并操作开始的节点 */
26             iter = merge(iter, i, min(2*i, l->count - j));
27         }
28     }
29 
30     /* 链表的头结点需要偏移到iter位置 */
31     l->iter = iter;
32 }
33 
34 clistcell* merge(clistcell* iter, int m, int count)
35 {
36     int i, j, cursor;
37     clistcell* iter_r = iter;
38     clistcell* temp = NULL;
39 
40     /* 找到右侧链表起始位置 */
41     for(i = 0; i < m; i++)
42     {
43         iter_r = iter_r->next;
44     }
45 
46     /* 在双向循环链表上实现原位归并 */
47     for(i = m, j = 0, cursor = 0; cursor < count; cursor++)
48     {
49         if((i >= count) || (j >= m) || LESS(iter->data, iter_r->data))  /* 见附录3 */
50         {
51             iter = iter->next;
52             j++;
53         }
54         else
55         {
56             temp = iter_r->next;
57             clist_cellmove_before(iter, iter_r);        /* 见附录4 */
58             iter_r = temp;
59             i++;
60         }
61     }
62 
63     return iter;
64 }

1.4 附录

1:

    #define SORT_RECURSION_MIN_DEPTH   10
    #define SYS_DEBUG_ERROR    printf
    #define SYS_ERR     -1
    #define SORT_ASCENDING_ORDER       1
    #define Safe_Free(data)    if(data){free(data), data = NULL;}
    #define SYS_OK      0

2:

/* 插入排序 */
void insertion(int a[], int start, int end)
{
    int i, j;
    for(i = start + 1; i <= end; i++)
    {
        for(j = i; j > start; j--)
        {
            if(a[j] < a[j - 1])
            {
                EXCH(a[j], a[j - 1])
            }
            else
            {
                break;
            }
        }
    }
}

3: 

    #define LESS(a, b) (*(int*)(a) < *(int*)(b))

(此处定义只是为了方便测试,大家可以根据实际情况做出更好的宏定义)

4:

/* 移动链表中元素位置 */
void clist_cellmove_before(clistcell* local, clistcell* mover)
{
    mover->pre->next = mover->next;
    mover->next->pre = mover->pre;

    mover->next = local;
    mover->pre = local->pre;

    local->pre->next = mover;
    local->pre = mover;
}

 

 

 

posted @ 2013-01-10 20:37  7星聚会  阅读(277)  评论(0编辑  收藏  举报