【数据结构】三万字图文讲解带你手撕八大排序(附源码)4

简介: 【数据结构】三万字图文讲解带你手撕八大排序(附源码)

8、计数排序


8.1 算法思想


思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。


我们先看一下计数排序的动图:



7bb61d502e0bda8383834e8eb1b84383.gif


计数排序实际上就是将数组中对应数据出现的次数,将数据出现次数映射到一个新数组中。在与数据相等值的下标处,将这个下标位置的元素自增。每出现一个数字就自增一次。


而平常的映射就是直接在其相等下标位置处理,叫做 绝对映射 ;还有一种映射方式叫 相对映射 。我们先看绝对映射。


绝对映射:


所谓绝对映射,就是开辟一个辅助数组 c c c ,数组大小为待排序数组的最大元素的大小 m a x max max 。


然后遍历数组,将数据映射到辅助数组 c c c 中。


acc62c088d37df7824873e8f5627f2c0.png


然后根据 c o u n t count count 数组中的元素,根据元素对应的下标,将下标的值填入 a a a 数组中,如果 c o u n t count count 数组中该位置为 0 0 0, 则不需要填。


cd14fe7f1ddc7117d20d5fa05be1a6ee.png


最后 a a a 数组中的元素就已经被排序好了。


其实讲到这里,大家也大约可以看出来 绝对映射 的缺点:当最大元素很大,或者是出现负数时,就无法映射了。因为空间开大了浪费空间,并且无法在负数下标自增。所以这就引出了 相对映射 。


相对映射:


相对映射,就是根据数据之间的相对情况来开辟数组大小,并在转换后的相对位置执行映射。


比如有这样一组数据: { 5000 , 5001 , 5500 , 5501 } \{5000, 5001, 5500, 5501\} {5000,5001,5500,5501} ,对于这组数据我们开 5501 5501 5501 个空间肯定是浪费的。


我们相对映射的思路就是遍历序列,找到序列最大值 m a x max max 和最小值 m i n min min ,然后开辟 m a x − m i n + 1 max - min + 1 max−min+1 个空间,让空间尽可能充分利用。


之后映射自增时,也使用相对位置,这个相对位置就是数组元素减去数组元素的最小值: a [ i ] − m i n a[i] - min a[i]−min 。


在最后将元素放到原数组中时,也需要将数组下标加上最小值: i + m i n i + min i+min 放回去就可以。


通过相对映射,对于元素有负数,和空间浪费的情况都可以解决。(ps:元素有负数的情况,无需特殊处理,因为相对映射的原因,这些步骤都可以正确进行,不信可以试验一下)。



8.2 代码实现


接下来,就使用相对映射的思路写出代码:

// 计数排序 正负数都可以排
void CountSort(int* a, int n)
{
  // 1. 找最小值和最大值
  int max = a[0], min = a[0];
  for (int i = 0; i < n; i++)
  {
    if (a[i] > max)
    {
      max = a[i];
    }
    if (a[i] < min)
    {
      min = a[i];
    }
  }
  // 2. 根据差值构建 count 数组
  int range = max - min + 1;
  int* count = (int*)malloc(sizeof(int) * range);
  if (count == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
    // 初始化
  memset(count, 0, sizeof(int) * range);
  // 3. 将值映射到count数组中
  for (int i = 0; i < n; i++)
  {
    count[a[i] - min]++; // 映射到相对位置
  }
  int cnt = 0;
  for (int i = 0; i < range; i++)
  {
    while (count[i]--)
    {
      a[cnt++] = i + min;
    }
  }
}


8.3 时空复杂度


计数排序的时间复杂度其实是由 r a n g e range range 和 N N N 的关系来衡量的,当我们不确定 r a n g e range range 和 N N N 的大小时,我们可以认为 计数排序的时间复杂度为 O ( m a x ( N , r a n g e ) ) O(max(N, range)) O(max(N,range)) 。取较大的一个。


而空间复杂度则是 O ( r a n g e ) O(range) O(range)。


实际上通过时空复杂度上看,我们发现计数排序在数据集中的情况下是非常厉害的,能达到几乎 O ( N ) O(N) O(N) 的时间复杂度,并且空间复杂度也不会太大。但是对于范围分散,跨度大的序列就不适合,不仅时间没啥优势,空间占比也是个大问题。所以计数排序的适用范围是有限的。





四、排序算法复杂度及稳定性分析


这里我们就用两张图概括:



4e0b302c265be17979efd288b064956c.png

这里提一下排序的稳定性:


稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中, r [ i ] = r [ j ] r[i]=r[j] r[i]=r[j],且 r [ i ] r[i] r[i] 在 r [ j ] r[j] r[j] 之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。


稳定性是排序算法一种额外的优点。如果一种排序可以通过某种措施,达到数据相对次序不变的效果,则称该排序是稳定的。



17a653244aab0d3019901aeb2d38a1f8.png



五、排序性能测试框架


void TestOP()
{
  srand(time(0));
  const int N = 10000000;
  int* a1 = (int*)malloc(sizeof(int) * N);
  int* a2 = (int*)malloc(sizeof(int) * N);
  int* a3 = (int*)malloc(sizeof(int) * N);
  int* a4 = (int*)malloc(sizeof(int) * N);
  int* a5 = (int*)malloc(sizeof(int) * N);
  int* a6 = (int*)malloc(sizeof(int) * N);
  int* a7 = (int*)malloc(sizeof(int) * N);
  for (int i = 0; i < N; ++i)
  {
    a1[i] = rand();
    a2[i] = a1[i];
    a3[i] = a1[i];
    a4[i] = a1[i];
    a5[i] = a1[i];
    a6[i] = a1[i];
    a7[i] = a1[i];
  }
  // clock 获取程序运行到这块的时间
  // end1 - begin1 = 排序时间
  // 获取的是毫秒
  // 时间过小时,计算不出来
  int begin1 = clock();
  InsertSort(a1, N);
  int end1 = clock();
  int begin2 = clock();
  ShellSort(a2, N);
  int end2 = clock();
  int begin3 = clock();
  SelectSort(a3, N);
  int end3 = clock();
  int begin4 = clock();
  HeapSort(a4, N);
  int end4 = clock();
  int begin5 = clock();
  QuickSortT(a5, 0, N - 1);
  int end5 = clock();
  int begin6 = clock();
  BubbleSort(a6, N);
  int end6 = clock();
  int begin7 = clock();
  MergeSort(a7, N);
  MergeSortNonR(a7, N);
  int end7 = clock();
  printf("InsertSort:%d\n", end1 - begin1);
  printf("ShellSort:%d\n", end2 - begin2);
  printf("SelectSort:%d\n", end3 - begin3);
  printf("HeapSort:%d\n", end4 - begin4);
  printf("QuickSort:%d\n", end5 - begin5);
  printf("BubbleSort:%d\n", end6 - begin6);
  printf("MergeSort:%d\n", end7 - begin7);
  free(a1);
  free(a2);
  free(a3);
  free(a4);
  free(a5);
  free(a6);
  free(a7);
}



六、结语



到这里本篇博客就到此结束了,对于数据结构的学习,我们也就暂时告一段落了。


接下来 a n d u i n anduin anduin 更新的内容大多就是 C + + C++ C++ 和算法笔记了。


对于这篇文章,博主个人觉得还是不错的,希望你们阅读完之后可以有所收获!


如果小伙伴们需要源码的话,可以到我的 g i t e e gitee gitee 打包下载源码:八大排序

我们下期见~





相关文章
|
10月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
856 9
|
3月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
187 3
|
6月前
|
存储 自然语言处理 数据库
【数据结构进阶】AVL树深度剖析 + 实现(附源码)
在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。
477 19
|
6月前
|
数据库 C++
【数据结构进阶】红黑树超详解 + 实现(附源码)
本文深入探讨了红黑树的实现原理与特性。红黑树是一种自平衡二叉搜索树,通过节点着色(红/黑)和特定规则,确保树的高度接近平衡,从而实现高效的插入、删除和查找操作。相比AVL树,红黑树允许一定程度的不平衡,减少了旋转调整次数,提升了动态操作性能。文章详细解析了红黑树的性质、插入时的平衡调整(变色与旋转)、查找逻辑以及合法性检查,并提供了完整的C++代码实现。红黑树在操作系统和数据库中广泛应用,其设计兼顾效率与复杂性的平衡。
959 3
|
7月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》图、查找、排序专题考点(含解析)
408考研——《数据结构》图,查找和排序专题考点选择题汇总(含解析)。
272 29
|
8月前
|
存储 人工智能 算法
【C++数据结构——内排序】二路归并排序(头歌实践教学平台习题)【合集】
本关任务是实现二路归并算法,即将两个有序数组合并为一个有序数组。主要内容包括: - **任务描述**:实现二路归并算法。 - **相关知识**: - 二路归并算法的基本概念。 - 算法步骤:通过比较两个有序数组的元素,依次将较小的元素放入新数组中。 - 代码示例(以 C++ 为例)。 - 时间复杂度为 O(m+n),空间复杂度为 O(m+n)。 - **测试说明**:平台会对你编写的代码进行测试,提供输入和输出示例。 - **通关代码**:提供了完整的 C++ 实现代码。 - **测试结果**:展示代码运行后的排序结果。 开始你的任务吧,祝你成功!
193 10
|
8月前
|
搜索推荐 算法 数据处理
【C++数据结构——内排序】希尔排序(头歌实践教学平台习题)【合集】
本文介绍了希尔排序算法的实现及相关知识。主要内容包括: - **任务描述**:实现希尔排序算法。 - **相关知识**: - 排序算法基础概念,如稳定性。 - 插入排序的基本思想和步骤。 - 间隔序列(增量序列)的概念及其在希尔排序中的应用。 - 算法的时间复杂度和空间复杂度分析。 - 代码实现技巧,如循环嵌套和索引计算。 - **测试说明**:提供了测试输入和输出示例,帮助验证代码正确性。 - **我的通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了代码运行的测试结果。 通过这些内容,读者可以全面了解希尔排序的原理和实现方法。
145 10
|
8月前
|
搜索推荐 C++
【C++数据结构——内排序】快速排序(头歌实践教学平台习题)【合集】
快速排序是一种高效的排序算法,基于分治策略。它的主要思想是通过选择一个基准元素(pivot),将数组划分成两部分。一部分的元素都小于等于基准元素,另一部分的元素都大于等于基准元素。然后对这两部分分别进行排序,最终使整个数组有序。(第一行是元素个数,第二行是待排序的原始关键字数据。本关任务:实现快速排序算法。开始你的任务吧,祝你成功!
195 7
|
10月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
377 16
|
10月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
657 8