✨✨[数据结构]——最经典的七大排序(超详细近两万字教程,你值得拥有)✨✨
Posted 是小明同学啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了✨✨[数据结构]——最经典的七大排序(超详细近两万字教程,你值得拥有)✨✨相关的知识,希望对你有一定的参考价值。
常见的排序算法:
插入排序:直接插入排序,希尔排序
选择排序:直接选择排序,堆排序
交换排序:冒泡排序,快速排序
归并排序:归并排序
一,插入排序
定义:插入排序的算法思想非常简单,就是每次将每次待排序序列中的 一个记录,按照其关键字值的大小插入
已经排好的记录序列中的适当位置上。关键就在于如何确定待插入的位置。 此文章只介绍其中的两种排序方式(直接插入排序,希尔排序).
1,直接插入排序
(1)基本思想
首先存在一组待排序的记录,首先将这一组中的第一个记录构成有序子序列,而剩下的记录序列构成无序子序列,然后每次将无序序列中的第一个记录,按照其关键字值的大小插入前面已经排好序的有序序列中,并使其仍然保持有序。(这种排序是通过顺序查找来确定待插入的位置的)
(2)主要步骤
a,首先将待排序的记录存放在数组a[0…n]中。
b,创建变量end,用来表示前面有序序列中的记录个数,将a[end+1] 暂存在临时变量tmp中(这就是无序序列中的第一个记录,准备插入前面的有序序列)。
c,将tmp与a[end] 进行比较,如果tmp的值小于a[end],那么就将a[end]后移一个位置,然后end自减,让tmp继续与a[end]进行比较,直到tmp的值大于a[end]。
d,将tmp的插入第a[end+1]的位置。
e,令end=0,1,2,3,…,n-1,重复步骤b,c,d。
(3)代码实现
//假设要求是升序排序
//基本思想:[0,end],是有序的,end+1位置的值插进去,使得[0,end+1]也有序。
void InsertSort(int *a,int n)
{
for (int i = 0; i < n - 1; i++)//如果i能等于n-1的话,将i赋给end,end+1就会直接越界了。(所以最大就是n-2)
{
int end = i;
int tmp = a[end + 1];//先将a[end+1]的数值保存起来,防止a[end + 1]被覆盖。
while (end >= 0)//这个循环只负责将指定的end+1的值插进数组,想排序整个数组,还需要外面的for来遍历end。
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
//有两种break的情况。
//1,这个要插入的数tmp太小了,是数组中最小的,end一直自减,直到end<0,然后直接从while循环中跳了出来。这时候 end指向-1,然后将tmp直接赋到a[end+1]上。
//2,如果tmp比a[end]大了,那么直接break,然后进行赋值。
}
}
a[end + 1] = tmp;//break之后,为两种情况进行赋值。
}
}
int main()
{
int a[] = {3,5,2,7,8,6,1,9,4,0};
int n = sizeof(a) / sizeof(a[0]);
InsertSort(a, n);
for (int i = 0; i < n; i++)//最后可以测试一下,最终输出的结果是0,1,2,3,4,5,6,7,8,9
{
printf("%d ",a[i]);
}
}
(4)性能分析
a,空间复杂度:排序过程中仅仅用了一个辅助单元a[0],因此空间复杂度为O(1)。
b,时间复杂度:看最坏的情况,在逆序的时候,每一趟排序都要将待插入的记录插入到记录表的最前面的位置。总的比较次数是一个等差数列,1+2+3+…+n-2,总共n(n-1)/2,总的移动次数是大约也是n(n-1)/2,所以时间复杂度就是O(n^2)。
c,算法稳定性:直接插入排序是一种稳定的排序算法。
d, 优点:如果原来数组很接近有序,那么时间复杂度就非常接近O(n)。
2,希尔排序
(1) 基本思想
希尔排序也是一种插入排序,它是直接插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。大体分为:先进行预排列,让数组接近有序 然后再直接插入排序。
(2) 主要步骤
举个例子 :(使之变成升序)
9,8,7,6,5,4,3,2,1,0
a,先分组,创建变量gap,然后间隔为3的为一组,可以设gap为3。
9,6,3,0一组,8,5,2一组,7,4,1一组。
b,分完组之后,将每一组都使用直接插入排序按顺序排好。最后就是:0,2,1,3,5,4,6,8,7,9。
c,以上为gap=3的时候进行的分组排序,可以逐次让gap减小,使得序列更加接近有序,最终令gap=1再进行一次排序,这个时候一定变成有序了,因为当gap=1的时候就相当于是直接插入排序了。
d,图片大概实现:
(3) 代码实现
void ShellSort(int* a,int n)//这个循环也就是翻版的直接插入排序,唯一的不同就是将1换成了gap。
{
int gap = n;
while (gap > 1)
{
gap = gap / 2;
for (int i = 0; i < n - gap; i++)//把间隔为gap的多组数据同时排。
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)//这个while完成的任务仅仅是end+gap这一个的插入,还没有将这它所在的同为gap间隔的其他数据进行排序。
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end = end - gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
void ArraySrot(int* a,int n)//负责输出数组遍历结果
{
for (int i = 1; i < n; i++)
{
printf("%d ",a[i]);
}
}
int main()
{
int a[] = { 9,8,7,6,5,4,3,2,1,0};
int n = sizeof(a) / sizeof(a[0]);
ShellSort(a,n);
ArraySrot(a,n);
return 0;
}
(4) 性能分析
a,空间复杂度:希尔排序中用到了直接插入排序,而直接插入排序 的空间复杂度为O(1),所以,希尔排序的空间复杂度时O(1).
b,时间复杂度:希尔排序的时间效率分析很困难,因为关键字的比较次数与记录的移动次数依赖于增量序列的选取。目前还没有一种选取最好的增量序列的方法。经过大量研究,选取一些增量序列可以使得其时间复杂度达到O(n^7/6次方),这就很接近O(n*logn)。
c,算法稳定性:不稳定,由于子序列的元素之间跨度较大,所以移动时就会引起跳跃性的移动。一般来说,如果排列过程中的移动是跳跃性的移动,则这种排列就是不稳定的排序。
二,选择排序
1,直接选择排序
(1)基本思想
每一次从待排序的数据元素中选出最小(最大)的一个元素,放在序列的起始位置,直到全部的待排序元素排完。
(2)主要步骤
这个排序过程并不复杂,可直接通过代码来理解。
(3)代码实现
因为此排序的效率较低,所以可以在原基本思想的基础上做一些优化:一次性选两个最值,分别放到序列的开始以及结尾位置。
//这个写个优化版本的,一次选两个数,一个最大的,一个最小的。这样效率可以快一倍
void SelectSort(int*a,int n)
{
int begin = 0, end = n - 1;//创建两个整型变量,一个指向数组的起点,一个指向数组的终点
while (begin < end)
{
int mini = begin, maxi = begin;//刚开始最小值的下标和最大值的下标都指向begin处的值
for (int i = begin; i <= end; ++i)//从起点开始向重点开始遍历,进行完一遍这个for循环可以选出数组中的一个最大值和一个最小值的下标
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);//最小的值换到最左边
if (begin == maxi)
//如果不加这个判断条件就会出现bug,因为原先的经过for循环已经确定了最大(maxi)和最小值(mini)的下标,但如果begin(下标为0)和maxi(下标也是0)重合了即最大的数恰好是数组中第一个数a[0],经过Swap,begin和mini的会进行数值交换,a[0]保存的变成了最小值,但是maxi标记的位置还是a[0]的位置,所以经过下面的Swap(& a[end], &a[maxi])交换的话会把最小值换到a[end]处
{
maxi = mini;//更改一下maxi的值(maxi是最大值的下标),使他重新指向最大值
}
Swap(& a[end], &a[maxi]);//最大的值换到最右边
begin++;
end--;
//经过依次这样的循环,整个数组待排序的数就变成了n-2个
}
}
void Swap(int* child, int* parent)//交换数值函数
{
int tmpt = *child;
*child = *parent;
*parent = tmpt;
}
(4)性能分析
a,空间复杂度:O(1),直接选择排序过程中,交换时要使用一个辅助单元。
b,时间复杂度:O(n^2),外循环控制循环的趟数,共需执行n-1次,内循环是控制每趟排序与关键字值比较的次数,需要执行n-2i(还剩下的次数),总的比较次数就是1/4 n*(n-1),时间复杂度就是O(n^2)。
c,算法稳定性:不稳定,虽然可以人为地控制相同的数字谁在前谁在后,但是在换位置的时候可能会影响其他的相同数字的相对位置。
2,堆排序
(1)基本思想&主要步骤
堆排序是一种选择排序,借助堆来实现这种排序。堆:逻辑结构是一棵完全二叉树,实际结构是一个数组。
(2)大堆和小堆
大堆要求:树中所有的父亲都是大于等于孩子的。
小堆要求:树中所有的父亲都是小于等于孩子的。
问题:为什么要用它来实现排序呢?
答:大堆可以保证堆顶的元素是最大的。小堆可以保证堆顶的元素是最小的。根据这个可以进行排序。如果有个数组想要排序,那么就先把它建成个堆(大堆或小堆)。逻辑上是:完全二叉树。
(3)父子结点
父子结点序号可以通过关于数组中的下标的公式来计算。
leftchild (左孩子)= parent*2+1
rightchild (右孩子)= parent*2+2
parent = (child-1)/2//无论是左孩子还是右孩子,他们的父亲结点都满足这个公式。
(3)主要步骤
首先将一个无序序列构造成一个初始堆(大堆或者小堆)(假设是大堆),再将堆顶最大值结点与最后一个结点(下标n-1)进行交换,交换后再调整构造成第2个堆,接着,再将堆顶大值结点与最后第2个结点(下标n-2)交换,交换后再调整构造成第3个堆,如此反复,直到整个无序子表只有一个元素为止,堆排序完成。
(4)向下调整法
在将无序序列(前提,其左右子树都必须是小堆。)构造成初始堆的时候需要用到这个方法(就是将左右子树都是小堆的无序序列调整成小堆,说白了使用这个方法导致的变化就是将根这一个数融入整个小堆。)
具体操作:按小堆为例子,选出左右孩子中小的那个,跟父亲比较,从根结点开始,如果比父亲小就交换。交换完之后,父亲作为小的二叉树的父亲继续向下比较交换。交换的只是数值。一直到叶子结点就终止。然后就变成了小堆。(如果左右子树有空的,空也算是小堆)
void Swap(int* child, int* parent)//交换
{
int tmpt = *child;
*child = *parent;
*parent = tmpt;
}
void AdjustDown(int* a,int n ,int root)//向下调整算法(这里默认是小堆)
{
int parent = root;
int child = parent * 2 + 1;//默认child是左孩子
while (child<n)//调到叶子就中止
{
//1,选出左右孩子中小的那一个。
if (a[child + 1] < a[child]&& child+1<n )//这里的child+1可能会越界,所以再加一个条件。
{
child++;
}
if (a[child] < a[parent])
{
Swapt(&a[child], &a[parent]);//就直接交换,交换的是双方的数值。
parent = child;//这个交换的是在二叉树中的位置。
child = parent * 2 + 1;
}
else
{
break;
}
}
}
以上的操作时建立在左右子树都是小堆的前提下。但是如果上面操作的无序序列的左右子树并不是小堆的话,就不能直接按以上的操作来了,就需要先将这个无序序列调整成左右子树都是小堆的形式。那么这个调整肯定是从底向上调整的。从最后一个非叶子结点(最后一个叶子结点的下标是n-1,找到它的父亲,然后从它的父亲开始就是非叶子结点)从这里开始向下调整,然后一直向上蔓延,一直到头结点为止。
(5)特别注意
排升序要建大堆。不能建小堆。
因为:排升序肯定是将最小的一个一个的拿出来。如果是小堆,它最小的数值在堆顶,将它拿出来之后,如果还想再拿次小值,但是剩下的树结构都已经乱掉了,需要重新从头建堆,而建堆的时间复杂度是O(n),那么这样堆排序就没有优势了。
重新建堆选数的时间复杂度是O(n),要再选n-1个数,也就是要进行重新建堆选数的次数是n-1次,那么总的堆排序的算法就是O(n方)。
将0这个最小值拿出来之后,就不再管0了,然后对剩余的树结构再重新建堆。这样就没有优势了。
你这样每次都是O(n),还不如直接遍历呢,遍历的时间复杂度也是O(n),遍历一遍选出最小的数,拿出来,然后再遍历选。。。,何必还要有堆排序这种算法呢?
正确的做法:建大堆,然后将最大的数与最后的数进行交换。
(6)代码实现
void Swap(int* child, int* parent)//交换数值
{
int tmpt = *child;
*child = *parent;
*parent = tmpt;
}
void AdjustDown(int* a,int n ,int root)//向下调整算法(这里默认是小堆)
{
int parent = root;
int child = parent * 2 + 1;//默认child是左孩子
while (child<n)//调到叶子就中止
{
//1,选出左右孩子中小的那一个。
if (a[child + 1] > a[child]&& child+1<n )//这里的child+1可能会越界,所以再加一个条件。
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);//就直接交换,交换的是双方的数值。
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);//这个时候已经算是将最大的那个数排除出去了,还剩n-1个,然后再进行向下调整构建新树。
end--;//每次都会排除出去最大的一个。
}
}
int main()
{
int a[] = {3,5,2,7,8,6,1,9,4,0};
int n = sizeof(a) / sizeof(a[0]);
HeapSort(a,n);
return 0;
}
(7)性能分析
a,空间复杂度:O(1),堆排序需要一个记录的辅助存储空间用于结点之间的交换。
b,时间复杂度:O(n*logn),计算公式就是:向下调整算法的时间复杂度 * 向下调整的次数。
建堆的时间复杂度是多少呢?
最终的复杂度不是所谓的O(n*logn)。而是O(n)
取最坏的情况:是一个满二叉树。
公式:每一层结点的个数*结点最多调整的次数的和。
这样的一个二叉树,设高度为h。
每一层的结点个数(从第一层开始):2h-4, 2h-3,2h-2,2h-1。
最多调整的次数(从第一层开始):h-1,h-2,h-3,h-4。
然后将他们对应相乘再相加出来就是时间复杂度。
整理一下:
进行错位相减法。算出来之后就是n-logn,也就是O(n).
向下调整最多调整多少次呢? 树的高度次。高度次就是logn次。我有n个数需要选择进行向下调整。
所以时间复杂度就是n*logn。
所以,最后的时间复杂度就是O(n*logn)。
c,算法稳定性:不稳定,在建好堆以后数会进行交换,会影响相对位置。
三,交换排序
1,冒泡排序
(1)基本思想
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点就是将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
(2)主要步骤
结合代码
(3)代码实现
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)//将最大的数,次大的数一个一个的放在最后面。
{
for (int j = 1; j < n-i; j++)//这个循环只是将一个最大的数放到了最后面。
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
}
}
}
}
void Swap(int* child, int* parent)
{
int tmpt = *child;
*child = *parent;
*parent = tmpt;
}
可以进行优化:这样保证如果接近有序的时候可以更快的结束循环。
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)//将最大的数,次大的数一个一个的放在最后面。
{
int exchange = 0;
for (int j = 1; j < n-i; j++)//这个循环只是将一个最大的数放到了最后面。
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
void Swap(int* child, int* parent)
{
int tmpt = *child;
*child = *parent;
*parent = tmpt;
}
(4)冒泡和直接插入相比较
直接插入更好。
如果在有序的情况下,都是O(n)
但是在接近有序的情况下,
1,2,3,5,4,6的情况下。
冒泡排序:(n-1)+(n-2)
直接插入排序:(n-1)+1
所以说直接插入排序对局部有序适应性更强。
所以直接插入排序在O(n^2)中算是比较牛的排序。
一般来说,如果比较随机的值,冒泡甚至都没有直接选择排序好。但是如果是比较有序的情况就比较好了。
(5)性能分析
a,空间复杂度:O(1),冒泡排序算法中有交换操作,需要用到一个辅助记录。