快速排序——浙大《数据结构》学习笔记

1.概述

大一学生第一篇博客,记录下自己看网课的心得,文中有错误或是不足的地方还请指出。


2.算法简介

快速排序(英语:Quicksort),又称划分交换排序,简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序 n个项目要O(nlog n)(大O符号)次比较。在最坏状况下则需要 O(n2)次比较,但这种状况并不常见。事实上,快速排序 O(nlog n)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。


3.算法实现

快速排序采用了一种分而治之的思想, 将数组分为三部分:pivot,比pivot小的数和比pivot大的数,其中pivot是从数组A中选的一个主元。算法的伪代码如下:

void quick_sort(int *a, int n)
{     
      if ( N < 2 )
	        return; 
			
	  pivot = 从a中选一个主元;
	  将 S = { a \ pivot } 分成2个独立子集: 
	  a1 = { b | b <= pivot } 和 
	  a2 = { b | b >= pivot }; 
	  a = quick_sort(a1,n1)
		            {pivot}
		  quick_sort(a2,n2);
}

  可以看到,整个算法的排序步骤就是划分子集后进行递归排序。

3.1.选主元

· 如果令 pivot = a[0]
在传统的快速排序中,往往选择头元素作为pivot,但是这样在数组基本有序的情况会出现很多次没有用处的比较。下面是一个已经有序的数组:我们可以看到的是
算法首先将1和后面的元素分成两部分:
  1 2 3 4 5 6 ··· N-1 N
&再将2和后面的元素分成两部分:
     2 3 4 5 6 ··· N-1 N
再将3和后面的元素分成两部分:
        3 4 5 6 ··· N-1 N
           ······
上述的过程用时间复杂度表示如下:
T ( N ) = O( N ) + T ( N–1 )
      = O( N ) + O ( N–1 ) + T( N–2 )
      = O( N ) + O ( N–1 ) + ··· + O( 1 )
      = O( N2 )

· 若使用rand()函数随机取pivot
采用此种方法会直接调库,相比于在数组中选择一个数字,调库更容易返回一个大于或小于数组中所有元素的值。这样的话,时间开销太大,十分浪费内存,显然不是最优解。

· 分别取头尾和中间的数,取其中位数作为pivot的值
例如8、12、3的中位数就是8,就使用8作为pivot,下面我们来看看代码

int median3(int* a, int left, int right)
{
	int center = left + (right - left) / 2;  // Use this form to prevent overflow
	if (a[left] > a[center])
		swap(a[left], a[center]);
	if (a[left] > a[right])
		swap(a[left], a[right]);
	if (a[center] > a[right])
		swap(a[center], a[right]);
	// In this time, a[left] <= a[center] <= a[right]
	// Hide the pivot on the position of right-1
	swap(a[center], a[right - 1]);
	return a[right - 1];
}

在第3行代码中,采用了防止溢出的形式;
第4~7行代码,将最小值交换到了 a[left];
第8~9行代码,确定了剩下两个元素的次序。
这时我们将 a[center] 与 a[right - 1] 交换了位置,之所以这样交换,是因为我们已经确定了a[center] <= a[right],所以将其交换到 a[right-1] 时其相对次序不变,这样暂时将pivot交换出去有利于后面比较部分的编程。

3.2.子集划分

首先来看代码

// Core recursion function
void q_sort(int* a, int left, int right)
{
	int pivot = 0;
	int cutoff = 3;
	int low = 0;
	int high = 0;

	// If the sequence elements are suffciently large, use quick sort
	if (cutoff <= right - left)
	{
		pivot = median3(a, left, right);
		low = left;
		high = right - 1;
		while (1)
		{
			// Move the sequence smaller than the reference to the left
			// and the big to the right
			while (a[++low] < pivot);
			while (a[--high] > pivot);
			if (low < high)
				swap(a[low], a[high]);
			else
				break;
		}
		// Change the pivot to the correct position
		swap(a[low], a[right - 1]);
		q_sort(a, left, low - 1);
		q_sort(a, low + 1, right);
	}
	// If there's too few elements, use simple sort
	else
		bubble_sort(a + left, right - left + 1);
}

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

下面用一个实例来讲解下子集划分的原理和代码。

[80 10 40 90 0 30 50 20 70 60] (60为median3交换到right-1位置的pivot)
我们分别令 low = 0,high = 9 作为这个序列的“指针”,当然它并不是一个真正的指针,只是起到了指针的作用。接下来我们要做的事就是将剩下的元素分为小于pivot的部分和大于pivot的部分。
当 a[low] 小于pivot的时候,low持续向右移动,直到 a[low] <= pivot。
当 a[high] 大于pivot的时候,high持续向左移动,直到 a[high] >= pivot。
由于刚才midian3函数进行了次序的交换,有效地防止了low和high“指针”的越界。
以下为一趟排序的过程:

当 low == 0 时,a[low] <= pivot,low停止移动。
当 high == 7 时,a[high] >= pivot,high停止移动。
于是,a[0]和a[7]交换位置:

[20 10 40 90 0 30 50 80 70 60]

接下来low继续移动到3,high继续移动到6,a[3]和a[6]交换位置:

[20 10 40 50 0 30 90 80 70 60]

low继续向右移动到6,high继续向左移动到5,此时,a[6]之前的数是小于pivot的,a[6]之后的数是大于等于pivot的,所以我们就能够确定pivot的正确位置就是a[6],所以我们将a[6]与a[9]交换:

[20 10 40 50 0 30 60 80 70 90]

这时第一趟排序就已经完成。但同时有一个新的问题:如果有元素正好等于pivot怎么办?
· 停下来交换
假设有一个元素全都相等的数组,就会进行很多次没有用处的交换,虽然看起来有点浪费,但是这样处理的话pivot会被换到接近中间的位置,使整个数组恰好被分成两部分。这种情况下按照时间复杂度来计算的话,最终的时间复杂度为O(NlogN),并不是最坏情况。
· 不理它,继续移动指针
还是那个元素全都相等的数组,如果我们不理它,继续移动指针,那么pivot就会被放在数组的末尾。这种情况下数组就被分成了最后一个元素和一个大小为n-1的数组,这样的时间复杂度就为O(N2)。于是我们经过一个时间复杂度为(N2)的排序算法却什么都没有做,这是很不划算的。
综上所述,我们采用停下来交换的方法。
第一趟排序结束后,我们递归调用算法,分别对 [20 10 40 50 0 30] 和 [80 70 90] 这两个子数组进行快速排序。

3.3.小规模数据的处理

快速排序时,如果用递归的话,对于时间和空间的占用都是非常严重的,同时对小规模的数据 (例如 n <= 100的) 时候可能效率还不如简单的排序。我们的解决方案就是当递归的数据规模充分小,则停止递归,直接调用简单排序 (如冒泡排序) 。在程序中定义一个cutoff的阈值,如果数据规模 n < cutoff,就采用冒泡排序,我自己代码中为了观察整个算法的执行过程所以将cutoff设置成了很小的值。
划重点!!!划重点!!!划重点!!!重要的事说11遍
cutoff初始化必须大于1
cutoff初始化必须大于1
cutoff初始化必须大于1

(以下的简单推导可选择性查看)
cutoff == 0 的时候由于下标访问会导致数组越界。
cutoff == 1 的时候median3函数会对其进行排序,假设数组为 [0 1] 或者 [1 0],经过median3后数组会变成 [0 1],low和high最终会移动到0上,while循环后的swap会将排好序的两个数字再次交换导致最后结果错误。

3.4.算法接口的统一

调用函数时参数太多显然不合适,所以必须要对算法的接口进行统一。封装函数代码仅一行:

void quick_sort(int* a, const int& n){
	q_sort(a, 0, n - 1);
}

3.5.代码概览

#include 
using namespace std;

void bubble_sort(int* a, const int& n);
// Uniform interface
void quick_sort(int* a, const int& n);
// Core recursion function
void q_sort(int* a, int left, int right);
int median3(int* a, int left, int right);
void swap(int& a, int& b);

int main(){
	int a[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
	for (int i = 0; i < 10; i++){
		cout << a[i] << " ";
	}

	quick_sort(a, 10);
	for (int i = 0; i < 10; i++){
		cout << a[i] << " ";
	}
	return 0;
}

// Time complexity best case: n, worst case: n^2
void bubble_sort(int* a, const int& n){
	int flag = 0;
	int tmp = 0;
	for (int p = n - 1; p; p--){
		flag = 0;
		for (int i = 0; i < p; i++){
			if (a[i] > a[i + 1]){
				tmp = a[i];
				a[i] = a[i + 1];
				a[i + 1] = tmp;
				flag = 1;  // Identification has exchanged
			}
			if (flag == 0){
				break;
			}
		}  // of for i
	}  // of for p
}  // of function

// Uniform interface
void quick_sort(int* a, const int& n){
	q_sort(a, 0, n - 1);
}

// Core recursion function
void q_sort(int* a, int left, int right){
	int pivot = 0;
	int cutoff = 2;
	int low = 0;
	int high = 0;

	// If the sequence elements are suffciently large, use quick sort
	if (cutoff <= right - left)	{
		pivot = median3(a, left, right);
		low = left;
		high = right - 1;
		while (1)		{
			// Move the sequence smaller than the reference to the left
			// and the big to the right
			while (a[++low] < pivot);
			while (a[--high] > pivot);
			if (low < high){
				swap(a[low], a[high]);
			}else{
				break;
			}
		}
		// Change the pivot to the correct position
		swap(a[low], a[right - 1]);
		q_sort(a, left, low - 1);
		q_sort(a, low + 1, right);
	}
	// If there's too few elements, use simple sort
	else{
		bubble_sort(a + left, right - left + 1);
	}
}

int median3(int* a, int left, int right){
	int center = left + (right - left) / 2;  // Use this form to prevent overflow
	if (a[left] > a[center]){
		swap(a[left], a[center]);
	}
	if (a[left] > a[right]){
		swap(a[left], a[right]);
	}
	if (a[center] > a[right]){
		swap(a[center], a[right]);
	}
	// In this time, a[left] <= a[center] <= a[right]
	// Hide the pivot on the position of right-1
	swap(a[center], a[right - 1]);
	return a[right - 1];
}

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

4.参考文献

[1].快速排序.中国大学MOOC 浙江大学何钦铭、陈越《数据结构》https://www.icourse163.org/learn/ZJU-93001?tid=1003997005#/learn/content?type=detail&id=1007588514
[2].快速排序.维基百科 https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F

你可能感兴趣的:(数据结构与算法)