二分搜尋

搜索算法

電腦科學中,二分搜尋演算法(英語:binary search algorithm),也稱折半搜尋演算法(英語:half-interval search algorithm[1]對數搜尋演算法(英語:logarithmic search algorithm[2],是一種在有序陣列中搜尋某一特定元素的搜尋演算法。搜尋過程從陣列的中間元素開始,如果中間元素正好是要搜尋的元素,則搜尋過程結束;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中搜尋,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列為空,則代表找不到。這種搜尋演算法每一次比較都使搜尋範圍縮小一半。

二分搜尋
概況
類別搜尋演算法
資料結構陣列
複雜度
平均時間複雜度
最壞時間複雜度
最佳時間複雜度
空間複雜度疊代:
遞迴:
(無尾呼叫消除)
最佳解Yes
相關變數的定義

二分搜尋演算法在最壞情況英語Best, worst and average case下是對數時間複雜度的,需要進行次比較操作(在此處是陣列的元素數量,大O記號對數)。二分搜尋演算法使用常數空間,對於任何大小的輸入資料,演算法使用的空間都是一樣的。除非輸入資料數量很少,否則二分搜尋演算法比線性搜尋更快,但陣列必須事先被排序。儘管一些特定的、為了快速搜尋而設計的資料結構更有效(比如雜湊表),二分搜尋演算法應用面更廣。

二分搜尋演算法有許多種變種。比如分散層疊英語fractional casacading可以提升在多個陣列中對同一個數值的搜尋的速度。分散層疊有效的解決了計算幾何學和其他領域的許多搜尋問題。指數搜尋英語Exponential search將二分搜尋演算法拓寬到無邊界的列表。二元搜尋樹B樹資料結構就是基於二分搜尋演算法的。

演算法

編輯

二分搜尋只對有序陣列有效。二分搜尋先比較陣列中位元素和目標值。如果目標值與中位元素相等,則返回其在陣列中的位置;如果目標值小於中位元素,則搜尋繼續在前半部分的陣列中進行。如果目標值大於中位元素,則搜尋繼續在陣列上部分進行。由此,演算法每次排除掉至少一半的待查陣列。

步驟

編輯
 

給予一個包含 個帶值元素的陣列 或是記錄 ,使 ,以及目標值 ,還有下列用來搜尋  中位置的子程式[3]

  1.     
  2. 如果 ,則搜尋以失敗告終。
  3.  (中間值元素)為 。(具體實現中,為防止算術溢位,一般採用 代替。)
  4. 如果 ,令  並回到步驟二。
  5. 如果 ,令  並回到步驟二。
  6.  ,搜尋結束;回傳值 

這個疊代步驟會持續透過兩個變數追蹤搜尋的邊界。有些實際應用會在演算法的最後放入相等比較,讓比較迴圈更快,但平均而言會多一層疊代[4]

大致匹配

編輯

以上程式只適用於完全匹配,也就是尋找一個目標值的位置。不過,因為有序陣列的順序性,將二分搜尋演算法擴展到能適用大致匹配並不是很重要。舉例來說,二分搜尋演算法可以用來計算一個賦值的排名(或稱,比它更小的元素的數量)、前趨(下一個最小元素)、後繼(下一個最大元素)以及最近鄰。搜尋兩個值之間的元素數目的範圍查詢英語Range query (data structures)可以藉由兩個排名查詢(又稱秩查詢)來執行[5]

  • 排名查詢可以使用調整版的二分搜尋來執行。藉由在成功的搜尋回傳 ,以及在失敗的搜尋回傳 ,就會取而代之地回傳了比起目標值小的元素數目[5]
  • 前趨和後繼查詢可以藉由排名查詢來執行。一旦知道目標值的排名,其前趨就會是那個位於其排名位置的元素,或者排名位置的上一個元素(因為它是小於目標值的最大元素)。其後繼是(陣列中的)下一個元素,或是(非陣列中的)前趨的下一個元素[6]。目標值的最近鄰可能是前趨或後繼,取決於何者較為接近。
  • 範圍查詢也是直接了當的。一旦知道兩個值的排名,不小於第一個值且小於第二個值的元素數量就會是兩者排名的差。這個值可以根據範圍的端點是否算在範圍內,或是陣列是否包含其端點的對應鍵來增加或減少1[7]

複雜度分析

編輯
時間複雜度
折半搜尋每次把搜尋區域減少一半,時間複雜度為 。(n代表集合中元素的個數)
空間複雜度
 。雖以遞迴形式定義,但是尾遞迴,可覆寫為迴圈。

應用

編輯

除直接在一個陣列中搜尋元素外,可用在插入排序中。

範例代碼

編輯

C 版本- 遞迴

編輯
int binary_search(const int arr[], int start, int end, int key) {
	if (start > end)
		return -1;

	int mid = start + (end - start) / 2;    //直接平均可能會溢位,所以用此算法
	if (arr[mid] > key)
		return binary_search(arr, start, mid - 1, key);
	else if (arr[mid] < key)
		return binary_search(arr, mid + 1, end, key);
	else
	    return mid;        //最後檢測相等是因為多數搜尋狀況不是大於要不就小於
}

C 版本- while 迴圈

編輯
int binary_search(const int arr[], int start, int end, int key) {
    int ret = -1;       // 未搜索到数据返回-1下标
    
	int mid;
	while (start <= end) {
		mid = start + (end - start) / 2; //直接平均可能會溢位,所以用此算法
		if (arr[mid] < key)
			start = mid + 1;
		else if (arr[mid] > key)
			end = mid - 1;
		else {            // 最後檢測相等是因為多數搜尋狀況不是大於要不就小於
			ret = mid;  
            break;
        }
	}
	
	return ret;     // 单一出口
}

JavaScript 版本

編輯
var arr = [1, 3, 5, 7, 9, 10, 11, 12, 14, 15, 19, 20];
const binarySearch = (arr, target) => {
  const search = (start, end) => {
    if (start > end) return -1;
    const mid = start + Math.floor((end - start) / 2);
    if (arr[mid] > target) {
      return search(0, mid - 1);
    } else if (arr[mid] < target) {
      return search(mid + 1, end);
    } else {
      return mid;
    }
  }
  return search(0, arr.length - 1);
}
console.log( binarySearch(arr, 4) );

Python3 版本 while 迴圈

編輯
def binary_search(arr, left, right, key):
    while left <= right:
        mid = left + (right - left) // 2
        if arr[mid] == key:
            return mid
        elif arr[mid] < key:
            left = mid + 1
        elif arr[mid] > key:
            right = mid - 1
    return -1

Python3 版本 遞迴

編輯
def binary_search(arr, start, end, key):
	if start > end:
		return -1
	mid = start + (end - start) // 2
	if arr[mid] > key:
		return binary_search(arr, start, mid - 1, key)
	if arr[mid] < key:
		return binary_search(arr, mid + 1, end, key)
	return mid

C# 版本

編輯
static int binary_search(int[] arr, int start, int end, int key)
{
    int mid;
    while (start <= end)
    {
        mid = (start + end) / 2;
        if (arr[mid] < key)
            start = mid + 1;
        else if (arr[mid] > key)
            end = mid - 1;
        else
            return mid; 
    }
    return -1;
}

Swift 版本

編輯
import Foundation
/// 二分搜索完全匹配
///
/// - Parameters:
///   - arr: 有序数组
///   - start: 起始位置
///   - end: 结束点
///   - key: 特点目标值
/// - Returns: 返回查找结果
func binarySearch(arr: [Int], start: Int, end: Int, key: Int) -> Int? {
    if start > end {
        return nil
    }
    let mid = start + (end - start) / 2
    if arr[mid] > key {
        return binarySearch(arr: arr, start: start, end: mid - 1, key: key)
    } else if arr[mid] < key {
        return binarySearch(arr: arr, start: mid + 1, end: end, key: key)
    } else {
        return mid
    }
}

golang 遞迴版本

編輯
func binary_search(arr []int, low, high, key int) int {
	if low > high {
		return -1
	}
	mid := low + (high-low)/2

	if arr[mid] > key {
		return binary_search(arr, low, mid-1, key)
	} else if arr[mid] < key {
		return binary_search(arr, mid+1, high, key)
	}

	return mid
}

golang 非遞迴版本

編輯
func binarySearch(arr []int, key int) int {
    low, high := 0, len(arr)-1
	for low <= high {
		mid := low + (high-low)/2
		if arr[mid] == key {
			return mid
		} else if key < arr[mid] {
			high = mid - 1
		} else if key > arr[mid] {
			low = mid + 1
		}
	}
	return -1
}

Java 遞迴

編輯
public static int binarySearch(int[] arr, int start, int end, int key){
    if (start > end)
        return -1;

    int mid = start + (end - start)/2;    //防止溢位
    if (arr[mid] > key)
        return binarySearch(arr, start, mid - 1, key);
    if (arr[mid] < key)
        return binarySearch(arr, mid + 1, end, key);
    return mid;  

}

Java while 迴圈

編輯
public static int binarySearch(int[] arr, int start, int end, int key){
    int result = -1;

    while (start <= end){
        int mid = start + (end - start)/2;    //防止溢位
        if (arr[mid] > key)
            end = mid - 1;
        else if (arr[mid] < key)
            start = mid + 1;
        else {
            result = mid ;  
            break;
        }
    }

    return result;

}

Julia版本

編輯
# Julia Sample : BinarySearch
function BinarySearch(A,Key)
	left,right = 1,length(A)

	while(left<=right)
		mid=left+floor(Int,((right-left)/2))
	
		if A[mid]==Key
			return mid
		elseif Key<A[mid]
			right = mid-1
		elseif Key>A[mid]
			left = mid+1
		end
	end
	return -1
end

# Main Code
A = [1,3,16,31,43,354,586]   # Already Arrange
println(A)                   # Original Array
println(BinarySearch(A,43))  # BinarySearch Search Array
println(BinarySearch(A,354)) # BinarySearch Search Array
println(BinarySearch(A,3))   # BinarySearch Search Array

歷史

編輯

在1946年,約翰·莫奇利摩爾學院講座英語Moore School Lectures上第一次提出二分搜尋的概念。[8]1957年,威廉·皮特遜英語William Wesley Peterson發表了第一個應用插值搜尋的演算法[8][9]。在此時,每個發表的二分搜尋演算法只對長度為2的冪減一的陣列有用。[10]直到1960年,德里克·亨利·萊默發表了一個對於所有長度的陣列都適用的演算法[11]。1962年,赫爾曼·博滕布魯赫發表了一個用ALGOL 60寫的二分搜尋,將判斷相等的步驟放到演算法末尾。雖然將平均疊代次數增加一,但是每次疊代中的比較次數減少了1次。[12]均勻二分搜尋則是史丹佛大學的A. K.錢德拉在1971年發明的[8]。1986年,伯納德·查澤爾列奧尼達斯·吉巴斯引入了分散層疊來解決計算幾何中大量存在的搜尋問題[13][14][15]

實現中的問題

編輯

儘管二分搜尋的基本思想相對簡單,但細節可以令人難以招架 ... — 高德納[2]

喬恩·本特利將二分搜尋問題布置給專業編程課的學生時,百分之90的學生在花費數小時後還是無法給出正確的解答,主要因為這些錯誤程式在面對邊界值的時候無法執行,或返回錯誤結果。[16]1988年開展的一項研究顯示,20本教科書裡只有5本正確實現了二分搜尋。[17]不僅如此,本特利自己1986年出版的《編程珠璣》一書中的二分搜尋演算法存在整數溢位的問題,二十多年來無人發現。Java語言的庫所實現的二分搜尋演算法中同樣的溢位問題存在了九年多才被修復。[18]

參考

編輯
  1. ^ Willams, Jr., Louis F. A modification to the half-interval search (binary search) method. Proceedings of the 14th ACM Southeast Conference: 95–101. 1975. doi:10.1145/503561.503582. 
  2. ^ 2.0 2.1 Knuth 1998,§6.2.1 ("Searching an ordered table"), subsection "Binary search".
  3. ^ Knuth 1998,§6.2.1 ("Searching an ordered table"), subsection "Algorithm B".
  4. ^ Bottenbruch, Hermann. Structure and Use of ALGOL 60. Journal of the ACM. 1962, 9 (2): 161–211.  Procedure is described at p. 214 (§43), titled "Program for Binary Search".
  5. ^ 5.0 5.1 Sedgewick & Wayne 2011,§3.1, subsection "Rank and selection".
  6. ^ Goldman & Goldman 2008,第461–463頁.
  7. ^ Sedgewick & Wayne 2011,§3.1, subsection "Range queries".
  8. ^ 8.0 8.1 8.2 Knuth 1998,§6.2.1 ("Searching an ordered table"), subsection "History and bibliography".
  9. ^ Peterson, William Wesley. Addressing for random-access storage. IBM Journal of Research and Development. 1957, 1 (2): 130–146. doi:10.1147/rd.12.0130. 
  10. ^ "2n−1". OEIS A000225頁面存檔備份,存於網際網路檔案館). Retrieved 7 May 2016.
  11. ^ Lehmer, Derrick. Teaching combinatorial tricks to a computer. Proceedings of Symposia in Applied Mathematics. 1960, 10: 180–181. doi:10.1090/psapm/010. 
  12. ^ Bottenbruch, Hermann. Structure and use of ALGOL 60. Journal of the ACM. 1962-04-01, 9 (2): 161–221. ISSN 0004-5411. doi:10.1145/321119.321120.  Procedure is described at p. 214 (§43), titled "Program for Binary Search".
  13. ^ Chazelle, Bernard; Liu, Ding. Lower bounds for intersection searching and fractional cascading in higher dimension. 33rd ACM Symposium on Theory of Computing. ACM: 322–329. 2001-07-06 [2018-06-30]. ISBN 978-1-58113-349-3. doi:10.1145/380752.380818. (原始內容存檔於2018-10-29).  存档副本. [2019-10-06]. 原始內容存檔於2018-10-29. 
  14. ^ Chazelle, Bernard; Guibas, Leonidas J. Fractional cascading: I. A data structuring technique (PDF). Algorithmica. 1986, 1 (1-4): 133–162 [2019-10-06]. CiteSeerX 10.1.1.117.8349 . doi:10.1007/BF01840440. (原始內容存檔 (PDF)於2016-03-03). 頁面存檔備份,存於網際網路檔案館
  15. ^ Chazelle, Bernard; Guibas, Leonidas J., Fractional cascading: II. Applications (PDF), Algorithmica, 1986, 1 (1-4): 163–191 [2019-10-06], doi:10.1007/BF01840441, (原始內容存檔 (PDF)於2016-03-04) 
  16. ^ Bentley 2000,§4.1 ("The Challenge of Binary Search").
  17. ^ Pattis, Richard E. Textbook errors in binary searching. SIGCSE Bulletin. 1988, 20: 190–194. doi:10.1145/52965.53012. 
  18. ^ Bloch, Joshua. Extra, extra – read all about it: nearly all binary searches and mergesorts are broken. Google Research Blog. 2006-06-02 [2016-04-21]. (原始內容存檔於2016-04-01). 頁面存檔備份,存於網際網路檔案館

外部連結

編輯