排序搜索之归并排序算法
二:归并排序算法
归并排序算法是基于互补过程的排序算法,它的优点主要有二:它是稳定的算法,对于任何输入,它的时间复杂度均为NlgN;它顺序的访问数据,因此可以高效的对链表等数据结构排序。它的缺点是所需的空间与N成正比,虽然我们可以克服这个缺点,但这样做非常复杂且开销巨大。
1.1 基本算法
归并排序算法首先将数组分为两个子数组来排序,然后合并这两个有序的子数组。
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中,归并完成。
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循环被简化了,实际运行效率反而更高。
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则成为主数组。
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的申请和拷贝操作封装起来,如下
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的有序子表。以此类推,直到整个数组有序。
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 链表实现
归并排序的数组实现中需要额外的内存空间,所以我们可以考虑使用链表来实现。也就是说,不使用辅助数组占用空间,而是使用链表。此时,归并排序算法没有使用额外的空间,实现了原位归并。
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; }