数据结构之排序算法(三)

归并排序

概述
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
自下而上的迭代;
算法描述

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。
  6. 在这里插入图片描述
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int *a = arr;
int *b = (int *) malloc(len * sizeof(int));
int seg, start;
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg * 2) {
int low = start, mid = min(start + seg, len), high = min(start + seg
* 2, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
int *temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
free(b);
}

快速排序

1、概述
快速排序是霍尔(Hoare)于1962年提出的一种二叉树结构的交换排序方法,其基本思路为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素
都排列在相应位置上为止。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sublists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
方法1:霍尔法
霍尔(Hoare)是最初发现快速排序的人,它使用的单趟排序算法被称为霍尔法。
1.选择基准元素,通常选择序列的第一个元素作为基准元素。
在这里插入图片描述

2.利用两个下标left和right分别指向待排数组的最左侧和最右侧,right指针找比key基准值小的数,left找比key基准值大的数。
在这里插入图片描述

3.right 先向左移动,直到找到第一个小于基准元素的值。
在这里插入图片描述
4.left 向右移动,直到找到第一个大于基准元素的值。
在这里插入图片描述
5.找到之后交换这两个下标对应的数值,即交换a[left]和a[right]。
在这里插入图片描述
6.重复步骤 3-5,直到 left 与 right相遇。
在这里插入图片描述
7.最后,交换基准元素 a[KeyIndex] 和 a[left],此时,基准元素所在位置就是它最终的位置。
在这里插入图片描述
8.函数返回 left(left标记了子序列的分界点)。
问题:为什么能够保证相遇位置比key小?
因为right先走,使得结局为以下两种:
right停下之后,left向右靠近并与right相遇,由于right位置定在比key小的值上,所以最终left和right都在比key小的位置处。
left停下之后,right与left进行交换,交换后left指向的值比key小,此时right遇到left的位置一定比key小

// Hoare法单趟排序
int PartSort(int* a, int left, int right)
{
// 选择基准元素的索引为 left
int KeyIndex = left;
while (left < right)
{
// right 从右侧开始,找到第一个小于基准元素的值
while (left < right && a[right] >= a[KeyIndex])
--right;
// left 从左侧开始,找到第一个大于基准元素的值
while (left < right && a[left] <= a[KeyIndex])
// 交换 left 和 right 的元素
Swap(&a[left], &a[right]);
}
// 将基准元素放置在最终的位置
Swap(&a[KeyIndex], &a[left]);
return left;
}

方法2:挖坑法
挖坑法是由 Tony Hoare(托尼·霍尔)在1960年提出的。他作为著名的计算机科学家,也是快速排序算法的发明者之一。
1.选择第一个元素作为基准元素,然后把基准元素用Key存放起来
在这里插入图片描述
2.HoleIndex是坑的位置,初始位置为基准元素的位置。left和right分别指向待排数组的最左侧和最右侧,right找比key基准值小的数,left找比key基准值大的数。
在这里插入图片描述
3.right从右侧开始,寻找第一个小于基准元素的值。
在这里插入图片描述
4.将该值移动到当前的坑中,该值原来位置成为新的坑位,因此更新 HoleIndex 为右侧坑的位置。
在这里插入图片描述
5.left从左侧开始,寻找第一个大于基准元素的值
在这里插入图片描述
6.将该值填充到右侧的坑中,该值原来位置成为新的坑位,因此更新 HoleIndex 为左侧坑的位置。
在这里插入图片描述
7.重复步骤 3-6,直到左侧指针 left 和右侧指针 right 相遇。
在这里插入图片描述
8.将基准元素放置到最后一个坑中,即填充到最终的位置。
在这里插入图片描述
9.返回坑的位置 HoleIndex 作为划分子序列的分界点。

// 挖坑法单趟排序
int PartSort2(int* a, int left, int right)
{
// 选择基准元素为左侧第一个元素
int Key = a[left];
int HoleIndex = left; // 坑的下标,初始为基准元素位置
while (left < right)
{
// 从右侧开始,找到第一个小于基准元素的值
while (left < right && a[right] >= Key)
--right;
// 将右侧小于基准元素的值填充到左侧的坑中
a[HoleIndex] = a[right];
HoleIndex = right;
// 从左侧开始,找到第一个大于基准元素的值
while (left < right && a[left] <= Key)
++left;
// 将左侧大于基准元素的值填充到右侧的坑中
a[HoleIndex] = a[left];
HoleIndex = left;
}
// 将基准元素放置到最后一个坑中,即填充到最终的位置
a[HoleIndex] = Key;
return HoleIndex;
}

方法3:前后指针法
相比于霍尔法和挖坑法,前后指针法只进行元素的比较和少量交换操作,没有显式地创建临时变量来记录基准值。这样能够简化代码实现,并且在实际运行中减少了一些内存和计算开销。
1.基准值 key 为最左边元素,prev 指向基准值, cur 指向基准值下一个位置。
在这里插入图片描述
2.从 cur 开始遍历数组,直到找到第一个小于等于基准值的元素,则先将prev指针向后移动一位,再将cur交换到prev指针指向的位置。
在这里插入图片描述
3.继续遍历数组,直到 cur 超过 right 为止。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
4.最后,将基准值交换到 prev 指向的位置,此时基准值左侧的元素均小于等于基准值,右侧的元素均大于基准值。
在这里插入图片描述
5.返回 prev(prev标记了子序列的分界点)。

// 前后指针法
int PartSort3(int* arr, int left, int right)
{
int key = left; // 将最左边的元素作为基准值
int prev = left; // 后指针(动得慢)
int cur = left + 1; // 前指针(动得快)
while (cur <= right)
{
// 当cur遍历到的元素小于等于基准值时,先将prev指针向后移动一位,再将cur交换到prev指
针指向的位置
if (arr[cur] <= arr[key] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
// 最后将基准值交换到prev指针指向的位置,以完成单趟排序
Swap(&arr[key], &arr[prev]);
return prev; // 返回基准值的位置,用于后续的划分
}

2、算法描述
以一个数为基准,将数组分为两个子序列,左子序列放比基准数小的,右子序列放比基准数大的数,然后再将左右两段子序列以同样方式分割,数组有序。
在这里插入图片描述

//快速排序
void Quick_Sort(int s[], int l, int r)
{
if (l < r)
{
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}

非递归法

// 非递归快速排序实现
void QuickSortNonR(int* a, int begin, int end)
{
ST st; // 定义一个栈,用于模拟递归过程
STInit(&st); // 初始化栈
// 将初始的 begin 和 end 压入栈,相当于递归的入口
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
// 从栈中取出当前处理的 begin 和 end
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
// 调用 PartSort 函数对序列进行划分,并得到基准元素的位置
int KeyIndex = PartSort(a, left, right);
// 判断是否需要对右侧子序列进行排序,并将其入栈
if (KeyIndex + 1 < right)
{
STPush(&st, right);
STPush(&st, KeyIndex + 1);
}
// 判断是否需要对左侧子序列进行排序,并将其入栈
if (left < KeyIndex - 1)
{
STPush(&st, KeyIndex - 1);
STPush(&st, left);
}
}
// 栈中所有任务处理完毕,排序完成
STDestroy(&st);
}

3、快速排序的优化
(1)优化基准数key的选取
优化快速排序的基准数选择对算法的性能和稳定性有很大的影响。其中,三值取中法是一种常用的优化策略,它通过选取子序列中某三个元素的中位数作为基准数(一般选择首、中、尾三数),从而尽可能防止选择的基准数为待排数组的最大、最小值,而导致最坏情况出现。
(2)减少递归层数
为了解决递归到较深处时,调用大量栈帧来实现短小的排序的“小题大做”问题,可以采用小区间优化的方法,即当待排序的子序列长度小于某个阈值时,不再使用递归方式进行排序,而是采用其他排序算法(如插入排序)对该子序列进行排序。通过直接对较短的子序列使用简单而高效的排序算法,避免了递归调用带来的开销,从而提高了排序的效率。

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值