1.排序的概念及其应用
1.1排序的概念
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。分内部排序和外部排序,若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
ps:本文将介绍常见的内部排序算法,外部排序暂不做介绍
排序的稳定性:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。
1.2排序的应用
- 数据查找和检索:在一个有序的数组中查找特定元素通常比在无序数组中更快。例如,电话黄页按姓氏排序后,查找某个人的电话号码变得更加容易。数字音乐库按作家名或歌曲名排序,搜索引擎按搜索结果的重要性排序,电子表格按某一列排序等,都是排序算法应用的实例。
- 数据压缩:排序算法在数据压缩中也起到了关键作用,通过重新排列数据以减少存储空间的需求。
- 计算机图形学:在计算机图形学中,排序算法用于优化图形渲染过程,确保图形元素按照特定的顺序进行绘制,以达到最佳的视觉效果。
- 计算生物学:排序算法在生物信息学中用于分析基因序列、蛋白质结构等,帮助科学家更好地理解生物系统的运作。
- 供应链管理:在供应链管理中,排序算法可以用于优化物流路径、库存管理等,以提高效率和降低成本。
- 组合优化:排序算法也应用于组合优化问题中,如旅行商问题、背包问题等,通过优化排序顺序来找到最优解。
- 社会选择和投票:在社会科学领域,排序算法可以用于选举和投票系统,确保公平性和透明度
2.常见排序算法的实现
2.1插入排序
2.1.1基本思想
把待排序的记录按照其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
当插入第i个元素时,前面的i-1个元素已经排好序,此时用i的排序码与i-1,i-2,......的排序码进行比较,原来位置上的元素向后移,找到插入位置就将arr[i]插入
日常生活中,我们在打扑克牌的时候,也用了插入排序的思想
2.1.2特性总结
- 元素越接近有序时,直接插入排序算法的时间效率越高
- 时间复杂度O(N^2)
- 空间复杂度O(1)
- 稳定性:稳定
2.1.3代码实现
void InsertSort(int* a, int n) {
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
2.2希尔排序
2.2.1基本思想
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
2.2.2特性总结
- 希尔排序是对直接插入排序的优化
- 当gap>1时都是预排序,目的是为了让数组更接近有序。当gap==1时,数组已经接近有序了。这样整体而言,可以达到优化的效果
- 时间复杂度:与gap的取值方式有关,不好计算。当n在某个特定的范围内时,希尔排序所需的比较和移动次数约为n^1.3。平均情况为O(nlogn)到O(n^2)
- 空间复杂度:O(1)
- 稳定性:不稳定。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
2.2.3代码实现
void ShellSort(int* a, int n) {
int gap = n;
while (gap > 0)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
2.3选择排序
2.3.1基本思想
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零
2.3.2特性总结
- 时间复杂度:O(N^2) 它包含了两层嵌套的循环,在最坏的情况下,对于每个未排序的元素,都需要比较其与未排序部分的所有元素,然后进行交换。这导致了一个二次方级别的复杂度
- 空间复杂度:O(1)
- 稳定性:不稳定。举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
2.3.3代码实现
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while(begin<end)
{
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);
if (maxi = begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
//上述代码,一趟选取无序区里最大和最小的两个元素
2.4堆排序
2.4.1基本思想
以大根堆为例讲解:首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端。将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1。将剩余的n-1个数再向下调整成大根堆,再将顶端的数与n-1位置的数交换,如此反复,便能得到有序数组。
注意:升序用大根堆,降序用小根堆
2.4.2特性总结
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.4.3代码实现
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//右孩子存在并且右孩子大于左孩子
if (child + 1 < n && a[child + 1] >a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}//数组直接建堆
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
尾声:本节我们介绍了插入排序,希尔排序,选择排序,堆排序四种常见的排序,在下一篇文章中,我将介绍剩下的常见的排序方法。有什么问题欢迎大家在评论区留言讨论~
点赞+收藏+关注,是博主不断更新,为大家带来优质好文的动力!