快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小
,然后再按此方法对这两部分数据分别进行快速排序
,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序算法通过多次比较和交换
来实现排序,其排序流程如下:
(1)首先设定一个分界值(这个分界值用枢纽来表示,pivot)
,通过该分界值将数组分成左右两部分
。
(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
(3)然后,左边和右边的数据可以独立排序
。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归
定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
所以这里主要讲解分界值的几种选择方式:
①将数组的第一个元素作为枢纽pivot:
代码流程:
- 定义一个临时变量pivot,将数组的第一个元素赋值给这个变量pivot
- 定义两个变量i,j,表示这个数组的起点、终点对应的下标.
- 先开始从右往左遍历,找到第一个小于pivot的数字arr[j],然后将这个数赋值给arr[i]
- 从左往右开始遍历,找到哦第一个大于pivot的数字arr[i],然后将这个数赋值给arr[j]
- 重复上述操作,直到i >= j
- 将pivot的值赋值给arr[i],这时候我们已经完成了一次快速排序
- 上诉步骤中的i是中间枢纽下标,即arr[i](即pivot)之前都是小于等于pivot的数,arr[i]之后的都是大于等于pivot的数,从而实现了将一个数组分割成为两个数组,然后对这两个数组进行快速排序(利用递归的方式)。当递归结束之后,整个数组有序。
对应的代码(实现了含有重复数字
的快速排序):
#include<stdio.h>
#define MAX_SIZE 1000
int count = 0;//表示进行了多少趟快速排序
void display(int arr[],int len){
int i;
for(i = 0; i < len; i++)
printf("%5d",arr[i]);
printf("\n");
}
/*
以第一个元素作为中间枢纽:
这时候,从数组的两边开始遍历
1、定义一个临时变量,表示中间枢纽pivot,
2、定义两个下标i,j,其中i = 数组第一个元素的下标low,j = 数组最后一个元素对应的下标high
3、在i < j的前提下,不断进行下面的操作:
①从右往左开始遍历,找到第一个小于pivot的数
找到之后,将这个数赋值给arr[i]
②从左往右开始遍历,找到第一个大于等于pivot的数
找到之后,我们需要判断i是否依旧小于j,如果是,就将
这个数赋值给arr[j]
4、重复上述的步骤,直到i >= j退出循环,这时候还没有完成,需要将我们定义的pivot赋值给
arr[i],只有这样才会实现一趟快速排序
5、i下标对应的是中间枢纽的下标,它前面都是小于等于pivot的数,后面都是大于等于pivot的数,
这时候我们利用递归的方式,分别对这两个子数组进行快速排序,从而实现了排序
*/
void quickSort2(int arr[],int low,int high,int n){
if(low < high){
int pivot,i,j;
pivot = arr[low];//以数组的第一个数作为枢纽
i = low;
j = high;
while(i < j){
while(i < j && arr[j] >= pivot) //从右往左开始遍历,直到找到小于pivot的数
j--;
if(i < j)
arr[i] = arr[j];//将小于pivot的数赋值给arr[i],从而实现左边都是小于pivot的数
while(i < j && arr[i] <= pivot)//从左往右开始遍历,直到找到大于pivot的数
i++;
if(i < j) //在没有相遇的时候,将大于pivot的数赋值给arr[j],从而实现右边都是大于pivot的数
arr[j] = arr[i];
}
//退出循环之后,需要将原来的中间枢纽的值赋值给arr[i]
arr[i] = pivot;
count++;
printf("第%d次快速排序:",count);
display(arr,n);//输出一趟快速排序之后的数组
//利用递归实现快速排序
quickSort2(arr,low,i - 1,n);
quickSort2(arr,i + 1,high,n);
}
}
int main(){
int arr[MAX_SIZE],n;
int i,j;
scanf("%d",&n);
for(i = 0; i < n; i++)
scanf("%d",&arr[i]);
printf("没有进行排序时:");
display(arr,n);
printf("进行快速排序:\n");
quickSort2(arr,0,n - 1,n);
return 0;
}
运行结果:
通过观察可以发现,这样进行排序的话,即使有重复数字,也可以实现快速排序。但是以数组的第一个元素作为中间枢纽真的合适嘛?如果我一开始输入的元素就已经是有序了,那么这时候,最后造成的快速排序最坏的情况,时间复杂度为O(n²),但是由于输入是有序的,实际却没有任何操作。所以使用第一个元素并不是适用于任何情况。
②利用三数中值法获取枢纽。
对应的代码(实现了含有重复数字
的快速排序):
#include<stdio.h>
#define MAX_SIZE 1000
int count = 0;//表示进行了多少趟快速排序
void display(int arr[],int n){
int i;
for(i = 0; i < n; i++)
printf("%5d",arr[i]);
printf("\n");
}
void swap(int arr[],int i,int j){
int tmp;
tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
int getPivot(int arr[],int low,int high){
int mid = (low + high) / 2;
//将首尾及中间的这三个数进行排序,从而实现arr[low] < arr[mid] < arr[high]
if(arr[low] > arr[mid])
swap(arr,low,mid);
if(arr[low] > arr[high])
swap(arr,low,high);
if(arr[mid] > arr[high])
swap(arr,mid,high);
//将arr[mid]和arr[high - 1]这两个数进行交换,从而实现下标在[1,high - 1]中
swap(arr,mid,high - 1);
return arr[high - 1];//返回中间枢纽
}
void quickSort3(int arr[],int low,int high,int n){
if(low < high){
//如果low < high的时候,说明这个子数组不止一个元素,所以需要进行快速排序,否则,不需要再次进行快速排序
int pivot = getPivot(arr,low,high);//利用三数中值法,获取这个数组中的枢纽
int i,j;
i = low;
j = high - 1; //j下标表示中间枢纽的起初下标
while(i < j){
while(arr[++i] <pivot); //这里先进行自增,然后再将对应下标的值进行比较,从而保证了含有重复数字的时候不会陷入死循环
while(arr[--j] > pivot);
if(i >= j)
break;
else
swap(arr,i,j);//如果不相等,那么就将两者进行交换
}
//退出循环之后,需要将arr[i]和arr[high - 1]进行交换,从而实现一次快速排序的完成
swap(arr,i,high - 1);
count++;
printf("第%d次快速排序:\n",count);
display(arr,n);
//对两个子数组进行快速排序
quickSort3(arr,low,i - 1,n);
quickSort3(arr,i + 1,high,n);
}
}
int main(){
int arr[MAX_SIZE],n;
int i,j;
scanf("%d",&n);
for(i = 0; i < n; i++)
scanf("%d",&arr[i]);
printf("没有进行排序时:");
display(arr,n);
printf("进行快速排序:\n");
quickSort3(arr,0,n - 1,n);
return 0;
}
运行结果:
有小伙伴会问,三数中值法那里获取pivot里面,将数组的首尾及中间的数字进行排序之后,为什么不直接返回arr[mid],而是返回arr[high - 1]?
如果返回是arr[mid]的话,这时候使用quickSort3方法的时候,j的初始值改变成为high,但是即使这样,对应的结果依旧是错误的。就拿20 15 46 23 60 14 74 15 95 86为例,最后得到的并不是有序的。(具体的大家可以去分析一下)
如果返回的是arr[mid],并且quickSort3的代码改成下面的样子可以嘛?
下面的代码只能实现没有重复数字的排序
:
void quickSort3(int arr[],int low,int high,int n){
if(low < high){
//如果low < high的时候,说明这个子数组不止一个元素,所以需要进行快速排序,否则,不需要再次进行快速排序
int pivot = getPivot(arr,low,high);//利用三数中值法,获取这个数组中的枢纽
int i,j;
/*
返回的pivot是arr[mid]的时候,这时候,代码可以实现没有重复数字的快
速排序,但是如果含有重复数字,并且这个数字不巧的作为了pivot,那么
就会陷入死循环中
*/
i = low + 1;//low下标的数是小于pivot的数
j = high - 1;//high下标的数是大于pivot的数
while(i < j){
while(i < j && arr[i] <pivot)
i++;
while(i < j && arr[j] > pivot)
j--;
if(i >= j)
break;
else
swap(arr,i,j);//如果不相等,那么就将两者进行交换
}
*/
//退出循环之后,需要将arr[i]和arr[high - 1]进行交换,从而实现一次快速排序的完成
count++;
printf("第%d次快速排序:\n",count);
display(arr,n);
//对两个子数组进行快速排序
quickSort3(arr,low,i - 1,n);
quickSort3(arr,i + 1,high,n);
}
}
运行结果:
但是如果枢纽重复的情况时,就会陷入死循环
中。所以如果使用上面的代码,并且返回的是arr[mid]的时候,那么这时候这个代码只能适用于没有重复数字的快速排序
。
快速排序的优化:
int getPivot(int arr[],int low,int high){
int mid = (low + high) / 2;
//将首尾及中间的这三个数进行排序,从而实现arr[low] < arr[mid] < arr[high]
if(arr[low] > arr[mid])
swap(arr,low,mid);
if(arr[low] > arr[high])
swap(arr,low,high);
if(arr[mid] > arr[high])
swap(arr,mid,high);
/*
如果直接返回arr[mid]的话,那么这时候,如果使用原来返回arr[high -
1]的快速排序的代码,那么会造成一定的错误(例如20 15 46 23 60 14
74 15 95 86这个例子)如果更改对应的代码,那么虽然可以实现pivot不重
复的快速排序,但是考虑到数字的随机性,所以
这样是不构完善的
*/
//将arr[mid]和arr[high - 1]这两个数进行交换,从而实现下标在[1,high - 1]中
swap(arr,mid,high - 1);
return arr[high - 1];//返回中间枢纽
//return arr[mid];
}
void insertSort(int arr[],int low,int high){
int i,j,tmp;
for(i = low + 1; i < high; i++){
tmp = arr[i];
for(j = i - 1; j >= 0; j--){
if(arr[j] <= tmp)
break;
arr[j+1] = arr[j];
}
arr[j + 1] = tmp;
}
}
void quickSort3(int arr[],int low,int high,int n){
if(low + N < high){
//如果low < high的时候,说明这个子数组不止一个元素,所以需要进行快速排序,否则,不需要再次进行快速排序
int pivot = getPivot(arr,low,high);//利用三数中值法,获取这个数组中的枢纽
int i,j;
i = low;
j = high - 1; //j下标表示中间枢纽的起初下标
while(i < j){
while(i < j && arr[++i] <pivot); //这里先进行自增,然后再将对应下标的值进行比较,从而保证了含有重复数字的时候不会陷入死循环
while(i < j && arr[--j] > pivot);
if(i >= j)
break;
else
swap(arr,i,j);//如果不相等,那么就将两者进行交换
}
swap(arr,i,high - 1);
count++;
printf("第%d次排序:\n",count);
display(arr,n);
//对两个子数组进行快速排序
quickSort3(arr,low,i - 1,n);
quickSort3(arr,i + 1,high,n);
}else{
//当数组的个数少于10个时候,那么就是(小数组)用插入排序
count++;
insertSort(arr,low,high);
printf("第%d次排序:\n",count);
display(arr,n);
}
}