系统程序员成长计划-动态数组(三)(下)
转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
排序
对于前面提的两点额外要求:
o 算法同时支持升序和降序。
o 算法同时支持多种数据类型。
只要认真阅读过前面章节的读者,马上会想到用回调函数。这是对的。软件设计的关键在于熟能生巧,我们反复练习这些基本技巧也意在于此。熟到凭本能就可以运用正确的方法时,那也离所谓的高手不远了。言归正传,我们已经定义过比较回调函数的原型了:
typedef int (*DataCompareFunc)(void* ctx, void* data);
我们先实现一个整数的比较函数,升序比较函数的实现如下:
int int_cmp(void* a, void* b)
{
return (int)a - (int)b;
}
降序比较函数的实现如下:
int int_cmp_invert(void* a, void* b)
{
return (int)b - (int)a;
}
比较函数也不依赖于具体的数据类型,这样我们就把算法的变化部分独立出来了,是升序还是降序完全由回调函数决定。下面我们看看算法的具体实现。
冒泡排序
冒泡排序的名字很形象的表达了算法的原理,对于降序排列时,排序可以认为是轻的物体不断往上浮的过程。不过对于升序排序,排序认为是重的物体不断向 下沉更为合适。升序排列更符合人类的思考方式,这里我们按升序排序来实现冒泡排序(通过使用不同的比较函数,同样支持降序排序)。
Ret bubble_sort(void** array, size_t nr, DataCompareFunc cmp)
{
size_t i = 0;
size_t max = 0;
size_t right = 0;
return_val_if_fail(array != NULL && cmp != NULL, RET_INVALID_PARAMS);
if(nr < 2)
{
return RET_OK;
}
for(right = nr - 1; right > 0; right--)
{
for(i = 1, max = 0; i < right; i++)
{
if(cmp(array[i], array[max]) > 0)
{
max = i;
}
}
if(cmp(array[max], array[right]) > 0)
{
void* data = array[right];
array[right] = array[max];
array[max] = data;
}
}
return RET_OK;
}
冒泡排序是最简单直观的排序算法,教科书上通常作为第一个排序算法来讲。从性能上来看,与其它高级排序算法相比,它似乎没有存在的理由。它除了教学目的之外,是否有实际价值呢?答案是有的。原因有两点:
o实现简单,简单的程序通常更可靠。虽然我很多年没有写过冒泡排序算法了,从写代码、编译到测试,都一次性通过了。写快速排序时却出了好几次错误, 而且最后参考了教科书才完成。如果非要自己动手写排序算法时,我会先写一个冒泡排序算法,直到一定需要更快的算法时才会考虑其它算法。
o 在小量数据时,所有排序算法性能差别不大。有文章指出,高级排序算法在元素个数多于1000时,性能才出现显著提升。在90%的情况下,我们存储的元素个数只有几十到上百个而已,比如进程数、窗口个数和配置信息等等的数量都不会很大,冒泡排序其实是更好的选择。
请记住:在完成同样任务的情况下,越简单越好。
同时记住:学从难处学,用从易处用。
快速排序
快速排序当然是以其性能优异出名了,而且它不需要额外的空间。如果数据量大而且全部在内存中时,快速排序是首选的排序方法。排序过程是先将元素分成 两个区,所有小于某个元素的值在第一个区,其它元素在第二区。然后分别对这两个区进行快速排序,直到所分的区只剩下一个元素为止。
void quick_sort_impl(void** array, size_t left, size_t right, DataCompareFunc cmp)
{
size_t save_left = left;
size_t save_right = right;
void* x = array[left];
while(left < right)
{
while(cmp(array[right], x) >= 0 && left < right) right--;
if(left != right)
{
array[left] = array[right];
left++;
}
while(cmp(array[left], x) <= 0 && left < right) left++;
if(left != right)
{
array[right] = array[left];
right--;
}
}
array[left] = x;
if(save_left < left)
{
quick_sort_impl(array, save_left, left-1, cmp);
}
if(save_right > left)
{
quick_sort_impl(array, left+1, save_right, cmp);
}
return;
}
Ret quick_sort(void** array, size_t nr, DataCompareFunc cmp)
{
Ret ret = RET_OK;
return_val_if_fail(array != NULL && cmp != NULL, RET_INVALID_PARAMS);
if(nr > 1)
{
quick_sort_impl(array, 0, nr - 1, cmp);
}
return ret;
}
战胜软件复杂度是《系统程序员成长计划》的中心思想之一。战胜软件复杂度包括防止复杂度增长和降低复杂度两个方面。降低复杂度的方法主要有抽象和分而治之两种,快速排序则是分而治之的具体体现。
归并排序
与快速排序一样,归并排序也是分而治之的应用。不同的是,它先让左右两部分进行排序,然后把它们合并起来。在排序左右两部分时,同样使用归并排序。快速排序可以认为是自顶向下的方法,而归并排序可以认为是自底向上的方法。
static Ret merge_sort_impl(void** storage, void** array, size_t low, size_t mid, size_t high, DataCompareFunc cmp)
{
size_t i = low;
size_t j = low;
size_t k = mid;
if((low + 1) < mid)
{
size_t x = low + ((mid - low) >> 1);
merge_sort_impl(storage, array, low, x, mid, cmp);
}
if((mid + 1) < high)
{
size_t x = mid + ((high - mid) >> 1);
merge_sort_impl(storage, array, mid, x, high, cmp);
}
while(j < mid && k < high)
{
if(cmp(array[j], array[k]) <= 0)
{
storage[i++] = array[j++];
}
else
{
storage[i++] = array[k++];
}
}
while(j < mid)
{
storage[i++] = array[j++];
}
while(k < high)
{
storage[i++] = array[k++];
}
for(i = low; i < high; i++)
{
array[i] = storage[i];
}
return RET_OK;
}
Ret merge_sort(void** array, size_t nr, DataCompareFunc cmp)
{
void** storage = NULL;
Ret ret = RET_OK;
return_val_if_fail(array != NULL && cmp != NULL, RET_INVALID_PARAMS);
if(nr > 1)
{
storage = (void**)malloc(sizeof(void*) * nr);
if(storage != NULL)
{
ret = merge_sort_impl(storage, array, 0, nr>>1, nr, cmp);
free(storage);
}
}
return ret;
}
归并排序需要额外的存储空间,其为被排序的数组一样大。大部分示例代码里,都在每次递归调用中分配空间,这些会带来性能上的下降。这里我们选择了事先分配一块空间,在排序过程中重复使用,算法更简单,性能也得到提高。
根据把要排序的数组分成N个部分,可以把归并排序称为N路排序。上面实现的归并排序实际是归并算法一个特例:两路归并。看似归并排序与快速排序相比 几乎没有优势可言,但是归并排序更重要的能力在于处理大量数据的排序,它不要求被排序的数据全部在内存中,所以在数据大于内存的容纳能力时,归并排序就更 能大展身手了。归并排序最常用的地方是数据库管理系统(DBMS),因为数据库中存储的数据通常无法全部加载到内存中来的。有兴趣的读者可以阅读相关资 料。
排序算法的测试
排序算法的实现不同,但它们的目的都一样:让数据处于有序状态。所以在写自动测试时,没有必要为每一种算法写一个测试程序。通过将排序算法作为回调函数传入,我们可以共用一个测试程序:
static void** create_int_array(int n)
{
int i = 0;
int* array = (int*)malloc(sizeof(int) * n);
for(i = 0; i < n; i++)
{
array[i] = rand();
}
return (void**)array;
}
static void sort_test_one_asc(SortFunc sort, int n)
{
int i = 0;
void** array = create_int_array(n);
sort(array, n, int_cmp);
for(i = 1; i < n; i++)
{
assert(array[i] >= array[i-1]);
}
free(array);
return;
}
void sort_test(SortFunc sort)
{
int i = 0;
for(i = 0; i < 1000; i++)
{
sort_test_one_asc(sort, i);
}
return ;
}
集成排序算法到动态数组中
把排序算法集中到动态数组并不合适,原因有:
o 绑定动态数组与特定算法不如让用户根据需要去选择。
o在动态数组中实现排序算法不利于算法的重用。
所以我们给动态数组增加一个排序函数,但排序算法通过回调函数传入:
Ret darray_sort(DArray* thiz, SortFunc sort, DataCompareFunc cmp);
本节示例代码请到这里下载。