⭐️二分⭐️
数组知识点:Java总结五:数组、排序和查找
1. 二分法的介绍
本文主要介绍二分法。二分法是一种很精妙的算法,效率很高,很多复杂的算法都采用了二分优化。
二分法用于在单调的序列内快速查找某个值,方法是序列分为两半,判断要查找的值在哪个区间,舍弃另一半,每次查询都会舍弃序列的一半。
算法:当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。
基本思想:假设数据是按升序排序的,对于给定值key
,从序列的中间位置k开始比较,
如果当前位置arr[k]
值等于key
,则查找成功;
若key
小于当前位置值arr[k]
,则在数列的前半段中查找,arr[low,mid-1];
若key
大于当前位置值arr[k]
,则在数列的后半段中继续查找arr[mid+1,high];
,直到找到为止
时间复杂度: O(log(n))。
2. 二分查找
🍀基本介绍
在有序的序列中查找x是否存在
🍀题目练习
题目链接:704. 二分查找
T.二分查找
📚题目详情
💡思路分析
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
💡二分法第一种写法:
第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:
while (left <= right)
要使用<=
,因为left == right
是有意义的,所以使用<=
if (nums[middle] > target)
,right
要赋值为middle - 1
,因为当前这个nums[middle]
一定不是target
,那么接下来要查找的左区间结束下标位置就是middle - 1
例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
class Solution {
public int search(int[] nums, int target) {
int l=0, r=nums.length-1;
while(l<=r){
//int mid=l+r>>1;
int mid = l + ((r - l) >> 1);// 防止溢出 等同于(left + right)/2
if(nums[mid]==target){
return mid;
}else if(nums[mid]>target){
r=mid-1;
}else{
l=mid+1;
}
}
return -1;
}
}
💡二分法第二种写法
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。
有如下两点:
while (left < right)
,这里使用<
,因为left == right
在区间[left, right)
是没有意义的if (nums[middle] > target)
,right
更新为middle
,因为当前nums[middle]
不等于target
,去左区间继续寻找,而寻找区间是左闭右开区间,所以right
更新为middle
,即:下一个查询区间不会去比较nums[middle]
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid;
}
return -1;
}
}
注意: l + r >> 1
可以写成 l + (r - l) / 2
。从数学角度两个式子是等量的同时也能防止右值过大而爆int使得mid溢出
🍀拓展
📚查找大于等于x的第一个位置
当我们查找到一个符合条件的值后,我们并不结束查找,而是将查找区间向左偏移后继续查找。
public static int Search(int x){
int l = 0, r = n-1;
while(l < r){
int mid = l + r >> 1;
// mid位置上的元素小于x,我们要查找的是大于等于的
if(arr[mid] < x){
// 所以mid和其左边的全部舍弃
l = mid + 1;
}
else{
//mid位置上的元素大于等于x,我们不能舍弃,但要向左偏移,所以令r=mid
r = mid;
}
}
return l;
}
当a[mid]
小于x
时,令l = mid + 1
,mid
及其左边的位置被排除了,可能出现解的位置是mid + 1
及其后面的位置;当a[mid] >= x
时,说明mid
及其左边可能含有值为x
的元素;当查找结束时,l
与r
相遇,l所在元素若是x
则一定是x
出现最小位置,因为l
左边的元素必然都小于x
。
📚查找小于等于x的最后一个位置
对于升序序列来说,当我们查找到一个符合条件的值后,我们要使查找区间向右偏移。
public static int Search(int x){
int l = 0, r = n-1;
while(l < r){
// 注意:这里加了1
int mid = l + r + 1 >> 1;
// mid位置上的元素大于x,我们要查找的是小于等于的
if(arr[mid] > x){
// 所以mid和其右边的全部舍弃
r = mid - 1;
}
else{
//mid位置上的元素小于等于x,我们不能舍弃,但要向右偏移,所以令l=mid
l = mid;
}
}
return l;
}
在以上的代码中, mid = (l + r + 1) / 2
为什么呢? 因为(l+r)/2
的结果会被下取整,当l = 0
, r = 1
时,mid = 0
如果此时arr[0]
正好等于x
的话, l
就会等于0
, 那么因为l
和r
并没有被更新,下一次循环mid
还是0
, 所以就会一直这样循环下去。为了避免死循环,需要特殊处理一下边界问题,也就是(l + r + 1) / 2
。这样得到的mid
就会不同了。
📚求X在序列中的起始位置和结束位置
通过上面两种二分方式我们可以得到大于等于X的第一个位置,和小于等于X的最后一个位置。显然,大于等于X的第一个位置就是X存在情况下的起始位置,小于等于X的最后一个位置就是X存在情况下的结束位置。
🍀题目练习(注意)
题目详情:789. 数的范围
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int q = sc.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
for (int i = 0; i < q; i++) {
int x = sc.nextInt();
int l = findl(x, a);
int r = findr(x, a);
if (x != a[l]) {
System.out.println(-1 + " " + -1);
} else {
System.out.println(l + " " + r);
}
}
}
private static int findr(int x, int[] a) {
int l = 0, r = a.length - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (a[mid] > x) r = mid - 1;
else l = mid;
}
return l;
}
private static int findl(int x, int[] a) {
int l = 0, r = a.length - 1;
while (l < r) {
int mid = l + r >> 1 ;
if (a[mid] < x) l = mid + 1;
else r = mid;
}
return l;
}
}
3. 局部最小值问题
定义何为局部最小值:
- arr[0] < arr[1],0位置是局部最小;
- arr[N-1] < arr[N-2],N-1位置是局部最小;
- arr[i-1] > arr[i] < arr[i+1],i位置是局部最小;
给定一个数组arr,已知任何两个相邻的数都不相等,找到随便一个局部最小位置返回
/**
* 定义何为局部最小值
* 1. arr[0] < arr[1],0位置是局部最小;
* 2. arr[N-1] < arr[N-2],N-1位置是局部最小;
* 3. arr[i-1] > arr[i] < arr[i+1],i位置是局部最小;
* @author hj
*/
public class 局部最小值问题 {
public static int getLessIndex(int[] arr) {
if(arr == null || arr.length == 0) {
return -1;
}
if(arr.length == 1 || arr[0] < arr[1]) {
return 0;
}
if(arr[arr.length-1] < arr[arr.length -2]) {
return arr.length - 1;
}
int left = 1;
int right = arr.length -2;
int mid = 0;
while(left < right) {
mid = (left + right) / 2;
if(arr[mid] > arr[mid - 1]) {
right = mid - 1;
} else if(arr[mid] > arr[mid + 1]) {
left = mid + 1;
} else {
return mid;
}
}
return left;
}
}