排序进阶(快速排序完整版)
排序
冒泡排序:
一种比较容易理解的排序算法,可以优化的程度也比较有限
平均:O(n^2)
最坏:O(n^2)
最好:O(n)
最好情况需要在进行循环之前加入标志位,若一次遍历中,没有任何两个数据被交换,则认为序列已经有序,跳出循环。此时,时间复杂度为O(n)
插入排序:
选择一个数据插入到前面已经排好的有序队列之中
平均:O(n^2)
最坏:O(n^2)
最好:O(n)
这两种算法中规中矩,便于理解但是对于较大规模的数据排序而言,显得有些笨重,所以学习一下比较高级的排序算法。
快速排序(基本思路)
快速排序的基本思想是,选择随机数的同时将大于该元素的所有其他元素需要放到后面,将小于该元素的所有其他元素放到前面,然后将该元素放中间位置,该位置即为最后该元素确定的位置。
举个栗子:这是一个待排序的序列
3 4 2 7 8 6 1 9 5
此时,假设我们随机选择了数字6
那么调整后的数据为
3 4 2 1 5 X 7 9 8
此时我们可以很清楚的看到,在将大于6和小于6的元素放到两边后,此时中间空余的X位置即为6需要插入的位置。(如果我随机取得数据和比较的数据相等怎么办?即队伍中有相同元素我应该将它放到左边还是右边?这个问题的答案将在程序设计的过程中给出)
然后我们递归的处理左右两边的数组,即可最终得到排序后的数组。
程序设计(递归)
3 4 2 7 8 6 1 9 5 还是以这个数列举栗
进入函数时,应当是这样的情况
3 4 2 7 8 6 1 9 5
↑ ↑
left right
此时我们随机取得数据(取中间数据)
之所以选择取中间数据是因为,假设我们原有数据即为有序,我们每次都取得的数据在开头,那最后插入的位置还是在开头,最后得到的两列数据并没有得到合理的分割,此时,运行的时间复杂度仍为O(N^2),这种结果是很可惜的,所以尽量在中间取值,使得数据可以尽量随机。
3 4 2 7 - 6 1 9 5 8
↑ ↑ ↑ ↑
left mid right X
此时我们取得数据8,原先所在的位置mid已经为空
此时我们现在left所在的数据换到mid处,空出left
- 4 2 7 3 6 1 9 5 8
↑ ↑ ↑ ↑
left mid right X
然后进入循环:
right所在的数据与随机取得X比较
如果大于X,则right向左移动一格,即right- -
反之right所在的数据放到left处,left++
5 4 2 7 3 6 1 9 - 8
↑ ↑ ↑
left right X
若发生数据交换,则开始从left比较
如果小于X,则left向右移动一格,即left++
反之left所在的数据放到right处,right–
5 4 2 7 3 6 1 9 - 8
↑ ↑ ↑
left right X
5 4 2 7 3 6 1 9 - 8
↑ ↑ ↑
left right X
。
。
。
5 4 2 7 3 6 1 9 - 8
↑ ↑ ↑
left right X
5 4 2 7 3 6 1 - 9 8
↑ ↑
left X
right
此时left==right,退出循环,然后插入X,
5 4 2 7 3 6 1 8 9
↑
X
递归的对X左边部分和右边部分进行上述操作
申明函数,需要三个参数
1.数组 :待排序的数组
2.left :需要排序的左边界
3.right:需要排序的有边界
递归(int * 数组,int left,int right){
保存left
保存right
if(数组长度小于等于1){ //设置递归结束条件
break;
}
随机取得元素X
while(1){ //循环
if(left==right){ //循环结束判断
break;
}
//在左边寻找大于X的数放到右边
if(falg==1){
if(left所在的数据大于X){
将该数据放到right所在位置
right--
falg=0
}
else{
left++
}
}
//在右边寻找小于X的数放到左边
else{
省略了,代码里会给出
}
}
递归(递归(int * 数组,左边部分left,左边部分right)
递归(递归(int * 数组,右边部分left,右边部分right)
}
程序
//快速排序,封装,方便使用
int Quick_Sort(int *list, int n) {
int left, right;
if (n == 0 || n == 1) {
return 1;
}
left = 0;
right = n - 1;
QS(list, left, right);
return 1;
}
//快速排序递归
int QS(int *list, int left, int right) {
int L, R;
int mid;
int data,flag=1;
//保存左右边界
L = left;
R = right;
if (left >= right) {
return 1;
}
//取比较元素
mid = (left + right) / 2;
data = list[mid];
list[mid] = list[right];
while (1) {
//遍历结束后跳出循环,左右边界标志位相遇
if (left == right) {
list[left] = data;
break;
}
//从左边开始找大于比较元素的值插入到右标志位所在的地方
if (flag == 1) {
if (list[left] >= data) {
list[right] = list[left];
flag = 0;
right--;
}
else {
left++;
}
}
//从右边开始找小于比较元素的值插入到左标志位所在的地方
else {
if (list[right] <= data) {
list[left] = list[right];
flag = 1;
left++;
}
else {
right--;
}
}
}
//递归计算左边部分
QS(list, L, right-1);
//递归计算右边部分
QS(list, right + 1, R);
return 1;
}
测试(对100000个随机数进行排序)
int main() {
int i, n = 100000;
int list[100000];
clock_t start, finish;
double duration;
for (i = 0; i < n; i++) {
list[i] = rand();
}
start = clock();
//Bubble_Sort(&list[0], n);
//qsort(&list[0], 100000, sizeof(int), comp);
Quick_Sort(list, n);
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf("%f seconds\n", duration);
printf("The end ..");
while (1);
return 0;
}
速度出乎意料的快,100000数据排序时间仅为0.01s,比起常规排序20+s的时间而言简直是飞跃。
细心的可能看到我在测试函数还测试了冒泡排序和stdlib中内置的快速排序进行了比较,看看结果吧!
简单易懂的冒泡排序表现确实令人失望。。。
再来看看官方内置函数
有点出乎意料,官方内置的函数运行时间略差与我自己写的快排算法,大概是有些细节,官方函数考虑的更加周全浪费了一些时间?看了下源码基本上不可读,原因要未来有空好好研究了。