常见排序算法及其稳定性分析

简介: 常见排序算法及其稳定性分析

前言:

排序算法可以说是每一个程序员在学习数据结构和算法时必须要掌握的知识点,同样也是面试过程中可能会遇到的问题,在早些年甚至还会考冒泡排序。由此可见呢,掌握一些常见的排序算法是一个程序员的基本素养。虽然现在的语言标准库里都有直接的排序函数,但是作为一个学习者,我们应当抱着“知其然,还要知其所以然”的态度去学习。

1.常见的排序算法有哪些?

常见的排序算法及其性能:

算法名称 平均时间复杂度 稳定性
直接插入排序 N^2 稳定
希尔排序 N^1.25--1.6N^1.25 不稳定
选择排序 N^2 不稳定
堆排序 NlogN 不稳定
冒泡排序 N^2 稳定
快速排序 NlogN 不稳定
归并排序 NlogN 稳定

这里呢我只讲一些效率比较高的排序算法,比如快排、并排、堆排序、希尔排序。

2.常见排序算法的实现

2.1希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成grap个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序(插入排序)。然后,重复上述分组和排序的工作。当分组间距为1时,所有记录在统一组内排好序。

代码实现:

 
// 希尔排序
void ShellSort(int* a, int n) {
  int gap = n - 1;//grap为分组之间的间隔
  while (gap > 1) {
    gap = gap / 3 + 1;//每次分组的间距都越来越小,直到间距为一
    for (int i = 0; i < gap; i++) {//i表示每组的开头位置
      for (int j = i; j < n - gap; j += gap) {//对每一组插入排序
        int end = j;
        int temp = a[j + gap];
        while (end >= 0) {
          if (temp < a[end]) {
            a[end + gap] = a[end];
            end -= gap;
          }
          else {
            break;
          }
        }
        a[end + gap] = temp;
      }
    }
  }
}

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时(相当于对整个数组进行插入排序),数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好多书中给出的 希尔排序的时间复杂度都不固定,目前也只能给出一个大概的复杂度。

2.2堆排序

关于堆排序在之前的博客中已经详细讲解过,有兴趣的可以去看看。

2.3快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2.3.1递归版

对于单趟排序,我们确定一个基准值key,并利用双指针从首尾两端移动。找到一个位置放key,使得,key左边的元素都小于key(以升序排序为例),右边的元素都大于key

快排的算法思想本质是用空间换取时间,每一次递归排序的区间都在不断地缩小,每一趟排序都确定了下一趟排序的左右两个区间,类似于二叉树的节点的左右儿子节点

代码实现

int PartSort1(int* a, int left, int right) {//单趟排序
  int end = right;
  int begin = left;
  int mid = GetMid(a, left, right);//基准值的下标
  Swap(&a[left], &a[mid]);//跟首元素交换
  int key = left;
  while (begin < end) {
    while (end > begin && a[end] >= a[key])end--;
    while (end > begin && a[begin] <= a[key])begin++;
    Swap(&a[end], &a[begin]);
  }
  Swap(&a[key], &a[begin]);
  return begin;//单趟排序后返回最后key所处的位置
}
void QuickSort(int* a, int left, int right){//递归
     if (left >= right)return;
      int mid = PartSort3(a, left, right);
    QuickSort(a, left, mid - 1);
    QuickSort(a, mid + 1, right);
}

2.3.2非递归版

利用,我们可以将单趟排序确定的两个子区间存起来,模拟函数栈帧的开辟。这样一来,我们就可以不用递归(递归较为耗损空间)就可以完成快速排序。

代码实现

void QuickSortNonR(int* a, int left, int right) {
  Stack ST;
  StackInit(&ST);
  StackPush(&ST, right);//根据栈的特性,先将右区间压栈
  StackPush(&ST, left);
  while (!StackEmpty(&ST)) {
    int l = StackTop(&ST);
    StackPop(&ST);
    int r = StackTop(&ST);
    StackPop(&ST);//取出一个区间[l,r]
    if (l > r)Swap(&l, &r);
    int keyi = PartSort1(a, l, r);//单趟排序
    if (keyi -1> l) {
      StackPush(&ST, keyi - 1);
      StackPush(&ST, l);
    }
    if (keyi + 1 < r) {
      StackPush(&ST, r);
      StackPush(&ST, keyi + 1);
    }
  }
  StackDestroy(&ST);
}

2.4归并排序

2.4.1递归版

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

实现思路:

因为要先满足每个子序列都有序的条件,我们可以把区间长度划分为1,这样每个区间都一定是有序的,再对每个区间先上合并。值得注意的是,有序地子区间合并之后也是有序的。同样是根据二叉树的思想,每个区间一分为两个子区间,直到区间长度为1,需要logN的时间复杂度,合并两个区间又是N的复杂度,所以总的时间复杂度为NlogN。

代码实现

void MergeSort(int* a, int begin, int end, int* temp) {//temp用于暂时存放合并后的数组
  if (begin >= end)return;
  int mid = (begin + end) / 2;
  MergeSort(a, begin, mid, temp);
  MergeSort(a, mid + 1, end, temp);//先递归区间
  
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;//两个区间的端点指针
  int i = begin;//合并数组的下标的指针
  while (begin1 <= end1 && begin2 <= end2) {
    if (a[begin1] < a[begin2]) {//比较两个区间的首元素,小的放入temp中,两个指针往后移
      temp[i++] = a[begin1++];
    }
    else {
      temp[i++] = a[begin2++];
    }
  }//有可能还有某个区间还有元素没有放进去
  while (begin1 <= end1) {
    temp[i++] = a[begin1++];
  }
  while (begin2 <= end2) {
    temp[i++] = a[begin2++];
  }
  //将合并好的数组复制到原数组中
  memcpy(a + begin, temp + begin, sizeof(int) * (end - begin + 1));
}

2.4.2非递归版

既然快速排序有非递归版本,那么归并排序同样可以有非递归的版本。但是对于非递归的归并排序来说,并不能像快速排序一样使用栈这种结构来维护递归的区间。为什么呢?因为归并排序的前提是合并两个有序的区间。所以,用栈来处理就不能先有序再合并。我们可以用类似希尔排序的分组思想,根据递归归并排序的思想,从最底层开始,依次往上合并。

代码实现:

 
void MergeSortNonR(int* a, int n) {
  int* temp = malloc(sizeof(int) * n);
  if (temp == NULL) {
    perror("malloc");
  }
 
  int grap = 1;//每组元素个数,根据递归区间思想,上一层的组长度是下一层的一半
  
  while (grap < n) {//分组的元素个数小于n,等于n意味着就是整个数组
    for (int i = 0; i < n; i += grap * 2) {//每次合并相邻两个区间,i为第一个区间的起点
      int begin1 = i, end1 = i + grap - 1;
      int begin2 = i + grap, end2 = i + grap * 2 - 1;
 
      if (end1 >= n || begin2 >= n) {//查看当前的相邻的两个区间是否在数组中
        break;
      }
      if (end2 >= n) {//可能不够均分,最后一个区间右端点取n-1
        end2 = n - 1;
      }
      int cur = i;//以下就是合并区间
      while (begin1 <= end1 && begin2 <=end2) {
        if (a[begin1] < a[begin2]) {
          temp[cur++] = a[begin1++];
        }
        else {
          temp[cur++] = a[begin2++];
        }
      }
 
      while (begin1 <= end1) {
        temp[cur++] = a[begin1++];
      }
      while (begin2 <= end2) {
        temp[cur++] = a[begin2++];
      }
      memcpy(a + i, temp+i, sizeof(int)*(end2 -i + 1));
    }
    grap = grap * 2;//上一层的每一个区间元素个数
  }
  free(temp);
}

3.排序算法的稳定性

对于某种排序算法,如果会将两个相同大小的元素的相对位置改变,那么我们就称这个算法是不稳定的,否者就是稳定的。

什么时候需要考虑稳定性?

针对多个字段进行排序,就可能需要考虑排序算法的稳定性

举例:

对以下数据进行排序:

序号 订单金额 订单时间
1 50 9:04
2 30 9:00
3 50 9:03
4 10 9:01

要求:

是按照订单金额进行升序排序,如果订单金额相同,则按照下单时间升序排序

先按照订单时间升序排序得到序号为:2、4、3、1

再从上一个序号组中按照订单金额升序排

1.假如排序算法不稳定

则可能得到:4、2、1、3

对于序号1和3,订单金额相同,但是时间小的反而排在后面,不符合要求。

2.假如排序算法稳定

则一定得到:4、2、3、1

对于序号1和3,订单金额相同,下单时间大的排在后面,符合要求。

根据以上举例可以看出来,在对多个字段排序的时候,往往需要稳定的排序算法进行排序。

这也是为什么同样的时间复杂度,在有些时候能用不稳定的快排,有些时候用稳定归并排序。

相关文章
|
26天前
|
机器学习/深度学习 边缘计算 算法
NOMA和OFDMA优化算法分析
NOMA和OFDMA优化算法分析
52 6
|
2天前
|
人工智能 自然语言处理 算法
2025 年 7 月境内深度合成服务算法备案情况分析报告
2025年7月,中央网信办发布第十二批深度合成算法备案信息,全国389款产品通过备案,服务提供者占比超七成。截至7月14日,全国累计备案达3834款,覆盖文本、图像、音视频等多模态场景,广泛应用于生活服务、医疗、金融等领域。广东以135款居首,数字人、AI客服等C端应用主导,民营企业成主力,国企聚焦公共服务。随着AI政策推动,备案已成为AI产品合规上线关键环节。
|
3月前
|
数据采集 机器学习/深度学习 算法
别急着上算法,咱先把数据整明白:大数据分析的5个基本步骤,你都搞对了吗?
别急着上算法,咱先把数据整明白:大数据分析的5个基本步骤,你都搞对了吗?
125 4
|
3月前
|
存储 监控 算法
员工行为监控软件中的 Go 语言哈希表算法:理论、实现与分析
当代企业管理体系中,员工行为监控软件已逐步成为维护企业信息安全、提升工作效能的关键工具。这类软件能够实时记录员工操作行为,为企业管理者提供数据驱动的决策依据。其核心支撑技术在于数据结构与算法的精妙运用。本文聚焦于 Go 语言中的哈希表算法,深入探究其在员工行为监控软件中的应用逻辑与实现机制。
109 14
|
4月前
|
自然语言处理 算法 安全
境内深度合成服务算法备案通过名单分析报告
本报告基于《境内深度合成服务算法备案通过名单》,分析了2023年6月至2025年3月公布的10批备案数据,涵盖属地分布、行业应用及产品形式等多个维度。报告显示,深度合成算法主要集中于经济发达地区,如北京、广东、上海等地,涉及教育、医疗、金融、娱乐等多行业。未来趋势显示技术将向多模态融合、行业定制化和安全合规方向发展。建议企业加强技术研发、拓展应用场景、关注政策动态,以在深度合成领域抢占先机。此分析旨在为企业提供参考,助力把握技术发展机遇。
境内深度合成服务算法备案通过名单分析报告
|
4月前
|
供应链 算法 搜索推荐
从公布的前十一批其他算法备案通过名单分析
2025年3月12日,国家网信办发布算法备案信息,深度合成算法通过395款,其他算法45款。前10次备案中,深度合成算法累计3234款,其他类别647款。个性化推送类占比49%,涵盖电商、资讯、视频推荐;检索过滤类占31.53%,用于搜索优化和内容安全;调度决策类占9.12%,集中在物流配送等;排序精选类占8.81%,生成合成类占1.55%。应用领域包括电商、社交媒体、物流、金融、医疗等,互联网科技企业主导,技术向垂直行业渗透,内容安全和多模态技术成新增长点。未来大模型检索和多模态生成或成重点。
从公布的前十一批其他算法备案通过名单分析
|
4月前
|
人工智能 自然语言处理 供应链
从第十批算法备案通过名单中分析算法的属地占比、行业及应用情况
2025年3月12日,国家网信办公布第十批深度合成算法通过名单,共395款。主要分布在广东、北京、上海、浙江等地,占比超80%,涵盖智能对话、图像生成、文本生成等多行业。典型应用包括医疗、教育、金融等领域,如觅健医疗内容生成算法、匠邦AI智能生成合成算法等。服务角色以面向用户为主,技术趋势为多模态融合与垂直领域专业化。
|
4月前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
5月前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
83 3
|
4月前
|
人工智能 自然语言处理 算法
从第九批深度合成备案通过公示名单分析算法备案属地、行业及应用领域占比
2024年12月20日,中央网信办公布第九批深度合成算法名单。分析显示,教育、智能对话、医疗健康和图像生成为核心应用领域。文本生成占比最高(57.56%),涵盖智能客服、法律咨询等;图像/视频生成次之(27.32%),应用于广告设计、影视制作等。北京、广东、浙江等地技术集中度高,多模态融合成未来重点。垂直行业如医疗、教育、金融加速引入AI,提升效率与用户体验。

热门文章

最新文章