hoare法实现
对于快速排序我们首先要关注的是一趟排序的过程,让right先去找比基准元素小的值,没找到一直减减,直到找到为止。right找到之后left去寻找比基准元素大的值,找到之后,将left和right所在位置的元素交换。最后再将相遇位置元素和基准元素进行交换,返回相遇点位置,即可完成一次排序的过程。剩下来就是划分左右区间,进行分治,递归完成排序的过程。
需要注意的是两点:
1.选用了左边的元素,一定要让右边的元素先走
假设我们选取数组最左边元素作为基准元素,若让左指针先移动,会出现问题。因为左指针起始位置的元素就是基准元素,左指针移动时,若遇到比基准元素大的元素就停止,此时右指针开始移动,若右指针在未找到比基准元素小的元素前就与左指针相遇,这就意味着相遇位置的元素是大于基准元素的。若把这个位置的元素和基准元素交换,会导致基准元素左边出现比它大的元素,不符合分区要求。
而让右指针先走,右指针从右往左移动,遇到比基准元素小的元素就停止。接着左指针从左往右移动,遇到比基准元素大的元素停止。然后交换左右指针所指元素。持续这个过程,直到左右指针相遇。此时相遇位置的元素必然小于等于基准元素,将该位置元素与基准元素交换,就能保证基准元素左边的元素都小于等于它,右边的元素都大于等于它。
2.while (left < right && a[right] >= tmp)
这个条件非常重要对于left<right,虽然在外层有关于这个的判断了,但是在里层是不断地进行减减操作的,所以也需要进行判断。对于a[right]>=tmp要关注的是这个=,如果没有等于号的话,有一种特殊情况就是左右两边都停留在和基准元素相同的值上,left和right就会一直停留,最终程序报错。
int partsort(int* a, int left, int right) {
int keyi = left;
int tmp = a[keyi];
while (left <right) {
while (left < right && a[right] >= tmp) {
right--;
}
while (left < right && a[left] <= tmp) {
left++;
}
swap(a[left], a[right]);
}
swap(a[left], a[keyi]);
return left;
}
void quicksort(int* a, int left, int right) {
if (left >= right) {
return;
}
int keyi = partsort3(a, left, right);
quicksort(a, left, keyi - 1);
quicksort(a, keyi + 1, right);
}
挖坑法
这个实现和上面的相似。不需要频繁的交换元素。
int partsort2(int* a, int left, int right) {
int hole = left;
int tmp = a[left];
while (left < right) {
while (left < right && a[right] >= tmp) {
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= tmp) {
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = tmp;
return left;
}
前后指针法
cur不断向右走,如果是比基准元素大的值就++,如果是比基准元素小的值就先++pre,再去交换pre和cur当前位置的值。最后再将基准元素和pre位置所在的值交换,就可以完成一趟排序了。
int partsort3(int* a, int left, int right) {
int tmp = a[left];
int cur = left+1;
int pre = left;
while (cur<=right){
if (a[cur] < tmp && ++pre != cur) {
swap(a[cur], a[pre]);
}
cur++;
}
swap(a[left], a[pre]);
return pre;
}
优化方案
三数取中
快速排序的平均时间复杂度为 O(nlogn),然而在某些特定情况下,它的时间复杂度会退为 O(n^2)。这种最坏情况通常出现在每次选择的基准元素都是数组中的最大或最小元素时。
例如,当数组已经是有序(升序或降序)的,并且总是选择第一个元素或最后一个元素作为基准元素时,每次分区操作都会将数组分成一个长度为 n−1 的子数组和一个长度为 0 的子数组,这样递归树会退化为一个高度为 n 的线性结构,导致时间复杂度变为 O(n^2)。
int Getmidnum(int* a, int left, int right) {
int mid = (left + right) / 2;
if (a[left] < a[right]) {
if (a[mid] < a[left]) {
return left;
}
else if (a[mid]> a[right]) {
return right;
}
else {
return mid;
}
}
else {
//a[right]<a[left]
if (a[mid] < a[right]) {
return right;
}
else if (a[mid] > a[left]) {
return left;
}
else {
return mid;
}
}
}
int partsort3(int* a, int left, int right) {
int mid = Getmidnum(a, left, right);
swap(a[left], a[mid]);
int tmp = a[left];
int cur = left+1;
int pre = left;
while (cur<=right){
if (a[cur] < tmp && ++pre != cur) {
swap(a[cur], a[pre]);
}
cur++;
}
swap(a[left], a[pre]);
return pre;
}
void quicksort(int* a, int left, int right) {
if (left >= right) {
return;
}
int keyi = partsort3(a, left, right);
quicksort(a, left, keyi - 1);
quicksort(a, keyi + 1, right);
}
小区间优化
快速排序运用分治法,将大规模排序问题分解成多个小规模子问题,通过递归方式处理。但递归调用存在一定开销,像函数调用时要保存和恢复上下文、分配栈空间等。当待排序的子数组规模较小时,递归调用的开销在整个排序时间中所占比重会增大,甚至可能超过排序本身的时间开销,导致算法效率降低.小区间优化的常见策略是,当子数组的长度小于某个阈值时,不再继续使用快速排序进行递归划分,而是采用插入排序对该子数组进行排序。
void insertsort(int* a, int n) {
for (int i = 0; i < n-1; i++) {
int end = i;
int tmp = a[end + 1];
while (end >= 0) {
if (a[end] > tmp) {
a[end + 1] = a[end];
end--;
}
else break;
}
a[end + 1] = tmp;
}
}
void quicksort(int* a, int left, int right) {
if (left >= right) {
return;
}
if (right - left <= 8) {
insertsort(a+left, right - left + 1);
}
else {
int keyi = partsort3(a, left, right);
quicksort(a, left, keyi - 1);
quicksort(a, keyi + 1, right);
}
}
非递归实现
非递归实现就是模拟压栈的过程,需要借助容器栈来实现。简单分析一下,把要排序就区间压入到栈中,要注意的是压入到栈中的顺序。要先把右区间先压入到栈中,左区间后压入到栈中。
void quicknoR(int* a, int left, int right) {
stack<int>s;
s.push(left);
s.push(right);
while (!s.empty()) {
int r = s.top();
s.pop();
int l = s.top();
s.pop();
if (l >= r)
continue;
int keyi = partsort3(a, l, r);
s.push(keyi + 1);
s.push(r);
s.push(l);
s.push(keyi - 1);
}
}