C语言的排序方法有很多种,最为简单粗暴的当属冒泡排序,除此之外还有许多其他的排序方法,今天着重讲一讲快速排序。
目录
快速排序算法
快速排序的核心思想是分治,基本思路是选择一个基准数,把基准数两侧的数字同其比较(这里假定排序后为升序序列),比基准数大的放在基准数的后面,而比基准数小的则放在其前面,然后在左右两边再次进行上述操作,直到基准数的左右只剩下一个数为止,这时排序完成,
假如说对这组数字进行升序排列:
首先我们选用3作为基准数,把3左边的所有大于3的数都放到右边,同时把3右边的所有小于3的数放到左边:
此时我们发现该基准数已经处在了正确的位置。
接着在该基准数的左右两部分分别采用相同的方法,以右边为例,选用8作为基准数:
进行一次操作后,8归位。
依照这个思路,可以实现排序。
void my_qsort(int* arr, int left, int right)
{
int i = left;
int j = right;
//假定*(arr + left)为基准数
int basic_num = *(arr + left);
if (i > j)
return;
//一轮交换
while (i < j)
{
//找基准数两边不满足的数字位置
//注意始终要保持i < j,因此每一个循环的条件i<j都必不可少
while (*(arr + j) >= basic_num && i < j)
j--;//这里顺序很重要
while (*(arr + i) <= basic_num && i < j)
i++;
//交换位置
if (i < j)
{
int tmp = *(arr + i);
*(arr + i) = *(arr + j);
*(arr + j) = tmp;
}
}//此时第i项的位置应为基准数归位后的位置
//交换第0项与第i项后基准数归位
arr[left] = arr[i];
arr[i] = basic_num;
//在递归处理基准数左边和右边的序列
my_qsort(arr, left, i - 1);
my_qsort(arr, i + 1, right);
}
例如对这一组序列排序:
int arr[10] = { 5,6,10,2,8,4,1,9,7,3 };
一轮循环实现步骤如下:
此时i = 4, j = 5,在进入循环,j--得4不满足i < j,直接跳出所有循环(所有循环条件均要满足i < j),并且此时i指向的位置刚好为基准数应该处在的位置,那么我们把arr[left]同arr[i]交换位置就可以把基准数5归位。
这也解释了为什么上面比较基准数和两边数大小的时候要注意顺序的问题。如果先考虑左边,那么基准数的位置就不再是arr[i]的位置了。
库函数之qsort函数
虽然我们已经实现了快速排序的算法,但这样的代码存在着一定的弊端。
即该函数并不能处理所有的数据类型,有时候我们也需要对字符串的内容进行排序,此时我们的代码显然就失效了。不过庆幸的是库里面专门有一个qsort函数,它可以处理任意数据类型的排序问题。
其定义于<stdlib.h>中。
void qsort( void *ptr, size_t count, size_t size,
int (*comp)(const void *, const void *) );
参数解释:
ptr | - | 指向待排序的数组的指针 |
count | - | 数组的元素数目 |
size | - | 数组每个元素的字节大小 |
comp | - | 比较函数。若首个参数小于第二个,则返回负整数值,若首个参数大于第二个,则返回正整数值,若两参数等价,则返回零。 |
用法举例
int comp(int* e1, int* e2)
{
return *e1 - *e2;
}
int main()
{
int arr[10] = { 5,6,10,2,8,4,1,9,7,3 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), comp);
int i = 0;
for (i = 0; i < sz; i++)
printf("%d ", *(arr + i));
return 0;
}
输出:
qsort函数的模拟实现
qsort函数能够处理所有的数据类型,首先需要它能够接收所有的类型,而void*指针刚好满足我们的要求,不过可惜的是void*指针能够接收任意类型的指针,却不能解引用,因此在每一步的操作过程中都需要强制类型转换,而要保证转换后所有类型的指针访问都没有问题,那么只能转换为指向内容最少的char*指针,这样所有的数据均可以读取到。
基于这个思路,可以首先写出函数框架:
void my_qsorts(void* ptr, int count, int size,int (*comp)(const void*, const void*))
{
int left = 0;
int right = count - 1;
my_qsort1((char*)ptr, left, right, size);
}
内部my_qsort1函数的实现大致与上述模拟的my_qsort相同:
void my_qsort1(char* arr, int left, int right, int size)
{
int i = left;
int j = right;
int k = 0;
char basic_num[100] = { 0 };
for (k = 0; k < size; k++)
{
basic_num[k] = *(arr + left * size + k);
}
if (i > j)
return;
while (i < j)
{
while (comp(arr + j * size, basic_num) >= 0 && i < j)
j--;
while (comp(arr + i * size, basic_num) <= 0 && i < j)
i++;
if (i < j)
swap(arr + j * size, arr + i * size, size);
}
swap(arr + i * size, arr + left * size, size);
my_qsort1(arr, left, i - 1, size);
my_qsort1(arr, i + 1, right, size);
}
虽然数据类型不确定,但我们可以保证使用者每次访问时都能找到其地址,这样使用者在使用函数时一定能得到正确的数据。
而对于两个数据的交换,只要找到起始位置,再根据它尺寸的大小一步步交换即可:
void swap(char* arr1, char* arr2, int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *(arr1 + i);
*(arr1 + i) = *(arr2 + i);
*(arr2 + i) = tmp;
}
}
而具体的比较方法,则交给使用者自己。
举例:
void test()
{
struct stu p[] = { {"张三", 18}, {"李四", 44}, {"王五", 36} };
int sz = sizeof(p) / sizeof(p[0]);
printf("测试根据结构体中的字符串排序\n");
my_qsorts(p, sz, sizeof(p[0]), cmp_p_name);
print_struct(p, sz);
printf("测试根据结构体中的整型排序\n");
my_qsorts(p, sz, sizeof(p[0]), cmp_p_age);
print_struct(p, sz);
}
int main()
{
test();
return 0;
}
这里的比较方法cmp_p_name和cmp_p_age都是需要使用者来书写的:
int cmp_p_name(const void* e1, const void* e2)
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
int cmp_p_age(const void* e1, const void* e2)
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
输出: