那个更优秀的排序助手:快速排序(小白专用0v0)

295 阅读6分钟

前言

相信大家在上一期的介绍中已经基本理解了快速排序的中心思想和方法,如果没有的话,链接在下面,有宝宝需要自取哈~

算法:关于快速排序的初步理解,You Know?(小白专用0v0)快速排序存在的意义:减少时间 以及快速排序的核心思想: - 掘金 (juejin.cn)

8.gif

其实,上一期介绍的快速排序,并不是完美的写法,在实际运用的过程中,我们会遇到各种各样的问题,比如10w个相同的数据,使得执行陷入死循环;或者遇到边界问题,如果pivot正好取到了边界而且还是最大(小)值,会导致无限划分.....因此这一篇文章将介绍另一种快速排序的写法

两版代码的比较

#include <bits/stdc++.h>
using namespace std;
const int n = 1e6 + 10;
int a[n];

int partition(int a[], int low, int high) {
	int pivot = a[low];

	while (low < high) {
		while (a[high] >= pivot && low < high) {
			high--;
		}
		a[low] = a[high];
		while (a[low] <= pivot && low < high) {
			low++;
		}
		a[high] = a[low];
	}

	a[low] = pivot;
	return low;
}

void Quicksort(int a[], int low, int high) {
	if (low < high) {
		int pivotpos = partition(a, low, high);
		Quicksort(a, low, pivotpos - 1);
		Quicksort(a, pivotpos + 1, high);
	}
}


int main() {
	int n ;
	cin >> n;
	for (int i = 0; i < n; i++) {
	  scanf("%d",&a[i]);
	}

	partition(a, 0, n - 1);//排序,分区

	Quicksort(a, 0, n - 1);//递归
	for (int i = 0; i < n; i++) {
		cout << a[i];
		cout << ' ';
	}

}            //第一版(上一期)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int a[N];

void quick(int l, int r) {
	int i = l - 1;
	int j = r + 1;
	int x = a[(i + j) / 2];
	if (l >= r)
		return;
	while (i < j) {
		do {
			i++;
		} while (a[i] < x);
		do {
			j--;
		} while (a[j] > x);
		if (i < j)
			swap(a[i], a[j]);
	}

	quick(l, j);
	quick(j + 1, r);

}

int main() {
	int n;
	int i;
	scanf("%d", &n);
	for (i = 0; i < n; i++) {
		scanf("%d", &a[i]);
	}
	quick(0, n - 1);
	for (i = 0; i < n; i++) {
		
		printf("%d ", a[i]);
	}
}  //第二版

开始解析

主函数

从主函数中看,差异不大,都是先录入数据再通过quicksort()排序,最后输出。这里quick(0,n-1)中,我个人喜欢按数组下标来,所以用的0和n-1,因为数组长度是从a[0]到a[n-1]。

分区的分析

由此对比可以看出来,第二版的分区与第一版稍有不同,第一版是选定了a[low],逐步通过替换找到了a[low]数字最终的位置。 最后 a[low]=50,low=4。 “4”即是分区点。

int partition(int a[], int low, int high) { int pivot = a[low]; 
while (low < high)
{ 
while (a[high] >= pivot && low < high) { high--; } 
a[low] = a[high]; 
while (a[low] <= pivot && low < high) { low++; } 
a[high] = a[low];
} 
a[low] = pivot; 
return low; }

2.png

11.png

而在这里不一样了,这里的分区是由这一段代码决定的:

void quick(int l, int r) { 
int i = l - 1; 
int j = r + 1; 
int x = a[(i + j) / 2]; 
if (l >= r) return; 
while (i < j) { 
do { i++; } while (a[i] < x); 
do { j--; } while (a[j] > x); 
if (i < j) swap(a[i], a[j]); 
} 
quick(l, j); 
quick(j + 1, r); }

第一版选择了a[low]的值为分界点,而这一版选用的为int x=a[(i+j)/2];

好处:如果数据是已经排序或者是逆序的话,分界点选择了a[low],则每一次划分都将使得右边数组为n-1个数,导致划分不均匀,使得原本O(nlogn)的时间复杂度退化为O(n^2),在做题时会导致 Time Limit Exceeded。 而选择数组的中间数则可以极大避免这种情况的出现。当然也可以不选择中间的数,利用random随机选择也可以。

逐句分析

void quick(int l, int r)

这一语句代入的是所要划分数组的边界,以l和r为边界。

int i = l - 1; 
int j = r + 1; 
int x = a[(i + j) / 2];
if (l >= r) return;

i和j代表着数组的下标,方便遍历数组用。x是此次分区的基准点数。 作用就是比x大的数放右边,比x小的数放左边。 l>=r return的作用就是当数组的边界不合理了就返回结束排序的操作,当它不合理了,就说明排序已经完成了。

0C12804B6E897EC9A5AAA13A0DCE7F1E.jpg

do-while循环可否被while循环代替?

但是相信各位uu们看到i和j有点疑问了,为什么i=l-1,j=r+1呢? 是因为下面的循环:

while (i < j) {
		do {
			i++;
		} while (a[i] < x);
		do {
			j--;
		} while (a[j] > x);
		if (i < j)
			swap(a[i], a[j]);
	}

这里的循环采用的是do while的形式,先使得i++,再判断,再使得j++,再判断。当i=l-1,j=r+1的时候才能遍历整个数组,不然会漏掉a[0]和a[n-1]两个数据。

哎!这个时候又有uu想说了,能不能用while循环代替do while循环呢??

D605761360777423209D658FBA3EB6A8.jpg 那咱就来看看吧。
int i = l;
int j = r;
int x = a[(i+j)/2];
while(i<j){
while(a[i]<x)  {i++;}
while(a[j]>x)  {j--;}
if(i<j) swap(a[i],a[j]);}

如果这样写的话,乍一看没什么问题,倘若我输入了10w个数据全部都相等会怎么样呢?

那就会造成 i 和 j始终得不到更新i自始至终都为0,j自始至终都为n-1,导致循环卡死,不会有任何的输出结果。所以在这个地方我们采用了do-while的循环以确保i和j都能够得到更新。

原代码中while(i<j)是否可以写成while(i<=j)?

void quick(int l, int r) { 
int i = l - 1; 
int j = r + 1; 
int x = a[(i + j) / 2]; 
if (l >= r) return; 
while (i < j) { 
do { i++; } while (a[i] < x); 
do { j--; } while (a[j] > x); 
if (i < j) swap(a[i], a[j]); 
} 
quick(l, j); 
quick(j + 1, r); }

NoNoNo absolutely not!54BF91D07BCA7EECA2718DA7BFE033F1.gif

这样写会有可能造成无限循环,假设一个数组a[2]; a[0]=1,a[1]=2,l=0,r=2,对它进行快排,第一次循环结束以后,j=-1,i=2,再进入第二次循环后,又变成quick(0,2),无限循环卡死。

可否使得while(a[i]<x)变为while(a[i]<=x),while(a[j]>x)变为while(a[j]>=x)?

不不不,不可以哦,如果设一个数组{50...........50},基准点int x=50,当i和j其中一个到达a[1]或者a[n-1]时,都会再次执行++或者--,使得下标越界。

能不能使得swap的if(i<j)变为if(i<=j)

可以,只不过让那个数字自己和自己换了一次,影响不大。

能不能让quick(l, j) quick(j + 1, r) 变为quick(l, j-1) quick(j, r)?

12.jpg 也不行哦!!!!!!!!!!!!!!!

void quick(int l, int r) { 
int i = l - 1; 
int j = r + 1; 
int x = a[(i + j) / 2]; 
if (l >= r) return; 
while (i < j) { 
do { i++; } while (a[i] < x); 
do { j--; } while (a[j] > x); 
if (i < j) swap(a[i], a[j]); 
} 
quick(l, j); 
quick(j + 1, r); }

再来看一下这个代码,循环导致的结果是a[i]>=x,a[j]<=x, a[l...i-1]<=x,a[j+1...r]>=x,而分区的标准是比基点小的在左边分区,比基点大的在右边分区,如果变成quick(l, j-1) quick(j, r),右边的分区是要大于基点的,但是a[j]<=x,所以不符合分区的规则,会使得排序错误,小的数到达不了左侧。

C++库中sort()的用法

在C++中也可以直接用sort(),例如sort(a,a+n), a是数组,左边a+?指的是从a[?]开始,如果没有数字就是a[0],右边是到a[?-1]结束,因为sort()结束迭代器用的是开区间,所以到a[?-1]结束。

结语

好啦,今天的内容就是这样了,写了一下关于快排的写法和常见错误,主要是练习用,考验自己的理解,如果有错希望各位大佬不吝赐教。 如果你觉得我的内容对你有帮助,请点个赞吧!

111.jpg