【總結】二分查詢 —— 一種減而治之的查詢方法(1)

spy_ucas發表於2018-11-08

  本文為本人學習鄧俊輝教授《資料結構》一書二分查詢部分的複習總結,如需轉載請註明此書以及本文地址。

0 引言

  西遊記第28回,孫悟空三打白骨精後被唐僧逐回,於是回到了花果山。

  大聖道:“我當時共有四萬七千群妖,如今都往哪裡去了?”
  群猴道:“自從爺爺走後,這山被二郎菩薩點上火,燒殺了大半。我們蹲在井裡,鑽在澗內,藏於鐵板橋下,得了性命。及至火滅煙消,出來時,又沒花果養贍(shan),難以存活,別處又去了一半。我們這一半,捱(ai)苦的住在山中,這兩年,又被些打獵的搶去一半也。”

  老舍先生在其一部表現抗戰北平淪陷區普通民眾生活與抗戰的長篇小說《四世同堂》中描寫到:

  月亮上來了。星漸漸的稀少,天上空闊起來。和微風勻到一起的光,象冰涼的刀刃兒似的,把寬靜的大街切成兩半,一半兒黑,一半兒亮。那黑的一半,使人感到陰森,亮的一半使人感到淒涼。

1 基本思想

  二分查詢(Binary Search)是一種“減而治之”的策略,具體如圖1.1所示。

(圖待補充)

  如果我們要在有序向量AA中區間A[lo,hi)A[lo, hi)的部分查詢目標元素ee,對於任意的元素x=A[mi]x=A[mi]我們可以將區間A[lo,hi)A[lo, hi)分成A[lo,mi)A[lo,mi)A[mi]A[mi]A(mi,hi)A(mi,hi)三個子區間。根據向量的有序性我們有
A[lo,mi)A[mi]A(mi,hi)A[lo,mi)≤A[mi]≤A(mi,hi)

   - 如果目標元素e<xe<x,則目標元素必然存在於左側的子區間A[lo,mi)A[lo, mi)或不存在,這時我們可以遞迴查詢子區間A[lo,mi)A[lo, mi)
   - 如果目標元素e>xe>x,則目標元素必然存在於右側的子區間A(mi,hi)A(mi, hi)或不存在,這時我們可以遞迴查詢子區間A(mi,hi)A(mi, hi)
   - 如果目標元素e=xe=x,則目標元素已在切分點mimi處命中,此時查詢就可以終止了。

2 樸素演算法

2.1 策略

  樸素的二分查詢演算法的切分點S[mi]S[mi]選自區間S[lo,hi)S[lo, hi)的中點,即mi=(lo+hi)/2mi=\lfloor(lo+hi)/2\rfloor。這種策略可以概括為“以當前區間內居中的元素作為目標元素的試探物件”。因為每一步迭代之後無論沿哪個方向深入新問題的規模都將縮小一半,因此從最壞的角度來看,這一策略是最優的。

2.2 實現

  一種遞迴方法的具體實現如下:

//樸素的二分查詢——遞迴版
//在有序向量A的區間[lo, hi)中查詢元素額,返回e的秩
template <typename T> int binSearch(T* A, T const& e, int lo, int hi)
{
	if(lo >= hi) return -1; //遞迴基或不合法,查詢失敗
	int mi = (lo + hi) >> 1; //以中點mi為軸點
	return (e < A[mi] ? binSearch(A, e, lo, mi) : //分情況討論
	       (A[mi] < e ? binSearch(A, e, mi + 1, hi) : mi);
}

  通過“遞迴消除”的方法或改變思考方式,我們還可以得到以下一種迭代版本:

//樸素的二分查詢——迭代版
//在有序向量A的區間[lo, hi)中查詢元素額,返回e的秩
template <typename T> int binSearch(T* A, T const& e, int lo, int hi)
{
	while(lo < hi) //如果區間[lo, hi)還有元素
	{
		int mi = (lo + hi) >> 1; //以中點mi為軸點
		if (e < A[mi]) hi = mi; //深入[lo, mi)繼續查詢
		else if (A[mi] < e) lo = mi + 1; //深入(mi, hi)繼續查詢
		else return mi; //命中
	}
	return -1; //查詢失敗
}

  我們可能會有如此疑惑:

● 三種情況(e&lt;A[mi]e&lt;A[mi]e&gt;A[mi]e&gt;A[mi]e=A[mi]e=A[mi])為何要如此排列?如果調整為(e=A[mi]e=A[mi]e&lt;A[mi]e&lt;A[mi]e&gt;A[mi]e&gt;A[mi])(一般書上給出的方案)會如何?

  在2.5節我們重點分析這個問題,以闡明為什麼將“”的情況放在最後。

2.3 例項

(待補充)

2.4 效能

根據2.1中的策略描述,有效的查詢區間寬度按1/2的幾何級數速度遞減。因此經過至多log2(hilo)\log_2 (hi-lo)步迭代必然終止。因為每次迭代僅需O(1)O(1)的時間,因此總體時間複雜度不超過O(log2(hilo))=O(logn)O(\log_2 (hi-lo))=O(\log n)。可以看出,相比於順序查詢,二分查詢的優化意義重大。

2.5 定性分析

對於二分查詢時間複雜度的計算,主要分為:元素的大小比較、秩的算術運算以及賦值。由於“秩”是無符號的整數,而“元素”通常更為複雜(第7章中有很多這樣的例子),甚至比較的複雜度可能不是O(1)O(1)(如對有序字典向量中進行匹配查詢),因此優先考慮比較的權重,整體的效率因此取決於比較操作的次數,這裡稱作“查詢長度”。

2.5.1 平均成功查詢長度

  對於長度為n的有序向量,不失一般地,假設n=2k1,kNn=2^k-1, k∈\mathbb{N}^*,也就是說kk為二分查詢樹的層數,假定查詢目標元素等概率分佈,因此我們需要求得此時平均成功查詢長度
Caverage(k)=i=12k1xi2C_{average}(k)=\frac{\sum_{i=1}^{2^k-1}{x_i}}{2}
其中xi{x_i}為第ii個元素的查詢長度,查詢長度總和為C(k)C(k),我們可以得到關係
C(k)=Caverage(k)n=Caverage(k)(2k1) \begin {aligned} C(k)&amp;=C_{average}(k)·n\\ &amp;=C_{average}(k)·(2^k-1) \end {aligned}
特別地,當k=1k=1時,成功查詢只有一種情況,因此
Caverage(1)=C(1)=2C_{average}(1)=C(1)=2
下面用“遞推分析”求出C(k)C(k)C(k1)C(k-1)的關係。

對於長度為n=2k1n=2^k-1的有序向量,每一步迭代都有三種可能:
● 左半邊元素:經過11成功的比較後原問題轉換為一個規模2k112^{k-1}-1的子問題;
● 右半邊元素:經過11失敗的比較11成功的比較後(共22次)原問題轉換為一個規模2k112^{k-1}-1的子問題;
● 中間的元素:經過22失敗的比較後在A[mi]A[mi]點命中,演算法終止。

根據上面的分析,我們可以得到如下遞推式
C(k)=[C(k1)+1(2k11)]+21+[C(k1)+2(2k11)]=2C(k1)+32k11 \begin {aligned} C(k)&amp;=[C(k-1)+1·(2^{k-1}-1)]+2·1+[C(k-1)+2·(2^{k-1}-1)]\\ &amp;=2·C(k-1)+3·2^{k-1}-1 \end {aligned}

因為C(k)C(k)指向量中2k12^k-1個元素的查詢長度的總和,因此C(k1)C(k-1)的係數僅為1。

F(k)=C(k)3k2k11F(k)=C(k)-3k·2^{k-1}-1,則F(k1)=C(k1)3(k1)2k21F(k-1)=C(k-1)-3·(k-1)·2^{k-2}-1,因而有
F(k)=2F(k1)F(k)=2·F(k-1)

對上式做驗證:
F(k)=C(k)3k2k11=2C(k1)+32k113k2k11=2C(k1)+62k26k2k22=2C(k1)+6(1k)2k22=2[C(k1)3(k1)2k21]=2F(k1) \begin {aligned} F(k)&amp;=C(k)-3k·2^{k-1}-1\\ &amp;=2·C(k-1)+3·2^{k-1}-1-3k·2^{k-1}-1\\ &amp;=2·C(k-1)+6·2^{k-2}-6k·2^{k-2}-2\\ &amp;=2·C(k-1)+6·(1-k)·2^{k-2}-2\\ &amp;=2·[C(k-1)-3·(k-1)·2^{k-2}-1]\\ &amp;=2F(k-1) \end {aligned}

我們可以得到
F(1)=231=2F(k)=2F(k1)=22F(k2)=23F(k3)=...=2k1F(1)=2k\begin {aligned} F(1)&amp;=2-3-1=-2\\ F(k)&amp;=2·F(k-1)=2^2·F(k-2)=2^3·F(k-3)=...\\ &amp;=2^{k-1}·F(1)=-2^k \end {aligned}
因此
C(k)=F(k)+3k2k1+1=2k+3k2k1+1=(3k4)2k1+1=(32k2)2k+1=(32k1)(2k1)+32k\begin {aligned} C(k)&amp;=F(k)+3k·2^{k-1}+1\\ &amp;=-2^k+3k·2^{k-1}+1\\ &amp;=(3k-4)·2^{k-1}+1\\ &amp;=(\frac{3}{2}k-2)·2^{k}+1\\ &amp;=(\frac{3}{2}k-1)·(2^{k}-1)+\frac{3}{2}k \end {aligned}
從而有
Caverage(k)=C(k)/(2k1)=32k1+3k2(2k1)=32k1+O(ε)\begin {aligned} C_{average}(k)&amp;=C(k)/(2^k-1)\\ &amp;=\frac{3}{2}k-1+\frac{3k}{2·(2^k-1)}\\ &amp;=\frac{3}{2}k-1+O(\varepsilon) \end {aligned}
亦即平均成功查詢長度為O(1.5k)=O(1.5logn)O(1.5k)=O(1.5\log n)

2.5.2 平均失敗查詢長度

  根據迭代版程式碼,失敗查詢的終止條件是lohilo≥hi,亦即有效區間寬度縮減為00時失敗告終。不難看出,對於對長度為nn向量進行的二分查詢,失敗的情況有n+1n+1種。
  可以證明,一般情況下平均失敗查詢長度不超過1.5log2(n+1)=O(1.5logn)1.5·\log_2(n+1)=O(1.5\log n)
  我們用數學歸納法來證明這一結論。
  (1)樸素情況:當n=1n=1時,平均失敗查詢長度為(1+2)/2=1.5(1+2)/2=1.5,此時結論成立。
  (2)一般情況:假設當nkn≤k時結論成立,我們要證明當n=2kn=2·k和當n=2k+1n=2·k+1時結論也成立。

▲ 當n=2kn=2·k時,左區間長度為kk,右區間長度為k1k-1
  左側區間向量總共包含k+1k+1種失敗情況,根據歸納假設,其平均長度不超過1+1.5log2(k+1)1+1.5·\log_2(k+1)
  右側區間向量總共包含kk種失敗情況,根據歸納假設,其平均長度不超過1+1.5log2k1+1.5·\log_2k

(後續內容待補充)

參考文獻

[1] 鄧俊輝. 資料結構(C++語言版). 北京:清華大學出版社, 2013年9月第3版, ISBN:9-787-302-330646
[2] 鄧俊輝. 資料結構習題解析. 北京:清華大學出版社, 2013年9月第3版, ISBN: 9-787-302-330653

相關文章