作者:Grey
原文地址:使用二分法來解決的問題
在一個有序陣列中,找某個數是否存在
OJ見:LeetCode 704. Binary Search
思路:
- 先得到中點位置,中點可以把陣列分為左右半邊。
- 如果中點位置的值等於目標值,直接返回中點位置。
- 如果中點位置的值小於目標值,則去陣列左邊按同樣的方式尋找。
- 如果中點位置的值大於目標值,則取陣列右邊按同樣的方式尋找。
- 如果最後沒有找到,則返回:-1。
程式碼
class Solution {
public int search(int[] nums, int target) {
if(nums == null || nums.length < 1) {
return -1;
}
int l = 0;
int r = nums.length - 1;
while (l <= r) {
int mid = l + ((r - l)>>1);
if (target > nums[mid]) {
l = mid + 1;
} else if (target == nums[mid]) {
return mid;
} else {
r = mid - 1;
}
}
return -1;
}
}
在一個有序陣列中,找大於等於某個數最左側的位置
OJ見:牛客-查詢某個位置
這個問題只需要在上例基礎上進行簡單改動即可,上例中,我們找到滿足條件的位置就直接return
了
if (target == nums[mid]) {
return mid;
}
在本問題中,因為要找到最左側的位置,所以,在遇到相等的時候,只需要先把位置記錄下來,不用直接返回,然後繼續去左側找是否還有滿足條件的位置。
同時,在遇到target < nums[mid]
條件下,也需要記錄下此時的mid
位置,因為這也可能是滿足條件的位置。
程式碼:
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int k = in.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = in.nextInt();
}
// 雖然不排序也可以通過,但是題目未說明一定是有序陣列
Arrays.sort(arr);
System.out.println(getNearestLeft(arr, k));
in.close();
}
public static int getNearestLeft(int[] nums, int target) {
if (nums == null || nums.length < 1) {
return -1;
}
int l = 0;
int r = nums.length - 1;
int ans = 0;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (target < nums[mid]) {
ans = mid;
r = mid - 1;
} else if (target > nums[mid]) {
l = mid + 1;
} else {
ans = mid;
r = mid - 1;
}
}
return ans;
}
}
LeetCode上有很多類似的問題,都可以用如上方式解答,比如:
LeetCode 35. Search Insert Position
程式碼見:LeetCode_0035_SearchInsertPosition
LeetCode 34. Find First and Last Position of Element in Sorted Array
程式碼見:LeetCode_0034_FindFirstAndLastPositionOfElementInSortedArray
區域性最大值問題
OJ見:LeetCode 162. Find Peak Element
思路
假設陣列長度為N
,首先判斷0
號位置的數和N-1
位置的數是不是峰值位置。
0
號位置只需要和1
號位置比較,如果0
號位置大,0
號位置就是峰值位置。
N-1
號位置只需要和N-2
號位置比較,如果N-1
號位置大,N-1
號位置就是峰值位置。
如果0
號位置和N-1
在上輪比較中均是最小值,那麼陣列的樣子必然是如下情況:
[0..1]
區間內是增長, [N-2...N-1]
區間內是下降
那麼峰值位置必在[1...N-2]
之間出現
此時可以通過二分來找峰值位置,先來到中點位置,假設為mid
,如果:
arr[mid] > arr[mid+1] && arr[mid] > arr[mid-1]
則mid
位置即峰值位置
否則,有如下兩種情況:
情況一,趨勢是:
則在[1...(mid-1)]
區間內繼續上述二分。
情況二,趨勢是:
則在[(mid+1)...(N-2)]
區間內繼續上述二分。
完整程式碼
public class LeetCode_0162_FindPeakElement {
public static int findPeakElement(int[] nums) {
if (nums.length == 1) {
return 0;
}
int l = 0;
int r = nums.length - 1;
if (nums[l] > nums[l + 1]) {
return l;
}
if (nums[r] > nums[r - 1]) {
return r;
}
l = l + 1;
r = r - 1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] > nums[mid + 1] && nums[mid] > nums[mid - 1]) {
return mid;
}
if (nums[mid] < nums[mid + 1]) {
l = mid + 1;
} else if (nums[mid] < nums[mid - 1]) {
r = mid - 1;
}
}
return -1;
}
}
分割陣列的最大值
給定一個非負整數陣列 nums 和一個整數 m ,你需要將這個陣列分成 m 個非空的連續子陣列。設計一個演算法使得這 m 個子陣列各自和的最大值最小。
OJ見:LeetCode 410. Split Array Largest Sum
PS: 此題也可以用四邊形不等式優化的動態規劃來解,但是最優解是二分法
思路
我們先求整個陣列的累加和,假設累加和為sum,我們可以得到一個結論,分割的m個非空連續子陣列的和的範圍一定在(0,sum]
區間內。轉換一下思路,如果某種劃分下的子陣列之和的最大值為max,則max首先肯定在(0,sum]
區間內。思路轉換為:
子陣列的累加和最大值不能超過max
的情況下,最少可分多少部分?
假設能分k個部分,
如果k <= m
,說明這種劃分是滿足條件的,我們看max
是否可以變的更小。
如果k > m
,說明這種劃分是不滿足條件的,我們需要調大max
的值。
這裡可以通過二分的方式來定位max
的值。即max
先取(0,sum]
的中點位置,得到的劃分部分k如果k <= m
,則max
繼續去左邊取中點位置來得到新的劃分k,
如果k > m
,max
繼續從右邊的中點位置來得到新的劃分k。
完整程式碼
class Solution {
public static int splitArray(int[] nums, int m) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int l = 0;
int r = sum;
int ans = 0;
while (l <= r) {
int mid = l + ((r - l) >> 1);
int parts = getParts(nums, mid);
if (parts > m) {
// mid越大,parts才會越小
l = mid + 1;
} else {
ans = mid;
r = mid - 1;
}
}
return ans;
}
// 達到aim要分幾部分
public static int getParts(int[] nums, int aim) {
for (int num : nums) {
if (num > aim) {
return Integer.MAX_VALUE;
}
}
int part = 1;
int all = nums[0];
for (int i = 1; i < nums.length; i++) {
if (all + nums[i] > aim) {
part++;
all = nums[i];
} else {
all += nums[i];
}
}
return part;
}
}
其中:int getParts(int[] nums, int aim)
方法表示,在不超過aim的情況下,最少需要幾個劃分部分。方法的主要邏輯是:
遍歷陣列,如果發現某個元素的值超過了aim,直接返回系統最大,說明無法得到劃分。如果沒有超過aim,則繼續加入下一個元素,直到超過aim,就定位出一個部分。依次類推,就可以得到最少有幾個劃分。