排序算法
算法分类
十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
![](https://img-blog.csdnimg.cn/f4f729769582466e8581bbb9797fd1d1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5b6q5qKm,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)
时间复杂度和空间复杂度
算法分析是指算法在正确的情况下,对其优劣的分析。一个好的算法通常是指:
- 算法对应的程序所耗时间少–程序效率相对较高
- 算法对应的程序所耗存储空间少—占用计算机空间少
- 算法结构性好、易读、易移植和调试
数据结构与算法的本质任务,是提高程序的时间空间效率,简单讲就是让程序的执行速度越快越好,所需内存空间越少越好。虽然在很多情况下,程序的时空特性是相互制约的,就像鱼和熊掌不可兼得,但我们可以根据程序实际解决问题的侧重点,去平衡时间和空间的对性能的消耗。
时间复杂度
时间复杂度并不考察一段代码运行所需要的绝对时间,因为不同的计算机的硬件参数不同,考察绝对时间没有意义。时间复杂度一般指的是代码的语句执行总次数,称为语句频度和问题规模的关系函数。
void counting(int n)
{
for(int i=0; i<n; i++)
{
printf("本行语句将会出现n次\n");
for(int j=0; j<n; j++)
{
printf("本行语句将会出现n*n次\n");
}
}
}
在上述代码中,程序执行的语句频度理论是:T(n)=n^2+n
但一般情况下,我们只关心多项式的最高次幂,于是上述代码的时间复杂度我们表示为:
T(n)=O(n^2)
推导大O阶:
1.用常数1取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。
得到的结果就是大O阶。
![](https://img-blog.csdnimg.cn/ba92ae08bd0546a3ad1b7f1705925ae7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5b6q5qKm,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)
空间复杂度
空间复杂度的概念更简单一点,就是一段程序运行时所需的内存字节量。
排序的稳定性
由于待排序 的记录序列中可能存在两个或两个以上的关键字相等的记录,那么排序结果就可能会存在不唯一的情况,因此出现了稳定与不稳定排序。
假设存在k[i]为a1,k[j]为a2,且a1 = a2; 如果在排序前a1在a2前面,排序后,a1任然总是在a2前面,则称该排序的稳定的;否则,若a1可能在a2后面,该排序是不稳定的。
内排序与外排序
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为: 内排序和外排序。
- 内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。
- 外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
算法复杂度
![](https://img-blog.csdnimg.cn/890a8f8b861b4ea0b9a14e53f7f693ad.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5b6q5qKm,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)
冒泡排序
冒泡排序(Bubble Sort)一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
![](https://img-blog.csdnimg.cn/d24520c0139945a2b18e768d1afafb5b.gif#pic_center)
//最原始的排序算法
void sort_bubble(int* ch,int count)//ch为数组名,count为数组的元素个数
{
int i,j;
int temp = 0;
for(i = 0;i < count-1;i++)
{
for(j = 0;j < count-1;j++)
{
if(ch[j] > ch[j+1])
{
temp = ch[j];
ch[j] = ch[j+1];
ch[j+1] = temp;
}
}
}
}
时间复杂度为O(n^2)
但该算法存在很多重复、不必要的步骤:
//优化后的排序算法
void sort_bubble(int* ch,int count)
{
int i,j;
int temp = 0;
int flat = 0;
for(i = 0;i < count-1;i++)
{
flat = 0;
for(j = 0;j < count-i-1;j++)//将已排好序的成员移除出后面的遍历范围
{
if(ch[j] > ch[j+1])
{
flat = 1;
temp = ch[j];
ch[j] = ch[j+1];
ch[j+1] = temp;
}
}
if(flat == 0)//如果该次循环没有进行交换,证明已经拍好序,退出函数
break;
}
}
时间复杂度为O(n^2)
最佳情况下冒泡排序只需一次遍历就能确定数组已经排好序,不需要进行下一次遍历,所以最佳情况下,时间复杂度为O(n) 。
最坏情况下冒泡排序需要n-1次遍历,第一次遍历需要比较n-1次,第二次遍历需要n-2次,…,最后一次需要比较1次,最差情况下时间复杂度为O(n2)。
插入排序
直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
![](https://img-blog.csdnimg.cn/00a6d9faf3514232b78f42ee9cda2bf7.gif#pic_center)
void sort_insert(int *ch,int count)
{
int i,j;
int temp = 0;
//第0号成员对应的序列就只有一个成员,是有序的,所以直接从第1号成员开始
for(i = 1;i < count;i++)
{
temp = ch[i];
j = i;
while(j != 0 && temp < ch[j-1])
{
ch[j] = ch[j-1];
j--;
}
ch[j] = temp;
}
}
时间复杂度O(n^2)
直接插入排序法比冒泡和简单选择排序的性能要好一些。
最好情况下,当待排序序列中记录已经有序时,则需要n-1次比较,不需要移动,时间复杂度为O(n) 。最差情况下,当待排序序列中所有记录正好逆序时,则比较次数和移动次数都达到最大值,时间复杂度为O(n2) 。平均情况下,时间复杂度为O(n^2)。
选择排序
简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之。
![](https://img-blog.csdnimg.cn/622ba9b352cb41059687ab7c13b58dac.gif#pic_center)
void sort_select(int *ch,int count)
{
int i,j;
int min = 0;
int temp = 0;
for(i = 0;i < count;i++)
{
min = i;
for(j = i+1;j < count;j++)
{
if(ch[j] < ch[min])
{
min = j;
}
}
if(min != i)
{
temp = ch[i];
ch[i] = ch[min];
ch[min] = temp;
}
}
}
时间复杂度为O(n^2)
尽管与冒泡排序同为O(n 2 ),但简单选择排序的性能上还是略优于冒泡排序。
简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为 O(n2) ,进行移动操作的时间复杂度为O(n) 。总的时间复杂度为**O(n^2) **。
最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。
简单选择排序是不稳定排序。
希尔排序
希尔排序是把数组按下标的一定增量进行分组,对每个组都使用直接插入排序算法排序;然后减少增量重新分组,继续对每个组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的成员越来越多,当增量减至1时,整个数组恰被分成一组,算法便终止。
一般选择初始增量为length/2,每次循环都除以2,知道增量为1时,将整个数组作为一个组,再进行直接插入排序。虽然这不是最优的增量序列,但这是最容易实现的;如果想要实现最优的希尔排序,需要根据每次循环选择合适的增量,这时的增量序列是没有规则的,实现困难。
希尔排序
-
插入排序:
- 逐步的局部有序到全局有序
-
希尔排序:
- 从整体宏观上有序逐步细节到局部的有序
希尔排序是一种改进版的插入排序,普通的插入排序算法中,是从第2个节点开始,依次插入到有序序列中,这种做法虽然“一次成形”,但研究发现时间效率上这么做并不划算,更“划算”的做法是这样的:
不严格一个个插入使之有序,而是拉开插入节点的距离,让它们逐步有序,比如如下图所示,有无无序列:
84、83、88、87、61、50、70、60、80、99
1、
第一遍,先取间隔为(Δ=5)(间隔一般选择len/2),即依次对以下5组数据进行排序:
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
注意,当对84和50进行排序时,其他的元素就像不存在一样。因此,经过上述间隔为5的一遍排序后,数据如下:
50、83、88、87、61、84、70、60、80、99
50、70、88、87、61、84、83、60、80、99
50、70、60、87、61、84、83、88、80、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
最终的结果(50、70、60、80、61、84、83、88、87、99)是经过这一遍间隔Δ=5的情况下达成的
2、接下去缩小间隔重复如上过程。例如让间距Δ=3(或者2):
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
将上述粗体的每一组数据进行排序,得到:
50、70、60、80、61、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
最终的结果(50、61、60、80、70、84、83、88、87、99)更加接近完全有序的序列。
接下去继续不断减小间隔,最终令Δ=1,确保每一个元素都在恰当的位置。
3、
50、60、61、70、80、83、84、87、88、99
![](https://img-blog.csdnimg.cn/bbf5d2c89eb543fc89b1b4f857f8861b.gif#pic_center)
//实现方法1
//直接插入算法
//该算法实现与上面介绍插入算法时的实现原理一样,只是变换了一下表达方式(也可以使用上面的插入排序的写法,将增量由1变为inc就好)----先看下面的希尔排序再来看这个比较容易理解
void sort_insert(int ch[],int count,int inc)//count为数组元素个数,inc为增量
{
int i,j;
int temp = 0;
for(i = inc;i < count*inc;i += inc)//这里的count*inc是因为count值是已经被除以inc了
{
temp = ch[i];
for(j = i-inc;j >= 0;j -= inc)
{
if(ch[j] > temp)
ch[j+inc] = ch[j];
else
break;
}
ch[j+inc] = temp;
}
}
//希尔排序
void sort_shell(int ch[],int count)//count如果为奇数,那么多出来的那个数也会在增量为1时加入排序中
{
int len = count/2;
int i;
//通过特定增量对数组进行分组,当增量值len为1时,将整个数组作为一个组进行直接插入排序
while(len != 0)
{
for(i = 0;i < len;i++)//有len个组,分别对这些组进行插入排序
{
sort_insert(ch,count/len,len);//将每次循环的组都分别进行直接插入排序
}
len /= 2;//通过特定的规律减少增量
}
}
//实现方法2---将两个函数合并起来
void sort_shell(int ch[],int count)
{
int i,j;
int temp = 0;
int len = count/2;
while(len != 0)
{
for(i = len;i < count;i++)//通过增量,一次性将所有组进行直接插入排序---先将第1组进行插入排序,然后第2组...第len组
{
temp = ch[i];
for(j = i - len;j >= 0;j -= len)
{
if(ch[j] > temp)
ch[j+len] = ch[j];
else
break;
}
ch[j+len] = temp;//记得加上len
}
len /= 2;
}
}
希尔排序又称“缩小增量排序”,它是基于直接插入排序的以下两点性质而提出的一种改进:(1) 直接插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。(2) 直接插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
快速排序
快速排序(Quick Sort)的基本思想是:通过一趟排序将数组分割成独立的两个子数组,其中一个数组的成员均比另一个数组的成员小,则可分别对这两部分子数组继续进行排序,以达到整个序列有序的目的。
基本思路
快排是一种递归思想的排序算法,先比较其他的排序算法,它需要更多内存空间,但快排的语句频度是最低的,理论上时间效率是最高的。
快速排序的基本思路是:在待排序序列中随便选取一个数据,作为所谓“支点”,然后所有其他的数据与之比较,以从小到大排序为例,那么比支点小的统统放在其左边,比支点大的统统放在其右边,全部比完之后,支点将位与两个序列的中间,这叫做一次划分(partition)。
一次划分之后,序列内部也许是无序的,但是序列与支点三者之间,形成了一种基本的有序状态,接下去使用相同的思路,递归地对左右两边的子序列进行排序,直到子序列的长度小于等于1为止。
比如:
int temp = data[start]; //temp = data[0] = 25
1、
25 1 20 15 13 2 7 35 48
–|----------------------------|
head= start -----------tail=end
先从尾元素对比 data[tail]<= temp 满足这个条件则把data[tail] 放在data[head],同时head+1; 不满足这个条件tail-1
2、
25 1 20 15 13 2 7 35 48
-| ---------------------- |
head= start--------tail
3、
25 1 20 15 13 2 7 35 48
|---------------------|
head= start----- tail
满足上述规则的条件
data[head] = data[tail];//data[0] = 7
4、
7 1 20 15 13 2 7 35 48
| ------------------ |
head------------ tail
只要满足一次上述条件 head+1 tail没变 但是data[tail]和data[head-1]重复
所以要转换顺序
5、
7 1 20 15 13 2 7 35 48
—| --------------- |
head------------tail
往右归类从当前head下标开始 不断对比temp
从当前head下标对应数据开始,不断和temp支点数比对,只要满足data[head] >temp; data[tail] = data[head],head不变 tail-1
不满足则head+1
6、temp = 25
7 1 20 15 13 2 7 35 48
------|--------------|
----head --------tail
7、8、9、
7 1 20 15 13 2 7 35 48
------------------| -|
-------------head tail
10、当head = tail时,data[head] = temp;
7 1 20 15 13 2 25 35 48
那么,第一轮结束后,整数数组就以25为支点,它左边的值都比25小,右边的值都比25大;然后再对支点左边的序列进行同样的操作,右边的序列也进行同样的操作。
11、
7 1 20 15 13 2 25
35 48
2 1 20 15 13 2 25
35
48
2 1 20 15 13 20 25
35
48
2 1 7
15 13 20 25
35
48
第2轮结束后,25右边的序列已经允许,对7的左边和7与25之间的序列进行同样的操作。
12、temp 1 = 2;temp2 = 15;
2 1 7
15 13 20 25
35
48
1 1 7
15 13 20 25
35
48
1 2
7
13 13 20 25
35
48
1 2
7 13 15
20 25
35
48
第3轮结束后,整个序列全部变为有序序列。
![](https://img-blog.csdnimg.cn/fe18b368554d41d1a1a06512e6d50d9b.gif#pic_center)
void sort_quick(int data[], int start, int end)
{
if (start >= end) //回归条件
return;
int head = start;
int tail = end;
int temp = data[start];
while (head != tail)
{
while (head != tail)
{
if (data[tail] >= temp) //添加=,可以保证算法的稳定性
tail--;//向左移一位
else//右边的数小于左边
{
data[head] = data[tail];
head++;
break;
}
}
//位置变换
while (head != tail)
{
if (data[head] <= temp)
head++;
else
{
data[tail] = data[head];
tail--;
break;
}
}
}
data[head] = temp;//temp记录着最开始的支点的值
//这里的参数不用担心越界问题,因为当head大于等于end是递归的退出条件
sort_quick(data, start, head - 1);//对支点左边的值进行同样的操作
sort_quick(data, head + 1, end);//对支点右边的值进行同样的操作
}
归并排序
归并排序是分治法的一个典型应用,它的主要思想是:将待排序序列分为两部分,对每部分递归地应用归并排序,在两部分都排好序后进行合并。
![](https://img-blog.csdnimg.cn/87c87dbc57674d7e9c2b77d7e0648258.gif#pic_center)
//归并排序算法
//将数组不断切割,直到数组只有一个成员,再不断地将两个数组进行有序地合并
int Split_Sort(int data[],int count)
{
if(count > 1)
{
int count1 = count/2;
int *data1 = (int *)malloc(count1*sizeof(int));
bzero(data1,sizeof(data1));
memcpy(data1,data,count1*sizeof(int));
Split_Sort(data1,count1);//递归调用
int count2 = count - count1;
int *data2 = (int *)malloc(count2*sizeof(int));
bzero(data2,sizeof(data2));
memcpy(data2,data+count1,count2*sizeof(int));
Split_Sort(data2,count2);//递归调用
Merge_Sort(data,data1,data2,count1,count2);
free(data1);
free(data2);
}
return 0;
}
//进来合并时,两个子数组已经是有序的了,将两个有序的数组合并为一个有序的数组
int Merge_Sort(int data[],int data1[],int data2[],int count1,int count2)
{
bzero(data,(count1+count2)*sizeof(int));
int n = 0;
int n1 = 0;
int n2 = 0;
while(n1 < count1 && n2 < count2)
{
if(data1[n1] > data2[n2])
{
data[n++] = data2[n2++];
}
else
{
data[n++] = data1[n1++];
}
}
while(n1 < count1)
{
data[n++] = data1[n1++];
}
while(n2 < count2)
{
data[n++] = data2[n2++];
}
return 0;
}
归并排序的时间复杂度为O(nlogn),它是一种稳定的排序。
参考: