? ? ? ?程序其實(shí)就是對(duì)數(shù)據(jù)的增刪改查 以及對(duì)我們所得的數(shù)據(jù)進(jìn)行排序。既然涉及到數(shù)據(jù)的處理就肯定會(huì)要牽扯到排序的算法選擇。今天我主要是想分享一下通過自己對(duì)一些比較基礎(chǔ)的小算法的研究得出了一些算法的應(yīng)用場(chǎng)景以及某些算法的優(yōu)缺點(diǎn)。當(dāng)然這是在特定環(huán)境下相對(duì)的。如最簡(jiǎn)單的冒泡排序,我們都知道冒泡排序的算法效率是很低的。但是如果在整個(gè)數(shù)據(jù)大部分已經(jīng)是有序的情況下,那么它的效率比其他的算法就相對(duì)來說會(huì)要高出許多。通過這個(gè)我只是想闡述我的一個(gè)觀點(diǎn):沒有最強(qiáng)的算法只有最適合的算法。
下面進(jìn)入本節(jié)的正題,主要采用例子來說明。個(gè)人覺得這樣更容易讓我們聯(lián)系到場(chǎng)景的應(yīng)用:
快速簡(jiǎn)潔-桶排序
? ? ? ? 一個(gè)班級(jí)有五個(gè)人 他們的考試成績(jī) 5 個(gè)同學(xué)分別考了 5分、3 分、5 分、2 分和 8 分 如果讓您給這五位同學(xué)的考試成績(jī)排序您會(huì)怎樣做?
? ? ? ? 正常的思維 如果代碼要求簡(jiǎn)單我們可能會(huì)采用冒泡排序,如果是對(duì)排序效率要求很高的同學(xué)可能會(huì)采用快速排序,但是當(dāng)我們寫好十幾行代碼給他們排序之后 我們可能會(huì)發(fā)現(xiàn)我們僅僅只是需要給五個(gè)數(shù)排序。有沒有殺雞用牛刀的感覺~~?
通過上面的數(shù)字特點(diǎn)我們可以發(fā)現(xiàn)這些數(shù)字存在某些特點(diǎn):數(shù)字都是小于10而且分?jǐn)?shù)都是整數(shù) 離散區(qū)間比較小。這時(shí)我們何不定義一個(gè)從1-10的數(shù)組?
int[ ] a = new int[10];
a[5] = a[5]+1;
a[3] = a[3]+1;
a[5] = a[5]+1;
a[2] = a[2]+1;
a[8] = a[8]+1;
? ? ? ? 這樣我們只要循環(huán)一次這個(gè)數(shù)組即可 數(shù)組的值代表這個(gè)下標(biāo)的值出現(xiàn)了幾次 這樣我們就給這一序列的數(shù)字排好序了。這也就是最簡(jiǎn)單的桶排序。桶排序從 1956 年就開始被使用,該算法的基本思想是由E.J.Issac 和 R.C.Singleton提出來的。當(dāng)然桶排序也有它的缺陷,如果數(shù)的離散區(qū)間過大那么采用桶排序就顯得有點(diǎn)浪費(fèi)空間了。例如需要排序數(shù)的范圍是 0~2100000000 之間,那你則需要申請(qǐng) 2100000001 個(gè)變量,也就是說要寫成 int a[2100000001]。因?yàn)槲覀冃枰?2100000001 個(gè)“桶”來存儲(chǔ) 0~2100000000 之間每一個(gè)數(shù)出現(xiàn)的次數(shù)。即便只給你 5 個(gè)數(shù)進(jìn)行排序(例如這 5 個(gè)數(shù)是 1、1912345678、2100000000、18000000 和 912345678),你也仍然需要 2100000001 個(gè)“桶”,這真是太浪費(fèi)空間了!還有,如果現(xiàn)在需要排序的不再是整數(shù)而是一些小數(shù),比如將 5.56789、2.12、1.1、3.123、4.1234這五個(gè)數(shù)進(jìn)行從小到大排序又該怎么辦呢?接下來我們就可以采用冒泡排序可以很好的解決這個(gè)問題
鄰居好說話——冒泡排序
? ? ? ?冒泡排序的基本思想是:每次比較兩個(gè)相鄰的元素,如果它們的順序錯(cuò)誤就把它們交換過來。
? ? ? ?例如我們需要將 12 35 99 18 76 這 5個(gè)數(shù)進(jìn)行從大到小的排序。
? ? ? ? 首先比較第 1 位和第 2 位的大小,現(xiàn)在第 1 位是 12,第 2 位是 35。發(fā)現(xiàn) 12比 35 要小,因?yàn)槲覀兿M叫≡娇亢舐?,因此需要交換這兩個(gè)數(shù)的位置。交換之后這 5 個(gè)數(shù)的順序是35 12 99 18 76。按照剛才的方法,繼續(xù)比較第 2 位和第 3 位的大小,第 2位是 12,第 3位是 99。12比99 要小,因此需要交換這兩個(gè)數(shù)的位置。交換之后這 5 個(gè)數(shù)的順序是 35 99 12 18 76。根據(jù)剛才的規(guī)則,繼續(xù)比較第 3 位和第 4 位的大小,如果第 3 位比第 4 位小,則交換位置。交換之后這 5 個(gè)數(shù)的順序是 35 99 18 12 76。最后,比較第 4 位和第 5 位。4 次比較之后 5 個(gè)數(shù)的順序是 35 99 18 76 12。經(jīng)過 4 次比較后我們發(fā)現(xiàn)最小的一個(gè)數(shù)已經(jīng)就位(已經(jīng)在最后一位,請(qǐng)注意 12 這個(gè)數(shù)的移動(dòng)過程),是不是很神奇。現(xiàn)在再來回憶一下剛才比較的過程。每次都是比較相鄰的兩個(gè)數(shù),如果后面的數(shù)比前面的數(shù)大,則交換這兩個(gè)數(shù)的位置。一直比較下去直到最后兩個(gè)數(shù)比較完畢后,最小的數(shù)就在最后一個(gè)了。就如同是一個(gè)氣泡,一步一步往后“翻滾”,直到最后一位。所以這個(gè)排序的方法有一個(gè)很好聽的名字“冒泡排序”。“冒泡排序”的原理是:每一趟只能確定將一個(gè)數(shù)歸位。
代碼實(shí)現(xiàn):
<pre>
#includeint main()
{
int a[100],i,j,t,n;
scanf("%d",&n); //輸入一個(gè)數(shù)n,表示接下來有n個(gè)數(shù)
for(i=1;i<=n;i++) //循環(huán)讀入n個(gè)數(shù)到數(shù)組a中
scanf("%d",&a[i]);
//冒泡排序的核心部分
for(i=1;i<=n-1;i++) //n個(gè)數(shù)排序,只用進(jìn)行n-1趟
{
? ? ? ? ?for(j=1;j<=n-i;j++)
//從第1位開始比較直到最后一個(gè)尚未歸位的數(shù),想一想為什么到n-i就可以了。
{
? ? ? ? ?if(a[j] >a[j+1]) //比較大小并交換
? ? ? ? ?{ t=a[j]; a[j]=a[j+1]; a[j+1]=t; }
? ? ? ?}
}
? ? ? ? ?for(i=1;i<=n;i++) //輸出結(jié)果
? ? ? ? ?printf("%d ",a[i]);
? ? ? ? ? getchar();getchar();
? ? ? ? ? ?return 0;
}
</pre>
注意:冒泡排序的核心部分是雙重嵌套循環(huán)。不難看出冒泡排序的時(shí)間復(fù)雜度是 O(N 2 )。這是一個(gè)非常高的時(shí)間復(fù)雜度。這里可以看出冒泡排序在處理一般的排序的時(shí)候,時(shí)間復(fù)雜度是相當(dāng) 高的一般情況是不推薦的,那么肯定有人會(huì)問那為什么要說,那是因?yàn)槊芭菖判蛟诖蠖鄶?shù)情況下效率低下,但是在如果一個(gè)數(shù)列本身大部分已經(jīng)高度有序的情況下,那么它在這種情況下排序的效率又是驚人的。所以說任何事物沒有絕對(duì)的好壞,存在即有它的道理。看到這里讀者可能已經(jīng)被這簡(jiǎn)單的排序弄得昏昏欲睡了,不是難而是太小兒科了??垂俨灰唛_接下來還有:
最常用的排序——快速排序
? ? ? ?上面的冒泡排序雖然解決了桶排序的空間浪費(fèi)問題,但是在算法效率上卻犧牲了很多它的時(shí)間復(fù)雜度達(dá)到了 O(N 2 )。假如我們的計(jì)算機(jī)每秒鐘可以運(yùn)行 10 億次,那么對(duì) 1 億個(gè)數(shù)進(jìn)行排序,桶排序只需要 0.1 秒,而冒泡排序則需要 1 千萬秒,達(dá)到 115 天之久,是不是很嚇人?快速排序在這樣的情況下就應(yīng)運(yùn)而生。即解決了效率低下又浪費(fèi)空間的問題。可以說快速排序是19實(shí)際最偉大的算法之一,一直到現(xiàn)在我們都可以看到很多程序中依然采用這個(gè)排序方法。
? ? ? ? 快速排序之所以比較快,是因?yàn)橄啾让芭菖判?,每次交換是跳躍式的。每次排序的時(shí)候設(shè)置一個(gè)基準(zhǔn)點(diǎn),將小于等于基準(zhǔn)點(diǎn)的數(shù)全部放到基準(zhǔn)點(diǎn)的左邊,將大于等于基準(zhǔn)點(diǎn)的數(shù)全部放到基準(zhǔn)點(diǎn)的右邊。這樣在每次交換的時(shí)候就不會(huì)像冒泡排序一樣只能在相鄰的數(shù)之間進(jìn)行交換,交換的距離就大得多了。因此總的比較和交換次數(shù)就少了,速度自然就提高了。當(dāng)然在最壞的情況下,仍可能是相鄰的兩個(gè)數(shù)進(jìn)行了交換。因此快速排序的最差時(shí)間復(fù)雜度和冒泡排序是一樣的,都是 O(N 2 ),它的平均時(shí)間復(fù)雜度為 O (NlogN)。其實(shí)快速排序是基于一種叫做“二分”的思想。
好了話不多說 讓我們采用鮮活的例子來助大家理解:
假設(shè)我們現(xiàn)在對(duì)“6 1 2 7 9 3 4 5 10 8”這 10個(gè)數(shù)進(jìn)行排序。首先在這個(gè)序列中隨便找一個(gè)數(shù)作為基準(zhǔn)數(shù)(不要被這個(gè)名詞嚇到了,這就是一個(gè)用來參照的數(shù),待會(huì)兒你就知道它用來做啥了)。為了方便,就讓第一個(gè)數(shù) 6 作為基準(zhǔn)數(shù)吧。接下來,需要將這個(gè)序列中所有比基準(zhǔn)數(shù)大的數(shù)放在 6 的右邊,比基準(zhǔn)數(shù)小的數(shù)放在 6 的左邊,類似下面這種排列。
3 1 2 5 4 6 9 7 10 8
在初始狀態(tài)下,數(shù)字 6在序列的第 1 位。我們的目標(biāo)是將 6挪到序列中間的某個(gè)位置,假設(shè)這個(gè)位置是 k?,F(xiàn)在就需要尋找這個(gè) k,并且以第 k 位為分界點(diǎn),左邊的數(shù)都小于等于 6,右邊的數(shù)都大于等于 6。想一想,你有辦法可以做到這點(diǎn)嗎?方法其實(shí)很簡(jiǎn)單:分別從初始序列“6 1 2 7 9 3 4 5 10 8”兩端開始“探測(cè)”。先從右往左找一個(gè)小于 6 的數(shù),再?gòu)淖笸艺乙粋€(gè)大于 6 的數(shù),然后交換它們。這里可以用兩個(gè)變量 i 和 j,分別指向序列最左邊和最右邊。我們?yōu)檫@兩個(gè)變量起個(gè)好聽的名字“哨兵 i”和“哨兵 j”。剛開始的時(shí)候讓哨兵 i 指向序列的最左邊(即 i=1),指向數(shù)字 6。讓哨兵 j 指向序列的最右邊(即 j=10),指向數(shù)字 8。
首先哨兵 j 開始出動(dòng)。因?yàn)榇颂幵O(shè)置的基準(zhǔn)數(shù)是最左邊的數(shù),所以需要讓哨兵 j 先出動(dòng),這一點(diǎn)非常重要(請(qǐng)自己想一想為什么)。哨兵 j 一步一步地向左挪動(dòng)(即 j??),直到找到一個(gè)小于 6的數(shù)停下來。接下來哨兵 i 再一步一步向右挪動(dòng)(即 i++),直到找到一個(gè)大于 6的數(shù)停下來。最后哨兵 j 停在了數(shù)字 5 面前,哨兵 i 停在了數(shù)字 7 面前。
現(xiàn)在交換哨兵 i 和哨兵 j所指向的元素的值。交換之后的序列如下。
6 1 2 5 9 3 4 7 10 8
到此,第一次交換結(jié)束。接下來哨兵 j 繼續(xù)向左挪動(dòng)(再次友情提醒,每次必須是哨兵j 先出發(fā))。他發(fā)現(xiàn)了 4(比基準(zhǔn)數(shù) 6 要小,滿足要求)之后停了下來。哨兵 i 也繼續(xù)向右挪動(dòng),他發(fā)現(xiàn)了 9(比基準(zhǔn)數(shù) 6 要大,滿足要求)之后停了下來。此時(shí)再次進(jìn)行交換,交換之后的序列如下。
6 1 2 5 4 3 9 7 10 8
第二次交換結(jié)束,“探測(cè)”繼續(xù)。哨兵 j 繼續(xù)向左挪動(dòng),他發(fā)現(xiàn)了 3(比基準(zhǔn)數(shù) 6 要小,滿足要求)之后又停了下來。哨兵 i 繼續(xù)向右移動(dòng),糟啦!此時(shí)哨兵 i 和哨兵 j 相遇了,哨兵 i 和哨兵 j 都走到 3 面前。說明此時(shí)“探測(cè)”結(jié)束。我們將基準(zhǔn)數(shù) 6 和 3 進(jìn)行交換。交換之后的序列如下。
3 1 2 5 4 6 9 7 10 8
到此第一輪“探測(cè)”真正結(jié)束。此時(shí)以基準(zhǔn)數(shù) 6 為分界點(diǎn),6 左邊的數(shù)都小于等于 6,6右邊的數(shù)都大于等于 6?;仡櫼幌聞偛诺倪^程,其實(shí)哨兵 j 的使命就是要找小于基準(zhǔn)數(shù)的數(shù),而哨兵 i 的使命就是要找大于基準(zhǔn)數(shù)的數(shù),直到 i 和 j 碰頭為止。OK,解釋完畢。現(xiàn)在基準(zhǔn)數(shù) 6 已經(jīng)歸位,它正好處在序列的第 6 位。此時(shí)我們已經(jīng)將原來的序列,以 6 為分界點(diǎn)拆分成了兩個(gè)序列,左邊的序列是“3 1 2 5 4”,右邊的序列是“9 7 10 8”。接下來還需要分別處理這兩個(gè)序列,因?yàn)?6 左邊和右邊的序列目前都還是很混亂的。不過不要緊,我們已經(jīng)掌握了方法,接下來只要模擬剛才的方法分別處理 6 左邊和右邊的序列即可?,F(xiàn)在先來處理 6 左邊的序列吧。左邊的序列是“3 1 2 5 4”。請(qǐng)將這個(gè)序列以 3為基準(zhǔn)數(shù)進(jìn)行調(diào)整,使得 3 左邊的數(shù)都小于等于 3,3 右邊的數(shù)都大于等于 3
上一個(gè)排序的過程圖:
由于上面冒泡采用的是c寫的所以下面用java代碼:
<pre>
int[] a = {3 ,1, 2 ,5, 4,6,9 ,7 ,10, 8}
public static void quickF(int[] a,int start,int end){
if(start < end){
// 如果左邊下標(biāo)小于右邊下標(biāo)則可以繼續(xù)
int mid = QuickSort.getMid(a, start, end);
quickF(a,start,mid);
quickF(a,mid+1,end);
}
}
public static int getMid(int[] a,int start,int end ){
// 經(jīng)過第一次循環(huán)將以左邊的基準(zhǔn)點(diǎn)的數(shù)為準(zhǔn) 將比這個(gè)數(shù)大的 和比這個(gè)數(shù)小的數(shù)進(jìn)行區(qū)分開
int low = start;
int tmp = a[start];// 以這個(gè)數(shù)作為基準(zhǔn)數(shù)
while(start < end ){
// 首先先從右邊的數(shù)開始比較
while(start < end && a[end] >= tmp){
end--;
}
// 以左邊的數(shù)開始比較
while(start < end && a[start] <= tmp){
start++;
}
// 將兩個(gè)數(shù)進(jìn)行交換
int t = 0;
t = a[end];
a[end] = a[start];
a[start] = t;
}
// 全部交換完了 最后在將基準(zhǔn)點(diǎn)的數(shù) 與左邊的數(shù)進(jìn)行交換
a[low] = a[end];
a[end] = tmp;
return end;
}
public static void main(String[] args) {
long startTime=System.nanoTime();? //獲取開始時(shí)間
QuickSort.quickF(a, 0, a.length-1);
for (int i : a) {
System.out.print(i+",");
}
long endTime=System.nanoTime(); //獲取結(jié)束時(shí)間
System.out.println("程序運(yùn)行時(shí)間: "+(endTime-startTime)+"ms");
}
</pre>
簡(jiǎn)介:快速排序由 C. A. R. Hoare(東尼·霍爾,Charles Antony Richard Hoare)在 1960 年提出,之后又有許多人做了進(jìn)一步的優(yōu)化。如果你對(duì)快速排序感興趣,可以去看看東尼·霍爾1962 年在 Computer Journal 發(fā)表的論文“Quicksort”以及《算法導(dǎo)論》的第七章。
由于時(shí)間的緣故本來還要嘮嗑一下堆排序,這個(gè)也是號(hào)稱有最高效排序之稱,僅僅比快速排序低一點(diǎn)點(diǎn),但是在插入數(shù)據(jù) 刪除數(shù)據(jù)之后在排序,那幾乎是無人可敵。這個(gè)經(jīng)典的算法下一期在介紹。如果您對(duì)我的簡(jiǎn)書有任何意見歡迎您給我留言。