排序算法-7-快速排序

快速排序

前面讲了插入排序选择排序冒泡排序归并排序以及冒泡排序的改进版鸡尾酒排序和插入排序的改进版希尔排序,下面来讲一种很经常使用的排序方法:快速排序。html

快速排序既然敢以快速命名,能够想见它的排序速度是很快的。事实也是如此,在实际应用中它的平均性能很是好,所以在通常状况下属于应用中的首选排序方式。web

原理

快速排序与归并排序同样用了分治的思想。把一个要排序的数组按某个元素值(通常称做主元)进行划分,分红两边,大于主元的放在一边,小于的放在另外一边,主元放两边的中间;而后再分别排序两边那两个子数组,这样就完成了排序。而那两个子数组排序的时候一样能够用快速排序再分红两个子数组进行分别排序。以此类推,能够一直分到分无可分,即数组中只有一个元素为止。数组

从上面的过程能够看出,快速排序是能够在原址上排序的,因此不须要额外的空间进行归并。其技巧在于如何按主元进行划分子数组。app

划分的过程有多种,这里咱们选择两种常见的。svg

  1. 选取最左边的元素为主元,从右往左过一遍,将大于主元的保持在右边区域,小于的放到左边区域,最后把主元放到两个区域的中间。右边区域与左边区域在最后一步以前始终是挨着的,左边区域要增长元素直接增长便可,右边区域要增长则须要从左边区域的最右边给腾出一个位置,放入该元素,并把左边区域的最右边元素移到左边区域的最左边。如此从右往左过一遍以后便可划分好。
  2. 一样选取最左边的元素为主元,采用两头往中间的方式过一遍全部元素,将大于主元的保持在右边区域,小于的放到左边区域,最后把主元放到两个区域的中间。右边区域从最右边开始增加,左边区域从最左边开始增加,两个区域直到最后才接触到一块儿。具体方法是先从右往左找到第一个小于主元的元素,将其放到左边区域的最右边,而后从左往右找到第一个大于主元的元素,将其放到右边区域的最左边;如此循环直到全部元素一遍过完,最后将主元放到中间。

实现

按照以上原理咱们来用代码实现。函数

下面就是用C语言实现的代码。分红三个函数来实现。性能

  • 要排序的数组a有n个元素。
  • quick_sort 函数进行封装,调用 quick_sort_。
  • quick_sort_ 调用 partition 函数进行子数组的切分,将数组a[low…high]切分红 a[low…pivot-1] 和 a[pivot+1…high] 两个子数组,而后分别对两个子数组递归调用 quick_sort_ 进行排序。
  • partition 的实现分两种,partition1 按第一种划分方式,partition2 按第二种划分方式。
void quick_sort(int a[], int n)
{
	if (n<=0) return;

	quick_sort_(a, 0, n-1);
}

void quick_sort_(int a[], int low, int high)
{
	if (low >= high) return;

	int pivot = 0;
	pivot = partition1(a, low, high);
	//pivot = partition2(a, low, high);
	quick_sort_(a, low, pivot-1);
	quick_sort_(a, pivot+1, high);
}

int partition1(int a[], int low, int high)
{
	int x = a[low];                 //取a[low]的值x做为主元
	/* 从右往左看,大于主元的保持在右边区域,小于的放到左边区域 */
	int i = high + 1;               //i指向右边区域的最左边
	for (int j=high; j>low; j--) {  //j指向左边区域的最左边
		if (a[j] >= x) {            //如有元素要放到右边区域
			i--;                    //从左边区域的最右边给腾出一个位置
			swap(&a[i], &a[j]);     //放入该元素,并把左边区域的最右边元素移到左边
		}                           // 区域的最左边,此时j依然指向左边区域的最左边
	}
	swap(&a[low], &a[i-1]);         //将主元放到中间
	return i-1;                     //返回主元所在位置下标
}

int partition2(int a[], int low, int high)
{
	int x = a[low];  //取a[low]的值x做为主元
	/* 把小于x的元素放到左边区域,其他放到右边区域,x放到中间 */
	while (low < high) {
		while (low<high && a[high]>=x) high--; //从右往左找到第一个小于主元x的元素
		a[low] = a[high];                      //将其放到左边区域的最右边
		while (low<high && a[low]<=x) low++;   //从左往右找到第一个大于主元x的元素
		a[high] = a[low];                      //将其放到右边区域的最左边
	}
	a[low] = x;      //将主元放到中间
	return low;      //返回主元所在位置下标
}

void swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

为了验证此函数的效果,加上了以下辅助代码,对3个数组进行排序,运行结果在最后,可见排序成功。ui

#include <stdio.h>
#include <stdlib.h>

#define SIZE_ARRAY_1 5
#define SIZE_ARRAY_2 6
#define SIZE_ARRAY_3 20

void quick_sort(int a[], int n);
void show_array(int a[], int n);

void main()
{
	int array1[SIZE_ARRAY_1]={1,4,2,-9,0};
	int array2[SIZE_ARRAY_2]={10,5,2,1,9,2};
	int array3[SIZE_ARRAY_3];

	for(int i=0; i<SIZE_ARRAY_3; i++) {
		array3[i] = (int)((40.0*rand())/(RAND_MAX+1.0)-20);
	}

	printf("Before sort, ");
	show_array(array1, SIZE_ARRAY_1);
	quick_sort(array1, SIZE_ARRAY_1);
	printf("After sort, ");
	show_array(array1, SIZE_ARRAY_1);

	printf("Before sort, ");
	show_array(array2, SIZE_ARRAY_2);
	quick_sort(array2, SIZE_ARRAY_2);
	printf("After sort, ");
	show_array(array2, SIZE_ARRAY_2);

	printf("Before sort, ");
	show_array(array3, SIZE_ARRAY_3);
	quick_sort(array3, SIZE_ARRAY_3);
	printf("After sort, ");
	show_array(array3, SIZE_ARRAY_3);
}

void show_array(int a[], int n)
{
	if(n>0)
		printf("This array has %d items: ", n);
	else
		printf("Error: array size should bigger than zero.\n");

	for(int i=0; i<n; i++) {
		printf("%d ", a[i]);
	}
	printf("\n");
}

运行结果:spa

Before sort, This array has 5 items: 1 4 2 -9 0
After sort, This array has 5 items: -9 0 1 2 4
Before sort, This array has 6 items: 10 5 2 1 9 2
After sort, This array has 6 items: 1 2 2 5 9 10
Before sort, This array has 20 items: 13 -4 11 11 16 -12 -6 10 -8 2 0 5 -5 0 18 16 5 8 -14 4
After sort, This array has 20 items: -14 -12 -8 -6 -5 -4 0 0 2 4 5 5 8 10 11 11 13 16 16 18

分析

时间复杂度

从代码可见,partition的过程是遍历一次 n n 个元素,而递归调用 partition 的次数与划分是否平衡有关。最好的状况是正好平衡,即每次划分红一半一半,这种状况下partition 的次数相似于归并排序中归并的次数,即 log n \log n ,因此快速排序的最佳时间复杂度为 O ( n log n ) O(n \log n) .net

然而在最坏的状况下,划分极度不平衡,每次有一个组只有一个元素,此时快速排序将相似于插入排序,划分次数会达到 n n ,因此快速排序的最坏时间复杂度为 O ( n 2 ) O(n^2)

所幸最坏的状况通常不会发生,通常来讲快速排序的性能仍是至关不错的。若是指望更加靠谱一点,能够采用随机化选取主元的方式。从代码可见,咱们前面的主元选取都是直接选了第一个元素,若是第一个元素正好就是最小的元素,则可能发生最坏的状况,而每次都随机化选择主元则能够避免这个状况。

空间复杂度

由于快速排序能够原址进行,因此这里须要的空间 O ( 1 ) O(1) 的。可是快速排序有递归调用,而调用的深度又取决于划分的状况,因此快速排序的最佳空间复杂度为 O ( log n ) O(\log n) ,最坏空间复杂度为 O ( n ) O(n)