一、快速排序算法
快速排序使用分治的思想来进行排序,其基本过程如下:
- 从待排序数组中选择一个元素作为枢轴(pivot)。
- 将数组分割成两个子数组,使得左侧子数组的元素都小于等于枢轴,右侧子数组的元素都大于等于枢轴。这个分割的过程称为划分(partition)。
- 对左右子数组分别递归地进行快速排序。
- 递归结束的条件是子数组只有一个元素或者为空。
时空复杂度:
- 平均时间复杂度:O(nlogn)
- 当数组有序时,最坏时间复杂度:O(n^2)
- 空间复杂度:O(logn)
1、hoare划分法
代码实现:
void QuickSortHoare(int* a, int begin, int end)
{// begin为首元素下标,end为尾元素下标if (begin >= end){return; // 递归结束条件:子数组只有一个元素或为空,不需要排序}int left = begin; // 左指针int right = end; // 右指针int key = begin; // 选择第一个元素作为基准元素while (left < right){// 从右侧开始,找到第一个比基准元素小的元素while (a[right] >= a[key] && right > left){right--;}// 从左侧开始,找到第一个比基准元素大的元素while (a[left] <= a[key] && right > left){left++;}// 如果左指针小于右指针,交换左右指针对应的元素if (left < right){swap(&a[left], &a[right]);}}// 将基准元素放到正确的位置上swap(&a[key], &a[left]);// 对基准元素左侧和右侧的子数组进行递归调用QuickSortHoare(a, begin, left - 1);QuickSortHoare(a, left + 1, end);
}
2、挖坑法
挖坑法是一种简洁的快速排序实现方式,它通过交替移动两个指针,将元素一个个填入坑位的方式来进行划分。与Hoare划分法相比,挖坑法在划分的过程中不需要频繁地交换元素,因此实现上会更为简单。
代码实现:
void QuickSortLomuto(int* a, int begin, int end)
{if (begin >= end){return; // 递归结束条件:子数组只有一个元素或为空,不需要排序}int curi = begin; // 当前坑位的索引int temp = a[begin]; // 枢轴元素的值int left = begin; // 左指针int right = end; // 右指针while (left < right){// 从右侧开始,找到第一个比枢轴小的元素的索引while (a[right] >= temp && left < right){right--;}a[curi] = a[right]; // 将右指针指向的元素填入当前坑位curi = right; // 更新当前坑位的索引// 从左侧开始,找到第一个比枢轴大的元素的索引while (a[left] <= temp && left < right){left++;}a[curi] = a[left]; // 将左指针指向的元素填入当前坑位curi = left; // 更新当前坑位的索引}a[curi] = temp; // 将枢轴元素填入最后一个坑位,确保枢轴元素的位置被确定// 对枢轴左侧和右侧的子数组进行递归调用QuickSortLomuto(a, begin, left - 1);QuickSortLomuto(a, left + 1, end);
}
3、前后指针法
代码实现:
void QuickSortPB(int* a, int begin, int end)
{if(begin > end){return; // 递归结束条件:子数组为空,不需要排序}int prev = begin; // prev指针指向枢轴元素的位置int cur = prev + 1; // cur指针指向待比较元素的位置int key = begin; // 枢轴元素的位置// 遍历数组,将小于等于枢轴元素的元素放在prev指针的后面while(cur <= end){if(a[cur] <= a[key]){prev++;if(cur != prev){swap(&a[prev], &a[cur]); // 交换prev指针和cur指针指向的元素}}cur++;}swap(&a[begin], &a[prev]); // 将枢轴元素放到prev指针的位置// 对枢轴左侧和右侧的子数组进行递归调用QuickSortPB(a, begin, prev-1);QuickSortPB(a, prev+1, end);
}
二、快速排序优化
"三数取中"的方法是从待排序数组中随机选择三个元素,然后取这三个元素的中间值作为枢轴元素。具体步骤如下:
- 选择开始位置
begin
、结束位置end
和中间位置mid
,计算mid = (begin + end) / 2
。 - 比较
a[begin]
、a[mid]
和a[end]
的大小,确定其中的中间值。 - 将选出的中间值与
a[begin]
进行交换,将中间值放在数组开始的位置,作为枢轴元素。 - 进行正常的快速排序算法。
通过使用"三数取中"的方法选择枢轴元素,可以尽量避免了最坏情况的发生。最坏的情况是枢轴元素选择的不好,导致每次划分只将数组分成一个很小的部分和一个很大的部分,使得快速排序的时间复杂度退化为O(n^2)。而"三数取中"的方法通过选择一个近似于中位数的元素作为枢轴,能够更平衡地划分数组,减少这种最坏情况的发生,从而提高快速排序的效率。
int GetMidi(int* a, int begin, int end)
{int midi = (begin + end) / 2; // 计算中间位置// begin midi end 三个数选中位数if (a[begin] < a[midi]) // 如果begin位置的值小于midi位置的值{if (a[midi] < a[end]) // 如果midi位置的值小于end位置的值return midi; // 返回midi位置else if (a[begin] > a[end]) // 如果begin位置的值大于end位置的值return begin; // 返回begin位置elsereturn end; // 返回end位置}else {if (a[midi] > a[end]) // 如果midi位置的值大于end位置的值return midi; // 返回midi位置else if (a[begin] < a[end]) // 如果begin位置的值小于end位置的值return begin; // 返回begin位置elsereturn end; // 返回end位置}
}int QuickSortHoare(int* a, int begin, int end)
{int midi = GetMidi(a, begin, end); // 找到中间位置的索引Swap(&a[midi], &a[begin]); // 交换中间位置的值和起始位置的值int left = begin, right = end; // 设置左右指针int keyi = begin; // 设置基准值索引while (left < right){// 右边找小while (left < right && a[right] >= a[keyi]) // 从右边开始找小于基准值的数{--right;}// 左边找大while (left < right && a[left] <= a[keyi]) // 从左边开始找大于基准值的数{++left;}Swap(&a[left], &a[right]); // 交换左右指针所指位置的值}Swap(&a[left], &a[keyi]); // 将基准值放到中间位置return left; // 返回中间位置的索引
}void QuickSort(int* a, int begin, int end)
{if (begin >= end)return; // 如果起始位置大于等于终止位置,则返回int keyi = QuickSortHoare(a, begin, end); // 使用快速排序的Hoare分区函数找到基准值的索引QuickSort(a, begin, keyi - 1); // 对基准值左边的数组进行快速排序QuickSort(a, keyi+1, end); // 对基准值右边的数组进行快速排序
}
三、快速排序的非递归实现
快速排序的非递归实现使用了栈来模拟递归的过程,这样可以避免使用系统栈导致的递归调用过深的问题。
非递归实现的思想如下:
-
创建一个栈,用于存储待处理子数组的边界。初始时,将整个数组的开始位置和结束位置入栈。
-
进行循环,直到栈为空。循环的目的是处理栈中的每个子数组。
-
弹出栈顶的子数组边界,得到开始位置和结束位置。
-
在子数组中选择枢轴元素,并使用 Hoare 划分方式将子数组划分成两个部分。将划分点的下标入栈,保证后续对其进行排序。
-
根据划分点的下标更新子数组边界,将左侧子数组的开始位置和结束位置入栈,再将右侧子数组的开始位置和结束位置入栈。注意保证先处理左侧子数组,再处理右侧子数组。
-
重复步骤 3 到步骤 5,直到栈为空,即所有子数组都被处理完毕。
通过上述步骤,可以将递归的快速排序算法转化为非递归的实现。该实现使用栈来保存待处理的子数组边界,以模拟递归过程。每次处理子数组时,选择一个枢轴元素进行划分,并将划分点的下标入栈。然后根据划分点将子数组分成两部分,分别将左侧和右侧的子数组边界入栈。这样可以确保先处理左侧子数组,再处理右侧子数组,达到快速排序的效果。
代码实现:
typedef int StackDataType;
typedef struct StackNode
{StackDataType data;struct StackNode* next;
}StackNode;typedef struct Stack
{StackNode* q;int size;
}Stack;void StackInit(Stack* s)
{StackNode* head=(StackNode*)malloc(sizeof(StackNode));head->next=NULL;s->q=head;s->size=0;
}void StackPush(Stack* s,StackDataType x)
{StackNode* newnode=(StackNode*)malloc(sizeof(StackNode));newnode->data=x;newnode->next=s->q->next;s->q->next=newnode; s->size++;
}int StackEmpty(Stack* s)
{if(s->q->next==NULL){return 1;}return 0;
}StackDataType StackTop(Stack* s)
{if(!StackEmpty(s)){return s->q->next->data;}else {return -1;}
}void StackPop(Stack* s)
{if(!StackEmpty(s)){StackNode* temp=s->q->next;s->q->next=s->q->next->next;free(temp);s->size--;}
}int get_keyi(int *a, int begin, int end)
{int left = begin;int right = end;int key = begin;while (left < right){// 从右侧开始,找到第一个小于 key 的元素while (a[right] >= a[key] && left < right){right--;}// 从左侧开始,找到第一个大于 key 的元素while (a[left] <= a[key] && left < right){left++;}// 交换找到的两个元素swap(&a[right], &a[left]);}// 将基准值放到中间位置swap(&a[key], &a[left]);return left; // 返回中间位置的索引
}void QuickSortNR(int* a, int begin, int end)
{Stack s;StackInit(&s);// 入栈起始和结束位置StackPush(&s, end);StackPush(&s, begin);int left, mid, right;// 当栈不为空时,循环执行while (!StackEmpty(&s)){left = StackTop(&s);StackPop(&s);right = StackTop(&s);StackPop(&s);// 如果左边界小于右边界,进行划分if (left < right){mid = get_keyi(a, left, right);}// 将左边未排序的部分入栈if (left < mid - 1){StackPush(&s, mid - 1);StackPush(&s, left);}// 将右边未排序的部分入栈if (right > mid + 1){StackPush(&s, right);StackPush(&s, mid + 1);}}
}