首页 > 代码库 > 快速排序

快速排序

常规快速排序

// 快速排序
 int Partition(int arr[], int lhs, int rhs)
 {
	 int pivot = arr[rhs]; @1
	 int i = lhs - 1; @2
	 int temp;
	 for (int j = lhs; j <= rhs-1; ++j)
	 {
		 if (arr[j] < pivot) @3
		 {
			 ++i; @4
			 temp = arr[i];
			 arr[i] = arr[j];
			 arr[j] = temp;
		 }
	 }
	 arr[rhs] = arr[i+1]; @5
	 arr[i+1] = pivot;
	 return i+1; @6
 }

 void QuickSort(int arr[], int lhs, int rhs)
 {
	 if (lhs < rhs) @7
	 {
		 int q = Partition(arr, lhs, rhs);
		 QuickSort(arr, lhs, q-1);
		 QuickSort(arr, q+1, rhs);
	 }
 }

快速排序是一种基于分治思想的算法,它不稳定,最坏情况为O(n^2),但实践证明快速排序算法是这么多算法中平均运行时间比较优秀的,也是很多库函数所采用的算法,比如STL的qsort用的就是快速排序实现的。

快速排序的核心思想:选出一个主元(pivot),然后以主元为基准划分出两拨,一拨小于主元,一拨大于或等于主元。但在每一拨里,元素的顺序是没有保证的,然后再把子序列继续同样处理,直到最后子序列中剩下一个元素时,不再继续递归,最终排序完成。

@1:选择主元,也就是划分的基准。采用最右或是最左都可以。

@2:i这个变量在整个过程中指向的位置:小于主元的那一拨里的最后一个元素的位置,在非递减排序中(本例如此),小于主元的在左侧

@3:快速排序是不稳定的,@3代码中的(<或是<=)会影响到快速排序的递归树模型,如果用(<),则j所指向的元素和i指向的元素相同且i不会增加,也不会交换;如果用(<=),则i会右移并且交换。

@4:i变量的指向增加,并且后面三行代码交换

@5:这两行代码是把主元安插在合适的位置,因为主元在最右侧,且大于或等于主元的一拨靠右侧,所以主元要和右侧一拨中的第一个交换(仅适用于非递减排序+最右选为主元)。

@6:主元的最后位置是在i+1处,此位置为分割的两拨的分割点。

@7:在递归控制的判断条件中,只有一个元素时停止递归,这一点和归并相似。


快速排序的优化

优化1

// 快速+插入排序
 void Quick_InsertSort(int arr[], int lhs, int rhs)
 {
	 if (rhs - lhs < 5) @1
	 {
		 DirectInsertSort(arr, lhs, rhs);
	 }else
	 {
		 int q = Partition(arr, lhs, rhs);
		 Quick_InsertSort(arr, lhs, q-1);
		 Quick_InsertSort(arr, q+1, rhs);
	 }	 
 }

@1:当子序列足够小的时候,用直接插入排序要比继续递归下去更合适。至于以多少个子序列元素为准,这个可以随意定,但不能过大。

这个改进是在每一次递归到足够小时,就就地直接插入排序,然后返回。


优化2

 void Quick_InsertSort1(int arr[], int lhs, int rhs)
 {
	 if (rhs - lhs < 3)
	 {
		 return ; @1
	 }else
	 {
		 int q = Partition(arr, lhs, rhs);
		 Quick_InsertSort(arr, lhs, q-1);
		 Quick_InsertSort(arr, q+1, rhs);
	 }
 }
 void QuickSort1(int arr[], int lhs, int rhs)
 {
	 Quick_InsertSort1(arr, lhs, rhs); @2
	 DirectInsertSort(arr, lhs, rhs);
 }

这个改进和上个改进相比,有一个优化的地方:当递归到足够小的子序列时,不排序而是直接返回。当调用完递归后,在整个序列上应用直接插入排序,由于递归后序列的有序度已经很高了,所以这个直接插入排序会很快。


优化3

// 从序列的左,中,右三个元素中取出中值,然后放到最右侧。
// 更科学的选择主元,提高了快速排序的效率
 void median3(int arr[], int lhs, int rhs)
 {
	 // 选择排序的思路找出最小值
	 int min = lhs;
	 int mid = (lhs + rhs) / 2;
	 if (arr[mid] < arr[min])
	 {
		 min = mid;
	 }
	 if (arr[rhs] < arr[min])
	 {
		 min = rhs;
	 }
	 if (min != lhs)
	 {
		 int temp = arr[min];
		 arr[min] = arr[lhs];
		 arr[lhs] = temp;
	 }
	 // 以上代码:确定出三个中的最小值,然后放到最左侧
	 // 下面再比较mid和rhs位置的元素,找出三个中的中值
	 if (mid != rhs && arr[mid] < arr[rhs])
	 {
		 // 将中值放到最右侧
		 int temp = arr[mid];
		 arr[mid] = arr[rhs];
		 arr[rhs] = temp;
	 }else
	 {
		 // 在次分支内,最右侧本来就是中值
	 }
 }
 // 一趟划分,采用三者取中值作主元
 int Partition1(int arr[], int lhs, int rhs)
 {
	 median3(arr, lhs, rhs); @1
	 int pivot = arr[rhs]; @2
	 int i = lhs;
	 int j = rhs - 1;

	 while (1) @3
	 {
		 while (i < j && arr[i] < pivot) @4
		 {
			 ++i;
		 }
		 while (i < j && pivot < arr[j]) @5
		 {
			 --j;
		 }
		 if (i < j) @6
		 {
			 int temp = arr[i];
			 arr[i] = arr[j];
			 arr[j] = temp;
			 ++i; --j;
		 }else
		 {
			 break; @7
		 }
	 }
	 if (arr[i] > pivot) @8
	 {
		 arr[rhs] = arr[i];
		 arr[i] = pivot;
	 }
	 return i; @9
 }

这个partition函数的思路:从左右两侧往中间并行推进,i左侧是小于主元的区间,j右侧是大于或等于主元的区间。i和j之间的是还未被处理的区间。

@1:先调用median3函数,取左,中,右三个值的中值,并把其放到数组的最右边。更科学的选取主元

@2:选取主元

@3:最外层循环使用死循环,在整个循环中,i和j满足某个条件会跳出循环

@4:i从左到右,遇到比主元小的直接continue,直到遇到一个比主元大的(或是i和j相遇),跳出小循环

@5:j从右到左,遇到比主元大于或等于的直接continue,直到遇到一个小于主元的(或是i和j相遇),跳出小循环

@6:当两个小循环都结束后,i指向一个大于主元的,j指向一个大于或等于主元的,然后i和j所指位置的元素互换,然后i和j分别移动一个位置

@7:如果(i < j)不成立,则一定是i和j相遇,那么一趟划分结束

@8:最后相遇的位置i或j就是分割点,如果这个点的元素比主元大(一般都要大,除非相等),交换。

@9:最后把分割点返回