一、快速排序的递归实现
基本思想::任取待排序元素序列中的某元素作为基准值,按照该排序码(即所选取的基准值在这一待排序元素中排好序的位置)将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
将区间按照基准值划分为左右两个部分的常见方式有三种:hore版本、挖坑法、左右指针法。
1.1 hore版本
注意:以下排序均默认为升序

1. 直接取待排序元素序列的left位置处的元素为基准值。定义两个变量left和right,left指向待排序元素的首位置,right指向待排序元素的尾位置,然后进行嵌套循环,外层循环控制将左边元素序列中大于基准值的部分和右边元素序列中小于基准值的部分依次进行交换。
2. 这里因为选取left位置为基准值,所以要指向尾位置处的right先向左走,找比基准值小的值,找到后停在此位置,然后指向首位置的left开始向右走,找到比基准值大的值,找到后停在此位置,然后将此时left和right分别指向的值进行交换,再继续刚才的找法。
3. 最终两者相遇,此时循环结束。
4. 出循环后,再将相遇位置的值与基准值进行交换,然后将相遇位置赋值给keyi,之后进行递归。
5. 递归分为两部分,一部分是[begin, keyi-1] ,然后是keyi,最后是 [keyi+1, end]。这里的begin是指首元素位置,end是指尾元素位置,之所以定义这两个变量是因为left和right在循环过程中是不断变化的,所以用这两个变量来接收left和right最初的值。分别对两部分进行上述操作,依次将每个选取的keyi都置于正确的位置,即结束排序。
如具体代码:
void QuickSort(int* a, int left, int right)
{
if (left >= right)//递归结束条件-------等于因为当left=right时,代表此时区间只有一个元素,无需再排-----大于是因为区间内没有元素了,更不需要排
return;
int end = right;
int begin = left;
int keyi = left;
//left++;//这里不能++,因为++后,若它们相遇的位置是在++后的位置,此时则需要交换left和keyi位置的值,因此是错误的
while (left < right)
{
//右边找小
while (left<right && a[right] >= a[keyi])//left<right是为了防止right越过left
right--;
//左边找大
while (left < right && a[left] <= a[keyi])//等于号是因为若遇到与关键值相等的,直接跳过,因为我们只需要把关键值放到排好序的位置上
left++;
//交换
Swap(&a[left], &a[right]);
}
//left和right相遇,交换left和keyi位置的值
Swap(&a[left], &a[keyi]);
keyi=left;//相遇位置,即所取keyi的最终位置,所以要为keyi重新赋值
//递归
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a,begin , keyi - 1);
QuickSort(a, keyi + 1, end);
}
上述代码中有几个易错问题需要注意:
- if (left >= right)//递归结束条件-------等于因为当left=right时,代表此时区间只有一个元素,无需再排-----大于是因为区间内没有元素了,更不需要排。
- left++;//这里不能++,即若选首位置为keyi,而left从第二个元素的位置开始,因为++后,若它们相遇的位置是在++后的位置,即left开始指向位置,此时则需要交换left和keyi位置的值,这样选择的基准值并未在正确位置上,因此是错误的。
- 内层循环条件中,left<right是为了防止right直接越过left,若right一直向左找,若一直没找到比基准值小的值则会直接越界。a[left] <= a[keyi]等于号是因为若遇到与基准值相等的,直接跳过,因为我们只需要把关键值放到排好序的位置上,至于与基准值相等的会在被分成的两个部分继续排。
- 出循环后,keyi=left;//相遇位置,即所取keyi的最终位置,所以要为keyi重新赋值。
- 注意:递归时,递归的传入参数。并且递归的真正目的是将每个区间中每次选取的基准值都排在正确的位置上。这样递归结束就能彻底排好序。
还有一个关键问题需要注意:那就是为什么从左边选取基准值时,要right先找。
因为要保证相遇位置的值比keyi小,或者相遇位置就是keyi的位置。
- R找到小,L找大没有找到,L遇到R,此时相遇位置的值小于keyi位置的值。
- R找小,没有找到,L要么已经找到(即R已经找到一次小了),要么L还没开始找,就在keyi位置,直接遇到L,此时要么相遇位置的值比keyi位置的值小,要么直接到keyi,此时两者的值相等
同理,右边做keyi,左边先走,相遇位置的值要比keyi位置的值大。
对于hore版本中的keyi的选取有两种方法可以改进,并且每种都大大提高了排序的效率。注意:无论是哪种方法都仅仅只是对keyi的值进行操作,然后以此确定此时keyi位置的值为基准值。
1. 随机选keyi:为了避免算法在某些极端情况下(例如输入数组已经有序或部分有序)出现最坏情况的时间复杂度O(n²)。通过随机选取基准值,可以提高划分的平衡性,从而使得平均时间复杂度保持在O(n log n)。这种方法有效降低了快速排序的整体运行时间,尤其是在处理大数据量时,显著提升了算法效率。
如以下写法:
int randi=left+(rand() % (right - left));//加left是因为对任意一区间进行排序,取模是为了保证randi在指定区间内
Swap(&a[left], &a[randi]);//将得到的randi和最左侧的left指向的值进行交换,目的是为了将keyi指向的值移到最左边
2. 三数取中:三数指的是left、right、以及left和right的平均值mid。这种方法目的是为了避免在某些特殊情况下(如输入数据呈现某种规律性或人工构造的“最坏情况”输入)导致算法时间复杂度退化为O(n²)。这种方法通过更合理的选择关键值,使得划分过程更加均衡,从而提高快速排序的整体性能和稳定性。
如以下写法:
int getmiddlenum(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
return mid;
else if (a[left] > a[right])
return left;
else
return right;
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
int midi=getmiddlenum(a,left,right);
Swap(&a[left], &a[midi]);
1.2 挖坑法

1. 首先选出left位置处的值为基准值,将此基准值用变量key保存起来,并将基准值的位置用一个变量hole来保存。
2. 然后嵌套循环,外层循环仍然控制right和left的移动。内层循环左区间找大,右区间找小,并负责将找到的位置对应的值放到定义的坑的位置即hole的位置处,并将hole指向此时找到的位置。
3. 结束循环后,left和right一定会相遇,且相遇位置就是此时坑的位置,再将保存的基准值key放到此时坑的位置。
4. 然后进行递归,注意此时坑hole的位置就是选取基准值的正确位置。此时递归区间是左区间[begin, hole-1], hole,右区间 [hole+1, end]。如以下代码:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int key = a[left];//保存选取的基准值
int hole = left;//保存基准值的位置
while (left < right)
{
// 右边找小
while (left < right && a[right] >= key)
--right;
a[hole] = a[right];//将找到的比基准值小的值放到定义的坑中
hole = right;//并将hole指向right此时的位置
// 左边找大
while (left < right && a[left] <= key)
++left;
a[hole] = a[left];//将找到的比基准值大的值放到定义的坑中
hole = left; //并将hole指向left此时的位置
}
a[hole] = key;//相遇位置一定在坑的位置
// 递归
//[begin, hole-1] hole [hole+1, end]
QuickSort(a, begin, hole - 1);
QuickSort(a,hole+1, end);
}
1.3 左右指针法

1. 首先定义变量prev,并用其保存left的值,再定义变量cur,cur保存left+1的值,并定义基准值的位置key是left的位置。
2. 进入循环,然后判断cur位置的值是否小于基准值,若小于,再判断prev+1后的位置是否与此时cur不在同一位置,若是,则交换此时prev和cur位置的值。注意,cur会一直往右找,直到找到小于基准值的位置,交换后,会继续向后找,最终循环结束。而prev会根据cur是否找到来决定是否继续向右找,若找到,则prev开始向右移动。
3. 出循环后,再交换此时prev和key位置的值,此时prev位置就是基准值排好后的准确位置。
4. 根据此时基准值的位置分出左右区间后,再分别进行递归。递归区间是[left, keyi-1] ,然后是keyi,最后右区间是 [keyi+1, right]。如以下代码:
void QuickSort(int*a,int left,int right)
{
if(left>=right)
return;
int prev = left;
int cur = left + 1;
int key = left;
while (cur<=right)
{
if (a[cur] < a[key] && ++prev != cur)//&&符号,若前面为真,则会执行后面的代码-------prev和cur只有不等才用交换,若相等就不用交换
Swap(&a[prev], &a[cur]);//只要找到小于key位置的值,就将此时prev和cur位置的值进行交换----cur只负责找小
cur++;
}
Swap(&a[prev], &a[key]);
int keyi = prev;
//递归
//区间[left,keyi-1]keyi[keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
1.4 小区间优化
此方法是针对快速排序的递归实现的。无论是上面哪种方法,都会将整个区间分为左右两个部分,并将基准值放到准确位置。随着递归下去,区间也在不断变小,当区间小于一定值时,就没必要再选择继续递归,直接将此区间使用插入排序进行排序。这样可以减少排序时间,提高排序效率。
如以下代码:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
if ((right - left + 1) > 10)//区间小于一定值时
{
int keyi = PartSort(a, left, right);//这是上面三种方法中的任意一种得到的基准值的位置
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
else
{
InsertSort(a + left, right - left + 1);//这是直接插入排序的函数进行排序
}
}
二、快速排序的非递归实现
在某些情况下(如处理非常大的数据集)递归可能导致栈溢出或效率降低。因此使用非递归形式进行排序是必要的。
基本思想:利用栈的先进后出,提前将要排序的区间存入栈中,然后取出栈顶元素。注意:这里存入和取出的元素都是数组的下标。
如以下代码:
void QuickSortNonR(int* a, int left, int right)
{
ST st;//定义一个栈的结构体
STInit(&st);//栈初始化
STPush(&st, right);//提前将要排序的区间存入栈中,注意栈是先进后出,所以存入栈的数据要注意顺序
STPush(&st, left);//注意存入的都是数组下标
while (!STEmpty(&st))//判断栈是否为空,若不为空则进入循环继续
{
int begin = STTop(&st);//取出栈顶元素
STPop(&st);//删除栈顶元素
int end= STTop(&st);
STPop(&st);
int keyi = PartSort(a, begin, end);//对指定区间进行单趟排序,并返回一个keyi,这里用的是上面三个方法中的任意一个
if (keyi + 1 < end)//先往栈中存入右边的部分,仍然注意先进后出
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
整个循环过程仍然跟递归一样,每递归一次就相当与进行一次循环。每次存入栈的区间就是递归过程中传入的区间,然后将递归过程改成循环过程。

859

被折叠的 条评论
为什么被折叠?



