排序算法-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)