排序
考虑大规模堆数据排序
只考虑内部排序:内存充分大,所有数据都能加载到内存中
排序的稳定性:任意两个相等的数据,排序前后的相对位置不发生改变
没有一种排序是任何情况下都是表现最好的
逆序对
- 对于下标i<j,如果a[i]>a[j],则(i,j)为逆序对(inversion)
- 交换两个相邻元素正好消去一个逆序对
- 任意N个不同元素组成对序列平均具有 N ( N − 1 ) / 4 N(N-1)/4 N(N−1)/4个逆序对。
- 任意一个以交换两个相邻元素对排序算法,平均时间复杂度为 Ω ( N 2 ) \Omega(N^2) Ω(N2)(下界,最好情况)
- 要提高算法效率,每次需要消去不止一个逆序对,每次要交换两个较远的元素。
以下默认int型升序排序
冒泡排序
int i,j;
for(i=0;i<N;i++){
for(j=0;j<N-i-1;j++){
......
}
}
for(i=N-1;i>=0;i--){
for(j=0;j<i;j++){
if(a[j]>a[j+1]){
swap(a[j],a[j+1]);
flag = 1;
}
if(!flag)// 如果一趟下来没有交换,则已经全部排好。
break;
}
}
最好
T
=
O
(
n
)
T=O(n)
T=O(n),最坏
T
=
O
(
n
2
)
T=O(n^2)
T=O(n2) ,稳定
后面是有序的,前面无序。
便于链表排序
插入排序
for(i=1;i<N;i++){//默认一个元素是有序的
tem = a[i];
for(j=i;j>0 && a[j-1]>tem;j--){
a[j] = a[j-1];
}
a[j] = tem;
}
最好
T
=
O
(
n
)
T=O(n)
T=O(n),最坏
T
=
O
(
n
2
)
T=O(n^2)
T=O(n2) ,稳定
前面是有序的,后面无序。比冒泡排序交换次数少。
若序列基本有序,则插入简单高效
希尔排序(shell)
为了提高算法效率,将每次交换的元素间隔拉大。
- N间隔排序:对i,i+N,i+2N,…,i+nN这些元素进行排序,然后再对i+1做上述操作,直到i=N。每次调整的是整个序列的一个子序列。然后将N变小,重复上述操作。为了保证序列有序,最后N必须等于1。
- N间隔排序后,在进行(N/2)间隔排序后,依然保持N间隔有序。
//原始希尔排序
for(i=N/2;i>0;i/=2){// i为间隔
for(j=i;j<N;j++){ // 插入排序
tem = a[j];
for(k=j;k>=0 && a[k-i]>tem;k-=i){
a[k]=a[k-i];
}
a[k]=tem;
}
}
最坏情况
T
=
θ
(
N
2
)
T=\theta(N^2)
T=θ(N2) (既是上界又是下界,即等价)
由于间隔序列不互斥,会导致小的间隔无效(即无元素交换)
Hibbard增量序列
D
k
=
2
k
−
1
D_k=2^k-1
Dk=2k−1相邻元素互斥
swdgewick增量序列
4
i
−
3
∗
2
i
+
1
4^i-3*2^i+1
4i−3∗2i+1
效果较好
选择排序
for(i=0;i<M;i++){
for(j=i;j<N;j++)
if(a[j]<min)
min = a[j];
swap(a[i],min);
}
T
=
θ
(
N
2
)
T=\theta(N^2)
T=θ(N2)
选择排序在交换两元素时不一定是交换相邻两个元素,故破坏了稳定性
堆排序
- 建堆 T ( N ) = O ( N l o g N ) T(N)=O(NlogN) T(N)=O(NlogN) ,需要额外空间 O ( n ) O(n) O(n),且复制元素需要时间
- 建最大堆,将最大元素放到当前堆末尾。平均比较次数 2 N l o g N − O ( N l o g l o h N ) 2NlogN-O(NloglohN) 2NlogN−O(NloglohN)
实际效果不如swdgewick增量序列堆希尔排序
归并排序
核心:有序子列堆归并
T
(
N
)
=
O
(
N
)
T(N)=O(N)
T(N)=O(N),但元素需要不停但复制,且需要额外的空间,不常用于内排序,常用于外排序。
稳定的排序
// 递归
void M(int a[],int tem[],int l,int r,int e){
int tem = l,m = r-1,beg= l;
while(l<= m && r<= e){
if(a[l] <= a[r]){ //保证稳定性
tem[in]=a[l];
l++
}elsr{
tem[in]= a[r];
r++;
}
in++;
}
while(l<=m){
tem[in]=a[l];
in++;
l++;
}
while(r<=e){
tem[in]=a[r];
r++;
in++;
}
for(i=beg;i<=e;i++)// 将数据倒回原来倒数组
a[i]=tem[i];
}
void sort(int a[],int tem[],int b,int e){
int m = (b+e)/2;
if(b<e){
sort(a,tem,b,m);
sort(a,tem,m+1,e);
m(a,tem,a,m+1,e);
}
}
// 非递归
// 合并子列
void M1(int a[],int tem[],int l,int r,int e){
int m=r-1,in= l;
while(l<=m && r<=e){
if(a[r]>a[l]){
tem[in] = a[r];
r++;
}else{
tem[in] = a[l];
l++;
}
in++;
}
while(r<=e){
tem[in]= a[r];
in++;
r++;
}
while(l<=m){
tem[in]= a[l];
in++;
l++;
}
}
// 合并长度为len的全部子列
void M_pass(int a[],int tem[],int n,int len){
for(i=0;i<n-2*len;i+=2*len)
M1(a,tem,i,i+len,i+2*len-1);
if(i+len <n)//最后剩一个完整倒和一个不完整的字串
M1(a,in,i,i+len,n-1);
else// 只剩一个不完整的字串
for(j=i;j<n;j++)
tem[j] = a[j];
}
void sort(int a[],int N){
int tem[N],len=1;
while(len<N){
M_pass(a,tem,N,len);
len *=2;
M_pass(a,tem,N,len);
}
}
快排
不是永远都是最好的。
且若实现的不好,则很慢
分而治之
选主元 pivot ,即选出中间数
当最好情况是主元是中位数,最坏是有序的。
选主元可以从头,中,尾三个数中取中位数。
快排快的原因之一,是主元直接放到了最终的位置上。
快排是用递归,不适合小规模数据。对于N<100,可能不如插入。
定义一个cutoff,小于该阈值,使用其他排序,如简单排序
//利用首,中,尾三个元素选主元
int median3(int a[],int l,int r){
int m = (r+l)/2;
// 保证左面最小,右面最大,实现排序
if(a[l]>a[m])
swap(a[l],a[m]);
if(a[l]>a[r])
swap(a[l],a[r]);
// 以上保证左面最小,以下保证右面最大
if(a[m]>a[r])
swap(a[m],a[r]);
swap(a[m],a[r-1]);
// 下一步划分子集时,只需要考虑l+1,r-2之间的元素
return a[r-1];
}
void q_sort(int a[],int l,int r){
int p;
if(r-l >cutoff){// 使用快排
p = median3(a,r,l); //选主元
i=l ;
j = r -1;
// 待划分的区域是i+1到r-2
for(;;){
while(a[++i]<p){}
while(a[--j]>p){}
if(i<j)
swap(a[i],a[j]);
else
break;
}
}else{ //规模太小,使用其他排序,如插入排序
other_sort(a,l,r);
}
}
void sort(int a,int l){
q_sort(a,0,l-1);
}
表排序
对比较复杂对元素进行排序,如复杂的结构体,不能不考虑交换元素之间的时间。只是移动对应的指针。是间接排序,利用一个新数组做为表(table),表中存放实际元素对应的指针,只需将指针排序即可。
若需要将元素按顺序排,而不仅仅是排指针,则需要移动元素,如何使移动元素所消耗时间最短是要解决的问题。下面给出线型时间复杂度对移动算法。
N个数字的排列是由若干个环组成的,如下图,红色为一个环,绿色为一个环,蓝色为一个环。
每次移动只对一个环内元素进行移动,首先需要保存环上第一个元素对应对数据,将其他元素调到对应对位置,每次移动一个元素后,将其table值设为对应对下标,即table[i]=i;当遇到table[i]=i时,一个环结束。
最好情况,有序
最坏情况:每个环有两个元素。
基数排序
实现某种情况下的线性时间复杂度。
桶排序
n个数据排序,只有m种取值,且n>>m,建立n个桶,扫描一次n个数据,将数据放到对应的桶中,近似线性。
当m>>n时,需要基数排序,实现近似线性复杂度。
当n=10,m=1000,使用次位优先(Least Significant digit)。用最低位进行排序,几位数决定需要排几次,每次拍完后,按序去取出
也可以对应多种排序方式
总结
一次后位置固定:冒泡,插入
reference
浙大 数据结构 mooc https://www.icourse163.org/course/ZJU-93001