内部串行排序算法
一、比较策略算法
1、冒泡排序
稳定性:稳定
思想:每次调用,都会至少有一个元素就位,整体复杂度呈现算术级数的形式。若一次调用过程没有发生元素交换,则说明所有元素就位,最差和最理想情况下的复杂度为
O
(
n
2
)
O(n^2)
O(n2)和
O
(
n
)
O(n)
O(n),平均复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
(1)基础版本:
bool bubble(Rank lo, Rank hi){
bool sorted = true;
while(++lo < hi){
if(Array[lo-1] > Array[lo]) {
sorted = false;
int holder = Array[lo-1];
Array[lo-1] = Array[lo];
Array[lo] = holder;
}
}
return sorted;
}
void bubbleSort(Rank lo, Rank hi){
while(!bubble(lo,hi--));
}
(2)优化版本:面对后缀部分元素已经就位的情况,原先的算法仍然会进行比较,因此可从此处着手改进,方法是记录上一次扫描交换进行的最后一次交换位置,将hi更新为这个值, 减少不必要的比较次数,最差和最理想情况复杂度依然是 O ( n 2 ) O(n^2) O(n2)和 O ( n ) O(n) O(n)。
bool bubble(Rank lo, Rank hi){
Rank last = lo ;
while(++lo < hi){
if(Array[lo-1] > Array[lo]) {
last = lo;
int holder = Array[lo-1];
Array[lo-1] = Array[lo];
Array[lo] = holder;
}
}
return last;
}
void bubbleSort(Rank lo, Rank hi){
while(lo< bubble(lo,hi));
}
优化版本和初始版本的对比如下图所示:
2、选择排序
(1)思想:依次从待排序序列中选出最小的元素放入排序序列。冒泡排序也可以看成是一种选择排序,但其最坏情况下需要
O
(
n
2
)
O(n^2)
O(n2),元素在转移至最终位置前往往要经过多次位置交换,选择排序则是一次交换直接将元素移动到目标位置。
(2)实现:
void selectionSort(Rank lo,Rank hi){
Rank i,j,smallest;
for( i = 0;i<(hi-lo-1);i++){
smallest = i;
for(j = i+1;j<hi;j++)
if(Array[j]<Array[smallest]) smallest = j;
int swap = Array[smallest];
Array[smallest] = Array[i];
Array[i] = swap;
}
}
(3)性能:
空间复杂度:O(1)
在所有情况下时间复杂度:O(n^2),但其主要消耗于比较操作,冒泡排序则是主要消耗于交换操作,因此选择排序的性能要较冒泡排序高。
稳定性:稳定
(4)能否对比较操作做进一步改进?
sure.
3、插入排序
(1)思想:
使用列表进行组织(不能使用向量), 方式可类比抽扑克牌在手中排好序
(2)实现:
(3)性能:
1、时间复杂度:
最好情况下
O
(
n
)
O(n)
O(n),最坏和平均情况下
O
(
n
2
)
O(n^2)
O(n2);
为求平均情况的时间复杂度,需假设各元素取值遵守均匀独立分布,采用后向分析(backward analysis)求每次插入平均需要的比较次数。
当L(r)完成插入后,是插入到了列表的什么地方呢?因为各元素取值均匀独立分布,因此,每个位置的可能性均为
1
/
(
r
+
1
)
1/(r+1)
1/(r+1),而从后到前所需的比较次数从1到r递增。
因此比较成本可表示为
[
0
+
1
+
.
.
.
+
r
]
/
(
r
+
1
)
+
1
=
r
/
2
+
1
[0+1+...+r]/(r+1)+1 = r/2+1
[0+1+...+r]/(r+1)+1=r/2+1,
和前缀长度呈线性关系。
将每个元素的比较成本相加,可得整体的比较成本为:
[
0
+
1
+
.
.
.
+
(
n
−
1
)
]
/
2
+
1
=
O
(
n
2
)
[0+1+...+(n-1)]/2+1 = O(n^2)
[0+1+...+(n−1)]/2+1=O(n2)
2、空间复杂度:
O
(
1
)
O(1)
O(1)
3、稳定性:稳定
(4)插入排序与逆序对
逆序对:两个元素,左侧的元素大于右侧,则称这两个元素构成了一个逆序对。
在插入排序中,设待插入结点为P,其在排好序的序列中逆序对的个数
i
(
P
)
i(P)
i(P)就是其在插入过程中需要经过的比较次数。
因此,整个序列中的逆序对数
∑
P
(
i
)
\sum_P(i)
∑P(i),即排序过程中比较次数总和,加上元素插入的时间总和
O
(
n
)
O(n)
O(n)即是插入排序总共所花费的时间。
在最好情况下,无逆序对,所有元素都是顺序输入的。
而在最坏情况下,任何一对元素都是逆序对。
可以将插入排序看成是将逆序对逐渐修复的过程,因此插入排序输入对“输入敏感的”排序算法。
4、比较策略排序算法的下界:
基于比较策略的算法的比较过程可用一颗二叉树来表示,最坏时间复杂度取决于树的深度。
高度为h的二叉树,叶结点数目小于等于2^h, 排序算法的叶结点<=n,因此排序算法的树高h> =log2n ,因此基于比较策略的排序算法的复杂度下界为
l
o
g
(
n
)
log(n)
log(n)。
二、 分治策略算法
1、归并排序
最好和最坏的情况下,复杂度都为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),稳定。
(1)思想:将待排序的序列一分为二(
O
(
1
)
O(1)
O(1) ),对子序列递归排序(
2
∗
T
(
n
/
2
)
2*T(n/2)
2∗T(n/2)),合并有序子序列(merge)。
(2)实现:
void merge(Rank lo,Rank mid , Rank hi){
//A指向区间的起点
int * A = Array + lo;
//将向量的前半段复制到新开辟的空间中
int lb = mid - lo ;int *B = new int[lb];
for(int i = 0;i<lb;i++)B[i] = A[i];
//C指向区间的后半段,C并不需要另辟空间缓存
int lc = hi - mid;
int * C = Array + mid;
/*
每次比较B,C两个子向量当前的首元素,
取出其中更小的项,放入到A中
j或k每次其中一个会加1,初始j+k = 0,最终j+k = n。
因此迭代最多只会进行n次,merge算法时间复杂度为O(n)
*/
for(Rank i =0,j=0,k=0;(j<lb)||(k<lc);){
if((j<lb)&&((k>=lc)||(B[j]<=C[k]))) A[i++] = B[j++];
if((k<lc)&&((j>=lb)||(C[k]< B[j]))) A[i++] = C[k++];
}
delete []B;
}
void mergeSort(Rank lo,Rank hi){
if((hi - lo)<2 )return ;
int mid = (hi + lo) >> 1 ;
mergeSort(lo, mid);
mergeSort(mid, hi);
merge(lo,mid,hi);
}
因为merge算法的时间复杂度为
O
(
n
)
O(n)
O(n)(见代码注释),
因此归并排序递推式为:
T
(
n
)
=
2
∗
T
(
n
/
2
)
+
O
(
n
)
T(n)=2*T(n/2)+O(n)
T(n)=2∗T(n/2)+O(n)
解得
T
(
n
)
=
O
(
n
l
o
g
n
)
T(n)=O(nlogn)
T(n)=O(nlogn)
2、快速排序
(1)与归并排序的区别:
相同点:和归并排序一样,都是将两个排好序的子序列合二为一。
不同点:
1、对两个要合并的序列,快速排序要求Smax(L)<= Smin( R),不用merge
2、归并排序能保证划分出来的两个子任务彼此规模相当,而快速排序则不能,具体取决于候选轴点的选取
(2)轴点转化算法:
/*
将候选元素转化为轴点,方法:
将候选轴点“取出”,原位置成为“空闲单元”,
设lo与hi两个指针,初始指向序列的首元素和末元素,
hi向左移动,直到hi指向的元素比候选轴点小,则将其放入当前的“空闲单元”,hi指向的位置成为新的“空闲单元”;
lo向右移动,直到指向的元素比候选轴点大,~。
lo和hi交替移动,直到lo和hi相遇,则其位置就是轴点所在,将候选元素放入。
*/
Rank partition(Rank lo,Rank hi){
//随机选取候选轴点
int seq = rand()%(hi-lo) + lo;
Rank n = Array[seq];
Array[seq] = Array[lo];
Array[lo] = n;
Rank pivot = Array[lo];
//轴点转化
while(lo<hi){
while((lo<hi)&&(pivot<=Array[hi]))hi--;
Array[lo] = Array[hi] ;
while((lo<hi)&&(pivot>=Array[lo]))lo++;
Array[hi] = Array[lo] ;
}
Array[lo] = pivot;
return lo;
}
void quicksort(Rank lo,Rank hi){
if(hi-lo<2) return ;
Rank mi = partition(lo,hi);
quicksort(lo, mi);
quicksort(mi+1, hi);
}
(3)性能分析
1、稳定性:不稳定,在轴点构造的过程中可能会颠倒元素的次序。
2、空间复杂度:可以就地实现,只需要维护常数个指针,空间复杂度O(1)
3、时间复杂度:
最优性能:每次选取的轴点都能讲序列均衡划分,此时:
T
(
n
)
=
2
∗
T
(
(
n
−
1
)
/
2
)
+
O
(
n
)
=
O
(
n
l
o
g
n
)
T(n) = 2*T((n-1)/2) + O(n) = O(nlogn)
T(n)=2∗T((n−1)/2)+O(n)=O(nlogn)
最差性能:每次轴点选取的都是最大/最小的元素,此时
T
(
n
)
=
T
(
n
−
1
)
+
T
(
0
)
+
O
(
n
)
=
O
(
n
2
)
T(n) = T(n-1)+T(0)+O(n) = O(n^2)
T(n)=T(n−1)+T(0)+O(n)=O(n2)
平均性能:
设序列中所有元素都是均匀独立分布,设选取的候选轴点最终的秩为k,则性能表示如下:
T
(
n
)
=
(
n
+
1
)
+
(
1
/
n
)
×
T(n)=(n+1)+(1/n)\times
T(n)=(n+1)+(1/n)×
∑
k
=
0
n
−
1
[
T
(
k
)
+
T
(
n
−
k
−
1
)
]
\sum_{k=0}^{n-1} [T(k)+T(n-k-1)]
∑k=0n−1[T(k)+T(n−k−1)]
化简得到:
T
(
n
)
/
(
n
+
1
)
=
2
/
(
n
−
1
)
+
2
/
n
+
T
(
n
−
2
)
/
(
n
−
1
)
T(n)/(n+1) = 2/(n-1) + 2/n +T(n-2)/(n-1)
T(n)/(n+1)=2/(n−1)+2/n+T(n−2)/(n−1)
可将T(n)/(n+1)看成S(n),T(n-2)/(n-1)看成S(n-2),由此递推…构成调和级数
调和级数与自然对数logN同阶,因此快速排序的平均运行时间不超过
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
(4)优化
快速排序算法的优化可以从两方面考虑:
1、优化候选轴点选取: 众数
2、优化轴点构造算法
(5)优化轴点选取
1、众数选取
4、希尔排序
三、 散列算法
1、桶排序
2、基数排序
3、奇数排序
四、 优先级队列
1、堆排序
2、锦标赛排序
https://play.google.com/apps/test/RQFlordB7z8/ahAIGJVrcSQNYxHGIYbG31j5ctZzYMIhwLHRyBXkaNfF1hWjOsI-P0oBIeGy-R0igQciAypFSaAdXXFhfEGOw_48xy