本來想寫Base64算法的,不過我仍是想寫點有趣的,而不是Base64這樣沒有什麼思惟難度的模版。在手賤在hihoCoder上看到了一個關於非有序數組的二分查找的題目,要求在非有序的數組上作線性的查找(因而就不能排序了),找到這個數組中第K大的值。作了一個下午,到晚上搞定,終於摳完幾個細節。不過想的過程仍是蠻棒的,這個算法是從快速排序衍生而來的,而從經典算法出發去yy,能讓咱們思考更多經典算法的細節,感覺經典算法的那種美。java
在講這個二分查找算法的以前咱們仍是先看看它的基礎:快速排序算法。雖然快速排序有不少缺點:它不穩定、難理解、會被奇葩數據拿去卡,但在實踐當中仍是個很快很靠譜的排序算法,比Java和Python所採用的TimSort不知道高到哪裏去了。我以爲做爲一個IT從業者,不能理解快排就好像一箇中國人沒讀過《論語》同樣。
講解快排的文章有不少,任何一本算法書也都不可能不講快排。不過我這裏須要特別闡述一下快排究竟在幹什麼。
基本的觀點是,每一「趟」的快排是針對原來數據的一個區間上進行的。首先在這個區間上隨意取一個值mid,而後遍歷一遍這個區間,使得可以把全部數據中小於等於mid的扔到區間的左部分,大於等於mid的扔到區間的右部分。而後左右分別繼續搞。這麼說太抽象了,咱們拿幾個數據玩玩看:
假設初始的數據是34 10 23 78 56,那麼若是咱們取第一個34爲mid,咱們就要遍歷一遍數組(在O(n)的時間內)變成十、2三、34在左邊,5六、78在右邊。至於十、2三、34它們之間的順序則可有可無,不管是十、2三、34仍是2三、3四、10仍是3四、十、23仍是別的什麼都無論,反正前三個都是小於等於34的,後兩個都是大於等於34的。python
其實34也可能在後面一半。這點很是重要,由於這就意味着mid的值位置是不肯定的,若是數據中有多個mid,它可能左右兩部都有分佈。實際上一趟快排結束後,咱們只能保證小於mid的在左邊,大於mid的在右邊,至於mid在哪?不知道。android
如今我放一段個人快排實現。我取mid通常是取中間那個位置,正常狀況下是要隨機取,而算法講解的時候通常取第一個。算法
void Qsort(int l,int r){ int mid=A[(l+r)/2]; do{ while (A[i]<mid) i++; while (A[j]>mid) j--; if (i<=j){ int k=A[i]; A[i]=A[j]; A[j]=k; i++;j--; } }while (i<=j); if (l<j) Qsort(l,j); if (i<r) Qsort(i,r); }
這段程序能保證的是,全部小於mid的都在l~j範圍內,而全部大於mid的都在i到r範圍內。同時j+1到i-1範圍內的(至多一個,多是零個),必定是mid。可是全部是mid的值,位置是不肯定的。數組
咱們如今能夠開始想怎麼在非有序的數組裏,避免所有排序整個數組,而找到某個數是第幾大了。由快速排序的算法的思想出發,咱們能夠發現,若是一趟快排結束,那麼咱們每次能把小於mid的扔到區間左邊,大於mid的扔到區間右邊,平均來看,查找範圍就縮小了一半。(固然被卡是可能的,咱們只是尋求一個指望複雜度的較優。)這樣一個算法可以實現,那麼總體複雜度就是
要注意的是i與j之間的差。在上述的快排程序跑過一趟以後,j<i,可是咱們並不肯定j和i中間是否會有一個數。這個數可能有,也可能沒有,必須特殊判斷。最後寫出來的線性查找算法以下:函數
int Search(int l,int r){ if (l==r) return A[l]; int i=l,j=r; int mid=A[(l+r)>>1]; do{ while (A[i]<mid) i++; while (A[j]>mid) j--; if (i<=j){ int k=A[i]; A[i]=A[j]; A[j]=k; i++;j--; } }while (i<=j); if (j==i-2){ if (j+1==K) return A[j+1]; } if (K<=j) return Search(l,j); if (K>=i) return Search (i,r); }
我拿了一個用algorithm庫提供的sort函數作的O(NlogN)排序程序作對拍,在N=106的狀況下,不開O2速度快一倍,開了O2快30%左右,效率提高仍是很顯著的。code