算法基本型——二分

引言

我还记得在高中的时候,有一道数学题是这么说的:在一片森林中,有一只啄木鸟守护着所有树木的健康,但啄木鸟面对一棵树,如何快速找到害虫呢?

我脑海里第一反应:从上啄到下,不就找到害虫了吗?

数学老师似乎看出了我的想法,他说:啄木鸟比同学们聪明些,它先啄树干的中间,观察害虫的活动范围是在上面还是下面,如果是上面,于是它再去啄上面树干的中间。于是啄木鸟每次只需要关注树干的一半,效率大大提升。

当时稚嫩的我并不知道这其中蕴含的精妙算法,只知道题就是这么做的。多年后,当我接触到编程算法,我才意识到,啄木鸟原来也懂二分,万能的造物者啊。

1. 二分

二分是编程算法中最简单的一个算法,它思想十分朴素,就是每次只关注目标的一半,一半又划分为一半,每次都缩小1/2,那么刚好符合对数的形式,因此二分的时间复杂度为O(lgn) < O(n)。二分的使用有一个重要限定,那就是目标数组是有序的

2. 二分基本型

比如我们需要在数组[2,4,6,8,9,12]中查找6,用二分代码如下:

	func search(nums []int, target int) int {
	    l, r := 0, len(nums)
	    for l < r {
            mid := l + ((r - l) >> 1) // 这里请大家注意,请用这种方式来二分
	        if nums[mid] == target {
	            return mid
	        }
	        if nums[mid] > target {
	            r = mid
	        } else {
	            l = mid + 1
	        }
	    }
	    return -1
	}

这就是二分的基本型,也称为标准二分。我们来分析一下。l < r ,说明这是左闭右开,左闭右闭可以吗?当然可以,对应代码如下:

	func search(nums []int, target int) int {
	    l, r := 0, len(nums)-1
	    for l <= r {
	        mid := l + ((r - l) >> 1) // 这里请大家注意,请用这种方式来二分
	        if nums[mid] == target {
	            return mid
	        }
	        if nums[mid] > target {
	            r = mid - 1
	        } else {
	            l = mid + 1
	        }
	    }
	    return -1
	}

这同样是标准二分。我们来观察一下对应关系:

左闭右开

变量 初始值 过程值 判断条件
l 0 mid + 1 l < r
r len(nums) mid

左闭右闭

变量 初始值 过程值 判断条件
l 0 mid + 1 l <= r
r len(nums)-1 mid - 1

聪明的你应该能体悟到这之间的对应关系。那么这就是二分的基本型。请大家记住这两个固定的关系


二分细节很复杂,哪里-1,哪里+1?细节是魔鬼,但是我们不用去玩弄那些细节,请记住基本型,我们所有的解题都基于基本型。

3. 二分基本型的应用

3.1. 二分找边界

二分更多的应用是用来找边界,题目LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置

找左边界:

func findLeftEdge(nums []int, target int) int {
	l, r := 0, len(nums)-1 
	for l <= r { 
		mid := l + ((r - l) >> 1) 
		if nums[mid] == target {
			r = mid - 1 // 收紧右边界,目标更靠近左边界
		} else if nums[mid] < target {
			l =  mid + 1
		} else {
			r = mid - 1
		}
	}
	// 返回左边界
	return l
}

查右边界

func findRightEdge(nums []int, target int) int {
	l, r := 0, len(nums)-1 
	for l <= r { 
		mid := l + ((r - l) >> 1) 
		if nums[mid] == target {
			l = mid + 1 // 收紧左边界,目标更靠近右边界
		}else if nums[mid] < target {
			l =  mid + 1
		}else {
			r = mid - 1
		}
	}
	// 返回右边界
	return r
}

可以很清楚的看到,我们查询边界没有做很复杂的改动,都是基于我们的基本型来的。所以基本型要牢记于心。该题题解如下:

func searchRange(nums []int, target int) []int {
	l := findLeftEdge(nums, target)
	// 越界 或 目标值不存在,直接返回
	if l == len(nums) || nums[l] != target {
		return []int{-1,-1}
	}
	r := findRightEdge(nums, target)
	return []int{l, r}
}

// 寻找左边界
func findLeftEdge(nums []int, target int) int {
	l, r := 0, len(nums)-1 
	for l <= r { 
		mid := l + ((r - l) >> 1) 
		if nums[mid] == target {
			r = mid - 1 // 收紧右边界,目标更靠近左边界
		}else if nums[mid] < target {
			l =  mid + 1
		}else {
			r = mid - 1
		}
	}
	// 返回左边界
	return l
}

// 寻找右边界
func findRightEdge(nums []int, target int) int {
	l, r := 0, len(nums)-1 
	for l <= r { 
		mid := l + ((r - l) >> 1) 
		if nums[mid] == target {
			l = mid + 1 // 收紧左边界,目标更靠近右边界
		}else if nums[mid] < target {
			l =  mid + 1
		}else {
			r = mid - 1
		}
	}
	// 返回右边界
	return r
}

3.2. 二分找峰值

请看LeetCode 162. 寻找峰值

我这边直接给出题解:

func findPeakElement(nums []int) int {
	l, r := 0, len(nums)-1
	for l <= r {
		mid := l + ((r - l) >> 1)
		// 收紧右边界,目标更靠近左边界
		if mid + 1 < len(nums) && nums[mid] == nums[mid+1] {
			r = mid - 1
		// 向右爬坡
		} else if mid + 1 < len(nums) && nums[mid] < nums[mid+1] {
			l = mid + 1
		// 向左爬坡
		} else {
			r = mid - 1
		}
	}
	return l
}

可以看到,我们依然是基于二分基本型来解题的。唯一不同的是,我们比较的目标是nums[mid+1],nums[mid] < nums[mid+1]向右爬坡,nums[mid] > nums[mid+1]向左爬坡,这个过程是不是就像爬坡一样。

3.3. 旋转数组

请看LeetCode 153. 寻找旋转排序数组中的最小值

我这边直接给出题解:

func findMin(nums []int) int {
    l, r := 0, len(nums)-1
	for l <= r {
		mid := l + ((r - l) >> 1)
		// 收紧右边界,目标更靠近左边界
		if nums[mid] == nums[len(nums)-1] {
			r = mid - 1
		// 向左下坡
		} else if nums[mid] < nums[len(nums)-1] {
            r = mid - 1
		// 向右爬坡
		} else {
            l = mid + 1
		}
	}
	return nums[l]
}

可以看到,我们还是基于二分基本型来解题的。不同的是,我们比较的目标是nums[len(nums)-1],nums[mid] < nums[len(nums)-1]向左下坡,nums[mid] > nums[len(nums)-1]向右爬坡,这里就是与找峰值的区别所在,就决定了我们的目标是比较nums[len(nums)-1]而不是nums[mid+1]。这个过程大家仔细去分析下程序就明白了。

4. 二分基本型扩展应用

那掌握基本型是不是就完了?不不不。

上面的题目都是直接给我们数组,我们对着数组就是二分。有难度的题目会那么明显让我们看出二分的意图吗?要有那么简单的话,世界就充满爱了。

我们直接上题:LeetCode 剑指 Offer II 073. 狒狒吃香蕉

请大家好好体味一下这道题,然后再来看题解。

func minEatingSpeed(piles []int, h int) int {
    sort.Ints(piles)
    l, r := 1, piles[len(piles)-1]
    for l <= r {
        mid := l + ((r - l) >> 1)
        if judge(piles, mid) == h {
            r = mid - 1
        }
        if judge(piles, mid) > h {
            l = mid + 1
        } else {
            r = mid - 1
        }
    }
    return l
}

func judge(piles []int, mid int) int {
    res := 0
    for i := 0; i < len(piles); i++ {
        res += piles[i] / mid
        if piles[i] % mid != 0 {
            res++
        }
    }
    return res
}

现在大家能明白了吧,我们要二分的数组是[1, piles[len(piles)-1]],它是隐藏的,需要我们自己挖掘。题目往往难点就在这里,需要通过分析题目后找出隐藏目标。

5. 结语

好了,二分基本型就到这里了。

算法基本型 文章被收录于专栏

算法基本型感悟

全部评论

相关推荐

bg:双非本,一段中小厂6个月测开实习今天发这个帖子主要是想聊一聊我秋招以来的一个发展我是在8月底辞职,打算秋招,可是看网上都说金九银十就想着自己就是一个普通本科生,现在九月份都是一些大神在争抢,所以9月份基本上没投,等到了10月份才开始秋招,可是这个时间好像已经有些晚了,今年秋招开启的格外早,提前到了7,8月份,我十月才开始,官网投了很多公司,没有任何一个面试机会,这个情况一直到了十月底才有了第一个面试,当时没有面试经验,所以不出意外的挂了后续就是漫长的投递,但是毫无例外没有面试,没有办法我只能另辟蹊径开始在BOSS上边投递,然后顺便也根据BOSS上边这个公司名称去浏览器搜索看看有没有官网投递渠道,毕竟官网上投递后还是可以第一时间被HR看到的,然后一直不停投递,一开始第一个星期基本上都是投的正式秋招岗位到了第二个星期才开始实习和正式一起投,到十一月底的时候已经沟通了700➕才有一共1个正式的,5个要提前实习的,3个实习的面试,最后结果是过了1个要提前实习的和2个实习的每次面试我都会复盘,发现这些小公司面试官问的五花八门,有的专问基础,有的专问项目,有的啥都问,不过自己也是看出来了一下门道,就是小公司不像大公司面试官那样能力比较强基本上你简历上边的他都会,然后会根据简历来问,小公司面试官他们更多的是看自己会什么,然后看看你简历上边哪些他也是会的然后来问,经过不断的复盘加上背各种各样面试题,到了11月底12月初才有了1个要提前实习的offer还有2个实习的offer,而且薪资待遇对我来说已经很可观了可是啊,人总是这样得了千钱想万钱,我又开始不满现状,但是此时的我面试能力经过这么多面试和复盘已经很强了,然后在十二月份运气爆棚,被极兔和小鹏补录捞起来面试,还有个百度测开的实习面试,这个时候因为有了offer所以感觉有了底气,面试也很自信,最后结果是全部都过了那个时候我感觉自己真的很厉害,我问了极兔那边的HR像我这样的双非本收到offer的在极兔有多少?他告诉我产研岗90%都是硕士,10%里边基本上都是211,985,想我这样的很少很少,那一刻感觉自己超级牛逼,小鹏就更不用说了,最后也是不出意外选择了小鹏所以我就我个人经历想对和我学历履历差不多的牛友一些建议第一:秋招一定要趁早,真到了9,10月,那个时候可能你投的结果可能还不如7,8,11月,第二:最好先拿小公司实习或者正式练练手,提升一下面试能力,我个人觉得因为小公司问的五花八门所以你会更加横向去提升自己能力,而且大公司其实面试没有那么难,除了一些非常卷的岗位,公司大神比较多会问的很难,一般好点的公司都不会问的那么难,他们也知道都是应届生不会要求那么高第三:当有一定能力后,就是坚持了,对于我们这样的学历,没有特别强的履历情况下,就是要抓住提前批和补录的机会,这个时候各方面不会卡的很严,是我们很好很好的一个机会第四:就是运气也是很重要的一部分,不过这个很难去说什么最后祝各位牛友都能收获自己满意的offer😁😁😁
秋招,不懂就问
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务