文章链接:
704. 二分查找
题目链接:力扣
文章讲解:代码随想录
视频讲解:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili
二分法:
二分法查找适用于有序数组(有序是必要条件)。相比于线性搜索(Worst Case 的时间复杂度为),二分搜索通过不断将搜索区间一分为二,二分法查找可以将 Worst Case 的时间复杂度降低到
。(两种算法的 Best Case 时间复杂度均为
)。
重难点:
基础的二分法在逻辑上有两个让人捉摸不定的地方:
1. 循环时是 while left < right 还是 while left <= right
2. 更新区间时是 left = middle+1, right = middle-1 还是 left = middle, right = middle 或者 left = middle+1, right = middle 之类的
关于这两个问题,代码随想录给出的解决方案是,搞清楚循环的“区间不变量”到底是左闭右闭还是左闭右开。这个解决方案很系统,但是我自己一开始理解起来还是稍微有点绕,甚至感觉有些牵强(可能是因为段位太低了),或许多理解几遍之后会发现这其中的精妙之处。我们先来讲解和总结一下这个循环区间不变量的方式,然后再记录一下我自己的理解。
首先我们要决定我们循环的区间是左闭右闭的,还是左闭右开的。这个所谓“循环的区间”就是我们要在哪个区间里搜索我们想要的目标值。而这个区间的开闭一旦定下来,就不要改变,在整个搜索过程中要贯彻到底。
如果区间是定成左闭右闭,那意味着我们之后将永远在一个左闭右闭的区间里寻找目标值,直到找到或确定目标值不存在。因此,当考虑是 while left < right 还是 left <= right 时,应该采用 while left <= right,因为当 left == right 时,区间定义是成立的。然后来解决 left/right = middle (+/- 1) 的问题。如果定义了区间是左闭右闭,那么如果有下一轮搜索,left/right 应该等于 middle-1/+1,而不是 middle 本身,因为下一轮会在新的 [left, right] 区间里进行搜索,而新的 left 和 right 作为边界值是会被搜索到的,因此我们不能把已经检查过的 原middle 设成 新left/right,因为 原middle 已经被检查过不是 target 了。
而如果区间是左闭右开,那么 while left <= right 在区间定义上是不成立的(e.g., [3, 3) 这个区间不存在)。而考虑更新区间时,left 还是等于 middle+1,因为“左闭”意味着每次搜索时,左边界是要被搜索到的,所以如果 原middle 被检测为不是 target,那么 新left 可以直接等于 原middle+1。也就是说新一轮的搜索时,新left 会被检测到,而 原middle 已经被检测过了,所以不需要再检测一遍 原middle,直接从 原middle+1 开始就可以了。但是 right 要等于 middle 而不是 middle-1,因为“右开”意味着在新一轮的检测中,右边界不会被检测到,而 原middle 被检测过,所以应该检测的值是 原middle-1,那右边界就应该是 原middle(e.g., [3, 6) 这个区间实际被检测的值只有3,4,5,没有6)。
后来发现一位录友总结得很精炼,稍微改动一下,当 right = len(nums) - 1 时,每次的搜索区间为 [left, right] (左闭右闭),此时对应的循环条件应为 while left <= right,终止条件为 left == right + 1,即 [right + 1, right],此时区间为空,故循环终止,程序返回 -1 即可;而当 left == right 时,[right, right] 这个区间还需要继续被搜索,直接返回-1是不对的。当 right = len(nums) 时,每次的搜索区间为 [left, right) (即左闭右开),此时对应的循环条件为 while (left < right),终止条件为 left == right,即 [right, right),此时区间不成立,跳出循环。(原文链接:https://blog.csdn.net/weixin_44605962/article/details/128837397)
上面应该是代码随想录给出的思路。我感觉有点绕(可能因为这道题太基础,就感觉没必要这么绕,但是遇到困难的复杂的问题可能还是有必要有一个系统的解决方案的),希望多理解几遍之后会好一些。我自己原先的思路比较浅薄且暴躁,永远都是 left = middle+1, right = middle-1,然后你想边界值嘛,如果 left = middle+1, right = middle-1 了,搜索到最后就会出现 left == right 的情形(e.g., [3, 4], left = 3, right = 4, middle = 3, 如果 target 是4,那 middle < target,就需要 left = middle+1 = 4,这样就 left == right 了)。既然会出现 left == right 的情形,那就需要继续看这个 left == right 是不是 target,所以这时不能跳出循环。所以循环的时候要 while left <= right。
27. 移除元素
题目链接:力扣
文章讲解:代码随想录
视频讲解:数组中移除元素并不容易! | LeetCode:27. 移除元素_哔哩哔哩_bilibili
数组:
由于数组的存储地址是连续的,删除元素的实质是元素的不断替换。即若想删除数组 [1,2,3,4] 里的2,实质是将2换成3,将3换成4,新数组为 [1,3,4,4],而编程语言会对新数组进行处理,只返回前三项 [1,2,3] 而已
暴力双循环:
首先遍历所有值,然后,当发现遍历到的值与想要删除的值相等时,再遍历该值和后面剩下的所有值,逐个进行替换。时间复杂度为。
双指针:
通过快慢指针,用一个循环完成两个循环的工作。需要搞清楚快慢指针的定义:
慢指针:指向原数组中需要替换的元素/位置;
快指针:寻找新数组中的元素,即用来替换原数组中删除的值的元素。新数组即不含有目标元素的数组。
因此,只需要将快指针指向的元素替换掉慢指针指向的元素即可。时间复杂度降为。