一、定义
二分查找又称折半查找,在处理有序且可以通过下标对元素进行访问的序列时(如有序数组),二分法会有非常好的效果。
二、思路
以在一长度为n的非递减数组A中查找是否有某元素K为例,二分查找的基本思路可以概括为:
1、定义好查找的边界left、right后,首先从中间位置mid开始查找
2、如果A[mid]的值等于K,说明已经找到该值,返回mid即可
3、如果A[mid]的值大于K,因为数组A是非递减的,因此mid以后的元素值肯定大于等于A[mid],也就大于K;所以此时我们将该问题的搜索空间直接减半,左边界不用改,右边界改为mid-1。然后重复步骤1
4、同理,如果A[mid]的值小于K,说明待查找元素K不可能存在于[left,mid]之间,将搜索空间减半,另left=mid+1,right不变;重复执行步骤1。
示例代码如下:
def findK(num, K):
left = 0
right = len(num)
while(left <= right):
mid = (left + right) // 2
if num[mid] == K:
return mid
elif num[mid] < K:
left = mid + 1
elif num[mid] > K:
right = mid - 1
return -1
三、leetcode441、排列硬币
1、原题
你总共有 n 枚硬币,你需要将它们摆成一个阶梯形状,第 k 行就必须正好有 k 枚硬币。给定一个数字 n,找出可形成完 整阶梯行的总行数。n 是一个非负整数,并且在32位有符号整型的范围内。
来源:力扣(LeetCode)
2、思路
(1)最容易想到的思路就是暴力遍历了,for循环的范围是从1到n,代码如下:
class Solution:
def arrangeCoins(self, n: int) -> int:
if n == 1:
return 1
tmp = n # tmp记录剩余待排列的硬币数
cnt = 0 # 记录可形成的整阶梯行的总行数
# i的值就表示第i个阶梯上需要放置i枚硬币
for i in range(1, n):
if i <= tmp: # 若剩余待排列的硬币数多余第i行要求的硬币
tmp -= i # 摆好第i行,同时更新剩余的硬币数
cnt += 1
else:
break
return cnt
class Solution:
def arrangeCoins(self, n: int) -> int:
return int((0.25+2*n)**0.5 - 0.5)
(3) 第三种思路就是二分法了。如何理解用二分法解这道题?
根据题意,给了n个硬币,我们要做的就是看这些硬币能摆出来几个完整的阶梯。那么,对于非负整数n,它最少是1个阶梯也摆不出来,比如n=0时,因此,二分法的左边界left肯定是0,意思就是最少能摆出0个阶梯;那么最多摆出几个阶梯呢?答案是n // 2 + 1,为什么?每一个下层阶梯肯定要比上层阶梯多1,而2*(n//2 + 1) 的乘积是大于等于n的。用它做右边界,既能保证比用n作右边界要减少一些计算量,还可以保证某些特殊值情况下也会返回正确结果,如n=1的情况。
选好边界以后,在循环体里面,mid取边界的中值,也就是说看一下mid层完整阶梯需要多少多少枚硬币。如果需要的硬币数正好等于n,则直接返回n;如果搭建mid层所需要的的硬币数超过了给定的n,那说明我们能搭建的完整阶梯数要少于n的,因此右边界更新为mid-1;同理,若搭建mid层所需要的的硬币数少于n,说明也许我们还可以再多搭建几层。这里用也许是考虑到剩余的硬币不足以再搭建一层的情况。参考代码如下:
def arrangeCoins(n):
left = 0
right = n // 2 + 1
while(left <= right):
mid = (left + right) // 2
# print(left, ' ', right, ' ', mid)
totle = 0.5 * mid * (1 + mid)
if totle > n:
right = mid - 1
elif totle < n:
left = mid + 1
else:
return mid
# print(left, ' ', right)
return right
执行arrangeCoins(8),代码中的输出语句输出的内容为:
0 5 2
3 5 4
3 3 3
4 3
重点解释一下第三行和第四行,在第三行中,左右的边界值都更新为3,此时mid的值只能取3;我们计算出搭建3层完整的阶梯需要6枚硬币,低于给定的8枚,因此左边界left会更新为4,此时循环条件被破坏,因此,最后返回的是right。