四、希爾排序
希爾排序其實是直接插入排序算法的一種變形,實質是分組插入排序。又稱縮小增量排序。
該方法首先要理解分組操作,其實是以間隔分組,即每間隔幾個數後就分做一組,然後進行插入排序。接着把間隔縮短一半,以此下去,直到間隔沒有,才停下操作。
如:有10個數據,開始的時候每間隔10/2 = 5 劃爲一組,即第1個數與第6個數爲一組,第2個數與第7個數爲一組。。。。。。以此類推,然後對每一組的數進行插入排序。排序好後縮短間隔,5/2 = 2,即每間隔2個數就劃爲一組,以此操作直到間隔爲0,無法縮小間隔爲止。
如,對10、49、23、1、5、50、44、8、23、2、6這10個數據進行希爾排序。
第一次分組的時候: inter = 10/2 = 5
第一次分組如圖,分成了5組,{A1,B1},{A2,B2},{A3,B3},{A4,B4},{A5,B5}這五個組,接着對每一組進行直接插入排序。(沒間隔幾個數,就分成幾組)
第二次分組的時候: inter = 5/2 = 2
現在分成了2組,同上,對每組進行直接插入排序
第三次分組的時候: inter = 2/2 = 1
現在只有一組了,仍然進行插入排序
第四次無法分組了: inter = 1/2 = 0
希爾排序實現的代碼如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10000;
int a[maxn];
int main()
{
int n;
while(cin>>n)
{
for(int i=1; i<=n; i++) cin>>a[i];
//開始不斷地縮短間隔,直到間隔爲0。第一次間隔爲n/2
for(int inter = n/2; inter>0; inter/=2)
{
//每一次縮短間隔,其間隔數等於所分的組數,排序的時候從第1組到第inter組進行插入排序。
for(int gap=1; gap<=inter; gap++)
{
//下面是插入排序的代碼,稍微改變了一下遞增遞減量,因爲是每次間隔inter進行排序。
//大致的插入排序代碼並沒有改變
for(int i=gap+inter; i<=n; i+=inter)
{
int cur;
for(cur=i-inter; cur>=1; cur-=inter)
{
if(a[cur]<a[i])break;
}
if(cur!=i-inter)
{
for(int k=i-inter; k>cur; k-=inter)
swap(a[k+inter], a[k]);
}
}
}
}
for(int i=1; i<=n; i++)
cout<<a[i]<<" ";
cout<<endl;
}
return 0;
}
爲了讓代碼儘可能地貼近所描述的語言,上面的代碼寫得過於複雜,看得有些頭疼,我們可以把該代碼縮減爲以下代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10000;
int a[maxn];
int main()
{
int n;
while(cin>>n)
{
for(int i=1; i<=n; i++) cin>>a[i];
//縮短間隔,分組。
for(int inter=n/2; inter>0; inter/=2)
{
//從第inter個數開始,這個與普通的直接插入排序從第二個數開始的意味相同
for(int i=inter; i<=n; i++)
{
//每個數與自己組裏的數進行直接插入排序
for(int cur=i-inter; cur>=1 && a[cur]>a[cur+inter]; cur-=inter)
swap(a[cur], a[cur+inter]);
}
}
for(int i=1; i<=n; i++)
cout<<a[i]<<" ";
cout<<endl;
}
return 0;
}
代碼精簡,不過不易懂,再次也不多講解這一個代碼,讀者自己思考一番吧。
五、堆排序
堆排序算法在處理大數據的效率是前面四種算法不可比的。
那麼要學堆排序,首先要對樹有個瞭解,準確地說是對二叉樹要又瞭解。堆排序就是一個特殊的完全二叉樹。
我們用數組存儲一堆數據,然後要把數組當做一個二叉樹來看。二叉樹特點是,一個父節點連接兩個兒子節點。開始的時候把一堆數存入數組裏,然後把數組看成二叉樹,即存數到數組裏後就建立好了一個二叉樹,此時的樹裏面的值都是混亂的。
如何去排序一棵二叉樹?以排序一個最小堆爲例。
一棵樹的“某一個節點”父子間的值所在的位置不對的時候,即兩個兒子中有值小於父親的值時,要排序父子關係的話,也會影響後面子孫的位置。
我們首先要對這種牽連一人而禍害一窩的行爲用一種方式進行處理(即用一個函數),然後遍歷每一個節點,用這種行爲處理方式處理每一個節點。
下面說說怎麼建立這種處理方式函數。
要找一種規律,就像數學思維一樣,任意取一個值設爲變量x,繞着x這個值進行某些變化直到某條件成立爲止。
這裏就任意取一個樹的節點作爲“變量節點 ”,從這個節點開始進行排序,首先找出該節點和該節點的兩個兒子之間的最小的,讓最小的與節點的值換一換。
如果沒有換值,那麼停止,證明了該節點與他的兒子順序是正確的;如果換值了,那麼接下來取與節點交換的兒子作爲“變量節點”,繼續以上操作,直到要麼是到了最後的葉處即到底了,要麼是兒子與父親的值沒有交換爲止。可見這裏是一個遞歸操作。
好的,找到了處理一個節點關係的方式(函數),那麼我們現在要遍歷每個節點,用這個方式處理每一個節點,以達到排序一個數組的目的。
那麼,如何遍歷纔是好呢?我們找到的處理方式是用來處理有兒子的節點,是吧。那麼從只有兒子沒有孫子的節點開始處理。
這樣從底部打好基礎,節省了很多的麻煩。
根據樹的定義,有N個節點,那麼兒子是葉的節點是從第n/2個節點開始的,前面的節點都有子孫了。那就是從第n/2個節點開始遍歷到根節點。
說來說去,有點繞,結合下面的代碼有助於消化堆排序。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100000;
int a[maxn];
//這個就是一個牽一髮而動全身的處理方式,一個遞歸函數
//參數即當前的節點的位置,還有樹的節點數
void softdown(int cur, int n)
{
//flag是爲了判斷該節點的關係是否正常,如果兒子的值與父親的值沒有交換過,則flag = 1;
//temp 爲了記錄最小值的位置
int flag = 0, temp;
while(cur*2<=n && flag==0)
{
//首先判斷左兒子與父親的值
if(a[cur]>a[cur*2]) temp = cur*2;
else temp = cur;
//判斷是否有右兒子存在
if(cur*2+1 <= n)
{
if(a[temp]>a[cur*2+1]) temp = cur*2+1;
}
//如果發現最小值的位置不在父節點處,那麼交換節點值,繼續下一個節點進行處理
//否則遞歸停止
if(cur != temp) swap(a[cur], a[temp]);
else flag = 1;
cur = temp;
}
}
void create(int n) //排序二叉樹
{
for(int i=n/2; i>=1; i--)
softdown(i, n);
}
//因爲根節點的值是最小的,所以每次輸出根節點的值
//然後把最後一位與根節點值交換,順便讓長度-1.再排序一次樹。
//這樣就成了數組裏存的值是從大到小排序,但輸出的時候是從小到大。
void Cout(int n) //輸出函數
{
while(n)
{
cout<<a[1]<<" ";
swap(a[1], a[n]);
n--;
softdown(1, n);
}
}
int main()
{
int n;
while(cin>>n)
{
for(int i=1; i<=n; i++) cin>>a[i];
create(n);
Cout(n);
cout<<endl;
//直接輸出數組裏的數進行比較一番
for(int i=1; i<=n; i++) cout<<a[i]<<" ";
cout<<endl;
}
return 0;
}