目录
扫描二维码关注公众号,回复:
14288686 查看本文章

1、排序的概念及其运用:
1.1、排序的概念:
排序
:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,
递增或递减
的排列起来的操作、
稳定性
:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,
若经过排序,这些记录的相对次序保持不变
,即在原序列中,r[ i ] = r[ j ]
,
且
r[ i ]
在
r[ j ]
之前,而在排序后的序列中,
r[ i ]
仍在
r[ j ]
之前,则称
这种排序算法是稳定的,否则称为不稳定的、
稳定性指的
不是性能波动
,
只要
一个算法的某一种写法
确保
能够做到稳定,
就称该算法是稳定的
,只要
一个算法的任何一种写法均
不能确保
做到稳定,就
称该算法是不稳定的、
稳定性的意义
:在
根据多种属性进行排序时会有巨大的意义
,比如我们先对学生按照学号进行了排序,再对学生进行了按照成绩进行排序,此时学号和成绩成为了
两种决定因素,如果我们当按照成绩进行排序时,所使用的算法是不具有稳定性的,那么在对成绩排序后,之前根据学号进行的排序就没有意义
了,此时就会出现
相同成绩,但是学号靠后的在前面,反之,如果我们选择的排序具有稳定性,那么成绩相同,学号靠前的应该在前面、
内部排序
:数据元素全部放在
内存
中的排序、
外部排序
:数据元素
太多
不能同时放在内存中
,根据排序过程的要求不能在内外存之间移动数据的排序,即
数据放在磁盘上、
区分内外排序:
1.2、常见的排序算法:
不存在简单排序这个概念、
2、常见排序算法的实现:
2.1、插入排序:
2.1.1、基本思想:
直接插入排序
是一种简单的插入排序法,其
基本思想
是:
把
待排序的记录按其关键码值的大小
逐个插入到一个
已经排好序的有序序列中
,直到所有的记录插入完为
止,得到一个
新的
有序序列、
实际中我们玩扑克牌时,就用了
插入排序
的思想:
2.1.2、直接插入排序:
当插入下标为
i(i>=1)
的
元素时,前面的
array[0],array[1],…,array[i-1]已经排好序
,此时用
array[ i ]
的排序码与array[i-1],array[i-2],…的排序码顺序进行比
较,找到插入位置即将array[ i ]插入,
原来位置上的元素顺序
后移、
假设已经知道
有序序列一为:2 4 5 10
,现在想要把7插入序列中,
使新的序列二仍然保持有序
,则依次
从后往前
去比较,假设为
升序排列
,若插入的数据
比原数组中最后一个元素
大或者等于
,则直接插入到原数组中最后一个元素的后面即可,若插入的数据比原数组中最后一个元素要
小
的话,比如插入数据
7,此时7比10小,则需要把10往后移动一位,然后再去跟5进行比较,7大于5,则直接插到5的后面即可,此时不存在数据覆盖的问题,即7不会把10给覆
盖掉,因为,10已经往后移动走了,若直接给定一个
随机数组
,让使用
插入排序的方法进行
升序排列
,则直接把数组中
第一个数据
看做一个
有序序列三
即
可,然后再把数组中第二个数据插入到该有序序列三中从而构成一个新的有序序列四,然后再把数组中第三个数据插入到该新的有序序列四中,再构成一
个新的有序序列五,重复上述操作,直到把数组中第N个数据插入到由数组中前N-1个数据构成的新的有序序列六中,从而得到最终的一个有序序列七,
将其打印出来就完成了升序操作,至于降序操作,只需要稍作改变即可、
若直接让数组按照
升序的方式以直接插入排序的方法进行排序时
,若把数组中的某一个数据插入到以该数据之前的所有数据构成的一个有序序列八中的
话,若该数据比该序列八中的最后一个数据小的话,不可以直接挪动序列八中的最后一个数据,否则会覆盖元素,所以此时要把该数据先记录下来,再挪
动有序序列八中的最后一个数据,再拿该数据去和有序序列八中倒数第二个数据进行比较,若
大于等于
倒数第二个数据,则直接插入到倒数第二个数据后
面即可,若小于的话,还需往后挪动有序序列八中倒数第二个数据,直到找到要插入的数据的应该在的位置为止、
直接插入排序的特性总结:
1、
元素集合越接近
顺序
,则
部分时间复杂度就越好
,直接插入排序算法的
时间效率越高、
2、
时间复杂度:
O(N^2)
、
3、
空间复杂度:
O(1)
,它是一种
稳定的排序算法、
4、
稳定性:
稳定、
以下述写法为例
,
直接插入排序的当前写法
能够确保做到稳定
,则直接插入排序算法是稳定的、
//插入排序、
//基本思想:有一个有序区间,插入一个数据后,仍保持有序、
//按照自己的方法直接计算时间复杂度则为:O(N^2),外层循环固定次数为:N-1,内存while循环次数不固定,则需要列出来具体的执行次数,而总的执行次数为:T(N)=(1+2+3+....+(N-1))、
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(1+2+3+....+(N-1)),部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数为:T(N)=N-1个1相加,等于N-1,则部分时间复杂度就是:O(N)、
//时间复杂度看最坏,则还是:O(N^2)、
//若不是最好的情况,即不是顺序的话,但是非常接近于顺序时,则总的执行次数就比最好情况下的执行次数N-1多一部分常数项次,则部分时间复杂度还是:O(N)、
//对于插入排序而言,顺序或者接近于顺序,是比较合适使用的,虽然时间复杂度是:O(N^2),但是若在该种情况下时,其部分时间复杂度是O(N),则很快就可以完成排序,时间效率较高、
//针对于插入排序进行优化,优化后叫做希尔排序,即希尔排序是插入排序的优化、
void InsertSort(int* a, int n)
{
assert(a);
for (int i = 0; i <= n - 2; i++)
{
//写排序时,先把单趟排序写出来,假设要把下标为end+1的元素插入到下标为[0,end]所构成的一个 有序 序列1中,使插入后构成的新的序列2仍是有序序列、
int end=i;
int tmp = a[end + 1];
while (end >= 0)
{
//降序、
//if (tmp > a[end])
//升序、
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
//若在while外部后面加上一条语句后,若是通过break出去的话,就会重复操作,所以可以直接把else里面的这条语句删除即可、
//删除-> a[end + 1] = tmp;
break;
}
}
//若要插入到有序序列1中的数据比该序列1中的所有元素都小的话,此时if中的end--后,则end就等于-1,然后再一次循环就进不去while循环内,则有序序列2
//中第一个位置上是空的,并没有把tmp放在该位置上,所以在while循环外部手动再把tmp放在有序序列2中的第一个位置上、
a[end + 1] = tmp;
}
}
//若要对比插入排序和冒泡排序的话,他们的时间复杂度都是:O(N^2),因为时间复杂度看的是最坏的情况、
//虽然时间复杂度是一样的,但是插入排序还是要比冒泡排序要好,两者在最坏的情况下执行的次数是相等的,但是在最好的情况下,两者的执行次数差的很多,即最好情况下的部分时间复杂度是不一样的,此时插入排序的时间效率要好于冒泡排序,即所用时间较少、
2.1.3、希尔排序( 缩小增量排序 ):
在直接插入排序之前若是能先将待排序列进行预排序,使待排序列成为顺序或者接近于顺序,然后再对该序列进行一次直接插入排序,因为此时直接插入排序的时
间复杂度为O(N),那么只要控制预排序阶段的时间复杂度不超过O(N^2),那么整体的时间复杂度就比直接插入排序的时间复杂度低、
希尔排序法又称缩小增量法,希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap个组,同一组内数据间的间隔为gap,并对每一组
内的记录进行插入排序,然后,取,重复上述分组和排序的工作,当gap到达
=1时,所有记录在同一组内排好序、
为什么要让gap由大到小呢?
gap越大,数据挪动得越快;gap越小,数据挪动得越慢 ,前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数、

希尔排序的特性总结:
1、
希尔排序是对
直接插入排序的优化、
2、
当
gap > 1
时都是
预排序
,目的是
让数组更接近于有序
,
当 gap == 1前面,数组已经接近有序的了
,
再进行gap==1的操作所需时间就很少了
,这样就会很快,
这样整体而言,可以达到优化的效果、
3、
希尔排序的
时间复杂度不好计算
,因为
gap
的取值方法很多,导致很难去计算,因此在好些书中给出的
希尔排序的时间复杂度都不固定
,推导出来
平均时间复杂
度: O(N^1.3—N^2),
记住
平均时间复杂度是:O(N^1.3)
即可,时间复杂度是:O(N*logN),空间复杂度是:O(1),平均时间复杂度是:O(N^1.3)、
4、
稳定性:
不稳定、
该算法无论何种写法,
相等的数据都有可能被分到不同的gap组中,即希尔排序
任何写法均不能确保做到稳
定,则希尔排序是不稳定的、
//希尔排序、
//平均时间复杂度为:O(N^1.3)、
//已知,直接插入排序的时间复杂度是:O(N^2),因为看的是最坏的情况,对于插入排序而言,最好的情况下的时间复杂度是:O(N),接近于顺序的情况下时间复杂度也是:O(N),若使用直接插入排序的方法,不考虑它的优化的话
//即使给定的是一个最好的情况,那么该方法的时间复杂度还是O(N^2),因为时间复杂度要看最坏的情况,虽然时间复杂度是O(N^2),但是还是希望是最好的情况或者接近于最好的情况,这样的话,就会节省很多时间,但是如果要求一个序列
//是顺序或者接近顺序,就比较难了,一般来说,要进行排序的序列都是随机的,并不能保证是顺序或者接近顺序,所以在使用插入排序之前能否先对序列进行调整,使之成为顺序或者接近于顺序,然后再进行插入排序就比较节省时间了,该方法
//即指所谓的希尔排序、
void ShellSort(int* a, int n)
{
assert(a);
方法一:
思路:一组间隔为gap的数据进行插入排序完毕后,再进行下一组间隔为gap的数据的插入排序,直到gap组数据都插入排序结束、
假设gap等于3、
int gap = 3;
先写出gap=3的预排序、
gap的值不能使用一个固定值,gap的最合适的值要随着数组元素个数N的不同而变换,gap的值要和数组元素个数N关联起来,N越来,最开始的gap就越大,预排序过程中并不是只能取一次gap值,可以取多次gap值、
先让gap从大开始,让大的数和小的数更快的到后面和前面去,然后gap再越来越小,再让序列越来越接近顺序,最后当gap等于1时,相当于进行插入排序即可,便可得到顺序序列了,即完成了升序排列、
控制gap组数据、
//int gap = n;
//while (gap > 1) 这个地方不能是>=1,因为经过后面的控制后,如果是>=1,就会陷入死循环、
//{
// //gap怎么变并没有规定,只是常使用如下方式:
// //当gap > 1 时,就是预排序的过程、
// //当gap = 1 时,就是直接插入排序的过程、
// //此处加1能保证最后一次一定是1,不管gap是大于3,等于3,还是小于3,最后的gap值一定是1、
// gap = gap / 3 + 1;
// for (int j = 0; j < gap; j++)
// {
// //控制间隔为gap的一组数据进行插入排序、
// for (int i = j; i < n - gap; i += gap) 为什么是n - gap?因为最后一个end是n-1-gap,end+gap一定小于n、
// {
// int end = i;
// int tmp = a[end + gap];
// while (end >= 0)
// {
// if (tmp < a[end])
// {
// a[end + gap] = a[end];
// end -= gap;
// }
// else
// {
// break;
// }
// }
// //到此有两种情况:
// //1、当tmp>=a[end]时,break出来的。
// //2、当end<0时,不进入while循环,从而达到此处、
// a[end + gap] = tmp;
// }
// }
//}
//方法一和方法二的时间复杂度是一样的、
//方法二:
//思路:和方法一的思路不太相同,把第一组数据中的第一个元素看做是一个有序序列,要排升序,则看做是升序序列,先进行第一组数据中第二个数据的插入,把第二组数据中的第一个元素看做是一个有序序列,要排升序,则看做是升序序列
//再进行第二组数据中第二个数据的插入,把第三组数据中的第一个元素看做是一个有序序列,要排升序,则看做是升序序列,再进行第三组数据中第二个数据的插入,则第一组数据中的前两个元素构成了升序序列,再进行第一组数据中第三个数据的插入,
//则第二组数据中的前两个元素构成了升序序列,再进行第二组数据中第三个数据的插入,则第三组数据中的前两个元素构成了升序序列,再进行第三组数据中第三个数据的插入,重复上述操作,直到把gap组数据中剩下的所有元素都插入排序结束、
//假设gap等于3、
//int gap = 3;
//gap的值不能使用一个固定值,gap的最合适的值要随着数组元素个数N的不同而变换,gap的值要和数组元素个数N关联起来,N越来,最开始的gap就越大,预排序过程中并不是只能取一次gap值,可以取多次gap值、
//先让gap从大开始,让大的数和小的数更快的到后面和前面去,然后gap再越来越小,再让序列越来越接近顺序,最后当gap等于1时,相当于进行插入排序即可,便可得到顺序序列了,即完成了升序排列、
int gap = n;
while (gap > 1)//这个地方不能是>=1,因为经过后面的控制后,如果是>=1,就会陷入死循环、
{
//gap怎么变并没有规定,只是常使用如下方式:
//当gap > 1 时,就是预排序的过程、
//当gap = 1 时,就是直接插入排序的过程、
//此处加1能保证最后一次一定是1,不管gap是大于3,等于3,还是小于3,最后的gap值一定是1、
//此处要是除2的话,直接写成:gap = gap/2即可,肯定能够使得gap取到1,除3要写成下面这样,除别的数不一定,但必须保证能够使得gap取到1才可以。
//gap = gap/2;
gap = gap/3+1;
for (int i = 0; i < n - gap; i++) //为什么是n - gap?因为最后一个end是n-1-gap,end+gap一定小于n、
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
//到此有两种情况:
//1、当tmp>=a[end]时,break出来的。
//2、当end<0时,不进入while循环,从而达到此处、
a[end + gap] = tmp;
}
}
}
//如果不对插入排序进行优化的话,直接使用插入排序的话,若是逆序的情况,即最坏情况下,部分时间复杂度就是O(N^2),假设n>1、
//若先进行预排,当gap从大减小到1时,进入for循环就相当于是直接插入排序,而当gap等于1,但未进入for循环的时候,此时序列已经很接近于顺序了,
//就可以认为此时再对该序列进行插入排序操作的话,此时最后一步的直接插入的部分的部分时间复杂度就是O(N),效率非常高,只要预排序叠加最后一次的直接插入排序的部分时间小于O(N^2),则希尔排序就起到了作用、
//当n=1时,gap等于1,不进入while循环,则希尔排序调用函数不起作用,当n>1时,此时把n的值赋给gap,则gap也是大于1的,然后gap再逐渐减小,当gap等于1时,进入for循环就相当于是直接插入排序,
//而当gap等于1,但未进入for循环的时候,此时序列已经很接近于顺序了,就可以认为此时再对该序列进行插入排序操作的话,效率非常高、
//希尔排序的缺点就是,若对数组排升序,本来就是升序的话,或者本来就是很接近于升序的话那么预排序就相当于是白做了,虽然该种情况下,预排序很快就执行完了,但总的来说还是白做了,效果不大,
//但是这种情况很少,多数情况下,所给的数组都是随机的,并不是顺序或者接近顺序的,所以在大部分情况下,希尔排序还是能够起到很大的作用的、
//当顺序或者接近顺序时,不管数据量的大小,此时插入都比希尔要好,因为希尔前面的预排序相当于白做了,当随机,部分顺序,或者逆序时,若数据量较大时,则希尔比插入更加合适,但是如果数据量很小
//的时候,希尔并不一定比插入要好、
2.2、选择排序:
2.2.1、基本思想:
每一次从待排序的数据元素中选出
最小(或最大)
的一个元素,存放在
序列的起始位置
,直到全部待排序的数据元素排完、
2.2.2、直接选择排序:
以
升序
为例,在
元素集合array[ i ]--array[ n-1 ]中
选择关键码
最大(小)的数据元素
若它不是这组元素中的
最后一个(第一个)元素
,则将它与这组元素中的
最后一个(第
一个)元素交换,
在剩余的
array[ i ]--array[ n-2 ]
(array[ i+1 ]--array[ n-1 ])
集合中,重复上述步骤,
直到集合剩余1个元素
,若选择的是
最小的数据元
素,因为是
升
序
,所以他
应该在元素集合array[ i ]--array[ n-1 ]中的第一个位置上
,若在第一个位置上,则不需要交换,若不在,则需要和该集合中的第一个位置上的元素进行交
换,然后再从array[ i+1 ]--array[ n-1 ] 该范围内去重复上述步骤,同理,若选择的是
最大的数据元
素,因为是
升序
,所以他
应该在元素集合array[ i ]--array[ n-1 ]中
的最后一个位置上
,若在最后一个位置上,则不需要交换,若不在,则需要和该集合中的最后一个位置上的元素进行交换,然后再从array[ i ]--array[ n-2 ]该范围内
去重复上述步骤,
直到集合只剩下一个元素为止、
给定
一个
随机数组
,假设要排
升序
,遍历第一遍,找出该数组中最小的元素,若该最小的元素已经在数组的首元素位置上,则不需要进行交换,否则,让其与该数
组的首元素交换位置,这样就把最小的元素放在了数组的起始位置,然后再遍历数组中除了该最小元素之外的所有元素,即遍历下标为:[ 1,n-1 ] 的所有元素,再
找到该范围内最小的元素,若该最小的元素已经在该范围的起始位置,则不需要交换,若不在该范围的起始位置,则需要把他与该范围中的首元素进行交换,这样
就可以把整个数组中次小的元素放在数组的第二个位置上,重复上述操作,直到剩余的所有元素构成的序列中只剩一个元素时,遍历结束、
现在
对直接选择排序进行优化:
给定一个随机数组,假设要排升序,遍历第一遍,找出该数组中最小的元素,再找出该数组中最大的元素,若该最小的元素已经在数组的首元素位置上,则不需要
进行交换,否则,让其与该数组的首元素交换位置,这样就把最小的元素放在了数组的起始位置,若该最大的
元素已经在数组的最后一个位置上,则不需要进行交
换,否则,让其与该数组的最后一个位置上的元素交换位置,这样就把最大的元素放在了数组的起始位置,然后再遍历数组中除了该最小元素和最大元素外的所有
元素,即遍历下标为:[1,n-2]的所有元素,再找到该范围内最小和最大的元素,若该最小的元素已经在该范围的起始位置,则不需要交换,若不在该范围的起始位
置,则需要把他与该范围中的首元素进行交换,若该最大的元素已经在该范围的尾位置,则不需要交换,若不在该范围的尾位置,则需要把他与该范围中的尾位置
上的元素进行交换,这样就可以把整个数组中次小和次大的元素分别放在数组的第二个位置和倒数第二个位置上,重复上述操作,若数组的元素个数为奇数个,则
遍历到剩余的所有元素构成的序列中只有一个元素时停止,若数组的元素个数为偶数个,则遍历到剩余的所有元素构成的序列中没有元素时停止、

直接选择排序,直接选择排序优化的特性总结:
1、直接选择排序思考非常好理解,但是
效率不是很好
,实际中很少使用、
2、时间复杂度:
O(N^2)、
3、空间复杂度:
O(1)、
4、稳定性:
不稳定、
不考虑优化的版本,只考虑未优化的情况
,以
3
3
5 7 8 1
排升序
为例,选出最小的数字1,和第一个
3
交换之后两个3的相对位置就发生了交换,或者,举例:
3
3
7
1
1
9 8 4 ,每次选一个最小的数进行交换,看似没问题,找最小值找到第一个
1
,需要把第一个
1
和第一个
3
交换,交换完是:
1
3
7
3
1
9 8 4 虽然两个1的前后顺序
没变,但是两个3的前后顺序改变了,即,该
直接选择排序未优化算法
无论何种写法,均
不能确保
做到稳定,则直接选择排序未优化版本是不稳定的、
//直接选择排序优化、
//void NewSelectSort(int* a, int n)
//{
// assert(a);
// int left = 0;
// int right = n - 1;
// while (left < right)
// {
// //此处若定义成int i = left+1,则把left赋值给mini和maxi,这样就一定不会出现错误,如果在[left+1,right]中找到了比下标为left的值更小的数的话,则更新mini,若找到了比下标为left的值
// //更大的数的话,则更新maxi,若在该范围内找不到比下标为mini的值更小的数据的话,比下标为maxi的数更大的数,则此时mini和maxi所指的数,即left所指的数为最小值,则left所指的数即为最大值、
// //此处若定义成int i = left+1,则把right赋值给mini的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更小的数的话,则更新mini,此时在范围[left+1,right]中的最小值就是下标为mini的数,
// //若在该范围内找不到比mini更小的数的话,则在范围[left+1,right]中的最小的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最小值,该最小值并不一定比left所指的数要小,如果该最小值小于等于left所指的数,则是正确的,
// //但若该最小值大于left所指的数,则程序就是错误的,所以尽量不要这样写、
// //此处若定义成int i = left+1,则把right赋值给maxi的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更大的数的话,则更新maxi,此时在范围[left+1,right]中的最大值就是下标为maxi的数,
// //若在该范围内找不到比maxi更大的数的话,则在范围[left+1,right]中的最大的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最大值,该最大值并不一定比left所指的数要大,如果该最大值大于等于left所指的数,则是正确的,
// //但若该最大值小于left所指的数,则程序就是错误的,所以尽量不要这样写、
// //所以当定义成int i = left+1的话,一定要把left赋值给mini和maxi,这是一定不会出现错误的,若把right赋值给mini和maxi的话,则有可能会出现错误、
// //若定义成定义成int i = left,则令mini和maxi为left或者right都可以,都不会出现错误、
// int mini = left;
// int maxi = left;
// //若定义成int i = left的话,只需要让数组中的任意一个值作为最大值或者最小值都可以,并不是只能让left和right所指的数作为最大值或者最小值,数组中的元素任意一个都是可以的、
// //若定义成int i = left+1的话,则只能让left所指的数作为最小值或者最大值、
// for (int i = left+1; i <= right; i++)
// {
// if (a[i] < a[mini])
// {
// mini = i;
// }
// if (a[i] > a[maxi])
// {
// maxi = i;
// }
// }
// //此时,left和maxi重叠,当执行第一次Swap时,则最大值9所在的位置并不是maxi,被调换了位置,导致出现问题、
// //这只是凑巧了,出现了极端情况,在这次示例中出现了错误,可能在其他的示例中并不会出现错误、
// Swap(&a[mini], &a[left]);
// //如果left和maxi重叠,则修正一下maxi即可、
// if (left == maxi)
// {
// maxi = mini;
// }
// Swap(&a[maxi], &a[right]);
// left++;
// right--;
// }
//}
//若按照自己的方法求时间复杂度,若数组元素个数为偶数,则外层while循环的次数为固定值N/2次,若数组元素个数为奇数,则外层while循环的次数接近为固定值N/2次,内层循环也为固定值是N-1次,则总的执行次数为:T=(N/2)*(N-1),则时间复杂度就是:O(N^2)、
//选择排序优化中不存在break,故不区分最好最坏,两者都是一样的,则
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,若数组元素个数为奇数个,则总的执行次数为:T(N)=N+(N-2)+..+3+1,部分时间复杂度是:O(N^2),若数组元素个数为偶数个,则总的执行次数为:T(N)=N+(N-2)+..+2+0,则部分时间复杂度还是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,若数组元素个数为奇数个,则总的执行次数为:T(N)=N+(N-2)+..+3+1,部分时间复杂度是:O(N^2),若数组元素个数为偶数个,则总的执行次数为:T(N)=N+(N-2)+..+2+0,则部分时间复杂度还是:O(N^2)、
//因为算法中不存在break,当进行升序排序时即使数组已经是升序状态了,也不会直接break出来,所以两层for循环必须都要执行完,则在该种情况下,不区分最好最坏,部分时间复杂度都是O(N^2)、
//时间复杂度看的是最坏的情况,虽然其时间复杂度和冒泡排序优化的时间复杂度都是一样的,即都是O(N^2),但是具体的执行次数是不一样的,冒泡排序优化最坏情况下的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1
//而选择排序优化的执行次数不论数组元素个数是奇数还是偶数,都比冒泡排序优化最坏情况下的执行次数少了将近一半,故在测试中,选择排序优化会好于冒泡排序优化,即在一般情况下,随机或者逆序情况下,选择排序优化会好于冒泡排序优化、
//但是如果数组是顺序或者接近顺序时,冒泡排序优化在测试中就会比选择优化要好,是因为,此时冒泡排序优化的部分时间复杂度可看成O(N),而选择优化的部分时间复杂度还是O(N^2),因为选择优化中,不存在break,即使是顺序或者接近顺序,那么所有的循环也必须都要全部执行完毕才可以
//所以该种情况下,冒泡优化会比选择优化要好、
//插入排序,顺序则部分时间复杂度是O(N),接近顺序则部分时间复杂度也是O(N)、
//冒泡排序优化,顺序则部分时间复杂度是O(N),接近顺序则部分时间复杂度也是O(N)、
//选择排序优化,顺序则部分时间复杂度是O(N^2),接近顺序则部分时间复杂度也是O(N^2)、
//选择排序,顺序则部分时间复杂度是O(N^2),接近顺序则部分时间复杂度也是O(N^2)、
//对于整体时间复杂度为:O(N^2)的四个排序,即,插入排序,冒泡优化排序,选择优化排序,选择排序而言:
//若是随机或者逆序时,则最好的是选择排序优化,而其他三者差不多、
//若是顺序的话,则插入和冒泡优化差不多,都算最好,其次是选择优化,最坏的是选择、
//若是接近顺序的话,则插入要稍微好于冒泡优化一点,其次是选择优化,最后是选择、
//部分顺序的话,插入排序也能体现出优势,应是最好,其次是冒泡排序优化,exchange的优化没有插入排序的优势大,冒泡排序优化在部分顺序下和选择优化比较的话,要看部分顺序的程度,两者不能明确得出结果,但是冒泡排序优化和选择优化在部分顺序下一定比选择要好、
//此处所指的好坏只是计算了在某些情况下循环的次数,但是在所用时间测试中不一定是上述结果,是因为时间测试中,也把其他非循环的代码执行也计算了他们的时间,这样时间也和非循环代码的量有关系了、
//虽然在随机或逆序情况下,选择优化会比插入更好一点,但是从整体而言,插入是最合适的,即,要从这四个时间复杂度为O(N^2)的方法中选择的话,优先选择插入排序、
//若顺序或者接近顺序,则选择排序的时间效率低于冒泡排序优化、
//若随机或者逆序时,选择排序和冒泡优化,时间效率差不多、
//直接选择排序,升序、
void SelectSort(int* a, int n)
{
assert(a);
int left = 0;
int right = n - 1;
while (left < right)
{
int mini = left;
//若定义 int i=left,则int mini=left/right 均可,若定义int i=left+1,则必须定义为: int mini =left,否则可能会出问题、
//升序或者降序、
//此处若定义成int i = left+1,则把left赋值给mini和maxi,这样就一定不会出现错误,如果在[left+1,right]中找到了比下标为left的值更小的数的话,则更新mini,若找到了比下标为left的值
//更大的数的话,则更新maxi,若在该范围内找不到比下标为mini的值更小的数据的话,比下标为maxi的数更大的数,则此时mini和maxi所指的数,即left所指的数为最小值,则left所指的数即为最大值、
//此处若定义成int i = left+1,则把right赋值给mini的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更小的数的话,则更新mini,此时在范围[left+1,right]中的最小值就是下标为mini的数,
//若在该范围内找不到比mini更小的数的话,则在范围[left+1,right]中的最小的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最小值,该最小值并不一定比left所指的数要小,如果该最小值小于等于left所指的数,则是正确的,
//但若该最小值大于left所指的数,则程序就是错误的,所以尽量不要这样写、
//此处若定义成int i = left+1,则把right赋值给maxi的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更大的数的话,则更新maxi,此时在范围[left+1,right]中的最大值就是下标为maxi的数,
//若在该范围内找不到比maxi更大的数的话,则在范围[left+1,right]中的最大的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最大值,该最大值并不一定比left所指的数要大,如果该最大值大于等于left所指的数,则是正确的,
//但若该最大值小于left所指的数,则程序就是错误的,所以尽量不要这样写、
//所以当定义成int i = left+1的话,一定要把left赋值给mini和maxi,这是一定不会出现错误的,若把right赋值给mini和maxi的话,则有可能会出现错误、
//若定义成定义成int i = left,则令mini和maxi为left或者right都可以,都不会出现错误、
for (int i = left+1 ; i <= right; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[mini], &a[left]);
left++;
}
}
//若按照自己的方法求时间复杂度,则外层while循环的次数为固定值N-1次,内层循环也为固定值是N-1次,则总的执行次数为:T=(N-1)*(N-1),则时间复杂度就是:O(N^2)、
//选择排序中不存在break,故不区分最好最坏,两者都是一样的,则
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+2+1,部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+2+1,部分时间复杂度是:O(N^2)、
//因为算法中不存在break,当进行升序排序时即使数组已经是升序状态了,也不会直接break出来,所以两层for循环必须都要执行完,则在该种情况下,不区分最好最坏,部分时间复杂度都是O(N^2)、
2.2.3、堆排序:
堆排序(Heapsort)
是指利用堆积树(堆)这种数据结构所设计的一种排序算法,
它是选择排序的一种
,它是
通过堆来进行选择数据
,需要注意的是
排升序要建大
堆,排降序建小堆、
堆排序的特性:
1、
堆排序
使用堆来进行排序、
2、
时间复杂度:
O(N*logN)、
3、
空间复杂度:
O(1)、
4、
稳定性:
不稳定、
如下图所示:

此时是一个大堆,我们想要得到的是升序的数据,此时如果将堆顶的8和4进行交换后,两个8的相对位置就发生了改变,即,该堆排序算法无论何种写法,均不能
确保做到稳定,则堆排序是不稳定的、
2.3、交换排序:
基本思想:
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置、
交换排序的
特点
是:
若升序,则将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动
,
若降序,则将键值较小的记录向序列的尾部移动,键值
较大的记录向序列的前部移动、
2.3.1、冒泡排序:
//交换函数,传址调用、
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//冒泡排序->本质上是交换排序、
//按照自己的方法直接计算时间复杂度则为:O(N^2),外层循环固定为N-1次,内层循环不固定,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1、
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1,部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数仍为:T(N)=(N-1)+(N-2)+..+3+2+1,则部分时间复杂度就是:O(N^2),因为算法中不存在break,当进行升序排序时
//即使数组已经是升序状态了,也不会直接break出来,只是不进入if语句,但两层for循环必须都要执行完,则在该种情况下,不区分最好最坏,部分时间复杂度都是O(N^2)、
//时间复杂度看最坏,则还是:O(N^2)、
方法一:
//void BubbleSort(int* a, int n)
//{
// assert(a);
// for (int i = 0; i < n-1; i++)
// {
// for (int j = 0; j < n - 1 - i; j++)
// {
// if (a[j]>a[j + 1])
// {
// Swap(&a[j], &a[j + 1]);
// }
// }
// }
//}
方法二:
//void BubbleSort(int* a, int n)
//{
// assert(a);
// for (int i = 0; i < n-1; i++)
// {
// for (int j = 1; j < n- i; j++)
// {
// if (a[j-1]>a[j])
// {
// Swap(&a[j-1], &a[j]);
// }
// }
// }
//}
//冒泡排序优化、
//按照自己的方法直接计算时间复杂度则为:O(N^2),外层循环固定为N-1次,内层循环不固定,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1、
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1,部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数仍为:T(N)=(N-1),则部分时间复杂度是:O(N),因为,若进行升序,而数组就是升序的话,
//这样的话,外层for循环则只进行了一次,在该次循环中,内层for循环进行了N-1次,然后从break直接出去了外层for循环、
//时间复杂度看最坏,则还是:O(N^2)、
//若不是最好的情况,即不是顺序的话,但是非常接近于顺序时,则总的执行次数就比最好情况下的执行次数N-1多一部分,有可能是T(N)=(N-1)+(N-2),则部分时间复杂度还是:O(N)、
void BubbleSort(int* a, int n)
{
assert(a);
for (int i = 0; i < n - 1; i++)
{
int exchange = 0;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j]>a[j + 1])
{
exchange = 1;
Swap(&a[j], &a[j + 1]);
}
}
//若前一趟冒泡排序若未进行交换,则说明已经满足升序或降序了,则剩余趟数的冒泡排序就不需要再进行了、
if (exchange == 0)
{
//前一趟冒泡排序若未进行交换、
break;
}
}
}
//不是特例的情况下,直接选择排序是最垃圾的、
//插入排序和冒泡排序优化的对比、
//虽然插入排序最坏是N^2,最好是N, 冒泡排序优化最坏是N^2,最好是N,但是两者还是不一样好、
//只有在逆序和顺序,即最坏和最好的情况下,两者是一样的,但是除了这两种情况,还是插入排序比较好,由于时间复杂度看的是最坏的情况,所以即使两者的时间复杂度相同,但是从细节上来说的话,还是插入排序更加合适、
//虽然插入排序在细节上比冒泡排序优化更加合适,但是对于两者而言,由于时间复杂度相同,所以还要看做是同一级别的排序、
//比如:
//数组元素分别为:1 2 3 4 5 6 8 7, ,此时插入排序只计算比较次数,则为:8次, 冒泡排序只计算比较次数则为:7+6=13次,这是因为第一趟冒泡排序比较了7次,交换了7和8,交换完之后
//虽然已经满足了升序,但是此时的exchange==1,所以还要再进入第二趟冒泡排序,第二趟冒泡排序比较次数为6次,但第二趟冒泡排序完之后,exchange仍为0,所以就不再进行第三趟冒泡排序了,
//则总的比较次数为13次,只有在前一趟冒泡排序中未进行交换,才不再进行下一趟冒泡排序,若在前一趟冒泡排序中进行了交换,但交换后满足了升序或者降序,此时是还需要再进行下一次冒泡排序的,这是因为
//前一趟冒泡排序进行了数据的交换,即使前一趟冒泡排序进行交换元素后是升序或者降序了,但是编译器还不知道此时已经是升序或者降序了,只有再进行一趟冒泡排序后未进行数据交换,编译器才知道已经是升序或者降序,就不再进行下一趟了、
//当是顺序时,即最好的情况下,两者一样,但是当部分顺序时,冒泡排序优化中的exchange优化作用不大,而对于插入排序而言,该情况下的那些部分顺序中,也能体现出它的价值,则插入排序更好,其适应性和比较次数都较少、
//当是逆序时,即最坏的情况下,两者一样,但是当部分顺序时,冒泡排序优化中的exchange优化作用不大,而对于插入排序而言,该情况下的那些部分逆序中,也能体现出它的价值,则插入排序更好,其适应性和比较次数都较少、
//所谓的接近顺序指的是接近顺序的程度很大的情况,若接近顺序的程度不大的话,则直接看做是局部顺序即可,当非常接近与顺序时,部分时间复杂度还是O(N),此时的exchange的优化作用很大、
冒泡排序和冒泡排序优化的特性总结:
1、两者都
是一种非常容易理解的排序、
2、
时间复杂度:
O(N^2)、
3、
空间复杂度:
O(1)、
4、稳定性:
稳定
、
以上述写法为例
,冒泡排序,冒泡排序优化
的当前写法
都能够确保做到稳定
,则
冒泡排序,冒泡排序优化算法是稳定的、

关于对稳定性的解释,可作参考,后期会再进行具体的阐述,其次,快速排序内容较多,将在新的一篇博客中进行展示,谢谢大家~