从头再来!社招找工作——算法题复习一:二分查找

写在前面的话

可能是我能力不足,或者运气太差,又或者真的是行业冥灯,工作短短几年就丢了两次工作,这次又要开始找新工作了。本就心情很低落,要准备简历、回顾项目、复习算法题,压力真的挺大的。在此,记录一下复习算法题的脉络,更好地进行准备。也是为了如果下次又被layoff了,有更详细的参考资料。
不知道自己能不能坚持把算法部分整理完全。或许以后还会把如何写简历、如何把项目聊清楚也单独写个博客。如果真的有可能,将来我会把这部分作为序曲单独抽出来,好好写写。这次就先絮叨到这,开始算法复习的第一章:二分查找。

本系列博客都以Java语言为代码编程语言。另外,想用一句话来总结我对写算法题代码的心得:一定要准备笔和纸、多画图,这比敲键盘有用。

二分查找(Binary Search)

二分查找应该算是算法题中最简单的那类题了。嗯,或许有人觉得那种模拟题可能更简单,但我觉得模拟题可能都称不上Algorithm,只能说是一个给定的Process让我们实现。所以算法篇的第一章,我们从二分查找开始。
具体原理和思想就不赘述了,主要是深究一些写代码时的细节。

二分查找的基本架构

给定一个函数

int binarySearch(int[] nums, int val) {}

其中nums是待查找的数组,val是要查找的数值

Example 1

最经典的二分查找算法题:
nums是严格递增的数组,val可能在nums数组中出现,也可能不出现。如果出现,val在nums中的index是什么?如果不出现,请返回-1
直接给出代码,细节部分已经注释在代码中:

int binarySearch(int[] nums, int val) {
	int left = 0;
	int right = nums.length - 1; // 有的版本是right = nums.length; 我的这个版本保证了nums[mid]永远不会越界,这在接下来的几个examples中很有用
	while (left <= right) { // 既然你之前都用了int right = nums.length - 1; 那么这里必须是<=
		int mid = left + (right - left) / 2; // 相信大部分人会写mid = (right + left) / 2; 但是如果leetcode刷多了,就会发现有些很恶心的case中right+left会大于Integer.MAXVALUE. 所以用我这种写法保证万无一失
		if (nums[mid] == val) {
			return mid;
		}
		else if (nums[mid] > val) { // val在nums[mid]的左边
			right = mid - 1; // 既然你之前都用了int right = nums.length - 1; 那么right = mid - 1; 你也必须跟我保持一致咯
		}
		else {
			left = mid + 1;
		}
	}
	return -1;
}

时间复杂度O(lgn), 空间复杂度O(1)。
当nums是严格递减的时候,大家可以对代码进行合理的改动,相信这应该不难。

Example 2

一个稍微复杂一点的变体:
nums是严格递增的数组,val可能在nums数组中出现,也可能不出现。如果出现,val在nums中的index是什么?如果不出现,那么val必定能被插入到nums的(k, k+1)中,这个k是多少?(换一个问法:nums中比val小的最大值的index是多少?)
基于这个变体问题,我们只需要思考在Example 1中,如果val不存在于nums中,当while(left <= right)循环条件不满足、跳出循环后,left和right的值会是什么?思考之后可以发现,right刚好等于left-1,且(right, left)区间刚好是题干中提到的(k, k+1)区间。所以,我们要返回的k就是right。
代码只需改动最后的return部分:

int binarySearch(int[] nums, int val) {
	...
	return right;
}

时间复杂度O(lgn), 空间复杂度O(1)。
同样地,当nums是严格递减的时候,对该代码小幅改动即可。

Example 3

nums是递增的数组(数组中可能有重复的数值),val可能在nums数组中出现,也可能不出现。如果出现,请给出val在该数组中最小的那个index;如果不出现,请返回-1
这个问题的难度在于如何去找最小的那个index。直接给出方法:当nums[k]==val时,比较nums[k-1]和val。如果nums[k-1]<val,说明k正好是这个递增数组第一次出现val的地方。否则,如果nums[k-1]==val,说明这个val并不是第一次出现了。
此时有经验的程序员应该就会留意到了,这里的索引下标出现了k-1,可能会导致越界问题!所以我们要提前对nums[0]进行特殊处理。
完整代码如下:

int binarySearch(int[] nums, int val) {
	// 对index=0进行特殊处理,防止数组下标出现-1导致越界
	if (nums[0] == val) {
		return 0;
	}
	int left = 1;
	int right = nums.length - 1;
	while (left <= right) {
		int mid = left + (right - left) / 2;
		// 接下来的三个判断条件的顺序很有讲究,我已经化成最简形式了。可以思考一下为什么要这样设置判断条件与顺序
		if (nums[mid] < val) {
			left = mid + 1;
		}
		if (nums[mid] == val && nums[mid-1] < val) {
			return mid;
		}
		else {
			right = mid - 1;
		}
	}
	return -1;
}

时间复杂度O(lgn), 空间复杂度O(1)。
同样地,当nums是严格递减的时候,对该代码小幅改动即可。有兴趣的朋友还可以想想另外两种变化:

  1. 想获取val在该数组中最的那个index
  2. 如果val不存在,找到可以插入的位置(k, k+1)

更多的例子

更多的例子可以参考Leetcode和牛客网上的编程题,比如旋转数组中的二分查找等。
总而言之,二分查找的代码题还算简单,至少它是有个框架可以去套的。至于内部的一些细节,大家可以多多画图、多多手动模拟,去找到最关键的三个if语句的判断内容,那么二分查找问题就可以迎刃而解了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值