【算法导论】快速排序

1. 快速排序的描述

快速排序像合并排序一样,也是基于分治模式的一种排序算法,

(1)方法(分治法):

对于一个典型子数组Arr[p…r]排序的分治过程的三个步骤:

分解: Arr[p…r] => Arr[p…q-1]<=Arr[q]<=Arr[q+1…r],其中Arr[q]是划分元。

解决: 递归的对Arr[p…q-1]和Arr[q+1…r]进行快速排序(临界条件:区间长度为1,空操作)。

合并: 空操作。因为两个子数组是就地排序的,将它们合并不需要操作,Arr[p…r]已排序。

(2)实现代码:

如下的过程实现了快速排序:

int quickSort(sortArr *sortNum, int p, int r)
{
	int q = 0;
	if(p < r)
	{
		q = partition(sortNum, p, r);	//返回主元位置,并使arr[p, q-1]<=arr[q]<=arr[q+1, r]
		quickSort(sortNum, p, q-1);		//第一个问题求解
		quickSort(sortNum, q+1, r);		//第二个问题求解
	}
	return OK;
}

为了排序一个完整的数组Arr[ ],最初的调用是:quickSort(Arr[], 1, length(Arr))。

(3)数组划分:

快速排序算法的关键就是数组的划分(partition()过程),他对子数组Arr[p…r]进行就地重排,实现代码:

int partition(sortArr *sortNum, int p, int r)
{
	int i = 0;
	int j = 0;
	int x = 0;

	x = sortNum->arr[r];	//将arr[r]作为划分元
	i = p - 1;	//i为活动指针(向右移动,始终保持arr[p...i] <= x)

	for(j = p; j <= r-1; j++)	//j对arr[p...r-1]进行扫描,始终都是i<j,i、j都是从左到右移动
	{
		if(sortNum->arr[j] <= x)	//在[i+1, ... , r-1]中发现arr[j]<=x元素,并交换元arr[i+1]
		{
			i = i + 1;
			sortNum->arr[0] = sortNum->arr[j];
			sortNum->arr[j] = sortNum->arr[i];
			sortNum->arr[i] = sortNum->arr[0];
		}
	}

	sortNum->arr[0] = sortNum->arr[r];
	sortNum->arr[r] = sortNum->arr[i+1];
	sortNum->arr[i+1] = sortNum->arr[0];

	return i+1;
}

下图展示了partition()在一个包含8个元素的数组上的操作过程:

《【算法导论】快速排序》

partiton()总是选择一个x = Arr[r]作为划分元(主元),并且围绕他来划分子数组Arr[p…r],上图中浅绿色部分的数组元素其值都不大于x,深绿色部分的元素其值都大于x,白色的部分为尚未被遍历到的元素,蓝色的为划分元。

(a) 初始的数组和变量的设置,数组元素均未被遍历。

(b) 数值2<=4,所以他和i所在的位置交换,并且被标上了浅绿色,放入了小于等于划分元的部分。这里i和他自身的位置相同,即是与他自己交换。

(c)~(d) 数值8、7均>4,所以不用交换,直接标上深绿色,表明大于划分元。

(e)  数值1<=4,所以他和8交换位置,标入浅绿色的部分。

(f) 3<=4,所以他和7交换位置,标入浅绿色的部分。

(g)~(h) 5、6均大于4,所以直接标入深绿色部分,遍历结束。

(i) 将划分元同(i+1)未知的元素交换位置,这样就位于了两个部分之间。

(4)稳定版本的数组划分

看上图,数组元素8和7的相对位置最后发生了变化,如果一开始Arr[2]存的也是7,那么可以看出两个7最后的相对位置发生了变化,所以partition()方法不是稳定的划分方法。

要实现数组的稳定的partition()划分可以另开辟一段空间tempArr[],然后扫描一遍原来的数组Arr[],把<=”划分元”的依次放到tempArr[],然后再扫描一次Arr[],把>”划分元”的放到tempArr[],接在刚才tempArr[]数组数据的后面,最后把tempArr的值拷贝到Arr[]中,这样可以保证划分是稳定的。代码如下:

int stablePartition(sortArr *sortNum, int p, int r)
{
	int i = 0;
	int j = 0;
	int x = 0;
	int mid = 0;
	int *tempArr = NULL;
	tempArr = (int *)malloc((r - p + 2) * sizeof(int));
	memset(tempArr, 0, ((r - p + 2) * sizeof(int)));

	j = 1;
	x = sortNum->arr[r];

	//第一遍扫描,将<=arr[r]的元素都放到tempArr[]中
	for(i = p; i < r; i++)
	{
		if(sortNum->arr[i] <= x)
		{
			tempArr[j] = sortNum->arr[i];
			j++;
		}
	}

	tempArr[j] = x;
	//mid = j;
	mid = p + j - 1;
	j++;

	//第二遍扫描,将>arr[r]的元素都放到tempArr[]中
	for(i = p; i < r; i++)
	{
		if(sortNum->arr[i] > x)
		{
			tempArr[j] = sortNum->arr[i];
			j++;
		}
	}

	j = 1;
	//第三遍扫描,将tempArr[]中的元素拷贝到arr[]中去。
	for(i = p; i <= r; i++)
	{
		sortNum->arr[i] = tempArr[j];
		j++;
	}

	free(tempArr);

	return mid;
}

partition()的空间复杂度为O(1),这里的为O(n),而且时间复杂度为T(n) = 
Θ(n) + Θ(n) + Θ(n) = Θ(3n),这样总的quickSort的时间复杂度为Θ(3nlogn)。等于是用时间和空间来换稳定。

2.快速排序的性能

(1)最坏的划分:Arr[p…q-1],Arr[q+1…r]有一个为空。

T(n) = T(n-1) + T(0) + Θ(n) = T(n-1) + Θ(n)。

利用代换法可以比较直接的求得T(n) = Θ(n^2)。

(2)最好的划分:一分为二,比较平均。

T(n) = 2*T(n/2) + Θ(n) = Θ(nlogn)

(3)平均划分:

假设每次划分产生的区间长度比固定(如1:9或者9:1)。

T(n) <= T(9n/10) + T(n/10) + cn

用递归树法求解上式。

T(n) <= cn * h = Θ(nlogn)


3.快速排序的随机化版本

由快速排序的性能分析可知当数组本身有序或者接近有序时,快速排序的耗时是最长的,针对这个问题我们可以不将Arr[r]作为主元,而是利用随机发生器从子数组Arr[p…r]中随机选择一个元素,即将Arr[r]同从Arr[p…r]中随机选择的一个元素交换。因为划分元是等可能的从Arr[p…r]中随机选择的,我们期望在平均的情况下对输入的数组的划分能够比较对称。

随机化版本的快速排序代码如下:

int randomizedQuickSort(sortArr *sortNum, int p, int r)
{
	int q = 0;

	if(p < r)
	{
		q = randomizedPartition(sortNum, p, r);
		randomizedQuickSort(sortNum, p, q-1);
		randomizedQuickSort(sortNum, q+1, r);
	}

	return OK;
}

随机化版本的partition()如下:

int randomizedPartition(sortArr *sortNum, int p, int r)
{
	int i = 0;

	srand((unsigned)time(NULL));	//设定随机数的种子
	i = rand() % (r-p+1) + p;	//生成一个 p-r 之间的随机数

	//将arr[i]同最后一个元素交换位置
	sortNum->arr[0] = sortNum->arr[i];
	sortNum->arr[i] = sortNum->arr[r];
	sortNum->arr[r] = sortNum->arr[0];

	return partition(sortNum, p ,r);
}


4.将快速排序和插入排序结合

对于插入排序而言,当期输入的数据是已经几乎排好序的,运行时间是很快的,在实践中可以充分利用这一特点来改善快速排序的运行时间。当一个长度小于k的子数组上调用快速排序的时候,让他不作任何排序就返回。当顶层的快速排序调用返回后,对整个数组运行插入排序来完成排序过程。

快速插入排序的代码如下:

int quickInsertionSort(sortArr *sortNum, int p, int r) {
    limitedQuicksort(sortNum, p, r, K);
    insertionSort(sortNum, p, r);

	return OK;
}
int limitedQuicksort(sortArr *sortNum, int p, int r, int threshold) {
    if (r - p > threshold) {
        int q = partition(sortNum, p, r);
        limitedQuicksort(sortNum, p, q - 1, threshold);
        limitedQuicksort(sortNum, q + 1, r, threshold);
    }

	return OK;
}

这一算法的时间复杂度为O(nk+nlog(n/k))。

5.完整版代码

/*
 * filename: quickSort.c
 *
 * <Introduction to Algorithms(second edition)>
 * chapter7  Quicksort
 * editor: vim v7.4
 * compiler: gcc v4.4.1
 * date: 2014-10-8
 * */


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

#define MAX 100
#define OK 0
#define ERROR 1

#define K 7 //快速插入排序的K值

typedef struct
{
	int arr[MAX+1];
	int length;		//数组arr[]中的元素个数
}sortArr;

/********************
 * Description: 
 *	对数组arr[]进行重排,
 *	将arr[]的末位元素(arr[r])放到它最终应该放置的位置,
 *	将比arr[r]小的元素都放在他的左边,比arr[r]大的元素都放在他的右边。
 * Input:
 * - sortNum: 存放待重排的数组
 * - p: 待排序子数组的首位元素下标
 * - r: 待排序子数组的末位元素下标
 * Return: 排序成功返回OK
 * ******************/
int partition(sortArr *sortNum, int p, int r)
{
	int i = 0;
	int j = 0;
	int x = 0;

	x = sortNum->arr[r];	//将arr[r]作为划分元
	i = p - 1;	//i为活动指针(向右移动,始终保持arr[p...i] <= x)

	for(j = p; j <= r-1; j++)	//j对arr[p...r-1]进行扫描,始终都是i<j,i、j都是从左到右移动
	{
		if(sortNum->arr[j] <= x)	//在[i+1, ... , r-1]中发现arr[j]<=x元素,并交换元arr[i+1]
		{
			i = i + 1;
			sortNum->arr[0] = sortNum->arr[j];
			sortNum->arr[j] = sortNum->arr[i];
			sortNum->arr[i] = sortNum->arr[0];
		}
	}

	sortNum->arr[0] = sortNum->arr[r];
	sortNum->arr[r] = sortNum->arr[i+1];
	sortNum->arr[i+1] = sortNum->arr[0];

	return i+1;
}

/********************
 * Description: 随机抽样版的partition
 * Input:
 * - sortNum: 存放待重排的数组
 * - p: 待排序子数组的首位元素下标
 * - r: 待排序子数组的末位元素下标
 * Return: 分割的数据的下标
 * ******************/
int randomizedPartition(sortArr *sortNum, int p, int r)
{
	int i = 0;

	srand((unsigned)time(NULL));	//设定随机数的种子
	i = rand() % (r-p+1) + p;	//生成一个 p-r 之间的随机数

	//将arr[i]同最后一个元素交换位置
	sortNum->arr[0] = sortNum->arr[i];
	sortNum->arr[i] = sortNum->arr[r];
	sortNum->arr[r] = sortNum->arr[0];

	return partition(sortNum, p ,r);
}

/********************
 * Description: 稳定版的partition
 *		另开一个数组tempArr[],然后扫描一遍原来的数组Arr[],把<="中间值"的依次放到tempArr[],
 *		然后再扫描一次Arr[],把>"中间值"的放到tempArr[],接在刚才tempArr[]数组数据的后面,
 *		最后把tempArr的值拷贝到Arr[]中,这样可以保证划分是稳定的。
 * Input:
 * - sortNum: 存放待排序的数组
 * - p: 待排序子数组的首位元素下标
 * - r: 待排序子数组的末位元素下标
 * Return: 排序成功返回OK
 * Others: stablePartition()的复杂度是O(3n),这样整个算法的复杂度是O(3nlogn)
 * ******************/
int stablePartition(sortArr *sortNum, int p, int r)
{
	int i = 0;
	int j = 0;
	int x = 0;
	int mid = 0;
	int *tempArr = NULL;
	tempArr = (int *)malloc((r - p + 2) * sizeof(int));
	memset(tempArr, 0, ((r - p + 2) * sizeof(int)));

	j = 1;
	x = sortNum->arr[r];

	//第一遍扫描,将<=arr[r]的元素都放到tempArr[]中
	for(i = p; i < r; i++)
	{
		if(sortNum->arr[i] <= x)
		{
			tempArr[j] = sortNum->arr[i];
			j++;
		}
	}

	tempArr[j] = x;
	//mid = j;
	mid = p + j - 1;
	j++;

	//第二遍扫描,将>arr[r]的元素都放到tempArr[]中
	for(i = p; i < r; i++)
	{
		if(sortNum->arr[i] > x)
		{
			tempArr[j] = sortNum->arr[i];
			j++;
		}
	}

	j = 1;
	//第三遍扫描,将tempArr[]中的元素拷贝到arr[]中去。
	for(i = p; i <= r; i++)
	{
		sortNum->arr[i] = tempArr[j];
		j++;
	}

	free(tempArr);

	return mid;
}

/********************
 * Description: 快速排序
 * Input:
 * - sortNum: 存放待排序的数组
 * - p: 待排序子数组的首位元素下标
 * - r: 待排序子数组的末位元素下标
 * Return: 排序成功返回OK
 * ******************/
int quickSort(sortArr *sortNum, int p, int r)
{
	int q = 0;
	if(p < r)
	{
//		q = partition(sortNum, p, r);	//返回主元位置,并使arr[p, q-1]<=arr[q]<=arr[q+1, r]
		q = stablePartition(sortNum, p, r);
		quickSort(sortNum, p, q-1);		//第一个问题求解
		quickSort(sortNum, q+1, r);		//第二个问题求解
	}

	return OK;
}

/********************
 * Description: 快速排序的随机化版
 * Input:
 * - sortNum: 存放待排序的数组
 * - p: 待排序子数组的首位元素下标
 * - r: 待排序子数组的末位元素下标
 * Return: 排序成功返回OK
 * ******************/
int randomizedQuickSort(sortArr *sortNum, int p, int r)
{
	int q = 0;

	if(p < r)
	{
		q = randomizedPartition(sortNum, p, r);
		randomizedQuickSort(sortNum, p, q-1);
		randomizedQuickSort(sortNum, q+1, r);
	}

	return OK;
}


/********************
 * Description: 带门限值的快速排序
 *		将待快速排序的数组分成门限值规定大小的区间就停止快排转而采用直接插入排序
 * Input:
 * - sortNum: 存放待排序的数组
 * - length: 待排序数组的首元素下标
 * - size : 待排序数组的末位元素的下标
 * - threshold: 门限值
 * Return: 排序结束返回OK
 * ******************/
int limitedQuicksort(sortArr *sortNum, int p, int r, int threshold) {
    if (r - p > threshold) {
        int q = partition(sortNum, p, r);
        limitedQuicksort(sortNum, p, q - 1, threshold);
        limitedQuicksort(sortNum, q + 1, r, threshold);
    }

	return OK;
}

/********************
 * Description: 直接插入排序
 * Input:
 * - sortNum: 存放待排序的数组
 * - p: 待排序数组的首元素下标
 * - r : 待排序数组的末位元素的下标
 * Return: 排序结束返回OK
 * ******************/
int insertionSort(sortArr *sortNum, int p, int r) 
{
    int i = 0;
	int j = 0;
	int key = 0;

    for (j = p + 1; j <= r; j++) {
        key = sortNum->arr[j];
        for (i = j - 1; i >= p && sortNum->arr[i] > key; i--) {
            sortNum->arr[i + 1] = sortNum->arr[i];
        }
        sortNum->arr[i + 1] = key;
    }

	return OK;
}

/********************
 * Description: 快速插入排序
 * Input:
 * - sortNum: 存放待排序的数组
 * - p: 待排序数组的首元素下标
 * - r : 待排序数组的末位元素的下标
 * Return: 排序结束返回OK
 * ******************/
int quickInsertionSort(sortArr *sortNum, int p, int r) {
    limitedQuicksort(sortNum, p, r, K);
    insertionSort(sortNum, p, r);

	return OK;
}

/********************
 * Description: 随机生成待排序数组
 * Input:
 * - sortNum: 存放待排序的数组
 * - length: 待排序数组的长度
 * - size : 待排序数组的数值范围
 * Return: 打印成功返回OK
 * ******************/
int creatSortArray(sortArr *sortNum, int length, int size)
{
	int i = 0;
	srand((unsigned)time(NULL));
	for(i = 1; i <= length; i++)
	{
		sortNum->arr[i] = rand() % size;
	}

	sortNum->length = length;

	return OK;
}

/********************
 * Description: 获取用户输入数据
 * Input:
 * - length: 待排序数组的长度
 * - size: 待排序数组的数值范围
 * Output:
 * - length: 保存用户指定的待排数组的长度,范围:1-100
 * - size: 保存用户指定的待排数据的数值范围
 * Return:
 * - 用户输入数据成功返回OK
 * - 用户输入数据不合法返回ERROR
 * ******************/
int inputNumber(int *length, int *size)
{
	printf("随机生成待排序数组\n");
	printf("请输入数组长度(1-100):");
	scanf("%d", length);
	if(!((*length>=1)&&(*length<=100)))
	{
		return ERROR;
	}
	printf("请输入数组数据范围(0-x):\nx = ");
	scanf("%d", size);

	return OK;
}

/********************
 * Description: 打印数组
 * Input:
 * - sortNum : 存放待打印的数组
 * Return: 打印成功返回OK
 * ******************/
int printArray(sortArr sortNum)
{
    int i = 0;

    for(i = 1; i <= sortNum.length; i++)
    {
        printf("%d ", sortNum.arr[i]);
    }
    printf("\n");

    return OK;
}

int main()
{

	int size = 0;
	int length = 0;

	sortArr *sortNum = NULL;
	sortNum = (sortArr *)malloc(sizeof(sortArr));
	memset(sortNum, 0, sizeof(sortArr));

	inputNumber(&length, &size);
	creatSortArray(sortNum, length, size);
	printf("待排序的数组为:\n");
	printArray(*sortNum);

	printf("排序之后的数组为:\n");
//	quickSort(sortNum, 1, sortNum->length);
//	randomizedQuickSort(sortNum, 1, sortNum->length);
	quickInsertionSort(sortNum, 1, sortNum->length);
	printArray(*sortNum);

	free(sortNum);

	return 0;
}

点赞