数据结构从入门到精通(第五篇) :排序的进阶(快速排序,归并排序,计数排序)

简介: 数据结构从入门到精通(第五篇) :排序的进阶(快速排序,归并排序,计数排序)

前言

在上一篇,我们已经讲了基本的排序方法,例如:插入排序,希尔排序,选择排序,冒泡排序


数据结构从入门到精通(第四篇) :排序的入门(插入排序,希尔排序,选择排序,冒泡排序)


这一篇将会讲解进阶的排序算法,例如:快速排序,归并排序,计数排序



快速排序

原理


任取待排序元素序列中的某元素作为基准值,按照该基准值将待排序列分为两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右序列重复该过程,直到所有元素都排列在相应位置上为止。


递归的实现

挖坑法

//快速排序(挖坑法)
void QuickSort2(int* a, int begin, int end)
{
  if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
  return;
  int left = begin;//L
  int right = end;//R
  int key = a[left];//在最左边形成一个坑位
  while (left < right)
  {
  //right向左,找小
  while (left < right&&a[right] >= key)
  {
    right--;
  }
  //填坑
  a[left] = a[right];
  //left向右,找大
  while (left < right&&a[left] <= key)
  {
    left++;
  }
  //填坑
  a[right] = a[left];
  }
  int meeti = left;//L和R的相遇点
  a[meeti] = key;//将key抛入坑位
  QuickSort2(a, begin, meeti - 1);//key的左序列进行此操作
  QuickSort2(a, meeti + 1, end);//key的右序列进行此操作
}


Hoare版本

//快速排序
void QuickSort1(int* a, int begin, int end)
{
  if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
  return;
  int left = begin;//L
  int right = end;//R
  int keyi = left;//key的下标
  while (left < right)
  {
  //right先走,找小
  while (left < right&&a[right] >= a[keyi])
  {
    right--;
  }
  //left后走,找大
  while (left < right&&a[left] <= a[keyi])
  {
    left++;
  }
  if (left < right)//交换left和right的值
  {
    Swap(&a[left], &a[right]);
  }
  }
  int meeti = left;//L和R的相遇点
  Swap(&a[keyi], &a[meeti]);//交换key和相遇点的值
  QuickSort1(a, begin, meeti - 1);//key的左序列进行此操作
  QuickSort1(a, meeti + 1, end);//key的右序列进行此操作
}


前后指针法

//快速排序(前后指针法)
void QuickSort3(int* a, int begin, int end)
{
  if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
  return;
  //三数取中
  int midIndex = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[midIndex]);
  int prev = begin;
  int cur = begin + 1;
  int keyi = begin;
  while (cur <= end)//当cur未越界时继续
  {
  if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
  {
    Swap(&a[prev], &a[cur]);
  }
  cur++;
  }
  int meeti = prev;//cur越界时,prev的位置
  Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容
  QuickSort3(a, begin, meeti - 1);//key的左序列进行此操作
  QuickSort3(a, meeti + 1, end);//key的右序列进行此操作
}


非递归的实现

Hoare版本

//Hoare版本(单趟排序)
int PartSort1(int* a, int left, int right)
{
  int keyi = left;//key的下标
  while (left < right)
  {
  //right走,找小
  while (left < right&&a[right] >= a[keyi])
  {
    right--;
  }
  //left先走,找大
  while (left < right&&a[left] <= a[keyi])
  {
    left++;
  }
  if (left < right)
  {
    Swap(&a[left], &a[right]);//交换left和right的值
  }
  }
  int meeti = left;//L和R的相遇点
  Swap(&a[keyi], &a[meeti]);//交换key和相遇点的值
  return meeti;//返回相遇点,即key的当前位置
}


挖坑法

//挖坑法(单趟排序)
int PartSort2(int* a, int left, int right)
{
  int key = a[left];//在最左边形成一个坑位
  while (left < right)
  {
  //right向左,找小
  while (left < right&&a[right] >= key)
  {
    right--;
  }
  //填坑
  a[left] = a[right];
  //left向右,找大
  while (left < right&&a[left] <= key)
  {
    left++;
  }
  //填坑
  a[right] = a[left];
  }
  int meeti = left;//L和R的相遇点
  a[meeti] = key;//将key抛入坑位
  return meeti;//返回key的当前位置
}


前后指针法

//前后指针法(单趟排序)
int PartSort3(int* a, int left, int right)
{
  int prev = left;
  int cur = left + 1;
  int keyi = left;
  while (cur <= right)//当cur未越界时继续
  {
  if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
  {
    Swap(&a[prev], &a[cur]);
  }
  cur++;
  }
  int meeti = prev;//cur越界时,prev的位置
  Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容
  return meeti;//返回key的当前位置
}


利用栈来实现

//快速排序(非递归实现)
void QuickSortNonR(int* a, int begin, int end)
{
  Stack st;//创建栈
  StackInit(&st);//初始化栈
  StackPush(&st, begin);//待排序列的L
  StackPush(&st, end);//待排序列的R
  while (!StackEmpty(&st))
  {
  int right = StackTop(&st);//读取R
  StackPop(&st);//出栈
  int left = StackTop(&st);//读取L
  StackPop(&st);//出栈
  //该处调用的是Hoare版本的单趟排序
  int keyi = PartSort1(a, left, right);
  if (left < keyi - 1)//该序列的左序列还需要排序
  {
    StackPush(&st, left);//左序列的L入栈
    StackPush(&st, keyi - 1);//左序列的R入栈
  }
  if (keyi + 1 < right)// 该序列的右序列还需要排序
  {
    StackPush(&st, keyi + 1);//右序列的L入栈
    StackPush(&st, right);//右序列的R入栈
  }
  }
  StackDestroy(&st);//销毁栈
}


归并排序

原理:

归并排序是采用分治法的一个非常典型的应用。其基本思想是:将已有序的子序合并,从而得到完全有序的序列,即先使每个子序有序,再使子序列段间有序。



递归实现

//归并排序(子函数)
void _MergeSort(int* a, int left, int right, int* tmp)
{
  if (left >= right)//归并结束条件:当只有一个数据或是序列不存在时,不需要再分解
  {
  return;
  }
  int mid = left + (right - left) / 2;//中间下标
  _MergeSort(a, left, mid, tmp);//对左序列进行归并
  _MergeSort(a, mid + 1, right, tmp);//对右序列进行归并
  int begin1 = left, end1 = mid;
  int begin2 = mid + 1, end2 = right;
  //将两段子区间进行归并,归并结果放在tmp中
  int i = left;
  while (begin1 <= end1&&begin2 <= end2)
  {
  //将较小的数据优先放入tmp
  if (a[begin1] < a[begin2])
    tmp[i++] = a[begin1++];
  else
    tmp[i++] = a[begin2++];
  }
  //当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
  while (begin1 <= end1)
  tmp[i++] = a[begin1++];
  while (begin2 <= end2)
  tmp[i++] = a[begin2++];
  //归并完后,拷贝回原数组
  int j = 0;
  for (j = left; j <= right; j++)
  a[j] = tmp[j];
}
//归并排序(主体函数)
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与原数组大小相同的空间
  if (tmp == NULL)
  {
  printf("malloc fail\n");
  exit(-1);
  }
  _MergeSort(a, 0, n - 1, tmp);//归并排序
  free(tmp);//释放空间
}


非递归实现

//归并排序(子函数)
void _MergeSortNonR(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
  int j = begin1;
  //将两段子区间进行归并,归并结果放在tmp中
  int i = begin1;
  while (begin1 <= end1&&begin2 <= end2)
  {
  //将较小的数据优先放入tmp
  if (a[begin1] < a[begin2])
    tmp[i++] = a[begin1++];
  else
    tmp[i++] = a[begin2++];
  }
  //当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
  while (begin1 <= end1)
  tmp[i++] = a[begin1++];
  while (begin2 <= end2)
  tmp[i++] = a[begin2++];
  //归并完后,拷贝回原数组
  for (; j <= end2; j++)
  a[j] = tmp[j];
}
//归并排序(主体函数)
void MergeSortNonR(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与待排序列大小相同的空间,用于辅助合并序列
  if (tmp == NULL)
  {
  printf("malloc fail\n");
  exit(-1);
  }
  int gap = 1;//需合并的子序列中元素的个数
  while (gap < n)
  {
  int i = 0;
  for (i = 0; i < n; i += 2 * gap)
  {
    int begin1 = i, end1 = i + gap - 1;
    int begin2 = i + gap, end2 = i + 2 * gap - 1;
    if (begin2 >= n)//最后一组的第二个小区间不存在或是第一个小区间不够gap个,此时不需要对该小组进行合并
    break;
    if (end2 >= n)//最后一组的第二个小区间不够gap个,则第二个小区间的后界变为数组的后界
    end2 = n - 1;
    _MergeSortNonR(a, tmp, begin1, end1, begin2, end2);//合并两个有序序列
  }
  gap = 2 * gap;//下一趟需合并的子序列中元素的个数翻倍
  }
  free(tmp);//释放空间
}


计数排序

原理:


简单理解:既然排序名字是计数排序,那么肯定要有统计数据这个过程。


下面先看一个简单的例子,咱们先用一种统计数据的方法对 3,2,2,5,4,0,5,4,5,1 这十个数排序。


经过观察发现,上面的十个数中,0出现了一次,1出现了一次,2出现了两次,3出现了一次,4出现了两次,5出现三次。


我们就可以用一个数组来存这些次数(数组下标代表存数的值,数组的值代表存了几次),


即count[0]=1,count[1]=1,count[2]=2,count[3]=1,count[4]=2,count[5]=3


然后再按顺序把这些统计的数打印出来是不是就完成了一个简单的排序?,下面看代码(我相信你一定能看懂)

//计数排序
void CountSort(int* a, int n)
{
  int min = a[0];
  int max = a[0];
  for (int i = 0; i < n; i++)
  {
  if (a[i] < min)
    min = a[i];
  if (a[i] > max)
    max = a[i];
  }
  int range = max - min + 1;
  int* count = (int*)calloc(range, sizeof(int));
  if (count == NULL)
  {
  printf("malloc fail\n");
  exit(-1);
  }
  for (int i = 0; i < n; i++)
  {
  count[a[i] - min]++;
  }
  int i = 0;
  for (int j = 0; j < range; j++)
  {
  while (count[j]--)
  {
    a[i++] = j + min;
  }
  }
  free(count);//释放空间
}


总结

快速排序,归并排序,计数排序都是比较进阶的排序算法,需要我们反复练习和模拟

特别是对递归的理解和运用


相关文章
|
6月前
|
算法 搜索推荐
快速排序-数据结构与算法
快速排序(Quick Sort)是一种基于分治法的高效排序算法。其核心思想是通过选择基准(pivot),将数组划分为左右两部分,使得左侧元素均小于基准,右侧元素均大于基准,然后递归地对左右两部分进行排序。时间复杂度平均为 O(n log n),最坏情况下为 O(n²)(如数组已有序)。空间复杂度为 O(1),属于原地排序,但稳定性不佳。 实现步骤包括编写 `partition` 核心逻辑、递归调用的 `quickSort` 和辅助函数 `swap`。优化方法有随机化基准和三数取中法,以减少最坏情况的发生。
325 13
|
8月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》图、查找、排序专题考点(含解析)
408考研——《数据结构》图,查找和排序专题考点选择题汇总(含解析)。
293 29
|
9月前
|
存储 人工智能 算法
【C++数据结构——内排序】二路归并排序(头歌实践教学平台习题)【合集】
本关任务是实现二路归并算法,即将两个有序数组合并为一个有序数组。主要内容包括: - **任务描述**:实现二路归并算法。 - **相关知识**: - 二路归并算法的基本概念。 - 算法步骤:通过比较两个有序数组的元素,依次将较小的元素放入新数组中。 - 代码示例(以 C++ 为例)。 - 时间复杂度为 O(m+n),空间复杂度为 O(m+n)。 - **测试说明**:平台会对你编写的代码进行测试,提供输入和输出示例。 - **通关代码**:提供了完整的 C++ 实现代码。 - **测试结果**:展示代码运行后的排序结果。 开始你的任务吧,祝你成功!
208 10
|
9月前
|
搜索推荐 C++
【C++数据结构——内排序】快速排序(头歌实践教学平台习题)【合集】
快速排序是一种高效的排序算法,基于分治策略。它的主要思想是通过选择一个基准元素(pivot),将数组划分成两部分。一部分的元素都小于等于基准元素,另一部分的元素都大于等于基准元素。然后对这两部分分别进行排序,最终使整个数组有序。(第一行是元素个数,第二行是待排序的原始关键字数据。本关任务:实现快速排序算法。开始你的任务吧,祝你成功!
203 7
|
11月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
883 9
|
11月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
224 59
|
4月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
53 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
9月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
335 77
|
8月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
169 11