二分查找應該都會,那麼二分查找的變體呢?




0. 前言

你們好,我是多選參數的程序鍋,一個正在」搗鼓「操做系統、學數據結構和算法以及 Java 的硬核菜雞。node

二分查找你們估計都會,可是二分查找的變體你們會嗎?我相信大佬都是會的,可是我這個菜雞就是不會了。還記得,在學習二分查找變體的時候,我像發現了新大陸通常,很開森,很開森,很開森。git

爲了整個知識的相對完整,下面仍是從最基本的二分查找開始講解,以後講解二分查找的變體,這個變體在刷 Leetcode 的有些題目的時候也會用到。最後對二分查找這種算法進行總結。另外,這個數據結構和算法系列的代碼都在 github 倉庫中能夠找到:https://github.com/DawnGuoDev/algos 。github

1. 二分查找及其變體

二分查找針對的是一個有序的數據集合(必須是有序),查找思想有點相似分治思想。每次都經過跟區間的中間元素對比,將待查找的區間縮小爲以前的一半(或者說剔除了另外一半數據),直到找到要查找的元素,或者區間被縮小爲 0。web

因爲通過一次查找,會剔除一半數據而剩下另外一半數據,所以通過 k 次查找以後,剩下的數據個數爲  ,整個二分查找當剩下一個元素的時候中止,所以須要通過  次查找,時間複雜度也就是  算法

1.1. 最基礎的實現

這邊先講解不存在重複元素的有序數組中,查找值等於給定值的元素的狀況(PS:全文的講解都以數據是從小到大排列爲前提)。數組

1.1.1. 非遞歸的方式

public int bsearch(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);
if (array[mid] == value) {
return mid;
} else if (array[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}

return -1;
}

在實現非遞歸算法時,須要注意如下幾個關鍵點:微信

  • 循環的條件是 low <= high,而不是 low < high。由於可能 low 和 high 重合的時候正是須要查詢的值,好比 1,2,3 那麼假如我要查詢 3 這個值的位置時,是在 low 等於 high 的時候才查詢到的。
  • mid = (low+high)/2 這種寫法不太嚴謹,由於 low 和 high 比較大的時候,可能就會溢出。因此,改進的方法是 mid = low +(high-low)/2。固然爲了追求性能的極致,那麼能夠將這裏的除以 2 改成移位操做。由於移位操做比除法運算來講,計算機處理前者會更快。最終爲 mid = low + ((high-low)>>1)。須要注意的是,考慮到移位操做和加法的優先級,這邊的括號必需要這樣。
  • low 和 high 值的更新,這邊必定要記得 +1 和 -1,不然的話可能會進入死循環。假如沒有+1 或者 -1 的操做,那麼 1,2,3 我要查詢的是 3 這個值,第一步 low=0, high=2;第二步 low=1,high=2;第三步仍是 low=1,high=2。

1.1.2. 遞歸的方式

public int bsearchInternally(int[] array, int low, int high, int value) {
if (low > high) {
return -1;
}

int mid = low + ((high - low) >> 1);
if (array[mid] == value) {
return mid;
} else if (array[mid] < value) {
return bsearchInternally(array, mid + 1, high, value);
} else {
return bsearchInternally(array, low, mid - 1, value);
}
}

這邊的注意點與非遞歸的注意點是一一對應的,遞歸方式注意的是循環的條件,非遞歸方式注意的則是遞歸終止的條件,這邊須要 low>high 而不是 low >= high,理由是同樣的,本身舉例看一下。其餘兩個注意事項是同樣的。數據結構

回憶一下遞歸方式編寫代碼的技巧:1.是先寫出遞歸式;2.肯定終止條件;3.翻譯成代碼。app

1.2. 查找第一個等於給定值的元素所在的 index

接下去講解二分查找的變體,主要考慮幾種典型的狀況。首先,將不存在重複元素的有序數組進行通常化,即有序數組集合中存在重複的數據。那麼咱們該如何找到第一個等於給定值的數據的 index 呢?數據結構和算法

假如按照最簡單的方式來實現查找的話(即上述的實現),那麼獲得的結果將不必定正確。好比下面這個存在重複數據的有序數組集合。假設要查找的數據是 8 ,那麼先拿 8 和第 4 個數據 6 進行比較,發現 8 比 6 大,因而在下標 5-9 之間尋找。結果發現第 7 個數據 8 正好是要查找的數據,而後將 index 7 返回,可是實際上第一個 8 的 index 應該是 5。

1  3  4  5  6  8  8  8  11  18

所以,對於這個變形問題,咱們須要改造一下以前的代碼。改造以後的代碼以下所示:

public int bsearchFirstEqual(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);
if (array[mid] < value) {
low = mid + 1;
} else if (array[mid] > value) {
high = mid - 1;
} else {
if (mid == 0 || array[mid - 1] != value) {
return mid;
}
high = mid - 1;
}
}
return -1;
}

這邊稍微解析一下代碼。a[mid]跟要查找的 value 的大小關係有三種狀況:大於、小於、等於。對於 a[mid] >value的狀況,說明等於狀況位於 low-mid 之間,因此 high = mid-1。對於 a[mid]<value 的狀況,說明等於狀況位於 mid-high 之間,因此 low = mid+1。對於 a[mid]=value的狀況,咱們須要確保 mid 這個 index 是否是第一個等於 value 的 index。所以,先判斷 mid 等不等於 0,假如等於的話,那麼確定是第一個了;以後判斷 mid-1 位置的元素等不等於 value,若是不等於 value,那麼說明 mid 是第一個等於 value 的 index。假如 mid-1 位置的元素等於 value,那麼說明第一個等於 value 在 mid 以前,因此 high=mid-1

1.3. 查找最後一個等於給定值的元素所在的 index

前面是查找第一個值等於給定值的元素,如今將問題稍微改一下,查找最後一個值等於定值的元素的 index。相應的實現代碼其實和前面的相似。

public int bsearchLastEqual(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);

if (array[mid] < value) {
low = mid + 1;
} else if (array[mid] > value) {
high = mid - 1;
} else {
if (mid == len -1 || array[mid + 1] != value) {
return mid;
}
low = mid + 1;
}
}

return -1;
}

這裏咱們就不分析了,分析思路跟上面的那種狀況相似。

1.4. 查找第一個大於等於給定值的元素所在的 index

看完查找值相等的狀況以後,接下去咱們查找值不相等的狀況。在有序數組中(可含重複元素),查找第一個大於等於給定值的元素的 index。好比針對序列:三、四、六、七、10,查找第一個大於等於 5 的元素,那就是 6 ,index 是 2。

public int bsearchFirstMore(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);

if (array[mid] < value) {
low = mid + 1;
} else {
if (mid == 0 || array[mid - 1] < value) {
return mid;
}
high = mid - 1;
}
}

return -1;
}

若是 mid 位置所在的元素小於 value,那麼第一個大於等於 value 的值的 index 是在 [mid+1, high] 之間,因此 low=mid+1。若是 mid 位置所在的元素已經大於 value,那麼須要判斷 mid 是否是第一個大於等於 value 的 index。假如 mid == 0 ,那麼確定是第一個了;或者 mid 前面的那個元素小於 value,那麼 mid 也是第一個大於等於 value 的 index。若是兩個條件都不知足,那麼第一個大於等於 value 的 index,是在 [low, mid-1] 之間,所以將 high 進行更新。

1.5. 查找最後一個小於等於給定值的元素所在的 index

如今將問題變成查找最後一個小於等於給定值的元素的 index。好比針對序列:三、五、六、八、九、10,最後一個小於等於給定值 7 的元素是 6, index 是 2 。代碼的實現思路與上述狀況類似。

public int bsearchLastLess(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);

if (array[mid] > value) {
high = mid - 1;
} else {
if (mid == len - 1 || array[mid + 1] > value) {
return mid;
}
low = mid + 1;
}
}

return -1;
}

這裏咱們就不分析了,分析思路跟上面的那種狀況相似。

2. 總結

2.1. 二分查找的侷限性

雖然二分查找的時間複雜度是 O(logn),查找效率極高,可是二分查找卻不是完美的,這種查找方法存在一些侷限性。

  • 二分查找依賴的是順序表結構,簡單點說就是數組。

    二分查找可否依賴其餘數據結構呢?好比鏈表。答案是不能夠的,主要緣由是二分查找算法是按照下標隨機訪問元素的,好比咱們訪問 mid 這個位置的數據就是經過下標隨機訪問的,這個時間複雜度是 O(1)。假如使用鏈表方式的話,須要遍歷到 mid 這個位置,那麼時間複雜度爲 O(n)。因此,若是數據使用鏈表存儲,二分查找的時間複雜度會變得高。

  • 二分查找針對的是有序數據,在動態變化的數據集合中不適用

    二分查找的時候要求查找的數據序列必須是有序的。若是數據不是有序的,那麼須要先排序才能查找。在使用時間複雜度爲 O(nlogn)的排序算法的狀況下。若是一組靜態的數據,沒有頻繁地插入、刪除等操做,二分查找仍是能夠接受的。由於咱們能夠進行一次排序,屢次二分查找。這樣排序的成本就會被均攤。可是,若是咱們的數據集合有頻繁的插入和刪除操做的話,要想二分查找。那麼每次插入、刪除以後都須要進行排序,從而反正數據序列的有序。這種狀況下,維護有序的時間成本時很高的。

    綜上,二分查找只能用於插入、刪除操做不頻繁,一次排序屢次查找的狀況。針對動態變化的數據集合,二分查找將再也不適合。

  • 數據量過小不適合二分查找

    要處理的數據量很小的話,徹底沒有必要用二分查找,順序遍歷就能夠了。好比要在 10 個有序的數組中查找一個元素,無論使用順序遍歷仍是二分查找,查找速度都查很少。可是這種狀況下有個例外,就是若是比較操做很是耗時的話,那麼也請用二分查找,由於雖然二者次數差很少,可是這種狀況下咱們是須要儘量減小比較的次數。顯然,二分查找的次數還會更少一點。

  • 數據量太大也不適合二分查找

    二分查找的底層須要依賴數組這種數據結構,而數組這種數據結構要求內存空間的連續。假如數據量太大,好比有 1GB 大小的數據,若是使用數組來存儲,那麼就須要 1GB 的連續內存空間。因此當要查找的數據集合特別大的時候二分查找也會不太適合。

2.2. 二分查找的優點

  • 二分查找在內存使用上更節省

    雖然大部分狀況下,用二分查找的方式能夠解決的問題,散列表、二叉樹均可以解決。可是,無論是散列表仍是二叉樹都須要額外的內存空間。而二分查找依賴的是數組,除了數據自己以外,不須要存儲額外的其餘信息。因此當二分查找須要 100MB 內存的狀況下,散列表或二叉樹須要的內存空間會更大(不止 100MB)。顯然,在這三種方式中二分查找是最省內存空間的。

  • 二分查找更適合用在「近似」查找問題。

    在這類問題上,二分查找的優點更加明顯,就好比這幾種變體。而查找「等於給定值」的問題,更適合散列表或二叉樹。這種變體的二分查找算法比較難寫,尤爲是細節上若是處理很差容易產生BUG,這些出錯的細節有:終止條件、區間上下界更新方法、返回值選擇。

後臺回覆【AI資料】和【學習資料】便可獲取優質的學習資料


純分享 | 全網推薦的 AI 視頻教程和書籍分享


另外附上整個《拿下數據結構與算法》系列準備完成的思惟導圖(不含詳細內容)



不甘於「本該如此」,多選參數 值得關注




本文分享自微信公衆號 - 多選參數(zhouxintalk)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索