【算法導論】之快速排序分析


前言

最近學習了算法導論上的快速排序部分,有不少體會。

今天就來分享一下。在此歡迎大家批評指正文中的錯誤。

快速排序

正文

1.快速排序的優點

說起快速排序,它的名字就顯現出快排最大的優點————快。到底有多快呢?咱們用數據說話:
RaYxOS.png

綜合一般情況來說,快排確實有(億點快)。特別是對較大數據量的排序。

快速排序的綜合時間復雜度是O(nlgn),但在極端最壞情況下,時間復雜度會到O(n^2)。

很多人在看到O(n^2)會產生疑問?為什么我們還要在實際生活中使用快排呢。

盡管快排的最壞時間復雜度很差,但是它的平均性能好,通常是實際排序中最好的選擇。它的期望時間復雜度是O(nlgn),(絕大多數是這樣,在下文快排的性能分析中會有說明),在排序中很少遇到最壞情況。並且O(nlgn)中隱含的常數因子非常小。
在C++庫函數中sort()函數底層大部分用快排實現的(可以參照下文的小區間優化)。

所以:快排,yyds!!!

2.快排的原理及實現

我們來理解快排的原理。快排運用了分治的思想。

RawLWj.png

在每一次排序中,選取一個數據作為主元x,在將數據的每個值與主元x進行比較,使每趟排序結束時,小於或等於x的在x數據的左邊,大於或等於x的在x的右邊。(和x相等的數可以在x的兩側,這就造成了快排的不穩定性)。

左子區間都是不大於x的數,右子區間都是不小於x的數。以x為界限,遞歸左右子區間。從大致有序到全部有序。

下面我們來簡單實現快速排序。

void quick_sort(int *a, int l, int r)
{
	if (l >= r)
		return;
	int x = a[l];    // 取最左邊的值作為主元x
	int i = l, j = r;
	while (i < j)
	{
		while (a[i] <= x && i<r) ++i;    // 循環結束時a[i]比x大
		while (a[j] >= x && j>l) --j;    // 循環結束時a[j]比x小
		if (i < j)
			swap(a[i], a[j]);       // 交換a[i],a[j]
	}
	// j為分界點
	swap(a[l], a[j]);     // 將主元x放在正確的位置,作為分界點

	// 遞歸左右子區間
	quick_sort(a, l, j - 1);
	quick_sort(a, j+1, r);
}

3.快排的性能分析

我們對上面的代碼進行分析:

  1. 最理想的情況:每次循環的主元x都在區間的中間,遍歷一次數據的復雜度為O(n),遞歸次數為lgn,所以時間復雜度為O(nlgn)。

  2. 最壞的情況:當輸入的數據基本有序(逆序)時,在遞歸時,產生兩個大小為n-1和0的子區間,使遞歸的深度為n,時間復雜度變為O(n^2)。效率不及插入排序(后面會介紹用插入排序實現小區間優化以減少遞歸的棧深度

  3. 就平均情況來看。左區間長度:右區間長度=n。當n為9:1或者99:1時,只要n為常數比例的,算法的時間復雜度總是O(nlgn)。

快排的幾種優化方式

對於快速排序,我們有很多優化方式避免快排達到最壞時間復雜度。

1.主元的隨機化選取

我們將主元的選取隨機化,可以降低快排達到最壞時間復雜度的可能性。

void quick_sort(int *a, int l, int r)
{
	if (l >= r)
		return;
	int k = rand() % (r - l + 1) + l;  // 產生l到r的隨機數
	 // 從a[l]到a[r]中隨機選取
	swap(a[l], a[k]);
	int x = a[l];
	int i = l, j = r;
	while (i < j)
	{
		while (a[i] <= x && i<r) ++i;    // 循環結束時a[i]比x大
		while (a[j] >= x && j>l) --j;    // 循環結束時a[j]比x小
		if (i < j)
			swap(a[i], a[j]);       // 交換a[i],a[j]
	}
	// j為分界點
	swap(a[l], a[j]);     // 將主元x放在正確的位置,作為分界點

	// 遞歸左右子區間
	quick_sort(a, l, j - 1);
	quick_sort(a, j + 1, r);
}

2.三數取中

三數取中比主元的隨機化更有優勢。
三數取中的意思是:在a[l],a[(l+r)/2],a[r]三個數中選取中間大的數作為主元,可以有效的優化快排效率。

int Getmid(int *a, int l, int r)
{
	// 取得中間值的下標
}

void quick_sort(int *a, int l, int r)
{
	if (l >= r)
		return;
	swap(a[l], a[Getmid(a, l, r)]);       // 三數取中
	int x = a[l];
	int i = l, j = r;
	while (i < j)
	{
		while (a[i] <= x && i<r) ++i;    // 循環結束時a[i]比x大
		while (a[j] >= x && j>l) --j;    // 循環結束時a[j]比x小
		if (i < j)
			swap(a[i], a[j]);       // 交換a[i],a[j]
	}
	// j為分界點
	swap(a[l], a[j]);     // 將主元x放在正確的位置,作為分界點

	// 遞歸左右子區間
	quick_sort(a, l, j - 1);
	quick_sort(a, j + 1, r);
}

3.減小遞歸的棧深度——小區間優化

在C++庫函數中sort()的底層實現是有一部分是快排的小區間優化。

我們看一下sort()函數底層實現的源代碼:

inline void
    __sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
	   _Compare __comp)
    {
      if (__first != __last)
	{
	// sort函數默認為內省排序,當子數組小於16時返回
	  std::__introsort_loop(__first, __last,
				std::__lg(__last - __first) * 2,
				__comp);
	// 對大致排序過的數組進行插入排序操作
	  std::__final_insertion_sort(__first, __last, __comp);
	}
    }

小區間優化:在子區間的長度小於16時,進行插入排序,減小遞歸的棧深度。
模擬實現:

void Insert_sort(int *a, int l, int r)
{
	for (int i = l + 1; i < r; ++i)
	{
		if (a[i] < a[i - 1])
		{
			int j = i, k = a[i];
			while (a[i]<a[--j] && j>l);
			int tmp = j;
			while (j < i)
			{
				a[j + 1] = a[j];
				++j;
			}
			a[tmp] = k;
		}
	}
}
void quick_sort(int *a, int l, int r)
{
	if (r - l < 16)           // 在子區間的長度小於16時,進行插入排序,減小遞歸的棧深度
		Insert_sort(a, l, r);
	int x = a[l];    // 取最左邊的值作為主元x
	int i = l, j = r;
	while (i < j)
	{
		while (a[i] <= x && i<r) ++i;    // 循環結束時a[i]比x大
		while (a[j] >= x && j>l) --j;    // 循環結束時a[j]比x小
		if (i < j)
			swap(a[i], a[j]);       // 交換a[i],a[j]
	}
	// j為分界點
	swap(a[l], a[j]);     // 將主元x放在正確的位置,作為分界點

	// 遞歸左右子區間
	quick_sort(a, l, j - 1);
	quick_sort(a, j+1, r);
}

4.其他優化:

算法導論還提到了其他的優化方法,比如用尾遞歸減少棧深度,針對相同元素值的快速排序等等。

今天就分享到這里,歡迎大家在評論區留言。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM