目录
3.根据希尔排序的思路可以把希尔排序分两部分:多次预排序 + 对整个序列进行直接插入排序
(2)在对整个序列一次预排序中,分别对gap组分别进行直接插入排序
1.写法一:在对整个序列一次预排序中,对gap组中的每一组分别进行直接插入排序。
2.写法二:在对整个序列一次预排序中,对整个序列中的gap组进行并排的直接插入排序。
4.2.一次预排序在最坏和最好情况的时间复杂度都可以看做是O(N)的分析过程。
4.3.对希尔排序的过程中整个序列要进行预排序的总次数n是logN(以2为底) 或者 logN(以3为底)进行分析。
(2)希尔排序在预排序过程到直接插入排序过程之间的时间复杂度变化图
排序OJ(可使用各种排序跑这个OJ)
直接插入排序
一、直接插入排序思路的分析过程
1.直接插入排序的思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
使用直接插入排序算法的案例:实际中我们玩扑克牌时,就用了插入排序的思想
2.直接插入排序的代码思路
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
图形解析:
二、直接插入排序的单趟
1.直接插入排序的单趟过程分析
(1)直接插入排序的单趟思路
在一个有序序列中插入一个数据后这个序列依然保持有序。
(2)在一个有序序列中插入一个数据总共会出现3种情况
注意:下标end一开始指向有序序列最后一个元素,而一开始下标end + 1指向插入元素x。从下面分析可知,直接插入排序的单趟结束后插入元素x最终总是在end的下一个位置插入数据,即插入数据x最终插入在下标end + 1的位置。
情况①
插入数据x比有序序列最后一个数据要大,则直接把数据x插入到有序序列最后一个数据的后面,即数据x应插入到下标end位置的下一个位置。
情况②
插入数据x比有序序列最后一个数据要小且比有序序列的第一个数据要大,则要通过不断的挪动数据找到插入数据x应该在有序序列的插入位置,即此时数据x应插入到下标end位置的下一个位置。
情况③
插入数据x比有序序列第一个数据还要小,则直接把数据x插入到有序序列的最前面,即此时数据x应插入到下标end位置的下一个位置。
2.直接排序单趟的代码
(1)以下是直接插入排序单趟的代码过程
注意:直接插入排序单趟的作用是在一个有序序列中插入一个数据后这个序列依然保持有序。
(2)对直接插入排序单趟代码进行解析
int end;//一开始end表示数组中有序序列最后一个数据的下标。下标end是用来从后往前遍历数组中的有序序列的。
int tmp = a[end + 1];
// 一开始end + 1表示数组中第一个插入到有序序列的插入数据的下标,所以a[end + 1]就是数组的第一个插入数据。由于插入数据a[end + 1]是通过挪动数据找到在有序序列中的插入位置进行插入的,所以必须用局部变量tmp把插入数据a[end + 1] 保存起来。
//注意:若数组有n个元素则数组要进行n - 1次直接插入排序的单趟才能把数组排成有序序列,进而使得在整个直接插入排序的过程中数组中有很多要插入到有序序列的数据,所以我们一般认为有序序列最后一个数据的后面一个数据就是数组中第一个要插入有序序列的数据。
//while(end >=0)循环语句的作用是:在有序序列中找到插入数据tmp插入到有序序列的位置。
//注意:由于把插入数据tmp插入到数组的有序序列总共有3种情况且最坏的情况是插入数据tmp要插入到有序序列的最前面位置且此时end = -1,所以while循环才用end >= 0作为停止查找插入位置的判断条件。
while(end >=0)
{
//排成升序(注意:若想排成降序的话,则下面if语句的判断表达式要写成tmp > a[end])
if(tmp < a[end])//此时局部变量tmp表示插入数据,a[end]表示有序序列的数据。
{
//由于插入数据tmp比有序序列数据a[end]要小,所以要利用while(end >=0)循环通过不断的挪动有序序
列的数据来找到插入数据tmp插入到有序序列的位置。
a [end + 1]= a[end];//把有序序列中的数据a[end]往后挪动。
--end;
}
else
{
break;//由于插入数据tmp大于或等于有序序列数据a[end],则此时说明已经找到了插入数据tmp插入到有
序序列的位置,所以此时才利用break来终止利用while(end >=0)循环查找插入数据的插入位置了。
}
}
//走到这一步说明此时已经在有序序列中找到插入数据tmp的插入位置了,即插入数据tmp插入到有序序列下标为end + 1的位置。
a[end + 1] = tmp;//由于把插入数据tmp插入到数组的有序序列总共有3种情况,无论那一种情况插入数据tmp都会插入到有序序列下标end位置的后一个位置,即插入数据tmp插入到有序序列下标为end + 1的位置。
三、直接插入排序的代码分析
1.直接插入排序的代码思路
由于不知道数组是否有序,所以一开始我们认为数组第一个元素组成一个有序序列,而且直接排序单趟的作用使得在有序序列中插入1个元素后这个序列依然保持有序,所以通过不断使用直接插入排序的单趟把数组从第二个元素开始往后的所有元素一个一个插入有序序列中,当数组最后一个元素插入有序序列后直接插入排序就结束了。
2.直接插入排序代码
//直接插入排序的时间复杂度
//1.最坏情况下的时间复杂度:
//(1)最坏情况:对逆序序列进行直接插入排序就是最坏的
//(2)时间复杂度:O(N^2)
//2.最好情况下的时间复杂度:
//(1)最好情况:对有序序列、接近有序序列进行直接插入排序就是最好的情况
//(2)时间复杂度:O(N)
//直接插入排序
void InsertSort(int* a, int n)
{
//for循环的作用是:进行n - 1次直接插入排序单趟把数组第2个元素开始往后的n - 1个数据插入到有序
序列进而把数组中n个数据排序成升序。
for (int i = 0; i < n-1; ++i)
{
//注意:我们一开始不认为整个要排序的数组是个有序序列,而是一开始我们认为数组的第一个元素
就是个有序序列而数组第二个元素就是要插入到有序序列中的数据,等到数组第二个元素插入到有序
序列之后会使得数组前2个元素组成一个有序序列,进而使得数组第三个元素就是插入到有序序列中的
据,以此类推下去。
//在一个有序序列中插入一个数据进行排序的单趟过程:
//1.用end指向数组中的有序序列最后一个元素
int end = i;//一开始end表示的数组中的有序序列最后一个元素的下标,而i是数组元素的下标。
//2.用变量tmp临时存储插入到有序序列的数据。而且这个插入数据是在有序序列最后一个元素的后
面。
int tmp = a[end + 1];//一开始下标end+1就是插入到有序序列中插入数据的下标。由于插入数据
a[end + 1]在插入的过程中有可能被有序序列中的其他数据覆盖掉所以我们要用临时变量tmp把插入
数据存储起来。
//3.插入数据tmp在有序序列中插入的过程
//3.1.在有序序列中找到比插入数据tmp要小的数
while (end >= 0)
{
//注意:即使一开始end表示的是有序序列最后一个元素的下标,但是我们是用end从后往前遍历
整个有序序列
if (tmp < a[end])//若插入数据tmp比有序序列元素a[end]要小,则有序序列元素a[end]要
往后挪动一步。
{
//挪动数据
a[end + 1] = a[end];
--end;
}
else//若插入数据tmp大于或等于有序序列的元素a[end],则停止比较有序序列元素和插入数据
tmp,然后在此时下标end+1的位置插入数据。
{
break;
}
}
//3.2.插入数据tmp插入到有序序列中比自己要小的数据的后面一个位置。
//插入数据tmp永远是在数组下标end的后一个位置插入的,即插入数据在数组下标end+1的位置插入
数据。
a[end + 1] = tmp;
}
}
3.对直接插入排序的时间复杂度进行分析
(1)最坏情况下的时间复杂度是:O(N^2)
①最坏情况指的是:对逆序序列进行直接插入排序就是最坏的情况。
②根据直接插入排序的思想可知直接插入排序最基本的操作就是挪动数据,在对逆序序列进行直接插入排序的整个过程中挪动数据总次数的值是个首项为1且公差为1的等差数列的前n - 1项和,所以直接插入排序函数InsertSort的最坏情况下的时间复杂度O(n^2)。
③逆序指的是:对降序序列进行直接插入排序成升序;对升序序列进行直接插入排序成降序。
4.对直接插入排序进行总结
(1)元素集合越接近有序,直接插入排序算法的时间效率越高
(2)时间复杂度:O(N^2)
(3)空间复杂度:O(1),它是一种稳定的排序算法
(4)稳定性:稳定
希尔排序
一、希尔排序思路的分析过程
1.希尔排序的背景
由于直接插入排序在对逆序序列进行排序时的时间复杂度很高,所有为了更高效的对逆序序列序列进行排序我们就对直接插入排序进行了优化,而优化后的排序算法就是希尔排序。希尔排序可以针对任何类型的序列进行高效的排序,例如对逆序序列进行希尔排序。
注意:①希尔排序是按直接插入算法的思想变形走的;②直接插入排序算法对顺序序列或者是近似顺序序进行排序的效率还行,但是直接插入排序算法对逆序序列进行排序的效率低。
2.希尔排序的思想
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap(注意:一开始gap大于1),把待排序序列中所有数据分成gap个组,而所有距离为gap的数据分在同一组内,并对每一组内的数据进行直接插入排序。然后重复上述分组和直接插入排序的工作。当gap=1时,对整个序列进行直接插入排序后序列中的所有数据都排好序了。
3.根据希尔排序的思路可以把希尔排序分两部分:多次预排序 + 对整个序列进行直接插入排序
3.1.当gap > 1时,对整个序列多次预排序
注:一次预排序要分gap组进行直接插入排序。分组排序的目的是让整个序列中大的数尽快跳到后面去,让小的数尽快跳到前面去。
(1)对整个序列一次预排序的分组思路
先假设整个序列中,间距为gap = 3的元素分为一组。而间距gap是多少整个序列就分为多少组进行直接插入排序,所以整个序列分gap = 3组分别独立进行直接插入排序。
图形解析:
(2)在对整个序列一次预排序中,分别对gap组分别进行直接插入排序
图形解析:
(3)对整个序列进行一次预排序的两种写法
① 一次预排序的写法一:利用利用两个for循环实现希尔循环的一次预排序
注意:写法一的思路是对gap组中的每一组分别进行直接插入排序。
②一次预排序的写法二:只利用一个for循环就可以实现希尔排序的一次预排序
注意:写法二的思路是对整个序列中的gap组进行并排的直接插入排序;对整个序列中的gap组并排指的是同时对gap组的序列进行直接插入排序。
gap组并排的思路:
3.2.当gap = 1时,对整个序列进行直接插入排序
二、希尔排序的代码
1.写法一:在对整个序列一次预排序中,对gap组中的每一组分别进行直接插入排序。
void ShellSort(int* a, int n)
{
//对整个数组进行多次预排序 + 对整个数组进行直接插入排序的过程:
int gap = n;
//1.while(gap > 1)循环的作用是:
//注意:希尔排序 = 预排序 + 直接插入排序
//(1)当gap > 1时此时while循环是对整个数组进行多次预排序的过程。
//(2)当gap == 1时此时while循环是对整个数组进行直接插入排序的过程。
while (gap > 1)
{
//1.1.对整个数组进行一次预排序的过程中,决定间距gap是多少以此决定把数组分成gap组直
//接直接插入排序。
gap = gap / 3 + 1; //注意:这里要加个1的目的是为了保证最后一次while (gap > 1)循环 时gap最后的值一定是1,这样就可以对整个数组进行直接插入排序。
//gap = gap / 2; //也可以写成这样来决定间距gap。
//for (int j = 0; j < gap; ++j)的作用是控制一组一组序列进行直接插入排序。整个数组中一共有gap组序列进行直接插入排序。
//注意:该种写法是先用直接插入排序排完一组序列后再用直接插入排序排另一组序列。
for (int j = 0; j < gap; ++j)
{
//for (int i = j; i < n - gap; i += gap)的作用是gap组中的一组序列进行直接插入排序。
for (int i = j; i < n - gap; i += gap)
{
//(1)gap组中的一组序列进行直接排序的过程
int end = i;//一开始end表示的是有序序列中最后一个元素的下标。end是用来从右向左 遍历有序序列。
int tmp = a[end + gap];//由于一组序列中每个数据的间距是gap,所以插入数据tmp的下标是end + gap。
//在有序序列中找比插入数据tmp要小的数
while (end >= 0)
{
//挪动数据
if (tmp < a[end])//由于一组序列中每个数据的间距是gap,若有序序列的元素a[end]比插入数据tmp要大,则有序序列元素a[end]往后挪动gap 步。
{
a[end + gap] = a[end];
end -= gap;
}
else//若有序序列的元素a[end]大于或者等于插入数据tmp,则停止比较有序序列元素和插入数据tmp,然后在此时下标end+gap的位置插入数据。
{
break;
}
}
//插入数据tmp插入到有序序列中比自己要小的数据的后面一个位置。由于一组序列中每个数据的间距是gap,
//插入数据tmp永远是在数组下标end的后一个位 置插入的,即插入数据在数组下标end+gap的位置插入数据。
a[end + gap] = tmp;
}
}
}
}
2.写法二:在对整个序列一次预排序中,对整个序列中的gap组进行并排的直接插入排序。
void ShellSort(int* a, int n)
{
//对整个数组进行多次预排序 + 对整个数组进行直接插入排序的过程:
int gap = n;
//1.while(gap > 1)循环的作用是:
//注意:希尔排序 = 预排序 + 直接插入排序
//(1)当gap > 1时此时while循环是对整个数组进行多次预排序的过程。
//(2)当gap == 1时此时while循环是对整个数组进行直接插入排序的过程。
while (gap > 1)
{
//1.1.对整个数组进行一次预排序的过程中,决定间距gap是多少以此决定把数组分成gap组直
//接直接插入排序。
gap = gap / 3 + 1; //注意:这里要加个1的目的是为了保证最后一次while (gap > 1)循环
//时gap最后的值一定是1,这样就可以对整个数组进行直接插入排序。
//gap = gap / 2; //也可以写成这样来决定间距gap。
//1.2.对整个数组进行一次预排序的过程,一次预排序是同时对数组中gap组序列一起进行直
//接插入排序。
for (int i = 0; i < n - gap; ++i)//for循环作用是让gap组序列同时进行直接插入排序。
{
//(1)gap组中的一组序列进行直接排序的过程
int end = i;//一开始end表示的是有序序列中最后一个元素的下标。end是用来从右向左
//遍历有序序列。
int tmp = a[end + gap];//由于一组序列中每个数据的间距是gap,所以插入数据tmp的
//下标是end + gap。
//在有序序列中找比插入数据tmp要小的数
while (end >= 0)
{
//挪动数据
if (tmp < a[end])//由于一组序列中每个数据的间距是gap,若有序序列的元素
//a[end]比插入数据tmp要大,则有序序列元素a[end]往后挪动gap 步。
{
a[end + gap] = a[end];
end -= gap;
}
else//若有序序列的元素a[end]大于或者等于插入数据tmp,则停止比较有序序
//列元素和插入数据tmp,然后在此时下标end+gap的位置插入数据。
{
break;
}
}
//插入数据tmp插入到有序序列中比自己要小的数据的后面一个位置。
//由于一组序列中每个数据的间距是gap,插入数据tmp永远是在数组下标end的后一个位
//置插入的,即插入数据在数组下标end+gap的位置插入数据。
a[end + gap] = tmp;
}
}
}
3.希尔排序预排序过程的特点:
预排序特点的图形解析:
(1)特点1
①在一次预排序的过程中gap越大的话则整个序列就分成很多组分别进行直接插入排序。由于间距gap越大导致一组中的元素之间的间距很大从而导致在直接插入排序时一组中越大的数或者越小的数挪动一次就会走gap步而不是走1步从而导致越大的数往后跳的越快而越小的数往前跳到也快。
总的来说:gap越大,大的数可以更快到后面,小的数可以更快的到前面。
②gap越大导致整个序列被分成很多组,也导致一组中越大或者越小的数在整个序列中最多可以走的步数很少,从而导致整个序列越不接近有序。(注意:这也是gap越大带来的问题)
总的来说:gap越大,则整个序列在进行一次预排序后,整个序列就越不接近有序。
(2)特点2
①在一次预排序的过程中gap越小的话则整个序列就分成很少组分别进行直接插入排序。由于间距gap越小导致一组中的元素之间的间距很小从而导致在直接插入排序时一组中越大的数往后跳的越慢而越小的数往前跳的也越慢。
总的来说:gap越小,数据跳动的越慢。
②gap越小导致整个序列被分成很少组,也导致一组中越大或者越小的数在整个序列中最多可以走的步数很多,从而导致整个序列越来越接近有序。
总的来说:gap越小,越来越有序。
4.希尔排序的时间复杂度可以认为是O(N^1.3)
4.1.
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,所以下面我引用了其他老师书籍中提到的计算方式。
《数据结构(C语言版)》--- 严蔚敏
《数据结构-用面相对象方法与C++描述》--- 殷人昆
因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(n^1.25)到O(1.6*n^1.25)来算。
4.2.一次预排序在最坏和最好情况的时间复杂度都可以看做是O(N)的分析过程。
注意:由于直接插入排序最坏的情况是对逆序序列进行排序,而一次预排序是gap组进行直接插入排序,所以可以认为一次预排序最坏情况是对逆序序列进行预排序。
(1)多次预排序过程中,预排序最好和最坏情况分析
①预排序最坏情况:对逆序序列进行第一次预排序是最坏情况。
分析:假设是对最坏情况逆序序列进行希尔排序。由于在预排序的过程中整个序列是不断变化的,随着希尔排序让整个序列在不断进行多次预排序使得每一次预排序时整个序列都不可能每次都是逆序序列进而导致整个序列都不总是最坏情况。而第一次预排序时是对逆序序列进行预排序则此时可以认为整个序列是最坏情况;
②预排序最好情况:当gap = 1时此时整个序列接近有序,则此时是预排序最好情况。
分析:当gap > 1时会进行多次预排序而当gap越来越小时整个序列会被预排序成越来越接近有序,当gap = 1时可以认为此时整个序列是最好情况因为此时整个序列接近有序。而且gap = 1时预排序已经结束同时开始对整个序列进行直接插入排序,而直接插入排序最好情况就是对接近有序序列进行排序,所以我们才认为当gap = 1时是预排序最好情况。
(2)预排序在最坏情况下挪动数据的总次数
①在一次预排序的过程中整个序列会被分成gap组分别进行直接插入排序,基本认为每组N/gap个数据。
②在一次预排序的过程中,gap组中的每组在最坏情况下进行直接插入排序时挪动数据的总次数:
1 + 2 + 3 + …… + N/gap – 1(等差数列)
③在一次预排序的过程中,gap组在最坏情况下进行直接插入排序时挪动的总次数:
(1 + 2 + 3 + …… + N/gap – 1) * gap
(3)预排序最坏情况的时间复杂度是O(N)的分析过程
①在多次预排序的过程中,只有第一次预排序才有可能遇到最坏情况且这个最坏情况是对逆序序列进行第一次预排序,所以这里假设第一次预排序就是对逆序序列进行预排序。
②对逆序序列进行第一次预排序的过程中,由于gap组序列都是在对逆序序列这种最坏情况下进行直接插入排序使得第一次预排序的时间复杂度是O(N)的分析过程:
由于一开始gap很大且开始时gap = N / 3 + 1 进而导致gap组在最坏情况下进行直接插入排序时挪动的次数(1 + 2 + 3 + …… + N/gap – 1) * gap ≈ N。
分析(1 + 2 + 3 + …… + N/gap – 1) * gap ≈ N的计算过程:(注意:一开始第一次进行预排序时gap = N / 3 + 1)
gap = N / 3 + 1 ≈ N / 3;
N / gap – 1 ≈ N / N / 3 – 1 = 2;
(1 + 2 + 3 + …… + N/gap – 1) * gap = (1 + N/gap – 1)*(N/gap – 1) / 2 * gap ≈ (1 + 2) * 2 / 2 * N / 3 = N。
总的来说,由于对逆序序列第一次预排序的过程中gap组在最坏情况下进行直接插入排序时挪动的次数是(1 + 2 + 3 + …… + N / gap – 1) * gap ≈ N,最终导致对逆序序列进行第一次预排序的时间复杂度是O(N)。
(4)预排序最好情况的时间复杂度是O(N)的分析过程
最好情况是gap = 1时预排序结束了,然后直接对整个序列进行直接插入排序而且此时整个序列在经过多次预排序后整个序列已经变得相对有序了,所以最好情况的时间复杂度是O(N)。
分析:
①在预排序快要结束时且gap变得很小时,由于整个序列经过多次预排序后整个序列已经变得有序了,所以当gap = 1时整个序列已经变得是最好情况了,所以此时是不能用最坏情况的公式(1 + 2 + 3 + …… + N / gap – 1) * gap来计算最好情况的时间复杂度的。
②当gap = 1时此时预排序已经结束了同时直接对整个序列进行直接插入排序,而直接插入排序最好情况的时间复杂度是O(N),由于此时整个序列已经相对有序了则此时对整个序列进行直接插入排序的时间复杂度是O(N)。
4.3.对希尔排序的过程中整个序列要进行预排序的总次数n是logN(以2为底) 或者 logN(以3为底)进行分析。
注意:若希尔排序代码使用gap = gap / 3 + 1控制每次预排序的间距,则预排序的总次数是logN(以3为底);若希尔排序代码使用gap = gap / 2控制每次预排序的间距,则预排序的总次数是logN(以2为底)。
分析过程:假设整个序列有N个元素。
(1)整个序列要进行预排序总次数n的计算过程
计算思路:当gap > 1时则希尔排序是进行预排序的过程,当gap = 1时则希尔排序是进行对整个序列进行直接插入排序的过程。所以计算预排序总次数n可以使用下面公式,公式如下图所示。
①类型1:gap = gap / 2 ——> 整个序列要进行预排序的次数n = log N(注意:这个对数是以2为底的)
推导过程:N/2/2/2/……2/2/2 = N / 2^n = 1,解得n = log N。
②类型2:gap = gap / 3 + 1 ——> 整个序列要进行预排序的次数n = log N(注意:这个对数是以3为底的)
推导过程:gap = gap / 3 + 1 ≈ gap / 3,使得N/3/3/3/……3/3/3 = N / 3^n = 1,解得n = log N。
(2)希尔排序在预排序过程到直接插入排序过程之间的时间复杂度变化图
假设是对最坏情况逆序序列进行希尔排序,而第一次预排序时间复杂度是O(N),最后的直接插入排序过程的时间复杂度是O(N),而中间的预排序的时间复杂度一定是比O(N)要大的。
以下是希尔排序在预排序过程到直接插入排序过程之间的时间复杂度的变化图:
总的来说,希尔排序的时间复杂度是O(N^1.3)。(注意:由于堆排序算法的时间复杂度是O(N*logN) 而希尔排序的时间复杂度是O(N^1.3),所以可以认为希尔排序算法和堆排序算法的排序效率是旗鼓相当的即希尔排序算法和堆排序算法是有的一比的)
5.对希尔排序的总结
(1)希尔排序是对直接插入排序的优化。
(2)当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。