数据结构和算法之排序总结 下

简介: 数据结构和算法之排序总结

💦 归并排序

🔑 核心思想 🔑

  归并排序 (MERGE-SORT) 是建立在归并操作上的一种有效的排序算法,该算法是采用分治法 (Divide and Conquer) 的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列有序。若将两个有序表合并成一个有序表,称为二路归并。

  归并排序核心步骤:

❗ 动图演示:❕

🧿 实现代码 —— 递归版 :

void _MergeSort(int* a, int left, int right, int* temp)
{
  //只有一个值
  if (left >= right)
    return;
  //[left, mid][mid+1, right]
  int mid = (right + left) / 2;
  //递归
  _MergeSort(a, left, mid, temp);
  _MergeSort(a, mid + 1, right, temp);
  //归并到temp
  int begin1 = left, end1 = mid;
  int begin2 = mid + 1, end2 = right;
    //&&其中一段区间结束就结束了
  int index = left;
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] < a[begin2])
    {
      temp[index++] = a[begin1++];
    }
    else
    {
      temp[index++] = a[begin2++];
    }
  }
    //begin2结束了,拷贝剩下的begin1
  while (begin1 <= end1)
  {
    temp[index++] = a[begin1++];
  }
    //begin1结束了,拷贝剩下的begin2
  while (begin2 <= end2)
  {
    temp[index++] = a[begin2++];
  }
    //归并后的结果,拷贝至原数组
  int i = 0;
  for (i = left; i <= right; i++)
  {
    a[i] = temp[i];
  }
}
void MergeSort(int* a, int n)
{
  //临时数组 
  int* temp = (int*)malloc(sizeof(int) * n);
  //子函数递归
  _MergeSort(a, 0, n - 1, temp);
  //释放临时数组
  free(temp);
}

🧿 实现代码 —— 非递归版 :

🔑 核心思想 🔑

void MergeSortNonR(int* a, int n)
{
  //临时数组 
  int* temp = (int*)malloc(sizeof(int) * n);
  int groupNum = 1;
  int i = 0; 
  while (groupNum < n)
  {
    for (i = 0; i < n; i += 2 * groupNum)
    {
      //[begin1, end1][begin2, end2]
      int begin1 = i, end1 = i + groupNum - 1;
      int begin2 = i + groupNum, end2 = i + groupNum * 2 - 1;
      //归并
      int index = begin1;
      //数组的数据个数,并不一定是按整数倍,所以分组可能越界或不存在
        //1.[begin2,end2]不存在或越界,修正为一个不存在的区间
      if (begin2 >= n)
      {
        begin2 = n + 1;
        end2 = n;
      }
        //2.end1越界,修正后归并
      if (end1 >= n)
      {
        end1 = n - 1;
      }
        //3.end2越界,修正后归并
      if (end2 >= n)
      {
        end2 = n - 1;
      }
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (a[begin1] < a[begin2])
        {
          temp[index++] = a[begin1++];
        }
        else
        {
          temp[index++] = a[begin2++];
        }
      }
      //begin2结束了,拷贝剩下的begin1
      while (begin1 <= end1)
      {
        temp[index++] = a[begin1++];
      }
      //begin1结束了,拷贝剩下的begin2
      while (begin2 <= end2)
      {
        temp[index++] = a[begin2++];
      }
    }
    //拷贝回原数组
    for (i = 0; i < n; i++)
    {
      a[i] = temp[i];
    }
    //迭代
    groupNum *= 2;
    //输出每层
    //PrintArray(a, n);
  }
  //释放临时数组
  free(temp);
}

❗ 归并排序的特性总结:❕

  1️⃣ 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题

  2️⃣ 时间复杂度:O(N*logN)

  3️⃣ 空间复杂度:O(N)

  4️⃣ 稳定性:稳定

💦 非比较排序

1、计数排序

🔑 核心思想 🔑

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

  计数排序核心步骤:

   1️⃣ 统计相同元素出现次数

   2️⃣ 根据统计的结果将序列回收到原来的序列中

❗ 动图演示:❕

🧿 实现代码 :

void CountSort(int* a, int n)
{
  //遍厉一遍找出最小值和最大值
  int min = a[0], max = a[0];
  int i = 0;
  for (i = 1; i < n; i++)
  {
    if (a[i] < min)
    {
      min = a[i];
    }
    if (a[i] > max)
    {
      max = a[i];
    }
  }
  //求出这个区间 
  int range = max - min + 1;
  //calloc空间,并初始化为0 
  int* count = (int*)calloc(range, sizeof(int));
  //统计  
  for (i = 0; i < n; i++)
  {
    //相对位置
    count[a[i] - min]++;
  }
  //根据count数组排序
  i = 0; 
  int j = 0; 
  for (j = 0; j < range; j++)
  {
    while (count[j]--)  
    {
      a[i++] = j + min;
    }
  }
  free(count);
} 

❗ 计数排序的特性总结:❕

  1️⃣ 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限

  2️⃣ 时间复杂度:O(MAX(N,范围))

  3️⃣ 空间复杂度:O(范围)

  4️⃣ 稳定性:稳定

  5️⃣ 只适合整数排序,浮点数/字符串不能排

2、基数排序

🔑 核心思想 🔑

  基数排序又称桶排序,它分别按数据的个、十、百、千、万 … 排序,当然也可以先万、千、…

❗ 动图演示:❕

❓ 这里就不实现了,为什么 ❔

  因为这种排序实际在校招中和现实中已经很少使用了,各位码友有兴趣的也可以自己了解下

💦 文件排序 (拓展)

⚠ 注意

  小文件排序是没有意义的,当然我们这里只是模拟,所以给 100 个数据

🔑 核心思想 🔑

  磁盘的读取速度相比内存差距非常大,所以我们不可能像在内存中两两归并。正确的归并方法是大文件平均分割成 N 份,保证每份大小都可以加载到内存,那么就可以把每个小文件加载到内存中,使用快排排序,再写回小文件,这时就达到文件中归并的先决条件

🧿 实现代码 :

void _MergeFile(const char* File1, const char* File2, const char* mFile)
{
  //读文件1
  FILE* fout1 = fopen(File1, "r");
  if (fout1 == NULL)
  {
    printf("打开文件失败\n");
    exit(-1);
  }
  //读文件2
  FILE* fout2 = fopen(File2, "r");
  if (fout2 == NULL)
  {
    printf("打开文件失败\n");
    exit(-1);
  }
  //写文件3,把文件1和文件2写到文件3里
  FILE* fin = fopen(mFile, "w");
  if (fin == NULL)
  {
    printf("打开文件失败\n");
    exit(-1);
  }
  int num1, num2;
  //对于内存中没有问题,但是磁盘就有问题了。不管num1和num2谁小谁大,只要读了fout1和fout2它们都会往后走
  /*while (fscanf(fout1, "%d\n", &num1) != EOF 
    && fscanf(fout2, "%d\n", &num2) != EOF)
  {
    if (num1 < num2)
      fprintf(fin, "%d\n", num1);
    else
      fprintf(fin, "%d\n", num2);
  }*/
  int ret1 = fscanf(fout1, "%d\n", &num1);
  int ret2 = fscanf(fout2, "%d\n", &num2);
  //下面保证了谁读谁走;fout1和fout2都不为空再比较
  while (ret1 != EOF && ret2 != EOF)
  {
    if (num1 < num2)
    {
      fprintf(fin, "%d\n", num1);
      ret1 = fscanf(fout1, "%d\n", &num1);//更新字符
    }
    else
    {
      fprintf(fin, "%d\n", num2);
      ret2 = fscanf(fout2, "%d\n", &num2); //更新字符
    }
  }
  /*注意这样会导致少写一个数据
    //fout2完了,写剩下的fout1
  while (fscanf(fout1, "%d\n", &num1) != EOF)
  {
    fprintf(fin, "%d\n", num1);
  }
    //fout1完了,写剩下的fout2
  while (fscanf(fout2, "%d\n", &num2) != EOF)
  {
    fprintf(fin, "%d\n", num2);
  }*/
  //fout2完了,写剩下的fout1
  while (ret1 != EOF)
  {
    fprintf(fin, "%d\n", num1);
    ret1 = fscanf(fout1, "%d\n", &num1);//更新字符
  }
  //fout1完了,写剩下的fout2
  while (ret2 != EOF)
  {
    fprintf(fin, "%d\n", num2);
    ret2 = fscanf(fout2, "%d\n", &num2); //更新字符
  }
  //关闭文件 
  fclose(fout1);
  fclose(fout2);
  fclose(fin);
}
void MergeSortFile(const char* file)
{
  FILE* fout = fopen(file, "r");
  if (fout == NULL)
  {
    printf("打开文件失败\n");
    exit(-1);
  }
  int n = 10;
  int a[10];
  int i = 0;
  int num = 0;
  char subfile[20];
  int filei = 1;
  memset(a, 0, sizeof(int) * n);
  //从fout文件流里读,直至EOF
  while (fscanf(fout, "%d\n", &num) != EOF)
  {
    //每次循环读10个数据放在内存中(if里先放9个,else再放最后一个)
    if (i < n - 1)
    {
      a[i++] = num;
    }
    else
    {
      a[i] = num;
      //快排10个数据
      QuickSort(a, 0, n - 1);
      //生成文件名sub_sort1/2/3...
      sprintf(subfile, "%d", filei++);
      //写文件,subfile里存储生成的文件名
      FILE* fin = fopen(subfile, "w");
      if (fin == NULL)
      {
        printf("打开文件失败\n");
        exit(-1);
      }
      //写回小文件
      for (int i = 0; i < n; i++)
      {
        fprintf(fin, "%d\n", a[i]);
      }
      //关闭文件
      fclose(fin);
      //重置i
      i = 0;
      memset(a, 0, sizeof(int) * n);
    }
  }
  //互相归并到文件,实现整体有序
  char mFile[100] = "12";
  char File1[100] = "1";
  char File2[100] = "2";
  for (i = 2; i <= n; i++)
  {
    //读取File1和File2,归并出mFile
    _MergeFile(File1, File2, mFile);
    //拷贝迭代File1的文件名12/123/1234...
    strcpy(File1, mFile);
    //循环迭代File2的文件名3/4/5...
    sprintf(File2, "%d", i + 1);
    //循环迭代mFile的文件名123/1234/12345...
    sprintf(mFile, "%s%d",mFile, i + 1);
  }
  //关闭文件
  fclose(fout);
}

💦 性能测试

❓ 测试所有排序 && 怎么保证它是公平的 ❔

  数组里放的数据都是一样的

//测试排序的性能对比
void TestOP()
{
  srand(time(0));
  const int N = 100000;
  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);
  int* a8 = (int*)malloc(sizeof(int) * N);
  int* a9 = (int*)malloc(sizeof(int) * N);
  int* a10 = (int*)malloc(sizeof(int) * N);
  int* a11 = (int*)malloc(sizeof(int) * N);
  int* a12 = (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];
    a8[i] = a1[i];
    a9[i] = a1[i];
    a10[i] = a1[i];
    a11[i] = a1[i];
    a12[i] = a1[i];
  }
  int begin1 = clock();
  InsertSort(a1, N);
  int end1 = clock();
  int begin2 = clock();
  ShellSort(a2, N);
  int end2 = clock();
  int begin2_1 = clock();
  ShellSortPro(a3, N);
  int end2_1 = clock();
  int begin3 = clock();
  SelectSort(a4, N);
  int end3 = clock();
  int begin3_1 = clock();
  SelectSortPro(a5, N);
  int end3_1 = clock();
  int begin4 = clock();
  HeapSort(a6, N);
  int end4 = clock();
  int begin5 = clock();
  BubbleSort(a6, N);
  int end5 = clock();
  int begin5_1 = clock();
  BubbleSortPro(a7, N);
  int end5_1 = clock();
  int begin6 = clock();
  QuickSort(a8, 0, N - 1);
  int end6 = clock();
  int begin6_1 = clock();
  QuickSortPro(a9, 0, N - 1);
  int end6_1 = clock();
  int begin6_2 = clock();
  QuickSortNonR(a10, 0, N - 1);
  int end6_2 = clock();
  int begin7 = clock();
  MergeSort(a11, N);
  int end7 = clock();
  int begin7_1 = clock();
  MergeSortNonR(a12, N);
  int end7_1 = clock();
  printf("InsertSort:%d\n", end1 - begin1);
  printf("ShellSort:%d\n", end2 - begin2);
  printf("ShellSortPro:%d\n", end2_1 - begin2_1);
  printf("SelectSort:%d\n", end3 - begin3);
  printf("SelectSortPro:%d\n", end3_1 - begin3_1);
  printf("HeapSort:%d\n", end4 - begin4);
  printf("BubbleSort:%d\n", end5 - begin5);
  printf("BubbleSortPro:%d\n", end5_1 - begin5_1);
  printf("QuickSort:%d\n", end6 - begin6);
  printf("QuickSortPro:%d\n", end6_1 - begin6_1);
  printf("QuickSortNonR:%d\n", end6_2 - begin6_2);
  printf("MergeSort:%d\n", end7 - begin7);
  printf("MergeSortNonR:%d\n", end7_1 - begin7_1);
  free(a1);
  free(a2);
  free(a3);
  free(a4);
  free(a5);
  free(a6);
  free(a7);
  free(a8);
  free(a9);
  free(a10);
  free(a11);
  free(a12);
}

💨 输出结果 (这里使用 Release 版本 && 10 万个数据)

  这里测试 3 次

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

❗ 稳定性 (比较重要,注意不要死记,要结合思想来看) ❕

  假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次

序保持不变,即在原序列中,r[i]=r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排

序算法是稳定的;否则称为不稳定的。

❓ 稳定性的意义 ❔

  假设有一考试,并规定前 6 名发奖状,如果分数相同,则按交卷时间的先后计算名次。此时排序稳定性的意义就有所体现了

四、概念选择题

1、快速排序算法是基于 ( ) 的一个排序算法

A. 分治法

B. 贪心法

C. 递归法

D. 动态规划法

📝 分析:快速排序是一种分治的算法,其次递归不是一种算法


2、对记录(54, 38, 96, 23, 15, 72, 60, 45, 83)进行从小到大的直接插入排序时,当把第8个记录45插入到有序表时,为找到插入位置需比较 ( ) 次?(采用从后往前比较)

A. 3

B. 4

C. 5

D. 6

📝 分析:

15 23 38 54 60 72 96 45

所以需要比较 5 次


3、以下排序方式中占用 O(n) 辅助存储空间的是 ( )

A. 简单排序

B. 快速排序

C. 堆排序

D. 归并排序

📝 分析:注意没有简单排序;归并排序的空间复杂度是 O(N)


4、下列排序算法中稳定且时间复杂度为 O(n2) 的是 ( )

A. 快速排序

B. 冒泡排序

C. 直接选择排序

D. 归并排序

📝 分析:

冒泡排序是稳定的算法,且时间复杂度是 O(N2)

直接选择排序是不稳定的,例如:

5 5 1

1 5 5 


5、关于排序,下面说法不正确的是 ( )

A. 快排时间复杂度为 O(N*logN),空间复杂度为 O(logN)

B. 归并排序是一种稳定的排序,堆排序和快排均不稳定

C. 序列基本有序时,快排退化成冒泡排序,直接插入排序最快

D. 归并排序空间复杂度为 O(N),堆排序空间复杂度的为 O(logN)

📝 分析:堆排序没使用递归,没有辅助空间,所以它的空间复杂度为 O(1)


6、下列排序法中,最坏情况下时间复杂度最小的是 ( )

A. 堆排序

B. 快速排序

C. 希尔排序

D. 冒泡排序

📝 分析:堆排序 (归并) 最坏情况下和最好情况下时间复杂度最小的 —— O(N*lonN)


7、设一组初始记录关键字序列为 (65,56,72,99,86,25,34,66),则以第一个关键字 65 为基准而得到的第一趟快速排序结果是 ( )

A. 34,56,25,65,86,99,72,66

B. 25,34,56,65,99,86,72,66

C. 34,56,25,65,66,99,86,72

D. 34,56,25,65,99,86,72,66

📝 分析:

我们前面已经了解到快速排序首次单趟的三种方法 —— hoare版本、挖坑版本、前后指针版本,注意在某些情况下需要都考虑到,因为三种版本得到的结果不一定都一样

结合情况此题选择 A 选项

相关文章
|
27天前
|
传感器 并行计算 算法
【无人机编队】基于非支配排序遗传算法II NSGA-II高效可行的无人机离线集群仿真研究(Matlab代码实现)
【无人机编队】基于非支配排序遗传算法II NSGA-II高效可行的无人机离线集群仿真研究(Matlab代码实现)
115 3
|
2月前
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
63 1
|
2月前
|
存储 监控 算法
基于跳表数据结构的企业局域网监控异常连接实时检测 C++ 算法研究
跳表(Skip List)是一种基于概率的数据结构,适用于企业局域网监控中海量连接记录的高效处理。其通过多层索引机制实现快速查找、插入和删除操作,时间复杂度为 $O(\log n)$,优于链表和平衡树。跳表在异常连接识别、黑名单管理和历史记录溯源等场景中表现出色,具备实现简单、支持范围查询等优势,是企业网络监控中动态数据管理的理想选择。
67 0
|
6月前
|
存储 算法 Java
算法系列之数据结构-二叉树
树是一种重要的非线性数据结构,广泛应用于各种算法和应用中。本文介绍了树的基本概念、常见类型(如二叉树、满二叉树、完全二叉树、平衡二叉树、B树等)及其在Java中的实现。通过递归方法实现了二叉树的前序、中序、后序和层次遍历,并展示了具体的代码示例和运行结果。掌握树结构有助于提高编程能力,优化算法设计。
181 10
 算法系列之数据结构-二叉树
|
6月前
|
算法 Java
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
149 3
 算法系列之数据结构-Huffman树
|
6月前
|
算法 Java
算法系列之数据结构-二叉搜索树
二叉查找树(Binary Search Tree,简称BST)是一种常用的数据结构,它能够高效地进行查找、插入和删除操作。二叉查找树的特点是,对于树中的每个节点,其左子树中的所有节点都小于该节点,而右子树中的所有节点都大于该节点。
182 22
|
6月前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
6月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
191 30
|
7月前
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
294 25

热门文章

最新文章