二分查找用法小结(Java实现)

7 篇文章 5 订阅

二分查找用法小结(Java实现)


参考文章:繁著的简书文章


一、二分法定义

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]x做比较,如果x=a[n/2],则找到x,算法中止;如果x<a[n/2],则只要在数组a的左半部分继续搜索x,如果x>a[n/2],则只要在数组a的右半部搜索x

二、基本二分及其变形用法

1. 基本的二分查找

二分思想就是每次都找中间的那个数,如果中间那个数小于待查找的数,那么找右半边,否则找左半边,然后一直循环上述操作,直到找到或者区间长度为0为止。

假设num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 16,查找过程如下:
img1

代码实现:

    public static int baseBinarySearch(int[] num, int target) {
        int left = 0, right = num.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2; // 防止数太大溢出
            if (target == num[mid]) {
                return mid;
            } else if (target > num[mid]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1;
    }

2. 如果有多个与target相等,返回第一个与target相等的值的下标,如果找不到target则返回-1

示例1num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 1,返回0
示例2num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 13,返回3
示例3num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 14,返回-1

这其实就是上一个的变形,只是当找到与target相等的值的时候不直接返回下标,而是令right = mid - 1继续循环,因为它左边可能还有与target相等的。当循环终止后,要取left作为结果而不是right,因为循环倒数第二轮的时候left一定是等于right并且都是指向答案的(至于为什么,多多实验就知道了),此时mid = (left + right) / 2 = left = rightnum[mid] = target导致下一轮right = mid - 1,即向左移了一位,所以最后一轮left一定是等于right + 1,可看如下图示,最后如果left小于数组的长度并且等于target就返回left,否则返回-1

假设num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 1,查找过程如下:
img2

    public static int findFirstEquality(int[] num, int target) {
        int left = 0, right = num.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (target > num[mid]) { // 这里不能取等号,因为当target等于中点值时,我们是要找到它左边的第一个,如果中点这个数没有重复,那么答案就是这个中点,如果有重复,那么答案只能在左边,故right要左移,而不是left右移
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (left < num.length && num[left] == target)
            return left; // 取left而不是取right是因为,循环倒数第二轮的时候left肯定会等于right,而且都是指向答案,而如果这个值不等于target直接返回-1,否则right会左移一位,所以用left
        return -1;
    }

3. 查找小于target且最接近target的数的下标,没有则返回-1

示例1num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 1,返回-1
示例2num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 13,返回2
示例3num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 14,返回4

有了上一题的理解,这个就更好理解了,在上一题中我们找到了target的最左下标,那么这个下标减1即本题答案,上一题中最后取的答案是left,同时也分析了最后一定是right = left - 1,所以right就是本题答案,只需要判断right是否大于0即可。

    public static int findLessThan(int[] num, int target) {
        int left = 0, right = num.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (target > num[mid]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (right >= 0) return right;
        return -1;
    }

4. 如果有多个与target相等,返回最后一个与target相等的值的下标,如果找不到target则返回-1

示例1num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 1,返回1
示例2num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 13,返回4
示例3num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 14,返回-1

与第二题反着来就行。

    public static int findLastEquality(int[] num, int target) {
        int left = 0, right = num.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (target >= num[mid]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (right >= 0 && num[right] == target)
            return right;
        return -1;
    }

5. 查找大于target且最接近target的数的下标,没有则返回-1

示例1num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 1,返回2
示例2num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 13,返回5
示例3num = {1, 1, 11, 13, 13, 16, 18, 18, 20}target = 14,返回5

与第三题反着来就行。

    public static int findMoreThan(int[] num, int target) {
        int left = 0, right = num.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (target >= num[mid]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (left < num.length)
            return left;
        return -1;
    }

6. 旋转数组查找最小值

旋转数组:即将有序数组的前面一部分整体移到数组末尾。

无重复数的旋转数组

leetcode第153题:寻找旋转排序数组中的最小值

示例1num = {5, 6, 9, 10, 1, 2, 3},返回4
示例2num = {10, 14, 1, 2, 5, 8},返回2
示例3num = {100, 1000, 10000, 1, 10},返回3

二分找中点,然后中点值与右边界值比较(要注意右边界是nums[nums.length - 1]而不是nums[right]),要是大于右边界,说明最小值一定还在右边,故左指针右移,否则右指针左移。

	public static int findMin(int[] nums) {
		int left = 0, right = nums.length - 1;
		while (left <= right) {
			int mid = left + (right - left) / 2;
			if (nums[mid] > nums[nums.length - 1]) { // 如果nums[mid]大于右边界点,说明最小值在mid的右边
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
		return left; // 不需要判断left是否越界,因为倒数第二次循环时候left一定等于right并且指向答案,那么最后一次循环是right左移,故取left不取right。
	}
有重复数的旋转数组

leetcode第154题:寻找旋转排序数组中的最小值II

示例1num = {5, 6, 9, 9, 10, 1, 1, 2, 3},返回5
示例2num = {10, 14, 1, 2, 5, 10},返回2
示例3num = {100, 1000, 1000, 10000, 1, 10},返回4

上题的变形,当有重复值的时候(如{3, 4, 5, 1, 2, 3}左指针值等于右指针值),我们除去一个重复值即可,故就让左指针右移进一步判断。

	public static int findMin(int[] nums) {
		int left = 0, right = nums.length - 1;
		while (left <= right) {
			if (left != right && nums[left] == nums[nums.length - 1]) {
				left++;
				continue;
			}
			int mid = left + (right - left) / 2;
			if (nums[mid] > nums[nums.length - 1]) { // 如果nums[mid]大于右边界点,说明最小值在mid的右边
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
		return left;
	}

7. 旋转数组搜索

无重复数的旋转数组

leetcode第33题:搜索旋转排序数组

示例1num = {5, 19, 21, 1, 2, 3}target = 1,返回3
示例2num = {10, 14, 1, 2, 5}target = 1,返回2
示例3num = {100, 1000, 1000, 10000, 1, 10}target = 2,返回-1

每次取mid的时候,数组都会被分成两段,并且一定有一段是有序的(比如{5, 19, 21, 1, 2, 3},被分成{5, 19}{1, 2, 3},无论怎么分一定会有一段是有序的),如果左半边有序,且target在里面,那么就继续二分左半边,否则二分右半边;如果右半边有序,且target在里面,那么就继续二分右半边,否则二分左半边。

	public static int search(int[] nums, int target) {
		int left = 0, right = nums.length - 1;
		while (left <= right) {
			int mid = left + (right - left) / 2;
			if (nums[mid] == target) {
				return mid;
			} else if (nums[left] <= nums[mid]) { // 如果左半边有序
				if (nums[left] <= target && target < nums[mid]) // 如果target在其中,就二分左半边
					right = mid - 1;
				else
					left = mid + 1;
			} else { // 如果右半边有序
				if (nums[mid] < target && target <= nums[right]) // 如果target在其中,就二分右半边
					left = mid + 1;
				else
					right = mid - 1;
			}
		}
		return -1;
	}
有重复数的旋转数组

leetcode第81题:搜索旋转排序数组II

示例1num = {5, 6, 9, 9, 10, 1, 1, 2, 3}target = 1,返回true
示例2num = {10, 14, 1, 2, 5, 10}target = 1,返回true
示例3num = {100, 1000, 1000, 10000, 1, 10}target = 2,返回false

上题变形,和上上题一样,加个判断就行。

	public static boolean search(int[] nums, int target) {
		int left = 0, right = nums.length - 1;
		while (left <= right) {
			if (left != right && nums[left] == nums[nums.length - 1]) {
				left++;
				continue;
			}
			int mid = left + (right - left) / 2;
			if (nums[mid] == target) {
				return true;
			} else if (nums[left] <= nums[mid]) {
				if (nums[left] <= target && target < nums[mid])
					right = mid - 1;
				else
					left = mid + 1;
			} else {
				if (nums[mid] < target && target <= nums[right])
					left = mid + 1;
				else
					right = mid - 1;
			}
		}
		return false;
	}

三、实例题目

1. 连续1的最长个数

题意:给定一个只包含01的数组,进行k01操作,问变k次后最长连续1串长度。

示例1num = {1, 0, 0, 1, 0, 1, 1},k = 1,答案为4,将num[4]变成1
示例2num = {1, 0, 0, 1, 0, 1, 1},k = 2,答案为5,将num[2]num[4]变成1

思路1:暴力法,两层循环,第一层循环代表左端点,然后第二层循环代表右端点不断向右试探,直到不满足条件(变完了k0),这个时间复杂度为 O ( N 2 ) O(N^2) ON2,肯定不是理想的解法。

	public static int solve(int[] num, int k) {
		int res = 0;
		int len = num.length;
		for (int i = 0; i < len; ++i) {
			int zeroSum = 0;
			for (int j = i; j < len; ++j) {
				if (num[j] == 0)
					zeroSum++;
				if (zeroSum <= k) {
					res = Math.max(res, j - i + 1);
				}
			}
		}
		return res;
	}

思路2:动态规划法,写出来是为了下面的解法做铺垫,我们利用一个二维数组zeroSum[][]存放ij0的个数,zeroSum[i][j]代表num[i] ~ num[j]0的个数,zeroSum[i][j] = zeroSum[i][j - 1] + (num[j] == 0 ? 1 : 0)时间复杂度为 O ( N 2 ) O(N^2) ON2

	public static int solve(int[] num, int k) {
		int res = 0;
		int len = num.length;
		int[][] zeroSum = new int[len][len];
		for (int i = 0; i < len; ++i)
			for (int j = i; j < len; ++j)
				if (i == j) zeroSum[i][j] = (num[j] == 0 ? 1 : 0);
				else zeroSum[i][j] = zeroSum[i][j - 1] + (num[j] == 0 ? 1 : 0);
		for (int i = 0; i < len; ++i)
			for (int j = i; j < len; ++j)
				if (zeroSum[i][j] <= k) res = Math.max(res, j - i + 1);
		return res;
	}

思路3:优化动态规划 + 二分法,大多数人都不会想到用二分法来求解,其实一般只要数据具有单调性,并且所问的问题诸如最多、最少、最大、最小、最长等等的,一般都可以利用二分法来求解。
在第一个解法中,右端点的查找重复算了很多次01,而第二个解法的二维数组导致时间复杂度下不来,其实可以优化为一维数组,故用一维数组zeroSum[]来存放0的个数,zeroSum[i]代表num[0] ~ num[i]0的个数,这样存的好处是可以快速求出num[i] ~ num[j]0的个数,比如num[2] ~ num[5]0的个数等于zeroSum[5] - zeroSum[1]。同样两层循环,第一层也是遍历左端点,不同的是右端点利用二分来查找,我们可以将第一层循环的i当成是已知的,然后需要的是右端点在满足k0的条件下尽可能走的远,那么就相当于在一个数组中找一个数的右边界(这个数组即{zeroSum[j] - zeroSum[i] | j ∈ [i, num.length)},解法参考上边二分查找的第四题),时间复杂度为 O ( N l o g 2 N ) O(Nlog_2N) ONlog2N,实现代码如下。

二分法实现:

	public static int solve(int[] num, int k) {
		int res = 0;
		int len = num.length;
		int[] zeroSum = new int[len]; // zeroSum[i]代表num[0]~num[i]有多少个0
		for (int i = 0; i < len; ++i) { // 初始化zeroSum数组
			if (i == 0) zeroSum[i] = (num[i] == 0 ? 1 : 0);
			else zeroSum[i] = zeroSum[i - 1] + (num[i] == 0 ? 1 : 0);
		}
		for (int i = 0; i < len; ++i) { // 遍历左端点
			int left = i, right = len - 1; // 二分查找右端点
			while (left <= right) {
				int mid = left + (right - left) / 2; // 防止数据溢出
				if (zeroSum[mid] - (i == 0 ? 0 : zeroSum[i - 1]) <= k) { // i如果等于0,要注意边界问题
					left = mid + 1;
				} else {
					right = mid - 1;
				}
			}
			res = Math.max(res, right - i + 1); // 更新答案
		}
		return res;
	}

思路4:双指针法(可理解为滑动窗口),思路一的优化版,即不重复计算0的个数,初始化leftright指针,固定left,right不断向右试探,但是循环一次后right不会重新初始化,而是利用上一轮循环结果,这样就大大提高了效率。

双指针实现:

	public static int solve4(int[] num, int k) {
		int res = 0;
		int tmp = 0; // left到right之间0的个数
		int len = num.length;
		for (int left = 0, right = 0; left < len; ++left) {
			while (right < len && tmp + (num[right] == 0 ? 1 : 0) <= k) // right不断向右试探,直到不满足条件为止
				tmp += (num[right++] == 0 ? 1 : 0);
			res = Math.max(res, right - left); // 更新答案
			tmp -= (num[left] == 0 ? 1 : 0); // 当前一轮循环结束,要减去左端点,给下一轮循环腾出0的空间
		}
		return res;
	}

2. 贪吃的小Q(腾讯2018年笔试题)

测试链接:https://www.nowcoder.com/questionTerminal/d732267e73ce4918b61d9e3d0ddd9182

题目:

小Q的父母要出差N天,走之前给小Q留下了M块巧克力。小Q决定每天吃的巧克力数量不少于前一天吃的一半,
但是他又不想在父母回来之前的某一天没有巧克力吃,请问他第一天最多能吃多少块巧克力。

输入描述:

每个输入包含一个测试用例。
每个测试用例的第一行包含两个正整数,表示父母出差的天数N(N<=50000)和巧克力的数量M(N<=M<=100000)。

输出描述:

输出一个数表示小Q第一天最多能吃多少块巧克力。

示例
输入

3 7

输出

4

思路:
要求第一天吃的最多,那么第一天之后的必须都吃最小值,即前一天的一半(向上取整)。
注意到,题目问的又是最多,并且数据也有单调性(蛋糕数量累加是递增的),那么二分又来了,我们二分试探第一天吃的数量,然后按照这个值把N天吃的累加起来(表示为sum),再和M比较,如果是sum <= M,说明第一天应该还可以多吃一点,这就等价于找数组的右边界。

AC代码:

import java.util.Scanner;

public class Main {
	public static void main(String[] args) {
		run();
	}
	private static void run() {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int m = scanner.nextInt();
		int left = 0, right = m;
		while (left <= right) {
			int mid = left + (right - left) / 2;
			int tmp = sum(mid, n);
			if (tmp <= m) {
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
		if (right > 0) {
			System.out.println(right);
		}
		scanner.close();
	}

	private static int sum(int init, int day) { // 初始值,吃day天,总共吃多少,每天按照前一天的一半算(向上取整)。
		int res = 0;
		for (int i = 0; i < day; ++i) {
			res += init;
			init = (int) Math.ceil(init * 1.0 / 2);
		}
		return res;
	}
}

3. 闹钟叫醒去上课(字节跳动2020秋招笔试题)

字节跳动2020秋招第一题:https://blog.csdn.net/hzj1998/article/details/99285786

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个使用Java实现二分查找的示例代码: ```java public class BinarySearch { public static int binarySearch(int[] array, int target) { int left = 0; int right = array.length - 1; while (left <= right) { int mid = (left + right) / 2; if (array[mid] == target) { return mid; } else if (array[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; } public static void main(String[] args) { int[] nums = {1, 3, 5, 7, 9, 11, 13}; int target = 7; int index = binarySearch(nums, target); if (index == -1) { System.out.println("目标元素不存在"); } else { System.out.println("目标元素在数组中的索引为: " + index); } } } ``` 该代码中,`binarySearch`方法接收一个已排序的整数数组和一个目标值,返回目标值在数组中的索引。如果目标值不存在于数组中,则返回-1。 在方法中,我们使用`left`和`right`两个指针表示当前搜索区间的左右边界。然后,我们使用`while`循环不断缩小搜索范围,直到找到目标元素或搜索区间为空。 在每次循环中,我们首先计算中间元素的索引`mid`。如果中间元素等于目标值,则直接返回该元素的索引;否则,如果中间元素小于目标值,则将搜索区间缩小到右半部分;如果中间元素大于目标值,则将搜索区间缩小到左半部分。 最后,如果循环结束时仍然没有找到目标元素,则返回-1表示目标元素不存在于数组中。 在上面的示例代码中,我们使用`main`方法来测试`binarySearch`方法。我们创建一个已排序的整数数组`nums`,并在其中搜索目标值`7`。如果找到了目标值,则输出其在数组中的索引;否则,输出“目标元素不存在”。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值