【C语言/数据结构】排序(快速排序及多种优化|递归及非递归版本)

简介: 【C语言/数据结构】排序(快速排序及多种优化|递归及非递归版本)

交换排序


快速排序

快排的过程图如下:



hoare版代码呈现

void QuickSort(int* a, int begin,int end)
{
  if (begin >= end)
  {
  return;
  }
  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]);
  keyi = left;
  // [begin,keyi-1] keyi [keyi+1,end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi+1, end);
}


分析:快排过程就是左边找比key大的值,右边找比key小的值,找到后就交换。直到left与right相遇,就交换keyi和left对应的值。这是单趟的,后续过程重复,可以思考二叉树的递归过程,快排递归与其相似(见下图)。




下图中,划红线的地方是容易出错的地方。




理解了前面,这里解释一下为什么相遇位置比keyi位置的值要小?


因为右边先走。


相遇有2种情况


1. R遇L->R没有找到比key小,一直走,直到遇到L,相遇位置是L,比key小。

2.L遇R->R先走,找到小的停下来了,L找大,没有找到,遇到R停下来了,相遇位置是R,比key小。

如果左边做key,R先走。

如果右边做key,L先走。


快排优化

1.三数取中法选key

2.递归到小的子区间时,可以考虑使用插入排序

三数取中法

快排对于有序的数据,效率不是很高。



如上图,我们前面的快排是固定选key的,也就是左边第一幅图,效率很低。理想情况下,每一次都二分,这样效率就能提高。这时就用到三数取中法。


三数取中法指三个数里面取中间大小的数,然后将他与key交换位置,让这个中间大小的数作key。


完整代码如下:


int GetMidi(int* a, int begin, int end)
{
  int midi = (begin + end) / 2;
  //begin end midi三个数中选中位数
  if (a[begin] < a[midi])
  {
  if (a[midi] < a[end])
    return midi;
  else if (a[begin] > a[end])
    return begin;
  else
    return end;
  }
  else
  {
  if (a[midi] > a[end])
    return midi;
  else if (a[begin] < a[end])
    return begin;
  else
    return end;
  }
}
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  return;
  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]);
  keyi = left;
  // [begin,keyi-1] keyi [keyi+1,end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
}


小区间优化


假设在理想情况下,每次递归都像二叉树那样,递归到最后面几层时,假设还剩7个数,我们还得递归7次,这样明显不好。我们就可以在最后几层时,使用其他排序方法进行。这里使用插入排序。


完整代码如下:

void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  return;
  if (end - begin + 1 <= 10)
  {
  InsertSort(a+begin, end - begin + 1);
  }
  else 
  {
  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]);
  keyi = left;
  // [begin,keyi-1] keyi [keyi+1,end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
  }
}


挖坑法


我们把不同方法的单趟排序重新弄成一个函数。


hoare版本:

//hoare版本 
int PartSort1(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;
}


挖坑法完整代码:


//挖坑法
int PartSort2(int* a, int begin, int end)
{
  int midi = GetMidi(a, begin, end);
  Swap(&a[midi], &a[begin]);
  int key = a[begin];
  int hole = begin;
  while (begin < end)
  {
  //右边找小,填到左边的坑
  while (begin < end && a[end] >= key)
  {
    end--;
  }
  a[hole] = a[end];
  hole = end;
  //左边找大,填到右边的坑
  while (begin < end && a[begin] <= key)
  {
    begin++;
  }
  a[hole] = a[begin];
  hole = begin;
  }
  a[hole] = key;
  return hole;
} 
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  return;
  int keyi = PartSort2(a, begin, end);
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi+1, end);
}

分析:挖坑法其实跟hoare版本比没啥提升,只不过更易理解,本质上没变。但不同的版本,单趟排序后的结果可能会不同。


前后指针版本



分析:


1.cur遇到比key大的值,cur++

2.cur遇到比key小的值,++prev,交换prev和cur位置的值,++cur

代码实现


//前后指针法
int PartSort3(int* a, int begin, int end)
{
  int midi = GetMidi(a, begin, end);
  Swap(&a[midi], &a[begin]);
  int keyi = begin;
  int prev = begin;
  int cur = prev + 1;
  while (cur <= end)
  {
  if (a[cur] < a[keyi] && ++prev!=cur)
    Swap(&a[prev], &a[cur]);
  ++cur;
  }
  Swap(&a[prev], &a[keyi]);
  keyi = prev;
  return keyi;
}


非递归版本快排

非递归版本的快排需要用到栈。下面给出需要的栈的函数:


void STInit(ST* pst)
{
  assert(pst);
  pst->a = NULL;
  pst->capacity = 0;
  pst->top = 0;
  //pst->top = -1;
}
void STDestroy(ST* pst)
{
  free(pst->a);
  pst->a = NULL;
  pst->capacity = pst->top = 0;
}
//栈顶插入删除
void STPush(ST* pst, STDataType x)
{
  assert(pst);
  if (pst->top == pst->capacity)
  {
  int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
  STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newcapacity);
  if (tmp == NULL)
  {
    perror("realloc fail");
    return;
  }
  pst->a = tmp;
  pst->capacity = newcapacity;
  }
  pst->a[pst->top] = x;
  pst->top++;
}
void STPop(ST* pst)
{
  assert(pst);
  assert(pst->top > 0);
  pst->top--;
}
STDataType STTop(ST* pst)
{
  assert(pst);
  assert(pst->top > 0);
  return  pst->a[pst->top - 1];
}
bool STEmpty(ST* pst)
{
  assert(pst);
  //if (pst->top == 0)
  //{
  //  return true;
  //}
  //else
  //{
  //  return  false;
  //}
  return pst->top == 0;
}


非递归代码实现:


void QuickSortNonR(int* a, int begin, int end)
{
  ST s;
  STInit(&s);
  STPush(&s, end);
  STPush(&s, begin);
  while (!STEmpty(&s))
  {
  int left = STTop(&s);
  STPop(&s);
  int right = STTop(&s);
  STPop(&s);
  int keyi = PartSort3(a, left, right);
  // [left,keyi-1] keyi [keyi+1,right]
 
  if (keyi+1 < right)
  {
    STPush(&s, right);
    STPush(&s, keyi+1);
  }
  if (left < keyi - 1)
  {
    STPush(&s, keyi - 1);
    STPush(&s, left);
  }
  }
  STDestroy(&s);
}



分析:栈是后进先出,这里用栈是模拟递归的过程。先模拟递归左边,像二叉树递归那样,先入右边的数,再入左边,这样出的时候就先出左边的,然后就可以模拟先往左边递归了。


目录
相关文章
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
383 59
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
713 4
|
12月前
|
算法 搜索推荐
快速排序-数据结构与算法
快速排序(Quick Sort)是一种基于分治法的高效排序算法。其核心思想是通过选择基准(pivot),将数组划分为左右两部分,使得左侧元素均小于基准,右侧元素均大于基准,然后递归地对左右两部分进行排序。时间复杂度平均为 O(n log n),最坏情况下为 O(n²)(如数组已有序)。空间复杂度为 O(1),属于原地排序,但稳定性不佳。 实现步骤包括编写 `partition` 核心逻辑、递归调用的 `quickSort` 和辅助函数 `swap`。优化方法有随机化基准和三数取中法,以减少最坏情况的发生。
727 13
|
搜索推荐 C++
【C++数据结构——内排序】快速排序(头歌实践教学平台习题)【合集】
快速排序是一种高效的排序算法,基于分治策略。它的主要思想是通过选择一个基准元素(pivot),将数组划分成两部分。一部分的元素都小于等于基准元素,另一部分的元素都大于等于基准元素。然后对这两部分分别进行排序,最终使整个数组有序。(第一行是元素个数,第二行是待排序的原始关键字数据。本关任务:实现快速排序算法。开始你的任务吧,祝你成功!
354 7
|
存储 算法 安全
【C语言程序设计——选择结构程序设计】按从小到大排序三个数(头歌实践教学平台习题)【合集】
本任务要求从键盘输入三个数,并按从小到大的顺序排序后输出。主要内容包括: - **任务描述**:实现三个数的排序并输出。 - **编程要求**:根据提示在编辑器中补充代码。 - **相关知识**: - 选择结构(if、if-else、switch) - 主要语句类型(条件语句) - 比较操作与交换操作 - **测试说明**:提供两组测试数据及预期输出。 - **通关代码**:完整代码示例。 - **测试结果**:展示测试通过的结果。 通过本任务,你将掌握基本的选择结构和排序算法的应用。祝你成功!
273 4
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
566 7
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
814 8
|
算法 C语言
【C语言】排序查找
【C语言】排序查找
125 2
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
580 1
|
算法
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序