自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文搞懂二分查找算法!

開發(fā) 前端
從定義可知,運用二分搜索的前提是數(shù)組必須是排好序的,另外,輸入并不一定是數(shù)組,也有可能是給定一個區(qū)間的起始和終止的位置。

基本介紹

二分搜索(折半搜索)是一種在有序數(shù)組中查找某一特定元素的搜索算法。

從定義可知,運用二分搜索的前提是數(shù)組必須是排好序的,另外,輸入并不一定是數(shù)組,也有可能是給定一個區(qū)間的起始和終止的位置。

他的時間復(fù)雜度是 O(lgn),非常高效。

基本特點

他的缺點要求待查找的數(shù)組或者區(qū)間是排好序的。

如果對數(shù)組進行動態(tài)的刪除和插入操作并完成查找,平均復(fù)雜度會變?yōu)?O(n)。

因此,當輸入的數(shù)組或者區(qū)間是排好序的,同時又不會經(jīng)常變動,而要求從里面找出一個滿足條件的元素的時候,二分搜索就是最好的選擇。

解題思路

二分搜索一般化的解題思路如下:

  • 從已經(jīng)排好序的數(shù)組或區(qū)間中取出中間位置的元素,判斷該元素是否滿足要搜索的條件,如果滿足,停止搜索,程序結(jié)束。
  • 如果正中間的元素不滿足條件,則從它兩邊的區(qū)域進行搜索。由于數(shù)組是排好序的,可以利用排除法,確定接下來應(yīng)該從這兩個區(qū)間中的哪一個去搜索。
  • 通過判斷,如果發(fā)現(xiàn)真正要找的元素在左半?yún)^(qū)間的話,就繼續(xù)在左半?yún)^(qū)間里進行二分搜索。反之,就在右半?yún)^(qū)間里進行二分搜索。

二分查找的解題框架

int binarySearch(int[] nums, int target) {
int left = 0, right = ...;

while(...) {
//計算 mid 時需要技巧防止溢出,建議寫成: mid = left + (right - left) / 2
int mid = (right + left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}

常見解法

遞歸解法

假設(shè)我們要從一個排好序的數(shù)組里 {1, 3, 4, 6, 7, 8, 10, 13, 14} 查看一下數(shù)字 7 是否在里面,如果在,返回它的下標,否則返回 -1。

遞歸寫法的代碼模板如下:

// 二分搜索函數(shù)的定義里,除了要指定數(shù)組 nums 和目標查找數(shù) target 之外,還要指定查找區(qū)間的起點和終點位置,分別用 low 和 high 去表示。
int binarySearch(int[] nums, int target, int low, int high) {
// 為了避免無限循環(huán),先判斷,如果起點位置大于終點位置,表明這是一個非法的區(qū)間,已經(jīng)嘗試了所有的搜索區(qū)間還是沒能找到結(jié)果,返回 -1。

if (low > high) {
return -1;
}
// 取正中間那個數(shù)的下標 middle。
int middle = low + (high - low) / 2;

// 判斷一下正中間的那個數(shù)是不是要找的目標數(shù) target,是,就返回下標 middle。
if (nums[middle] == target) {
return middle;
}

// 如果發(fā)現(xiàn)目標數(shù)在左邊,就遞歸地從左半邊進行二分搜索。
if (target < nums[middle]) {
return binarySearch(nums, target, low, middle - 1);
} else {
return binarySearch(nums, target, middle + 1, high);
}//否則從右半邊遞歸地進行二分搜索。
}

注意:

  • 在計算 middle 下標的時候,不能簡單地用 (low + hight) / 2,可能會導(dǎo)致溢出。
  • 在取左半邊以及右半邊的區(qū)間時,左半邊是 [low, middle - 1],右半邊是[middle + 1, high],這是兩個閉區(qū)間。因為已經(jīng)確定了 middle 那個點不是我們要找的,就沒有必要再把它加入到左、右半邊了。
  • 對于一個長度為奇數(shù)的數(shù)組,例如:{1, 2, 3, 4, 5},按照 low + (high - low) / 2 來計算,middle 就是正中間的那個位置,對于一個長度為偶數(shù)的數(shù)組,例如 {1, 2, 3, 4},middle 就是正中間靠左邊的一個位置。

最后的時間復(fù)雜度是 O(logn)。

非遞歸解法

非遞歸寫法的代碼模板如下:

int binarySearch(int[] nums, int target, int low, int high) {
// 在 while 循環(huán)里,判斷搜索的區(qū)間范圍是否有效
while (low <= high) {
// 計算正中間的數(shù)的下標
int middle = low + (high - low) / 2;

// 判斷正中間的那個數(shù)是不是要找的目標數(shù) target。如果是,就返回下標 middle
if (nums[middle] == target) {
return middle;
}

// 如果發(fā)現(xiàn)目標數(shù)在左邊,調(diào)整搜索區(qū)間的終點為 middle - 1;否則,調(diào)整搜索區(qū)間的起點為 middle + 1
if (target < nums[middle]) {
high = middle - 1;
} else {
low = middle + 1;
}
}

// 如果超出了搜索區(qū)間,表明無法找到目標數(shù),返回 -1
return -1;
}

為什么 while 循環(huán)的條件中是 <=,而不是 < ?

答:因為初始化 high 的賦值是nums.length - 1,即最后一個元素的索引,而不是 nums.length。

這二者可能出現(xiàn)在不同功能的二分查找中,區(qū)別是:前者相當于兩端都閉區(qū)間 [left, right],后者相當于左閉右開區(qū)間 [left, right),因為索引大小為 nums.length 是越界的。

我們這個算法中使用的是 [left, right] 兩端都閉的區(qū)間。這個區(qū)間就是每次進行搜索的區(qū)間。

實際應(yīng)用

我們知道Kafka是一款性能強大且常用的分布式消息隊列,常常用于對流量進行消峰、解耦系統(tǒng)和異步處理部分邏輯以提高性能的場景。

在Kafka中,所有的消息都是以日志的形式存儲的。

你可以認為Kafka的海量消息就是按照寫入的時間順序,依次追加在許多日志文件中。

那在某個日志文件中,每條消息自然會距離第一條消息有一個對應(yīng)的offset,不過這里的offset更像是一個消息的自增ID,而不是一個消息在文件中的偏移量。

Kafka的每個topic會有多個partition,每個partition下的日志,都按照順序分成一個個有序的日志段,順次排列。

怎么找到消息

Kafka雖然不允許從尾部以外的地方插入或者修改數(shù)據(jù),但我們在Kafka中還是很可能需要從某個時間點開始讀數(shù)據(jù)的,這就意味著我們要通過一個offset,快速查找到某條消息在日志文件的什么位置。

但由于每條消息的消息體不同,每條消息所占用的磁盤大小都是不同的,只有offset,沒有辦法直接定位到文件的位置。

所以我們要么遍歷日志文件進行查找,要么我們?yōu)槿罩疚募⒁惶姿饕到y(tǒng),將消息offset和在文件中的position關(guān)聯(lián)起來,這樣我們就可以利用消息offset的有序性,通過二分法加速查找了。

下面是某個topic的某個partition下(日志文件)的存儲情況:

00000000000000000000.log
00000000000000000000.index
00000000000000000000.timeindex
00000000000000000035.log
00000000000000000035.index
00000000000000000035.timeindex
  • .log文件就是存儲了消息體本身的日志文件;
  • .index文件就是用于幫我們快速檢索消息在文件中位置的索引文件;
  • 這里還有個.timeindex后綴的文件,它和index其實差不多都是索引文件,只不過在這個文件中關(guān)聯(lián)position的變成了時間戳。

例題分析

找確定的邊界

邊界分上邊界和下邊界,有時候也被成為右邊界和左邊界。確定的邊界指邊界的數(shù)值等于要找的目標數(shù)。

例題:LeetCode 第 34 題,在一個排好序的數(shù)組中找出某個數(shù)第一次出現(xiàn)和最后一次出現(xiàn)的下標位置。

示例:輸入的數(shù)組是:{5, 7, 7, 8, 8, 10},目標數(shù)是 8,那么返回 {3, 4},其中 3 是 8 第一次出現(xiàn)的下標位置,4 是 8 最后一次出現(xiàn)的下標位置。

解題思路

在二分搜索里,把第一次出現(xiàn)的地方叫下邊界,把最后一次出現(xiàn)的地方叫上邊界。

那么成為 8 的下邊界的條件:

  • 該數(shù)必須是 8;該數(shù)的左邊一個數(shù)必須不是 8;8 的左邊有數(shù),那么該數(shù)必須小于 8;8 的左邊沒有數(shù),即 8 是數(shù)組的第一個數(shù)。

成為 8 的上邊界的條件:

  • 該數(shù)必須是 8;該數(shù)的右邊一個數(shù)必須不是 8:8 的右邊有數(shù),那么該數(shù)必須大于8;8 的右邊沒有數(shù),即 8 是數(shù)組的最后一個數(shù)。

代碼實現(xiàn)

用遞歸的方法來尋找下邊界,代碼如下:

int searchLowerBound(int[] nums, int target, int low, int high) {
if (low > high) {
return -1;
}

int middle = low + (high - low) / 2;
//判斷是否是下邊界時,先看看 middle 的數(shù)是否為 target,并判斷該數(shù)是否已為數(shù)組的第一個數(shù),或者,它左邊的一個數(shù)是不是已經(jīng)比它小,如果都滿足,即為下邊界。
if (nums[middle] == target && (middle == 0 || nums[middle - 1] < target)) {
return middle;
}

if (target <= nums[middle]) {
return searchLowerBound(nums, target, low, middle - 1);
} else {
return searchLowerBound(nums, target, middle + 1, high);
} //不滿足,如果這個數(shù)等于 target,那么就得往左邊繼續(xù)查找。
}

查找上邊界的代碼如下:

int searchUpperBound(int[] nums, int target, int low, int high) {
if (low > high) {
return -1;
}

int middle = low + (high - low) / 2;

//判斷是否是上邊界時,先看看 middle 的數(shù)是否為 target,并判斷該數(shù)是否已為數(shù)組的最后一個數(shù),或者,它右邊的數(shù)是不是比它大,如果都滿足,即為上邊界。
if (nums[middle] == target && (middle == nums.length - 1 || nums[middle + 1] > target)) {
return middle;
}

if (target < nums[middle]) {
return searchUpperBound(nums, target, low, middle - 1);
} else {
return searchUpperBound(nums, target, middle + 1, high);
} //不滿足時,需判斷搜索方向。
}

找模糊的邊界

二分搜索可以用來查找一些模糊的邊界。模糊的邊界指,邊界的值并不等于目標的值,而是大于或者小于目標的值。

例題:從數(shù)組 {-2, 0, 1, 4, 7, 9, 10} 中找到第一個大于 6 的數(shù)。

解題思路

在一個排好序的數(shù)組里,判斷一個數(shù)是不是第一個大于 6 的數(shù),只要它滿足如下的條件:

  • 該數(shù)要大于 6;該數(shù)有可能是數(shù)組里的第一個數(shù),或者它之前的一個數(shù)比 6 小。只要滿足了上面的條件就是第一個大于 6 的數(shù)。

代碼實現(xiàn)

Integer firstGreaterThan(int[] nums, int target, int low, int high) {
if (low > high) {
return null;
}

int middle = low + (high - low) / 2;

//判斷 middle 指向的數(shù)是否為第一個比 target 大的數(shù)時,須同時滿足兩個條件:middle 這個數(shù)必須大于 target;middle 要么是第一個數(shù),要么它之前的數(shù)小于或者等于 target。
if (nums[middle] > target && (middle == 0 || nums[middle - 1] <= target)) {
return middle;
}


if (target < nums[middle]) {
return firstGreaterThan(nums, target, low, middle - 1);
} else {
return firstGreaterThan(nums, target, middle + 1, high);
}
}

旋轉(zhuǎn)過的排序數(shù)組

二分搜索也能在經(jīng)過旋轉(zhuǎn)了的排序數(shù)組中進行。

例題:LeetCode 第 33 題,給定一個經(jīng)過旋轉(zhuǎn)了的排序數(shù)組,判斷一下某個數(shù)是否在里面。

示例:給定數(shù)組為 {4, 5, 6, 7, 0, 1, 2},target 等于 0,答案是 4,即 0 所在的位置下標是 4。

解題思路

對于這道題,輸入數(shù)組不是完整排好序,還能運用二分搜索嗎?

由于題目說數(shù)字了無重復(fù),舉個例子:

1 2 3 4 5 6 7 可以大致分為兩類,

第一類 2 3 4 5 6 7 1 這種,也就是 nums[start] <= nums[mid]。此例子中就是 2 <= 5。

這種情況下,前半部分有序。因此如果 nums[start] <=target

第二類 6 7 1 2 3 4 5 這種,也就是 nums[start] > nums[mid]。此例子中就是 6 > 2。

這種情況下,后半部分有序。因此如果 nums[mid]

代碼實現(xiàn)

int binarySearch(int[] nums, int target, int low, int high) {
if (low > high) {
return -1;
} //判斷是否已超出了搜索范圍,是則返回-1。

int middle = low + (high - low) / 2; //取中位數(shù)。

if (nums[middle] == target) {
return middle;
} //判斷中位數(shù)是否為要找的數(shù)


if (nums[low] <= nums[middle]) { //判斷左半邊是不是排好序的。
if (nums[low] <= target && target < nums[middle]) { //是,則判斷目標值是否在左半邊。
return binarySearch(nums, target, low, middle - 1); //是,則在左半邊繼續(xù)進行二分搜索。
}
return binarySearch(nums, target, middle + 1, high); //否,在右半邊進行二分搜索。
} else {
if (nums[middle] < target && target <= nums[high]) { //若右半邊是排好序的那一半,判斷目標值是否在右邊。
return binarySearch(nums, target, middle + 1, high); //是,則在右半邊繼續(xù)進行二分搜索。
}
return binarySearch(nums, target, low, middle - 1); //否,在左半邊進行二分搜索。
}
}

不定長的邊界

前面介紹的二分搜索的例題都給定了一個具體范圍或者區(qū)間,那么對于沒有給定明確區(qū)間的問題能不能運用二分搜索呢?

例題:有一段不知道具體長度的日志文件,里面記錄了每次登錄的時間戳,已知日志是按順序從頭到尾記錄的,沒有記錄日志的地方為空,求當前日志的長度。

解題思路

可以把這個問題看成是不知道長度的數(shù)組,數(shù)組從頭開始記錄都是時間戳,到了某個位置就成為了空:{2019-01-14, 2019-01-17, … , 2019-08-04, …. , null, null, null …}。

思路 1:順序遍歷該數(shù)組,一直遍歷下去,當發(fā)現(xiàn)第一個 null 的時候,就知道了日志的總數(shù)量。很顯然,這是很低效的辦法。

思路 2:借用二分搜索的思想,反著進行搜索。

  • 一開始設(shè)置 low = 0,high = 1
  • 只要 logs[high] 不為 null,high *= 2
  • 當 logs[high] 為 null 的時候,可以在區(qū)間 [0, high] 進行普通的二分搜索

代碼實現(xiàn):

// 先通過getUpperBound函數(shù)不斷地去試探在什么位置會出現(xiàn)空的日志。
int getUpperBound(String[] logs, int high) {
if (logs[high] == null) {
return high;
}
return getUpperBound(logs, high * 2);
}

// 再運用二分搜索的方法去尋找日志的長度。
int binarySearch(String[] logs, int low, int high) {
if (low > high) {
return -1;
}

int middle = low + (high - low) / 2;

if (logs[middle] == null && logs[middle - 1] != null) {
return middle;
}

if (logs[middle] == null) {
return binarySearch(logs, low, middle - 1);
} else {
return binarySearch(logs, middle + 1, high);
}
}`


責任編輯:武曉燕 來源: 日常加油站
相關(guān)推薦

2021-11-01 12:55:43

網(wǎng)絡(luò)

2020-12-08 06:32:04

Kafka二分查找

2024-04-12 12:19:08

語言模型AI

2022-03-24 08:51:48

Redis互聯(lián)網(wǎng)NoSQL

2021-03-22 10:05:59

netstat命令Linux

2023-09-08 08:20:46

ThreadLoca多線程工具

2023-09-15 12:00:01

API應(yīng)用程序接口

2022-10-12 07:24:18

大文件哈希算法Hash

2021-01-13 05:21:59

參數(shù)

2021-06-30 08:45:02

內(nèi)存管理面試

2022-08-15 15:39:23

JavaScript面向?qū)ο?/a>數(shù)據(jù)

2023-04-03 15:04:00

RPCPHP語言

2023-10-16 08:16:31

Bean接口類型

2024-06-05 11:43:10

2020-03-18 14:00:47

MySQL分區(qū)數(shù)據(jù)庫

2019-11-19 08:00:00

神經(jīng)網(wǎng)絡(luò)AI人工智能

2023-08-24 16:50:45

2022-06-07 10:13:22

前端沙箱對象

2021-04-23 09:12:09

Java數(shù)據(jù)結(jié)構(gòu)算法

2020-12-07 06:19:50

監(jiān)控前端用戶
點贊
收藏

51CTO技術(shù)棧公眾號