快排(霍尔排序实现+前后指针实现)(递归+非递归)

前言

快排是很重要的排序,也是一种比较难以理解的排序,这里我们会用递归的方式和非递归的方式来解决,递归来解决是比较简单的,非递归来解决是有点难度的

快排也称之为霍尔排序,因为发明者是霍尔,本来是命名为霍尔排序的,但是霍尔这个人对于自己创造的排序很自信,所以命名为快排

当然也是如他所料,快排确实很快,但是还没有达到第一批次那个程度

快排gif

快排实现逻辑排序

单趟实现逻辑:
1.假设左边为keyi,也就是对比数值
2,右边R先走,循环寻找比keyi小的数值
3,左边l走,循环寻找比keyi大的数值
4,交换找到的比keyi大的数值和小的数值,此时会导致小的在左边,大的在右边,最后相遇的时候交换keyi和相遇的元素

多趟实现:
1,多趟实现可以采取递归和非递归,但是总体逻辑都是一样的,这里先讲解一下递归的方式2,此时,我们会发现keyi下标所在位置,就是从前往后6,的位置,所以6回到自己的位置,我们以keyi为分界点进行切割【left,keyi-1】keyi【keyi+1,right】
进行递归,从而实现简易版的速排
完善逻辑:
1,此时是快排还是有点问题的,当数值本身就是顺序的时候
会发现此时的时间复杂度就变成了n^2,也就是不快了
2,原因是当本身就是排序好的时候,right右边会一直往左边寻
找,直到找到left,和自己交换,此时的时间复杂度也就是
n,n-1..1.0
3,解决办法,我们可以三个数值取中,什么意思?也就是不管什么情况,我们都取出前三个数值,比如这里的
6 1 2
我们取出6 1 2,取出中间的位置,2,和keyi进行交换其他逻辑不变
完善逻辑:
1,此时我们发现逻辑没有问题,但是速度还是和堆排序有点差距,那么此时我们继续进行优化
2,因为这里是用递归来实现的,我们发现,递归的实现是逐级实现的,也就是
第-层->第n层:1->2->3->4->…->n-1->n
这样的递归方式来实现的,所以越到下面,递归的越多
我们可以把最后十层的递归用插入排序来实现一下,
3,为什么用插入排序?在排序里面有希尔排序,冒泡排序,选
择排序,堆排序
希尔排序->插入排序的进阶(书写复杂)
冒泡排序->时间复杂度高
选择排序->时间复杂度和冒泡一样,比较高
堆排序->处理大型数字问题,这里没必要
所以我们采取插入排序的方式进行解决
4,解决,我们只需要在递归的时候加一个判断,就可以,当数
值小于等于10 的时候,我们直接调用插入排序解决问题。
此时速排(霍尔排序),递归的方式也就结束了。

图解:

快排单趟实现

快排多趟实现

简易版本的代码实现

//霍尔方法(递归实现)
void QuickSort01(int* a, int left, int right)
{//递归停止条件if (left >= right)return;//单趟实现//右边找小,左边找大int begin = left; int end = right;int keyi = begin;while (begin < end){//右边找小,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] <= a[end]){end--;}//左边找大,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] >= a[begin]){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[keyi], &a[begin]);keyi = begin;//多趟递归实现//[left,keyi-1] keyi [keyi+1,right]   这里传递的是区间//  1     0      1     2      1       当只剩一个数值的时候,也就是这个区间的时候,递归停止 QuickSort01(a, left, keyi - 1);QuickSort01(a, keyi + 1, right);
}

解释:

  1. 函数定义QuickSort01函数接受一个整数数组的指针a以及两个整数leftright,分别表示要排序的数组部分的起始和结束索引。

  2. 递归终止条件:如果left大于或等于right,则当前子数组无需排序,递归结束。

  3. 初始化:设置两个指针beginend分别指向子数组的起始和结束位置,keyi作为基准元素的索引,初始位置设为left

  4. 单趟排序

    • 使用两个内层循环,一个从右侧向左寻找比基准值小的元素,另一个从左侧向右寻找比基准值大的元素。
    • 当找到合适的元素时,交换这两个元素的位置,然后继续寻找,直到beginend相遇。
  5. 基准值定位:将基准值a[keyi]begin指向的元素交换,此时begin指向的位置是基准值的最终位置。

  6. 递归排序:对基准值左边的子数组[left, keyi - 1]和右边的子数组[keyi + 1, right]递归调用QuickSort01函数进行排序。

  7. 效率:快速排序的平均时间复杂度为O(n log n),但在最坏情况下(如数组已经排序)时间复杂度会退化到O(n^2)。霍尔方法通过减少不必要的数据交换来提高排序效率。

  8. 辅助函数Swap函数用于交换两个元素的值,虽然在代码中未给出定义,但它是快速排序中交换元素的关键操作。

快速排序算法的效率和性能在实际应用中通常优于其他O(n log n)算法,如归并排序,尤其是在数据量较大时。然而,其稳定性不如归并排序,且在最坏情况下性能较差。在实际应用中,快速排序通常与其他排序算法结合使用,以提高整体排序性能。

注意事项

注意事项1

这里有一个关键点是很重要的,也就是我们是从右边先出发的,因为我们的keyi的位置在左边。

如果我们的keyi的下标在左边,并且左边先走的话,就会产生一种结果

如图

注意事项2

不是等于就交换,是等于会跳过往下找

当我们写的是不等于的时候

快排完整代码实现-(三数值取中)

此时存在的最大问题就是如果排序本身就是顺序排序的情况下,这时间复杂度反而高了,所以我们对排序进行修改

//交换函数
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//霍尔方法(递归实现)
//三数取中
int GetMid(int* a, int left, int right)
{//三数取中传参的是下标,我们取中也是根据下标进行计算的int mid = (left + right) / 2;if (a[left] < a[right]){if (a[mid] < a[left])//a[mid] < a[left] < a[right]{return left;}else if(a[mid] > a[right])// a[left] < a[right] < a[mid] {return right;}else{return mid;}}else//a[left] > a[right]{if (a[mid] > a[left])//a[mid] > a[left] > a[right]{return left;}else if (a[mid] < a[right])//a[left] > a[right] > a[mid] {return right;}else{return mid;}}
}
void QuickSort01(int* a, int left, int right)
{//递归停止条件if (left >= right)return;//三数取中int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);//单趟实现//右边找小,左边找大int begin = left; int end = right;int keyi = begin;while (begin < end){//右边找小,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] <= a[end]){end--;}//左边找大,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] >= a[begin]){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[keyi], &a[begin]);keyi = begin;//多趟递归实现//[left,keyi-1] keyi [keyi+1,right]   这里传递的是区间//  1     0      1     2      1       当只剩一个数值的时候,也就是这个区间的时候,递归停止 QuickSort01(a, left, keyi - 1);QuickSort01(a, keyi + 1, right);
}

总结:

  1. 函数目的:选择一个合适的基准值,以提高快速排序算法的效率。

  2. 传入参数:接受一个整数数组的指针a,以及表示数组部分边界的整数leftright

  3. 计算中间索引:通过(left + right) / 2计算中间元素的索引mid

  4. 三数取中逻辑

    • 如果数组的第一个元素a[left]小于最后一个元素a[right]
      • 如果中间元素a[mid]小于第一个元素,则选择第一个元素作为基准。
      • 如果中间元素大于最后一个元素,则选择最后一个元素作为基准。
      • 否则,选择中间元素作为基准。
    • 如果第一个元素大于或等于最后一个元素(即数组首尾元素已经排序或相等):
      • 如果中间元素大于第一个元素,则选择第一个元素作为基准。
      • 如果中间元素小于最后一个元素,则选择最后一个元素作为基准。
      • 否则,选择中间元素作为基准。
  5. 返回值:函数返回基准值的索引。

  6. 优化目的:通过三数取中法选择基准,可以减少快速排序在特定情况下性能退化的问题,如数组已经排序或包含大量重复元素。

  7. 适用场景:适用于快速排序算法中,作为选择基准值的策略。

  8. 性能影响:选择一个好的基准值可以确保数组被均匀地划分,从而接近快速排序的最佳平均时间复杂度O(n log n)。

三数取中法是一种简单而有效的基准选择策略,它通过比较数组的首元素、尾元素和中间元素,来确定一个相对平衡的基准值,有助于提高快速排序的整体性能和稳定性。

快排完整代码实现-(减少递归次数)

此时我们还面临的问题就是底层的递归次数过多的问题,递归会随着次数的增加呈现倍数增长,就像三角形一样

最后我们减少递归次数,把最底层从递归改为插入排序,逻辑完成

快排完整代码

//交换函数
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//霍尔方法(递归实现)
//三数取中
int GetMid(int* a, int left, int right)
{//三数取中传参的是下标,我们取中也是根据下标进行计算的int mid = (left + right) / 2;if (a[left] < a[right]){if (a[mid] < a[left])//a[mid] < a[left] < a[right]{return left;}else if(a[mid] > a[right])// a[left] < a[right] < a[mid] {return right;}else{return mid;}}else//a[left] > a[right]{if (a[mid] > a[left])//a[mid] > a[left] > a[right]{return left;}else if (a[mid] < a[right])//a[left] > a[right] > a[mid] {return right;}else{return mid;}}
}
void QuickSort01(int* a, int left, int right)
{//递归停止条件if (left >= right)return;//当区间数值小于10个,此时我们直接采取插入排序进行排序if (right - left + 1 <= 10){//这里记得是左右区间,所以不能只传递a,而是传递a + leftInsertionSort(a + left, right - left + 1);}else{//三数取中int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);//单趟实现//右边找小,左边找大int begin = left; int end = right;int keyi = begin;while (begin < end){//右边找小,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] <= a[end]){end--;}//左边找大,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] >= a[begin]){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[keyi], &a[begin]);keyi = begin;//多趟递归实现//[left,keyi-1] keyi [keyi+1,right]   这里传递的是区间//  1     0      1     2      1       当只剩一个数值的时候,也就是这个区间的时候,递归停止 QuickSort01(a, left, keyi - 1);QuickSort01(a, keyi + 1, right);}
}

代码解释:

  1. 三数取中函数 GetMid:

    • 计算中间索引 mid
    • 通过比较数组的首元素、尾元素和中间元素,选择一个合适的基准值。
    • 如果首元素小于尾元素,选择中间元素和首尾元素中较小或较大的一个作为基准。
    • 如果首元素大于尾元素,选择中间元素和首尾元素中较大或较小的一个作为基准。
  2. 快速排序函数 QuickSort01:

    • 递归停止条件:如果当前区间的左右索引 left 和 right 交叉或重合,则不需要排序。
    • 当区间大小小于或等于10时,使用插入排序,因为小数组上插入排序更高效。
    • 使用 GetMid 函数获取基准值的索引,并将基准值与首元素交换。
    • 霍尔方法的分区操作,通过两个指针 begin 和 end 进行分区。
    • 递归地对基准值左边和右边的子区间进行快速排序。
  3. 辅助函数 Swap:

    • 交换两个元素的值,虽然代码中未给出定义,但通常是一个简单的值交换操作。

总结:

  • 算法优化: 通过三数取中法选择基准值,可以提高基准值的选中质量,从而提高快速排序的效率。
  • 小数组优化: 当子数组的大小,小于或等于10时,使用插入排序代替快速排序,因为小数组上插入排序的性能通常更好。
  • 递归与迭代: 快速排序是一个递归算法,但在小数组上切换到迭代的插入排序可以减少递归开销。
  • 分区策略: 使用霍尔方法进行分区,这是一种高效的分区策略,可以减少不必要的数据交换。
  • 适用场景: 这种改进的快速排序适用于大多数需要排序的场景,尤其是在大数据集上,它能够保持较好的性能。
  • 时间复杂度: 平均情况下时间复杂度为 O(n log n),最坏情况下(已排序数组)时间复杂度为 O(n^2),但由于三数取中法和插入排序的结合,最坏情况出现的概率降低。

通过这种改进,快速排序算法在处理小数组时更加高效,同时在大数据集上也能保持较好的性能,使其成为一种更加健壮的排序算法。

快排的时间复杂度

快速排序算法的时间复杂度取决于分区过程中基准值的选择。

理想情况下

基准值会将数组均匀地分成两部分,每部分的元素数量大致相等。对于这种理想情况,快速排序的时间复杂度是 O(n log n),其中 n 是数组中的元素数量。

最坏情况下

如果基准值的选择非常不均匀,从而导致每次分区都极不平衡,快速排序的最坏时间复杂度会退化到 O(n^2)。这种情况通常发生在数组已经排序或所有元素相等的情况下。

在当前代码中

使用了三数取中法来选择基准值,这种方法可以在大多数情况下避免选择极端值作为基准,从而减少最坏情况发生的概率。但是,如果数组的元素分布非常不均匀,或者存在大量重复元素,仍然可能遇到性能退化的情况。

此外,代码中还引入了一个优化:当子数组的大小小于或等于10时,使用插入排序而不是快速排序。这是因为对于小数组,插入排序的性能通常比快速排序更好,而且插入排序是稳定的。这个优化可以提高算法在处理小数组时的效率。

总的来说,这个改进的快速排序算法的平均时间复杂度是 O(n log n),但在最坏情况下仍然是 O(n^2)。然而,由于三数取中法和插入排序的优化,最坏情况的发生概率被大大降低了。在实际应用中,这种改进的快速排序算法通常能够提供非常高效的排序性能。

前后修改之后速度进行对比

优化,和不优化之间的区别

这里计算的是一千万个数值进行排序的毫秒数值,也就是不到一秒,还是很快的,尤其是修改之后,解决了大量的递归问题

注意事项

这里调用的插入排序是升序,写的快排也是升序,如果你需要测试降序,那么你需要把插入排序一起改成降序,不然会导致排序冲突

快排(前后指针-递归解决)

前言

快排解决办法有很多种,这里我再拿出来一种前后指针版本

虽然这个版本的时间复杂度和霍尔一样,逻辑也差不多,但是实际排序过程,确实会比霍尔慢一点

快排gif

快排前后指针实现逻辑:

前后指针实现逻辑(升序):
单趟排序:
1,我们使用递归来进行实现,所以我们先实现单趟排序
2,定义两个下标,也就是所谓的指针,初始的时候,一个指向最左边第一个元素(prev),一个指向第二个元素(cur),最后定义一个对比keyi3,此时先判断我们的cur是不是小于keyi。cur小于keyi的话:prev++,交换,之后cur++4,但是我们如果和自己交换此时没有什么意义,所以这里我们需要做一个处理
5,继续往前走,如果遇见的是:比keyi下标大的元素此时,cur++,直到遇见的是比keyi下标小的元素,循环执行.prev++,交换,之后cur++

6,最后cur走到最后一个元素,我们交换keyi的下标元素和prev的下标元素

多趟实现:
1,递归进行分割:【left,keyi-1】keyi【keyi+1,right】
2,停止条件就是当left>=right
原因:【left, keyi-1】keyi【keyi+1, right】

快排单趟实现

这里只是图解单趟实现逻辑,因为多趟实现的逻辑和霍尔的一样,也是递归,所以不进行多次书写

代码实现

这里的代码实现的核心逻辑不一样,大的框架是一样的,也就是三数取中,以及减少递归从而使用插入排序这样的逻辑是一样的,下面我们来看看这个新的组装怪

//快排(前后指针方法)(递归实现)
void QuickSort02(int* a, int left, int right)
{//递归停止条件if (left >= right)return;//创建两个变量,作为前后指针使用int prev = left; int cur = prev + 1;int keyi = left;//当快指针到尾的时候,跳出循环->交换while (cur <= right){//前后指针中间是比a[keyi]大的数值,所以遇见大的++,小的停止if (a[keyi] > a[cur]){//停止之后,慢指针++,并且进行交换,因为中间才是大的数值,cur遇见大数值++。遇见小数值才停下来prev++;Swap(&a[prev], &a[cur]);//同理快指针也进行++,往后移动cur++;}else{cur++;}}Swap(&a[prev], &a[keyi]);keyi = prev;//多趟递归实现//[left,keyi-1] keyi [keyi+1,right]   这里传递的是区间//  1     0      1     2      1       当只剩一个数值的时候,也就是这个区间的时候,递归停止 QuickSort02(a, left, keyi - 1);QuickSort02(a, keyi + 1, right);
}

总结:

  1. 算法基础:快速排序是一种分而治之的排序算法,它通过递归地将数组分为较小的子数组,然后对这些子数组进行排序。

  2. 分区策略:使用前后指针(prevcur)进行分区,而不是传统的左右指针。这种方法在某些情况下可以减少元素交换的次数。

  3. 基准值选择:基准值(keyi)是数组的第一个元素,即left索引对应的元素。

  4. 指针移动规则

    • prev作为慢指针,用于跟踪比基准值小的元素的边界。
    • cur作为快指针,从left + 1开始遍历数组。
  5. 元素交换:当快指针指向的元素大于基准值时,不进行交换,快指针继续移动;当快指针指向的元素小于或等于基准值时,与慢指针所指元素交换,然后慢指针和快指针都向前移动。

  6. 递归排序:在基准值确定之后,递归地对基准值左边和右边的子数组进行排序。

  7. 时间复杂度:平均情况下,快速排序的时间复杂度为O(n log n)。最坏情况下,如果每次分区都极不平衡,时间复杂度会退化到O(n^2)。

  8. 空间复杂度:由于递归性质,快速排序的空间复杂度为O(log n)。

  9. 算法优化:通过前后指针方法,可以在某些情况下提高快速排序的性能,特别是当基准值接近数组中间值时。

  10. 适用场景:快速排序适用于大多数需要排序的场景,特别是在大数据集上,它通常能够提供非常高效的排序性能。

优化

这里我们可以看到,cur无论是if还是else里面都需要++,所以我们直接放到外面

其次我们为了效率,不和自己交换,我们不和自己交换,进行一个判断条件

快慢指针代码优化(完整)

//交换函数
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//快排(前后指针方法)(递归实现)
void QuickSort02(int* a, int left, int right)
{//递归停止条件if (left >= right)return;if (right - left + 1 >= 10){InsertionSort(a + left, right - left + 1);}else{//三数取中int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);//单趟实现//创建两个变量,作为前后指针使用int prev = left; int cur = prev + 1;int keyi = left;//当快指针到尾的时候,跳出循环->交换while (cur <= right){//前后指针中间是比a[keyi]大的数值,所以遇见大的++,小的停止if (a[keyi] > a[cur] && prev++ != cur){//停止之后,慢指针++,并且进行交换,因为中间才是大的数值,cur遇见大数值++。遇见小数值才停下来Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;//多趟递归实现//[left,keyi-1] keyi [keyi+1,right]   这里传递的是区间//  1     0      1     2      1       当只剩一个数值的时候,也就是这个区间的时候,递归停止 QuickSort02(a, left, keyi - 1);QuickSort02(a, keyi + 1, right);}
}

总结:

  1. 基本递归结构:算法使用递归调用来处理子数组,这是快速排序算法的核心结构。

  2. 小数组优化:当子数组的大小小于或等于10时,算法使用插入排序而不是快速排序,因为插入排序在小规模数据上更高效。

  3. 三数取中法:为了更均匀地分割数组,算法使用三数取中法选择基准值,这有助于减少最坏情况发生的概率。

  4. 前后指针方法:算法使用两个指针(prevcur),其中prev作为慢指针,cur作为快指针,通过这种方式进行一次遍历完成分区。

  5. 分区操作:快指针从left + 1开始遍历,如果当前元素小于基准值,则与慢指针所指的元素交换,然后慢指针向前移动。

  6. 递归排序子数组:基准值确定后,算法递归地对基准值左边和右边的子数组进行排序。

  7. 时间复杂度:平均情况下,算法的时间复杂度为O(n log n),最坏情况下为O(n^2)。但由于采用了小数组优化和三数取中法,最坏情况的发生概率降低。

  8. 空间复杂度:算法的空间复杂度为O(log n),这主要由于递归调用导致的栈空间使用。

  9. 适用场景:这种改进的快速排序算法适用于大多数需要排序的场景,尤其是在大数据集上,它能够提供非常高效的排序性能,同时在小数据集上也表现出较好的效率。

  10. 算法稳定性:由于使用了插入排序对小规模子数组进行排序,算法在处理小规模数据时具有稳定性。

  11. 注意:在实际测试·里面,前后指针比霍尔排序慢一点

通过这种混合排序策略,算法在保持快速排序优点的同时,也提高了在特定情况下的排序效率,使其成为一种更加健壮的排序方法。

注意事项

这里调用的插入排序是升序,写的快排也是升序,如果你需要测试降序,那么你需要把插入排序一起改成降序,不然会导致排序冲突

快排(霍尔版本-非递归解决)

前言

快拍不仅需要学习递归,还需要学东西非递归,这样更有助于我们理解快拍

首先我们需要知道,非递归的学习需要使用栈,所以如果我们的栈的学习是不完善的,建议学习一下栈

非递归gif

这里其实单词循环是谁其实不重要,可以是前后指针,也可以是霍尔方式,这里我们拿出来霍尔的gif来观看

实现图解

非递归实现主要是依赖栈来进行实现,依赖栈的特点,先进后出,后进前出

1,首先我们需要写一个栈的库进行调用

2,入区间,调用单次排序的实现思路

3,入区间的时候,我们需要把握入栈和出栈的关键

代码实现(前后指针)

首先我们调用栈

头文件

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int STDataType;
typedef struct Stack
{STDataType* _a; // 首元素的地址int _top;		// 栈顶,初始化为0,也就是等同于size,初始化为-1,等同于下标int _capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈尾元素 
STDataType Stackhop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);

实现文件

#include"Stack.h"
// 初始化栈 
void StackInit(Stack* ps)
{ps->_a = NULL;ps->_capacity = ps->_top = 0;
}
// 销毁栈 
void StackDestroy(Stack* ps)
{assert(ps && ps->_top);free(ps->_a);ps->_a = NULL;ps->_capacity = ps->_top = 0;
}// 入栈 
void StackPush(Stack* ps, STDataType data)
{//判断需不需要扩容,相等的时候需要扩容if (ps->_capacity == ps->_top){//判断空间是不是0,因为为0的时候,再多的数值*2,也是0int newcapacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;STDataType* tmp = (STDataType*)realloc(ps->_a, sizeof(STDataType) * newcapacity);if (tmp == NULL){perror("StackPush:tmp:err:");return;}ps->_capacity = newcapacity;ps->_a = tmp;}ps->_a[ps->_top] = data;ps->_top++;
}// 出栈 
void StackPop(Stack* ps)
{assert(ps);ps->_top--;
}
// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{//这里必须大于0 因为我们这里等同size,也就是个数,等于0都不行assert(ps);return ps->_a[ps->_top - 1];
}
// 获取栈尾元素 
STDataType Stackhop(Stack* ps)
{assert(ps && ps->_top > 0);return ps->_a[0];
}
// 获取栈中有效元素个数 
int StackSize(Stack* ps)
{//获取有效元素的时候,里面可以没有元素assert(ps);return ps->_top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps)
{//这里的判断是不是空,也就是里面是不是有数值,这里等于是一个判断,没有的话返回ture,有的话返回falseassert(ps);return ps->_top == 0;
}

其次调用前后指针来实现

//快排(前后指针方法)(单趟)
int one_QuickSort02(int* a, int left, int right)
{//三数取中//int mid = GetMid(a, left, right);//Swap(&a[mid], &a[left]);//单趟实现//创建两个变量,作为前后指针使用int prev = left; int cur = prev + 1;int keyi = left;//当快指针到尾的时候,跳出循环->交换while (cur <= right){//前后指针中间是比a[keyi]大的数值,所以遇见大的++,小的停止if (a[keyi] > a[cur] && prev++ != cur){//停止之后,慢指针++,并且进行交换,因为中间才是大的数值,cur遇见大数值++。遇见小数值才停下来Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[keyi], &a[prev]);return prev;                                                                                                                                                                   //快排 非递归实现
void QuickSort003(int* a, int left, int right)
{//非递归实现主要是用栈来模拟实现,在c++里面我们可以直接调用栈,但是在C语言里面我们只能写出来栈再进行调用//思路(霍尔方式)//1单趟的思路还是一样的,如果是升序的情况下,依旧是先从右边出发(找小),后从左边出发(找大)//2,循环递归过程我们改为利用进栈出栈来实现。首先我们需要明确这里传递的是区间,也就是利用栈实现的时候,我们传递的是数组和区间,利用区间进行计算。这里的关键在于传递区间的时候,我们需要详细知晓栈的特点,先进后出,后进后出,。所以在传递区间的时候,如果多趟循环,一分为二的时候,我们需要先传递右侧的区间,再传递左侧区间,因为我们需要先计算左侧。同理进去之后,我们需要继续入栈,需要先-入计算左侧的区间的右侧区间,后入左侧区间。这样就会先计算左侧区间。栈的特性,先进后出,后进先出// // 所以这里我们把霍尔排序单趟实现来单独拿出来,这样的话我们接受的返回值是中间值//[left,keyi-1] keyi [keyi+1,right]//这里需要用非递归来解决Stack ps;StackInit(&ps);StackPush(&ps, right);StackPush(&ps, left);while (!StackEmpty(&ps)){int begin = StackTop(&ps);StackPop(&ps);int end = StackTop(&ps);StackPop(&ps);//假设入栈区间此时来到-> 0-2int mini = one_QuickSort02(a, begin, end);//经过计算之后,此时中间值是,keyi=1//0 1 2  三个区间三个数值,此时进行入栈判断//[begin,keyi-1]keyi[keyi+1,end]//[  0  ,  0   ] 1  [  2   , 2 ]//所以不继续入栈if (mini + 1 < end){//右边先入栈,后计算StackPush(&ps, end);StackPush(&ps, mini + 1);}if (mini - 1 > begin){//左边区间后入栈,先计算StackPush(&ps, mini - 1);StackPush(&ps, begin);}}StackDestroy(&ps);
}

解释:

one_QuickSort02 函数

这个函数是快速排序算法中的单趟排序实现。它使用前后指针法来实现,具体步骤如下:

  1. 初始化指针prev 初始化为 leftcur 初始化为 prev + 1keyi 也初始化为 left
  2. 循环:当 cur 小于等于 right 时,执行循环体内的操作。
  3. 比较和交换:如果当前 cur 指向的元素小于 keyi 指向的元素,并且 prev 指针不等于 cur,说明找到了一个比基准值小的元素,需要交换。将 a[prev] 和 a[cur] 交换,并将 prev 指针向前移动一位。
  4. 移动快指针:无论是否发生交换,cur 指针都向前移动一位。
  5. 交换基准值:循环结束后,将 keyi 指向的元素与 prev 指向的元素交换,此时 prev 指向的是比基准值小的元素的最后一个位置。
  6. 返回值:函数返回 prev 的值,这个值是下一次分区的起始位置。

QuickSort003 函数

这个函数是快速排序的非递归实现,使用栈来模拟递归过程。具体步骤如下:

  1. 初始化栈:创建并初始化一个栈 ps
  2. 入栈:将 left 和 right 作为初始区间入栈。
  3. 循环:只要栈不为空,就执行循环。
  4. 单趟排序:每次从栈中取出两个值作为区间的左右边界,调用 one_QuickSort02 函数进行单趟排序,得到中间值 mini
  5. 判断区间:根据 mini 的位置,判断是否需要继续对左右区间进行排序。
    • 如果 mini + 1 < end,则说明右侧还有元素需要排序,将 end 和 mini + 1 入栈。
    • 如果 mini - 1 > begin,则说明左侧还有元素需要排序,将 begin 和 mini - 1 入栈。
  6. 出栈:每次循环结束,都会从栈中弹出两个值,直到栈为空。
  7. 销毁栈:循环结束后,销毁栈。

对于栈和队列不是很明白的,推荐看一下栈和队列篇章

数据结构-栈和队列(速通版本)-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Jason_from_China/article/details/138715165

代码实现(霍尔排序)

这里其实不管是前后指针,还是霍尔排序,其实都是一样的,因为本质上都是让数值到应该到的位置,所以本质上是一样的,这里我再调用一个霍尔的方式是因为一方面和前后指针的调用形成对比,一方面有不同的注释

//交换函数
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//霍尔方法(单趟实现)
int one_QuickSort01(int* a, int left, int right)
{//三数取中int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);//单趟实现//右边找小,左边找大int begin = left; int end = right;int keyi = begin;while (begin < end){//右边找小,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] <= a[end]){end--;}//左边找大,不是的话移动,是的话不移动,并且跳出循环while (begin < end && a[keyi] >= a[begin]){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[keyi], &a[begin]);return begin;
}
//霍尔方法再次调用
void QuickSort03(int* a, int left, int right)
{Stack ps;StackInit(&ps);StackPush(&ps, right);StackPush(&ps, left);while (!StackEmpty(&ps)){//取出左区间int begin = StackTop(&ps);StackPop(&ps);//取出右边区间int end = StackTop(&ps);StackPop(&ps);int mini = one_QuickSort01(a, begin, end);//计算区间//假设传递的区间是2-4 --->>> 传递过来的数值也就是下标是(1)4-2=2/2=1 --->>>此时mini=2,也就是此时我们返回的数值要么是第一个数值,要么的第二个数值的下标,不管是哪个,此时都会变成一个数值//此时我们继续入栈,入栈的是mini+1 也就是3-4,继续传递区间,此时传递回来的mini还是3,但是此时3+4==4了 所以不继续入栈,因为数值只有一个,不是区间了//右区间入栈,后出if (mini + 1 < end){//入右边,之后左边,这样取的时候栈顶先取左边,之后右边StackPush(&ps, end);StackPush(&ps, mini + 1);}//左区间入栈,先出if (mini - 1 > begin){StackPush(&ps, mini - 1);StackPush(&ps, begin);}}StackDestroy(&ps);
}

解释:

one_QuickSort01 函数

这个函数是霍尔快速排序算法的单趟实现,具体步骤如下:

  1. 三数取中:使用 GetMid 函数找到数组 a 中间位置的元素,并将其与数组第一个元素交换(left 索引位置的元素)。
  2. 初始化指针begin 初始化为 leftend 初始化为 rightkeyi 初始化为 begin
  3. 循环:使用 while 循环,只要 begin 小于 end,就继续执行循环。
  4. 右边找小:从 end 向 begin 扫描,找到第一个小于基准值 a[keyi] 的元素。如果找到,end 指针向前移动,否则跳出循环。
  5. 左边找大:从 begin 向 end 扫描,找到第一个大于基准值 a[keyi] 的元素。如果找到,begin 指针向后移动,否则跳出循环。
  6. 交换元素:将找到的两个元素 a[begin] 和 a[end] 交换位置。
  7. 基准值交换:循环结束后,将 keyi 指向的元素与 begin 指向的元素交换,此时 begin 指向的是基准值的正确位置。
  8. 返回值:函数返回 begin 的值,这个值是下一次分区的起始位置。

QuickSort03 函数

这个函数是快速排序的非递归实现,使用栈来模拟递归过程:

  1. 初始化栈:创建并初始化一个栈 ps
  2. 入栈:将初始区间的左右边界 left 和 right 入栈。
  3. 循环:只要栈不为空,就继续执行循环。
  4. 单趟排序:每次从栈中取出两个值作为区间的左右边界,调用 one_QuickSort01 函数进行单趟排序,得到中间值 mini
  5. 计算新区间:根据 mini 的位置,计算新的左右区间。
    • 如果 mini + 1 < end,则说明右侧还有元素需要排序,将 end 和 mini + 1 入栈。
    • 如果 mini - 1 > begin,则说明左侧还有元素需要排序,将 begin 和 mini - 1 入栈。
  6. 栈的特性:由于栈是后进先出(LIFO)的数据结构,所以先入栈的是右侧区间,后入栈的是左侧区间,这样在出栈时,会先处理左侧区间,再处理右侧区间。
  7. 销毁栈:循环结束后,销毁栈。

这种非递归实现的快速排序算法利用了栈的特性来避免递归调用,从而减少了函数调用的开销,并且在处理大数据集时,可以避免递归深度过大导致的栈溢出问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/29745.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Spring Boot+VUE旧物置换网站

1前台首页功能模块 旧物置换网站&#xff0c;在系统首页可以查看首页、旧物信息、网站公告、个人中心、后台管理等内容&#xff0c;如图1所示。 图1系统功能界面图 用户注册&#xff0c;在用户注册页面通过填写用户名、密码、姓名、性别、头像、手机、邮箱等内容进行用户注册&…

FlinkCDC介绍及使用

CDC简介 什么是CDC&#xff1f; cdc是Change Data Capture(变更数据获取)的简称。核心思想是&#xff0c;监测并捕获数据库的 变动(包括数据或数据表的插入&#xff0c;更新以及删除等)&#xff0c;将这些变更按发生的顺序完整记录下来&#xff0c;写入到消息中间件以供其它服…

IPv4的报头详解

1.ipv4的报头&#xff1a; 注意&#xff1a;ipv4的报头长度共有20个字节&#xff0c;数据包通过ipv4协议传输后&#xff0c;会进行封装和解封装&#xff1a; 封装&#xff1a;tcp/ip五层参考模型 应用层网络层传输层数据链路层物理层 网络层&#xff1a;tcp/udp 传输层&#…

【odoo】如何开启开发者模式,开启有什么作用?

概要 在 Odoo 中&#xff0c;开发者模式&#xff08;Developer Mode&#xff09;是一种专门为开发和调试提供的模式。启用开发者模式可以让开发人员访问到更多的功能和信息&#xff0c;从而更方便地进行模块开发、调试和测试。 启用方式&#xff08;主要两种&#xff09; 1.设…

「iOS」UI——无限轮播图实现与UIPageControl运用

「OC」UI 文章目录 「OC」UI无限轮播图的实现以及UIPageControl的实际运用明确要求简单滚动视图的实现UIPageControl的实现设置NSTimer实现自动移动补充实现 进行无限滚动视图的修改思路实现 完整代码展示 无限轮播图的实现以及UIPageControl的实际运用 明确要求 我们要实现一…

四、C#类型转换

在C#中&#xff0c;类型转换是将一个数据类型的值转换为另一个数据类型的过程。 C#中的类型转换可以分为两种&#xff1a;隐式类型转换和显式类型转换&#xff08;也称为强制类型转换&#xff09;。 隐式类型转换 隐式转换是不需要编写代码来指定的转换&#xff0c;编译器会…

SSM旅游系统

摘要 旅游业正处于快速发展阶段&#xff0c;旅游系统的建设已经成为了旅游业发展的重要核心问题。在这样的背景下&#xff0c;SSM框架正逐步发展为一种主要的架构。但目前青海省旅游业信息化的发展仍面临诸多问题&#xff0c;包括系统功能不完善、用户体验不佳、数据管理不规范…

c语言中的字符函数

1.字符分类函数 c语言中有一系列函数是专门做字符分类的&#xff0c;也就是一个字符属于什么类型的字符。这些函数的使用需要包含一个头文件是ctype.h 可能你看这些感觉很懵&#xff0c;我以islower举例 #include<ctype.h> int main() {int retislower(A);printf("…

Apache Doris 之 Docker 部署篇

前言 在现代数据驱动的商业环境中&#xff0c;实时数据分析和高并发查询能力是企业成功的关键因素之一。传统的数据仓库和分析工具在面对大规模数据处理和实时分析需求时&#xff0c;往往力不从心。Apache Doris 作为一个现代的 MPP 数据库管理系统&#xff0c;凭借其强大的查…

uni微信小程序使用lottie

在uni插件市场找到 lottie-uni https://ext.dcloud.net.cn/plugin?id1044按照文档要求安装 HBuilderX 引入 下载或导入示例获取插件 import lottie from /common/lottie-miniprogram.jsindex.vue <template><uni-popupref"popup"type"center"ba…

俄罗斯ozon爆款推荐丨ozon学生受众产品

在俄罗斯电商平台OZON上&#xff0c;学生受众是一个庞大且活跃的群体。为了满足他们的需求&#xff0c;OZON平台上涌现出了一系列受学生欢迎的爆款产品。以下是一些针对学生受众的OZON爆款推荐&#xff1a; OZON选品工具&#xff1a;D。DDqbt。COM/74rD Top1 UNO纸牌游戏 俄语…

【OpenGL学习】OpenGL不同版本渲染管线汇总

文章目录 一、《OpenGL编程指南》第6版/第7版的渲染管线二、《OpenGL编程指南》第8版/第9版的渲染管线 一、《OpenGL编程指南》第6版/第7版的渲染管线 图1. OpenGL 2.1、OpenGL 3.0、OpenGL 3.1 等支持的渲染管线 二、《OpenGL编程指南》第8版/第9版的渲染管线 图2. OpenGL …

windows 下安装Nuclei 详细教程

一、软件介绍 Nuclei是一款基于YAML语法模板的开发的定制化快速漏洞扫描器。它使用Go语言开发&#xff0c;具有很强的可配置性、可扩展性和易用性。 二、下载安装 官网&#xff1a;https://docs.projectdiscovery.io/tools/nuclei/overview Nuclei项目地址&#xff1a;​​…

记一次 .NET某机械臂上位系统 卡死分析

一&#xff1a;背景 1. 讲故事 前些天有位朋友找到我&#xff0c;说他们的程序会偶发性的卡死一段时间&#xff0c;然后又好了&#xff0c;让我帮忙看下怎么回事&#xff1f;窗体类的程序解决起来相对来说比较简单&#xff0c;让朋友用procdump自动抓一个卡死时的dump&#x…

0. 云原生之基于乌班图远程开发

云原生专栏大纲 文章目录 安装乌班图配置静态IP重置root密码开启root远程登录开启远程SSH访问安装docker安装docker-compose安装Edge浏览器安装搜狗输入法安装TeamViewer安装虚拟显示器安装JDK安装maven安装vscodevscode插件安装VSCode配置maven、git、jdk、自动报错vscode快捷…

30v-180V降3.3V100mA恒压WT5107

30v-180V降3.3V100mA恒压WT5107 WT5107是一款恒压单片机供电芯片&#xff0c;它可以30V-180V直流电转换成稳定的3.3V直流电&#xff08;最大输出电流300mA&#xff09;&#xff0c;为各种单片机供电。WT5107的应用也非常广泛。它可以用于智能家居、LED照明、电子玩具等领域。比…

jupyter使用的一个奇怪bug——SyntaxError: invalid non-printable character U+00A0

bug来由&#xff1a;从其他部分例如kaggle里复制来的代码直接粘贴在jupyter notebook里&#xff0c;每一行代码都会出现&#xff1a; Cell In[5], line 1 warnings.filterwarnings(ignore) ^ SyntaxError: invalid non-printable character U00A0 单元格 In[5]&#xff0c;第 …

多网页登录Cookie免登通俗理解

背景&#xff0c;现在有A、B两个系统&#xff0c;其中B是乾坤框架的微前端&#xff0c;里面又有若干可以单独运行的系统C、D、E、F&#xff0c;现在的目标是&#xff0c;如果没有登录过其中任一系统&#xff0c;则需要跳转登录页登录&#xff0c;登录后&#xff0c;所有的A-F都…

Python 自动化测试入门有哪些内容?

自动化测试是软件测试领域中的一个重要技术&#xff0c;它利用脚本和工具来执行测试任务&#xff0c;减少了人工操作的工作量和时间消耗。Python 是一种功能强大且易于学习的编程语言&#xff0c;被广泛应用于自动化测试领域。本文将从0到1讲解如何使用 Python 进行自动化测试&…

网络编程(一)基本概念

文章目录 一、概念&#xff08;一&#xff09;网络发展阶段1. ARPAnet阶段2. TCP/IP两个协议阶段3. 网络体系结构和OSI开放系统互联模型4. TCP/IP协议簇体系结构&#xff08;1&#xff09; 应用层&#xff1a;&#xff08;2&#xff09;传输层&#xff1a;&#xff08;3&#x…