【数据结构与算法】二分类型 算法 怎么写

/**
 * 二分类算法怎么写?
 * 几个点吧:
 * 1.条件用i < j,因为退出循环时i肯定等于j,不用区分用i还是j;
 * 2.最终一定是两个元素;
 * 3.关于ij的更新有两种情况:
 *   2.1 m取下整,i = m + 1或者j = m;右区间不包含m,左区间包含;(i + j) >>> 1
 *   2.2 m取上整,j = m - 1或者i = m;左区间不包含m,右区间包含;(i + j + 1) >>> 1
 * 4.每一次排除掉一半的元素,找到剩余的区间。看是2.1和2.2的那种情况;
 */

https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/

 

不知大家是否和我有同感,二分类的算法写起来有点吃力,思路很乱。这次研究了下规律。

二分涉及的算法主要有二分查找的递归和非递归以及二分插入排序还有找小于某个数的最大值或者大于某个数的最小值(前提是已排序)。二分查找还算简单,后面两个如果不想好写起来很麻烦。

二分最核心的思想是“一分为二”,先看中间的元素是否满足需求,如果满足返回,否则再去查看左半边(low,m-1)的中间或者右半边(m+1,high)的中间。这就是二分类算法的主体思路。很显然这里是一个循环的结构们当然也可以是递归。不论选哪种,都要先解决一个问题,那就是终止条件。如果是while循环,那么只要low<=high就应该进入循环,因为low<=high,那么中间的元素是合法的,就需要进入循环检查。如果是递归,那么return语句的条件是low>high,原因同上。一旦中间元素不满足,那么更新下标时应该除去此次的中间元素,因为它在这次循环中已经被证明不成立。这就是m+1和m-1 的原因。

弄清楚这些,其实二分查找很好理解,本质上两者没有区别,都是更新下标而已。

有一个问题,就是这个二分的过程是怎样的?我之前总是被这个问题弄晕。。。其实可以推算出来,二分退出循环或者递归之前的一次情况只有种,high - low ==0,high - low ==1.也就是说最后二分的序列长度只可能为1和2(长度为1,如果中间不合法,那么下一次循环肯定low>high,长度为2,如果中间不合法,那么往左半边找下一次也会low>high,右半边则转换为长度为1的情形,因此low和high在最后不一定出现相等的情况),因为其他任意多于2的序列最终都被分解为以上两种情况。

 

package main;

/**
 * 
 * @author miracle
 *二分查找在事先排序的前提下的时间复杂度为logn,低于顺序查找
 */
public class Algorithm {

	public static int binarySearch(int[] array ,int k){
		return search(array, 0, array.length - 1, k);
	}
	
	private static int search(int[] array ,int low, int high, int k){
		if (low > high) {
				return -1;
		}else {
			int middle = (low + high) / 2;
			if (array[middle] == k) {
				return middle;
			}else if (array[middle] < k) {
				return search(array, middle + 1, high, k);
			}else {
				return search(array, low, middle - 1, k);
			}
		}
	}
	
	//非递归实现,当l<h时,说明没有找到,返回-1,否则就进入循环体,看中间,决定找左还是右。
	public static int binarySearch1(int[] array ,int k){
		int low = 0, high = array.length - 1;
		while(low <= high){
			int m = (low + high) / 2;
			if(array[m] == k){
				return m;
			}else if (array[m] > k) {
				high = m - 1;
			}else {
				low = m + 1;
			}
		}
		return -1;
	}
}

 

 

 

 

 

下一个是如何找小于t的最大值(非递归),还是按照上面的思路,只不过在循环体内部分为两种情况,如果中间大于t,因为我们要找小于t的元素,因此查找左边,如果中间小于t,因为要找其中最大的,所以向右边查找,如果等于,很简单,向左,因为我们要找小于t的,这样最终low > high 时,应该返回low还是high?答案是high,可以通过特例来记忆,比如只有一个元素,如果大于等于t,那么那么往左,high减1,最后high是-1,符合。如果小于,那么往右,low加1,high指向0,也符合。

那么大于t的最小数也可以按照上面的思路得到。二分插入排序中找插入位置的思路其实就是找大于t的最小数,这个位置有一个特殊的名字,叫insert位置,意思就是这个位置是插入t的合适的位置,使得整个数组仍是有序的。过这里需要返回的是low,也可以通过特例来记忆。只有一个元素的特例来记忆。

除此之外,也可以按照特征,找第一个小于,返回high,找第一个大于返回low,小high大low,正好相反。

low和high也是t紧挨着的左右两边的元素,high在左,low在右。

 

/**
	 * 第一个大于和第一个小于返回的值不同均对应了各自一个特殊情况:
	 * 第一个大于:在array均小于等于的情况下,应该指向array.length,表明没有;
	 * 第一个小于:在array均大于等于的情况下,应该指向-1,表明没有;
	 * 可以通过上述特殊情况配合array只有一个元素来判断返回low还是high。比如找第一个大于,array=[3],t=10,发现array[m]小于,则右边,那么low要加1,因此low是正解。
	 * 也可以通过字面意思记忆,比如要找第一个大于,如果全小于等于,则找右边,那么low加1,返回low。正好相反,low对应大于,high对应小于。
	 * 由于有上述特殊情况存在,在实际使用的时候需要检查是否出现,否则直接用可能越界。
	 * 还有一点,第一个大于也叫作insert位置,也就是说如果把t加入array中应该处在的位置。在二分插入中会用到。
	 * java里面的collections和arrays提供的binaryserch就包含了上述功能,如果存在返回下标,如果不存在返回(-index)-1,这样只要存在就是正整数,不存在是负数,而且负数还是具有意义的,可以得到insert位置。
	 */
	public int minGreaterThan(int[] nums, int t){
		int low = 0, high = nums.length - 1;
		while(low <= high){
			int m = (low + high) / 2;
			if (nums[m] <= t) {
				low = m + 1;
			}else {
				high = m - 1;
			}
		}
		return low;
	}
	
	public int maxLessThan(int[] nums, int t){
		int low = 0, high = nums.length - 1;
		while(low <= high){
			int m = (low + high) / 2;
			if (nums[m] < t) {
				low = m + 1;
			}else {
				high = m - 1;
			}
		}
		return high;
	}

 

其实我们用的平台的库函数会对二分有很好的支持,比如java的集合类,有binarysearch的实现,这个函数很强大,如果找到,返回下标,如果找不到,返回(-insertPosition-1),这是个负数用于区分是否找到,并且这个负数包含了insert位置这样很有用的信息。

代码如下:

 

	public static int binarySearch(int[] array, int k){
		int low = 0, high = array.length - 1;
		while(low <= high){
			int m = (low + high) / 2;
			if(array[m] == k)
				return m;
			else if(array[m] > k)
				high = m -1;
			else low = m + 1;
		}
		return -low - 1;
	}

 

 

 

 

 

上述思路还可以扩展到其他应用场景,比如找到第一个不符合或者符合某一条件。假设从前往后找第一个不符合的,那么其中的逻辑就是只要中间符合就往右,否则往左,最后返回low即可,返回low还是high用特例。其实之前的第一个小于或者大于就是这个问题的特例。

在实际应用的时候,要注意int越界问题,事先转为long来做处理。

比如leetcode278题:https://leetcode.com/problems/first-bad-version/

 

public class Solution extends VersionControl {
       public int firstBadVersion(int n) {
    	long low = 1, high = n;
        while(low <= high){
        	int m = (int) ((low + high) / 2);
        	//bad
        	if(isBadVersion(m)){
        		high = m - 1;
        	}else {
        		low = m + 1;
        	}
        }
        return (int)low;
    }
}

有了这些,那么写二分类的算法算是思路比较清晰了。
 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值