手撕排序算法5:快速排序非递归版本和计数排序

简介: 手撕排序算法5:快速排序非递归版本和计数排序

一.算法剖析

1.非递归版本的设计

递归版本的缺陷:

1.效率低(早期),目前效率并不低

2.极端情况下(当递归的深度太深时)会导致栈溢出

递归深度太深,程序是没错的,但是栈的空间不够用

递归改非递归

1.直接改循环(简单)

2.借助数据结构栈模拟递归过程(复杂一点)

非递归的效率并没有递归的效率的很大的提升,只不过是防止了栈溢出

在这里我们使用数据结构中的栈来帮助我们实现非递归,改循环的话非常困难

因为C语言没有标准模板库,所以我们必须自己实现栈

栈写好之后,所以我们只需要知道怎么用就行,所以在这里只把头文件给大家了

Stack.h
typedef int STDataType;
typedef struct Stack
{
  STDataType* a;
  int top;
  int capacity;
}ST;
//初始化栈
void StackInit(ST* ps);
//销毁栈
void StackDestroy(ST* ps);
//入栈(压栈)
void StackPush(ST* ps, STDataType x);
//出栈
void StackPop(ST* ps);
//取栈顶元素
STDataType StackTop(ST* ps);
//求栈中数据个数
int StackSize(ST* ps);
//判断栈是否为空
bool StackEmpty(ST* ps);

2.在这里栈需要存什么,怎么利用

1.因为我们在这里使用数据结构的栈去替代函数栈帧来解决问题

那么我们就要让数据结构栈实现函数栈帧的功能

2.函数栈帧是用来存放局部变量的,所以我们要用栈来存放对应的局部变量

3.那么在这里的函数栈帧所存放的局部变量是什么呢?

大家可以看一下这张图片

下面我们来看一下栈的存放过程

二.代码实现

void print(int a[], int sz)
{
  for (int i = 0; i < sz; i++)
  {
    printf("%d ", a[i]);
  }
}
int PartSort(int* a, int left, int right)//前后指针法
{
  int index = GetMidIndex(a, left, right);//三数取中优化版本
  Swap(&a[left], &a[index]);
  int keyi = left;
  int prev = left;
  int cur = left + 1;
  while (cur <= right)
  {
    if (a[cur] < a[keyi])
    {
      ++prev;
      Swap(&a[cur], &a[prev]);
    }
    ++cur;
  }
  Swap(&a[keyi], &a[prev]);
  return prev;
}
void QuickSortNonR(int* a, int n)
{
  ST st;
  StackInit(&st);
  StackPush(&st, n - 1);//栈是后进先出,要想后续先拿区间左侧,就要先入区间右侧,这里先入区间右侧
  StackPush(&st, 0);
  while (!StackEmpty(&st))
  {
    int left = StackTop(&st);//取区间左侧
    StackPop(&st);
    int right = StackTop(&st);
    StackPop(&st);
    int KeyIndex = 0;
    KeyIndex = PartSort(a, left, right);
    //[left,KeyIndex-1] KeyIndex [KeyIndex+1,right] 
    if (KeyIndex + 1 < right)//当右区间长度为1时不许入栈,因为已经长度为1的区间本来就是有序的
    {
      StackPush(&st, right);
      StackPush(&st, KeyIndex + 1);
    }
    if (KeyIndex - 1 > left)
    {
      StackPush(&st, KeyIndex - 1);
      StackPush(&st, left);
    }
  }
  StackDestroy(&st);
}

从这里我们就可以看出用数据结构栈去模拟递归实现也类似于递归实现

也就是说它本质上还是递归的思路和实际的操作步骤

唯一不同的就是递归是将局部变量开辟在栈帧上.

而数据结构的栈是在堆上开辟的

这个非递归也可以通过队列来实现,只不过这个题用队列来实现的话,过程就不想递归了,而是一层一层地去进行的那趟排序,类似于二叉树的层序遍历

而用栈模拟跟递归一样都类似于二叉树的先序遍历

非递归的效率并没有递归的效率的很大的提升,只不过是防止了栈溢出

稳定性

快速排序不具有稳定性

因为我们一般只会左边找大,右边找小再去交换,相等的数我们根本就不管

三.计数排序(属于非比较排序)

1.基本思想

1.利用数组的下标来映射,来统计每个数出现的次数

数组大小为原数据当中最大的值+1,

因为要能够映射最大的那个元素

要统计完以后使用次数就可以排序了

<<剑指offer>>中出现过

但是如果我们遇到的数据是这样的呢?

100 101 102 101 109 105

我们发现,原数组只有6个数,但是最大值缺为109,

那么我们如果申请出一个能容纳110个整数的数组的话是不是就有些大材小用了,

所以我们称上面的映射叫做绝对映射

而我们最好使用下面介绍的相对映射

与绝对映射的不同点在于:

1.我们只需要申请一个数组,这个数组的大小为max-min+1即可

2.绝对映射的下标位置是num,而相对映射的下标位置是num-min

3.绝对映射是还原下标,而相对映射是还原下标+min

下面我们来实现一下代码(相对映射的版本)

2.代码实现

void CountSort2(int* a, int n)
{
  int max = a[0];
  int min = a[0];
  int i = 0;
  for (i = 0; i < n; i++)
  {
    if (a[i] > max)
    {
      max = a[i];
    }
    if (a[i] < min)
    {
      min = a[i];
    }
  }
  int range = max - min + 1;//用range为相对映射
  int* tmp = (int*)malloc(sizeof(int) * range);
  if (tmp == NULL)
  {
    printf("malloc fail\n");
    exit(-1);
  }
  //1.遍历初始化为0
  //for (i = 0; i <= max; i++)
  //{
  //  tmp[i] = 0;
  //}
  //2.用memset来初始化为0
  memset(tmp, 0, sizeof(int) * range);
  //统计次数
  for (i = 0; i < n; i++)
  {
    tmp[a[i] - min]++;//-min为相对映射
  }
  //开始排序
  int j = 0;
  for (i = 0; i <= max; i++)
  {
    int count = tmp[i];
    int num = i;
    while (count--)
    {
      a[j] = num + min;//+min为相对映射
      j++;
    }
  }
}

3.时间复杂度

时间复杂度:O(N+范围)

说明它适用于范围集中一组的整形数据排序

空间复杂度:O(范围)

不过计数排序只适用于整数排序

思想很巧,但是适用范围具有局限性

以上就是手撕排序算法的快排非递归版本和计数排序的讲解,希望能对大家有所帮助

相关文章
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
654 4
|
11月前
|
算法 搜索推荐
快速排序-数据结构与算法
快速排序(Quick Sort)是一种基于分治法的高效排序算法。其核心思想是通过选择基准(pivot),将数组划分为左右两部分,使得左侧元素均小于基准,右侧元素均大于基准,然后递归地对左右两部分进行排序。时间复杂度平均为 O(n log n),最坏情况下为 O(n²)(如数组已有序)。空间复杂度为 O(1),属于原地排序,但稳定性不佳。 实现步骤包括编写 `partition` 核心逻辑、递归调用的 `quickSort` 和辅助函数 `swap`。优化方法有随机化基准和三数取中法,以减少最坏情况的发生。
685 13
|
存储 算法 Java
算法系列之递归反转单链表
递归反转链表的基本思路是将当前节点的next指针指向前一个节点,然后递归地对下一个节点进行同样的操作。递归的核心思想是将问题分解为更小的子问题,直到达到基本情况(通常是链表末尾)。
420 5
算法系列之递归反转单链表
|
存储 搜索推荐 Python
用 Python 实现快速排序算法。
快速排序的平均时间复杂度为$O(nlogn)$,空间复杂度为$O(logn)$。它在大多数情况下表现良好,但在某些特殊情况下可能会退化为最坏情况,时间复杂度为$O(n^2)$。你可以根据实际需求对代码进行调整和修改,或者尝试使用其他优化策略来提高快速排序的性能
327 61
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
311 2
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
520 1
|
5月前
|
机器学习/深度学习 算法 机器人
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
496 0
|
5月前
|
数据采集 分布式计算 并行计算
mRMR算法实现特征选择-MATLAB
mRMR算法实现特征选择-MATLAB
325 2
|
6月前
|
传感器 机器学习/深度学习 编解码
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
302 3
|
5月前
|
机器学习/深度学习 算法 机器人
使用哈里斯角Harris和SIFT算法来实现局部特征匹配(Matlab代码实现)
使用哈里斯角Harris和SIFT算法来实现局部特征匹配(Matlab代码实现)
256 8

热门文章

最新文章