《算法》系列 知识整理(C++描述)
算法学习历程
- 排序算法
- 查找算法
- 图
- 字符串问题
- 智能算法
学习目录
- 排序算法
- 初级排序算法
- 归并排序
- 快速排序
- 优先队列
- 排序算法的应用
本文主要内容
本片文章主要讲述初级排序算法。
包括:选择排序、插入排序、希尔排序、选择排序与插入排序的比较 。
初级排序算法
选择排序
算法宏观描述:首先,在数组中选择其中最小(或最大)的一个元素,将其与第一个元素进行交换,重复选择操作,在剩余的数组中选择最小的元素,将其与第二个元素交换,依次推类。
在宏观描述中可以看到,有两个核心的步骤:选择和交换 ,而在“选择”这个步骤内,核心的是进行比较。
因此要衡量和实现选择排序算法,就要分析比较和交换的次数。
首先来看结论,然后我们来进行分析和证明。
结论:对于长度为N的数组,先择排序需要大约 N²/2次比较和N次交换。
推导:
先分析0~N-1中每排好一个元素需要的交换和比较次数,再累加便得到总的次数。
在确定0~N-1中的第i个位置元素时,需要在这个元素及其后共N-i个元素中选择出最小(或最大)的元素,即需要比较N-i-1次,找到最小元素后进行交换,即交换1次。固:
总的交换次数为N;
总的比较次数为:(N-1)+(N-2)+……+2+1=
N
(
N
−
1
)
2
\frac{N(N-1)}{2}
2N(N−1)~
N
²
2
\frac{N²}{2}
2N²
总结出选择排序的特点如下:
- 运行时间与输入的待排序数组内容无关。
当选择排序扫描完一次数组后,会完成一个位置的排序,但本次扫描并不能为下一次扫描提供任何信息。因此当我们分别用“完全无序”以及“基本有序”的两种序列测试选择排序算法时,会发现所花费的时间几乎完全一样。 - 数据的移动次数和其他算法相比,几乎是最少的。
每交换一次,即代表完成了一个位置的排序,对于长度为N的序列,排好序仅需N次交换。
最后附上C++描述的选择排序代码。
void exch(int *a,int i,int j)
{
int t=a[i];
a[i]=a[j];
a[j]=t;
}
void Selection_sort(int *a,int N)
{
//将a按照升序排列,n代表数组长度.
for(int i=0;i<N;i++)
{
int minn=i;
for(int j=i+1;j<N;j++)
{
if(a[j]<a[minn])
minn=j;
}
exch(a,i,minn);
}
}
插入排序
算法宏观描述: 同选择排序,当前索引的左侧元素都为有序的(初始时单个元素即为有序),对于当前状态,将索引的右侧的元素依次插入到索引左侧,并保证每次插入过后索引左侧仍为有序。当索引到达数组右侧时,排序完成。
在算法的描述中,我们可以发现,插入排序对于接近有序的序列排序是非常快的。
关于插入排序,我们也来分析一下它的比较和交换次数。
对于当前的索引元素,其左侧是已经有序的序列,索引元素及右侧元素为待排序元素。
现考虑平均状况,对于每个索引元素(记为第i个元素,从0开始标号),都有可能插入至其左侧序列的中间位置,即需要交换
i
2
\frac{i}{2}
2i次,比较
i
2
\frac{i}{2}
2i+1次。其中除法均为整除。
为方便下一步计算,举例:第4个元素可能经过会经过2次交换和3次比较,第5个元素同样可能会经过2次交换和3次比较;同样地,第6、7个元素会经过3次交换和4次比较……第n-1和第n个元素经过
N
2
\frac{N}{2}
2N次交换和
N
2
\frac{N}{2}
2N+1次比较。
所以我们得到,平均状态下:
总的交换次数为1+1+2+2+……+
N
2
\frac{N}{2}
2N+
N
2
\frac{N}{2}
2N=
N
²
4
\frac{N²}{4}
4N²+
N
2
\frac{N}{2}
2N~
N
²
4
\frac{N²}{4}
4N²
总的比较次数为交换的次数加上一个常数项,在上面的例子中我们看到,比较次数会比交换次数大1,原因是当索引元素到达目的地后,通过比较前面的一个元素,发现不需要再往前移动了,即该元素排序完成,这最后一次比较并没有发生交换,因此会多1。所有的元素会多出N-1(第一项不需要进行比较)。
所以总的比较次数在平均状况下同交换次数相同,大致满足
N
²
4
\frac{N²}{4}
4N²。
在最坏情况下,每个元素都可能会插入到已排序序列的左端,即需要经历整个有序序列。此时:
总的交换次数为:1+2+……+N=
N
²
2
\frac{N²}{2}
2N²+
N
2
\frac{N}{2}
2N~
N
²
2
\frac{N²}{2}
2N²
总的比较次数为:总的交换次数+(N-1)~
N
²
2
\frac{N²}{2}
2N²
在最好的情况下,序列已经有序,因此每个元素只需要比较1次,不需要经过交换。即:
总的交换次数为0
总的比较次数为N-1
最后,附上插入排序的C++代码:
void exch(int *a,int i,int j)
{
int t=a[i];
a[i]=a[j];
a[j]=t;
}
void Insertion_sort(int *a,int N)
{
for(int i=1;i<N;i++)
{
for(int j=i;j>0;j--)
{
if(a[j]<a[j-1])
exch(a,j,j-1);
}
}
}
对于插入排序,我们考虑一种情况——部分有序。在这种情况下,选择排序的效率几乎可以说比其他任何排序算法都要高。
介绍部分有序的概念前,先介绍倒置。
倒置:制两个顺序颠倒的元素。
部分有序:当数组中倒置的数量较少时(官方定义为小于数组大小的某个倍数,没有太精确的范围),称这个数组为部分有序的。
几个典型的部分有序数组:
- 数组中每个元素举例它的最终位置都不远
- 一个有序的大数组接一个不一定有序的小数组
- 数组中只有几个元素的位置不正确
在这种情况下,插入排序非常有效,且此时:
交换次数=数组中倒置的数量
倒置的数量≤比较次数≤倒置的数量+数组的大小-1
证明这个并不难:
每次交换会改变一对元素的位置,因此每次交换相当于减少一对倒置,即交换次数与倒置数量相同。
每次交换会伴随一次比较,因此最少的比较次数与交换的数量相同,一般的情况下,1到N-1之间的每个i都可能需要一次比较。
选择排序与插入排序的比较
- 对于随机排序的无重复数组,插入排序与选择排序的运行时间是平方级别,二者之比为一个较小的常数。
希尔排序
希尔排序算法是基于插入排序算法的改进。
当数组混乱无序且规模较大时,插入排序的很慢,因为插入排序交换的只是相邻的元素,当数组很长时,很容易出现移动举例较远的情况,而此时元素只能一步一步地通过交换移动到目的地。
这让我们不禁猜想,是否有可能让元素“大步走“,通过交换不相邻的元素,使数组快速地进行排序。
算法宏观描述:
将数组中任意间隔为h的元素看作一个子数组,通过插入排序,将h个子数组独立地排序,再逐步缩小h的值,使数组逐渐有序。
举个栗子:
排序:S H E L L S O R T E X A M P L E
取h=13,h/=3
输入:S H E L L S O R T E X A M P L E
取13:P H E L L S O R T E X A M S L E
取4 :L E E A M H L E P S O L T S X R
取1 :A E E E H L L L M O P R S S T X
从其中不难发现一个问题:h的值如何确定?
很遗憾,这个问题并没有确定的回答。,常使用的递增序列为:1,3,13,40,121,……,3*(n-1)+1,……
当使用这个序列时,比较的次数不超过N的若干倍乘递增序列长度。
目前关于希尔排序效率的最重要结论为:运行时间达不到平方级别。
最后,附上希尔排序的C++代码:
void exch(int *a,int i,int j)
{
int t=a[i];
a[i]=a[j];
a[j]=t;
}
void shell_sort(int *a,int N)
{
int h=1;
while(h<N/3)
h=3*h+1; //h=1,4,13,40,121...
while(h>=1)
{
for(int i=h;i<N;i++)
{
for(int j=i;j>=h;j-=h)
{
if(a[j]<a[j-h])
exch(a,j,j-h);
}
}
h=h/3;
}
}
关于初级排序算法,暂且先整理这些内容,后续若有新的想法,或应评论区执政,会再进行补充、修改。
欢迎提出宝贵意见。