当前位置: 首页 > news >正文

排序算法:高效数据处理的核心

摘要

排序算法是将一组无序数据按预设规则(如升序、降序)重新排列为有序序列的方法,其核心意义是优化数据处理效率、支撑上层应用实现

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

内部排序:当待排序的数据量较小,能够全部加载到计算机内存中,排序过程仅在内存中完成,无需依赖外部存储(如硬盘、磁带等)。排序速度快(内存读写速度远高于外存),算法设计主要优化内存中的比较、交换或移动操作,时间复杂度通常以内存操作效率为衡量标准。

外部排序:当待排序的数据量极大(如 GB、TB 级),无法全部装入内存,必须借助外部存储(如硬盘文件),排序过程需要在内存与外存之间频繁交换数据。受限于外存读写速度(远慢于内存),算法设计的核心是减少 I/O 操作次数,时间复杂度主要取决于外存访问效率。

排序在生活当中运用是非常广泛的,比如你点击一个购物商城,价格升序价格降序,凡是用到排名之类的都需要运用到排序算法,所以掌握排序算法是非常重要的

常见的排序算法

插入排序-直接插入排序

基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

算法原理讲解

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

实现代码:

// 最坏时间复杂度O(N^2) -- 逆序
// 最好时间复杂度O(N) -- 顺序有序
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; ++i){// [0,end] 插入 end+1 [0, end+1]有序int end = i;int tmp = a[end + 1];while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];--end;}else{break;}}a[end + 1] = tmp;}
}

总结:

1.元素集合越接近有序,直接插入排序算法的时间效率越高

2.时间复杂度O(N^2)

3.空间复杂度O(1)

4.稳定性:稳定(你相同元素的大小的相对位置没有改变,比如两个1,他们还是第一个1在左边)

插入排序-希尔排序

基本思想:希尔排序法又称缩小增量法。

希尔排序(Shell Sort)是一种改进的插入排序,其核心思想是通过逐步缩小增量(间隔)的方式,将原数组分割成若干个较小的子序列,对每个子序列分别进行插入排序,最终当增量缩小到 1 时,完成最后一次全局插入排序,使整个数组有序。

算法原理讲解

假设gap=3

此时9   5    8    5为一组,也就是红线的为一组

1   7    6蓝线为一组,2   4   3绿线为一组

这样就分为了3组,gap的含义就是用来分组的

gap越大,跳的越大,交换的越远,这样能够大概接近有序

gap越小,跳的越慢,越接近有序

所以我们是不断调整gap,一开始gap大,然后慢慢小,最后=1

我们先进行gap分组,组内我们使用直接插入排序,然后不断地缩小gap,当gap=1的时候就完成了排序,这就是shellsort,因为直接插入排序在接近有序的时候时间复杂度会有优化,希尔就是让它接近有序,相当于直接插入排序的一种优化方法

gap>1:预排序     gap==1:直接插入排序

代码

// O(N^1.3)
void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){//gap = gap / 2;gap = gap / 3 + 1;// [0,end] 插入 end+gap [0, end+gap]有序  -- 间隔为gap的数据for (int i = 0; i < n - gap; ++i){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = tmp;}}
}

这里i<n-gap是为了防止下面的end+gap越界

gap的计算方式有很多种,这里gap的不同策略可以自己搜索

总结:

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

2.当gap>1是都是预排序,目的是让数组更接近与有序。当gap==1时,数组已经接近有序的了,这样就会很快。就整体而言,可以达到优化的效果,我们实现后可以进行性能测试的对比

3.希尔排序的时间复杂度不好计算,需要进行推导,大概是O(N^1.3~N^2),而且不同的gap都是有不同的时间复杂度的,已知最优的增量序列可使希尔排序的最坏时间复杂度接近 O(n logn)

4.稳定性:不稳定,很明显它是分组,可能相同元素在不同的组别,导致组内交换的时候相对顺序变了

选择排序-直接选择排序

基本思想:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

算法原理讲解:

每次遍历数组,然后从其中挑选出min,然后交换即可

代码

// 最坏时间复杂度:O(N^2)
// 最好时间复杂度:O(N^2)
void SelectSort(int* a, int n)
{int begin = 0, end = n - 1;while (begin < end){// 选出最小的放begin位置// 选出最大的放end位置int mini = begin, maxi = begin;for (int i = begin + 1; i <= end; ++i){if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}}Swap(&a[begin], &a[mini]);// 修正一下maxiif (maxi == begin)maxi = mini;Swap(&a[end], &a[maxi]);++begin;--end;}
}

这行修正的目的是解决 “最大元素的位置被最小元素交换覆盖” 的问题,避免最大元素的索引失效。

也就是可能begin的位置是maxi,但是mini与begin交换的时候已经把begin覆盖了,此时如果你步更新maxi就可能出错,要把maxi更新到之前mini的位置

总结:

1.直接选择排序思考非常好理解,但是效率不是很好,实际中很少使用

2.时间复杂度O(N^2)

3.空间复杂度O(1)

4.稳定性:不稳定,比如2 1,1交换2,会把第一个2变到后面1 2 2

选择排序-堆排序

堆排序在数据结构精讲中的树中精讲了,如果这里讲解不明白可以移步到我的数据结构精讲中关于树的讲解

基本思想

如果排降序就需要构建小堆,排升序就需要大堆

一开始先构建大堆,构建完了之后每次选堆顶的元素和最后一个元素交换之后,在使用一次向下调整算法,依次循环

算法原理讲解

给定一个数组先构建大堆,然后20和3进行交换,因为堆顶是最大元素,所以把最大放到最后,在使用向下调整算法调整成堆,注意向下调整算法中孩子的计算方式

代码

void AdjustDown(int* a, int n, int parent)
{int minChild = parent * 2 + 1;while (minChild < n){// 找出小的那个孩子if (minChild + 1 < n && a[minChild + 1] > a[minChild]){minChild++;}if (a[minChild] > a[parent]){Swap(&a[minChild], &a[parent]);parent = minChild;minChild = parent * 2 + 1;}else{break;}}
}
// O(N*logN)
void HeapSort(int* a, int n)
{// 大思路:选择排序,依次选数,从后往前排// 升序 -- 大堆// 降序 -- 小堆// 建堆 -- 向下调整建堆 - O(N)for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}// 选数 N*logNint i = 1;while (i < n){Swap(&a[0], &a[n - i]);AdjustDown(a, n - i, 0);++i;}
}

总结:

1.堆排序使用堆来选数,效率就高了很多

2.时间复杂度O(N*logN),建堆O(N)+选数O(N*logN),整体时间复杂度由高阶项决定

3.空间复杂度O(1)

4.稳定性:不稳定

交换排序-冒泡排序

基本思想

所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

算法原理讲解

每次两个数字进行比较,如果左边大于右边,那就交换,为什么叫冒泡,因为一次冒泡就可以把一个最大的数字放到最右边

代码

// 最坏情况:O(N^2)
// 最好情况:O(N)
void BubbleSort(int* a, int n)
{for (int j = 0; j < n; ++j){int exchange = 0;for (int i = 1; i < n - j; ++i){if (a[i - 1] > a[i]){Swap(&a[i - 1], &a[i]);exchange = 1;}}if (exchange == 0){break;}}
}

这里的优化处理就是exchange,如果exchange==0,那说明这次没有进行交换,那就证明是有序的,所以不需要进行下一次冒泡了,所以如果是有序的话,遍历一轮发现exchange==0,那就不需要下一次遍历了,所以时间复杂度最好是O(N)

总结

1.冒泡排序是一种非常容易理解的排序

2.时间复杂度O(N^2)

3.空间复杂度O(1)

4.稳定性:稳定

交换排序-快速排序

基本思想:

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

这里就相当于选择6作为基准值

算法原理讲解

代码:

int GetMidIndex(int* a, int left, int right)
{int mid = left + (right - left) / 2;//防止溢出if (a[left] < a[mid]){if (a[mid] < a[right]){return mid;}else if (a[left] > a[right]){return left;}else{return right;}}else // a[left] >= a[mid]{if (a[mid] > a[right]){return mid;}else if (a[left] < a[right]){return left;}else{return right;}}
}

// [left, right] -- O(N)
// hoare
int PartSort1(int* a, int left, int right)
{// 三数取中int mid = GetMidIndex(a, left, right);//printf("[%d,%d]-%d\n", left, right, mid);Swap(&a[left], &a[mid]);int keyi = left;while (left < right){// 6 6 6 6 6// R找小while (left < right && a[right] >= a[keyi]){--right;}// L找大while (left < right && a[left] <= a[keyi]){++left;}if (left < right)Swap(&a[left], &a[right]);}int meeti = left;Swap(&a[meeti], &a[keyi]);return meeti;
}// 挖坑法
int PartSort2(int* a, int left, int right)
{// 三数取中int mid = GetMidIndex(a, left, right);Swap(&a[left], &a[mid]);int key = a[left];int hole = left;while (left < right){// 右边找小,填到左边坑while (left < right && a[right] >= key){--right;}a[hole] = a[right];hole = right;// 左边找大,填到右边坑while (left < right && a[left] <= key){++left;}a[hole] = a[left];hole = left;}a[hole] = key;return hole;
}// 前后指针法
int PartSort3(int* a, int left, int right)
{// 三数取中int mid = GetMidIndex(a, left, right);Swap(&a[left], &a[mid]);int keyi = left;// 基准值的初始索引(此时已通过三数取中优化)int prev = left; // 前指针:指向“小于基准值区域”的最后一个元素int cur = left + 1;// 后指针:用于遍历数组,寻找小于基准值的元素while (cur <= right){// 找小if (a[cur] < a[keyi] && ++prev != cur)Swap(&a[cur], &a[prev]);++cur;}Swap(&a[keyi], &a[prev]);return prev;
}
void QuickSort(int* a, int begin, int end)
{if (begin >= end){return;}//if (end - begin <= 8)//{//	InsertSort(a + begin, end - begin + 1);//}//else//{int keyi = PartSort3(a, begin, end);//[begin, keyi-1] keyi [keyi+1, end]QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);//}
}

对于GetMidIndex就是给区间内,左区间,右区间,中间的,三个位置的值进行比较,返回第二大的下标,避免因基准值选择极端值(如最大 / 最小值)导致快速排序退化(时间复杂度降至 O (n²))。

PartSort1:还是先进行三数取中,然后放到left的位置,keyi基准值不要变(因为left是<=就会++,所以第一个left会跳过),在右边区间当中寻找,left寻找比基准值大的,right寻找比基准值小的,如果left还是在right左边那就进行交换,那这样循环完结果就是【基准值,小于基准值,大于基准值】,然后再寻找一个合适的位置把基准值交换即可。

PartSort2:寻找一个坑然后依次填写,先把最左边标记为一个坑,然后right--一直找一个小于基准值的,让其填补左边,然后再更新坑的位置,再让左边找大的去填右边坑,直到left>=right,然后再找个合适位置填写基准值即可

前后指针法 PartSort3:让keyi就是基准值,然后prev就是比keyi小的最后一个元素,如果满足小于条件,prev会进行++,然后再交换,这样prev填的位置就是对的,划分区间,【基准值,小于基准值,大于基准值】,所以最后再给基准值找个位置,那就是prev,直接交换即可,所以最后就变成了【小于基准值,基准值,大于基准值】

对于QuickSort:使用递归即可,依次递归下去,直到递归返回,他就是一个有序的数组,注意这一部分如果数据量很小,可以采用插入排序,是快速排序的一种优化策略

采用的原因是:

快速排序的递归开销:快速排序通过递归分割区间,当区间长度很小时(如≤8),递归调用的额外开销(函数调用、栈帧创建等)会超过排序本身的成本,导致效率下降。

插入排序对小数据更友好:插入排序的时间复杂度是 O (n²),但对于极小规模数据(如 n≤10),其常数因子(实际执行的指令数)远小于快速排序。例如,对 8 个元素排序,插入排序可能只需几次比较和移动,而快速排序的分区、递归等操作反而更耗时。

数据局部性:插入排序是顺序访问数据,缓存利用率更高;而快速排序的分区操作可能导致数据跳跃访问,缓存效率较低,在小数据场景下这一差异更明显。

总结

1.快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫做快速排序

2.时间复杂度O(N*logN):有没有发现快排就相当于一颗树,为什么要三数取中,就是因为要让左子树和右子树的个数都比较平均,所以要三数取中,结点的个数就是数组的大小,所以是N,然后每次进行划分左右区间,所以高度就是logN,前面的系数n是因为划分下去也是要进行遍历一遍数组的比较,每一层都需要遍历一遍数组,所以是相乘,所以时间复杂度为O(NlogN)

3.空间复杂度O(logN):是因为递归调用栈,栈空间也是空间,所以是logN

4.稳定性:不稳定

归并排序-归并排序

基本思想:

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

算法原理讲解

就是进行分组,然后每一组排完序,排序之后再进行合并,采用分治思想将数组分割为子序列,排序后再合并,最终实现整体有序

代码

void _MergeSort(int* a, int begin, int end, int* tmp)
{if (begin >= end)return;int mid = (end + begin) / 2;// [begin, mid] [mid+1, end]_MergeSort(a, begin, mid, tmp);_MergeSort(a, mid+1, end, tmp);// 归并 取小的尾插// [begin, mid] [mid+1, end]int begin1 = begin, end1 = mid;int begin2 = mid+1, end2 = end;int i = begin;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}// 拷贝回原数组 -- 归并哪部分就拷贝哪部分回去memcpy(a+begin, tmp+begin, (end-begin+1)*sizeof(int));
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int)*n);if (tmp == NULL){perror("malloc fail");return;}_MergeSort(a, 0, n - 1, tmp);free(tmp);tmp = NULL;
}

_MergeSort中:递归排序左边和递归排序右边,也就是上面那张图的绿色部分,递归到只剩一个元素,将子区间不断分割,直到每个子区间长度为 1(触发终止条件)。下面则为合并部分

总结

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

2.时间复杂度:O(N*logN),和选择排序一样,需要往下一直划分,所以高度是logN,每一层都需要对比完之后再放入原数组,所以是N*logN

3.空间复杂度:O(N),因为借助了额外的数组tmp

4.稳定性:稳定,因为我们再比较的时候是小于等于,所以=的时候我们是先取左边放入tmp数组

区分快速排序和归并排序

有时候很容易将两种方法区分不清,我们要理解本质

快速排序的特点:先分割后排序,分割过程直接改变原数组,无需额外空间存储子区间。

  • 适合:大规模无序数据、对空间复杂度敏感的场景(如内存有限)、追求平均排序速度的场景。
  • 不适合:要求排序稳定、数据接近有序(需额外优化基准值选择)的场景。

合并排序的特点:先排序后合并,分割仅划分区间,排序和合并依赖子区间的有序性

  • 适合:要求排序稳定的场景(如多关键字排序)、数据量极大(可外部排序)、对最坏情况时间复杂度有严格要求的场景。
  • 不适合:对空间复杂度敏感的场景(额外 O (n) 空间开销)。

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

排序方法适用场景不适用场景
冒泡排序数据量极小(如几十条)、数据基本有序数据量较大(如百条以上)
选择排序数据量极小、对交换操作次数敏感(如硬件层面交换成本高)数据量较大、要求排序稳定
插入排序数据量较小、数据基本有序数据量较大
希尔排序中等规模数据(数万条)、对稳定性要求不高数据量极大、要求稳定排序
堆排序大规模数据、要求原地排序(空间限制严格)、对稳定性无要求要求稳定排序、数据量较小(常数因子较大,小数据效率不如简单排序)
归并排序大规模数据、要求排序稳定、对最坏时间复杂度有严格要求对空间复杂度敏感(需要额外 O (n) 空间)
快速排序大规模无序数据、对平均排序速度要求高、空间限制适中数据接近有序(易退化)、要求稳定排序

在实际开发中,快速排序是使用最广泛的排序算法,原因如下:

  • 平均时间复杂度 O (n log n),且常数因子小,实际运行速度快。
  • 多数编程语言的标准库排序函数(如 C++ 的sort、Python 的sorted)底层都采用快速排序的优化版本(如结合插入排序处理小数据、三数取中优化基准值等)。

此外,归并排序在需要稳定排序的场景(如对象数组排序)也较为常用(如 Java 对对象数组的排序);堆排序在一些对空间要求严格的嵌入式场景偶有使用;而冒泡、选择、插入排序主要用于教学或数据量极小的特殊场景。

http://www.dtcms.com/a/555064.html

相关文章:

  • 网站架构设计师工资水平自助建网站系统源码
  • 网站建设go华为网站建设和阿里云哪个好
  • 宁波规划建设局网站顺企网怎么发布公司信息
  • 校园网站建设系统设计做网站去什么公司
  • 房产网站开发报价山西seo基础教程
  • 网站建设方案预计效果教育网站制作视频
  • python使用ffmpeg对视频进行转码
  • 做网站 注意外贸网站源码php
  • 计算机图形学·2 图像形成
  • 简单了解一下哈希表(C++)
  • 如何在旅游网站上做攻略唐山官方网站建设
  • 金属材料东莞网站建设浙江企业黄页大全
  • 做销售在哪些网站注册好制作网站软件排行榜
  • 网站备案能查到什么东西怎么提高自己网站的知名度
  • 小说网站论文摘要论坛做网站好吗
  • 西局网站建设怎样进入建设通网站
  • 备案的网站可以攻击吗盐城公司网站建设
  • SQL Studio:一个基于浏览器的数据库查询工具
  • 微信微网站建设平台外包一个企业网站多少钱
  • 建设 市民中心网站wordpress前端可视化编辑器
  • 帝国做的网站wordpress 附件显示设置
  • 网站福利你们会回来感谢我的ui设计作品解析
  • 行政单位网站建设立项依据网站公司云建站怎么样
  • 网站做301重定向扁平化网站特效
  • P3379 【模板】最近公共祖先(LCA)(st表,tarjan两种版本)
  • 找设计方案的网站互联网营销平台
  • 佛山网站优化公司排名wordpress 伪静态 403
  • POI搜索:图文教程!多种条件搜索POI数据,支持地图可视化,支持导出SHP、GEOJSON、DXF等文件格式
  • IoControlCode=20IOCTL_ICA_STACK_CONNECTION_SEND分析
  • 网站品牌高端定制设计网站公司价格