排序
考慮大規模堆數據排序
只考慮內部排序:內存充分大,所有數據都能加載到內存中
排序的穩定性:任意兩個相等的數據,排序前後的相對位置不發生改變
沒有一種排序是任何情況下都是表現最好的
逆序對
- 對於下標i<j,如果a[i]>a[j],則(i,j)爲逆序對(inversion)
- 交換兩個相鄰元素正好消去一個逆序對
- 任意N個不同元素組成對序列平均具有個逆序對。
- 任意一個以交換兩個相鄰元素對排序算法,平均時間複雜度爲(下界,最好情況)
- 要提高算法效率,每次需要消去不止一個逆序對,每次要交換兩個較遠的元素。
以下默認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;
}
}
最好,最壞 ,穩定
後面是有序的,前面無序。
便於鏈表排序
插入排序
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;
}
最好,最壞 ,穩定
前面是有序的,後面無序。比冒泡排序交換次數少。
若序列基本有序,則插入簡單高效
希爾排序(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;
}
}
最壞情況 (既是上界又是下界,即等價)
由於間隔序列不互斥,會導致小的間隔無效(即無元素交換)
Hibbard增量序列
相鄰元素互斥
swdgewick增量序列
效果較好
選擇排序
for(i=0;i<M;i++){
for(j=i;j<N;j++)
if(a[j]<min)
min = a[j];
swap(a[i],min);
}
選擇排序在交換兩元素時不一定是交換相鄰兩個元素,故破壞了穩定性
堆排序
- 建堆 ,需要額外空間,且複製元素需要時間
- 建最大堆,將最大元素放到當前堆末尾。平均比較次數
實際效果不如swdgewick增量序列堆希爾排序
歸併排序
核心:有序子列堆歸併
,但元素需要不停但複製,且需要額外的空間,不常用於內排序,常用於外排序。
穩定的排序
// 遞歸
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