参加了一个代码随想录的算法训练营,跟着大家系统学习一遍,此博客开始,每日题目总结记录一下,加油....
今日任务
- 数组理论基础 ( 文章讲解 参考: 代码随想录)
- 704 二分查找 (题目: . - 力扣(LeetCode))
- 27 移除元素 (题目: . - 力扣(LeetCode))
二分查找
想法:
看到这个题目(一个有序数组,返回给出的 target值的下标),第一反应就是暴力啦;但是根据题目的标题以及有序数组的关键信息,要使用有 middle 下标的二分去解决.
问题:
在找 middle 的过程中,存在一些问题,left 和 right 的值如何判断,循环该怎么做. (下面会附上我做错的几种情况).
解题思路:
在看了视频讲解之后,顿时明白了二分查找的核心思路(一般分两种写法:区间左闭右闭、左闭右开). 在写代码之前要先自己想好要以哪个规则来写, 不需要问为什么,就是先带入一个规则,然后接下来就可以逐步确定 left、right、middle 的值和之间的关系.(需要参考下面的视频 理解)
视频讲解:手把手带你撕出正确的二分法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili
文章: 代码随想录
// 二分法 左闭右闭
// (1)区间两侧都能作为下标取数,所以left = 0 ,r = len(nums)-1;
// (2)选取循环的条件应该是根据在左闭右闭的情况下l 和 r 相等是否合法,
// 例如数组[1],左闭右闭,左右都可以取得到下标 0,所以可以相等.左闭右开则不合法.
// (3)对于 num[mid]的值大于 target 时,则接下来的计算应该是 left~mid 之间的元素,
// 则 right要向左移,然后 right为什么是等于 mid-1,因为右闭,及 right对应的元素
// 会参与到计算中,而 mid 对应的元素已经比较过大小了,无需再次比较,则 right = mid -1.
// num[mid]的值小于 target 时,同理可得.
func search(nums []int, target int) int {
if len(nums) < 1 {
return -1
}
left, right := 0, len(nums)-1
for left <= right {
mid := (left + right) / 2
if nums[mid] == target {
return mid
}
if nums[mid] < target {
left = mid + 1
}
if nums[mid] > target {
right = mid - 1
}
}
return -1
}
// 二分法 左闭右开
// (1)left能作为下标取数l = 0 ,right 不能直接用来取数,因此r = len(nums)即可;
// (2)选取循环的条件应该是根据 在左闭右开的情况下l 和 r 相等是否合法,
// 例如[1],左闭右开,左右下标不可能相等,左闭右开则不合法.
// (3)对于 num[mid]的值大于 target 时,则接下来的计算应该是 left~mid 之间的元素
// ,则 right要向左移,然后 right为什么是等于 mid,因为右开,right下标对应的元素
// 不会参与到其中的计算,无需考虑 right=mid 时,还会将已经比较过的 num[mid]参与
// 计算的情况.则 right = mid. num[mid]的值小于 target 时,因为左闭,左边的元素
// 还是会参与到计算中,所以 left=mid+1.
func search(nums []int, target int) int {
if len(nums) < 1 {
return -1
}
left, right := 0, len(nums)
for left < right {
mid := (left + right) / 2
if nums[mid] == target {
return mid
}
if nums[mid] < target {
left = mid + 1
}
if nums[mid] > target {
right = mid
}
}
return -1
}
错误代码
// 最开始在不了解具体二分法的写法规则时瞎写的,只知道要找 mid下标
// 唯一能通过的测试用例应该是target 值在数组最左侧.
// 这样写肯定不行的呀,mid每次都是重新计算的,最终mid的值不准。
func search(nums []int, target int) int {
if len(nums) == 0 {
return -1
}
mid := (len(nums) - 1) / 2
if nums[mid] == target {
return mid
} else if nums[mid] > target {
return search(nums[:mid], target)
} else if nums[mid] < target {
return search(nums[mid+1:], target)
}
return -1
}
// 这种写法会导致一直循环,
// 此时也是不知道左闭右闭那些规则的,理不清 left、right、mid 之间的关系
// search([]int{-1, 0, 3, 5, 9, 12}, 12)
// left right mid
0 5 2
2 5 3
3 5 4
4 5 4
4 5 4
func search(nums []int, target int) int {
if len(nums) < 1 {
return -1
}
left, right := 0, len(nums)-1
for left < right {
mid := (left + right) / 2
if nums[mid] == target {
return mid
}
if nums[mid] < target {
left = mid
}
if nums[mid] > target {
right = mid - 1
}
}
return -1
}
移除元素(双指针)
在原数组上移除指定的 target 值,最后返回移除后的数组大小,然后题目的输出会根据你返回的数值去原数组上取新的数组
想法:
这题我连暴力都没写出来,代码能力属实有点弱了....
问题:
看着好像只需 for 循环把相应的 target 找出来即可,但是因为要保持你的原数组能根据你返回的值,直接取前多少个元素作为新数组输出, 所以找到 target 后,要把后面的元素往前移.
解题思路:
看了老师的视频,讲解双指针(快慢指针)的用法,顿时明白了,快指针先行一步外出寻找元素(目标元素丢弃,非目标元素传回家),慢指针在家等候,将快指针收集到的元素存放好....
其实看了视频讲解,很容易理解,然而我想到了好像也可以使用首尾两个指针(因为刚做了二分..)去解决啊,好像比“快慢指针”更方便,不用每个重新赋值.( 具体思路见代码)
视频讲解:数组中移除元素并不容易! | LeetCode:27. 移除元素_哔哩哔哩_bilibili
文章: 代码随想录
// 首尾两个指针
// 1、left 等于 0、right = len(nums)-1;
// 2、如果 nums[left]的元素不是我们要去除的元素,就让 left 右移
// 如果 nums[left]是我们要去除的元素,那么就让 right 对应的元素赋值过来,
不去考虑本身 right对应的元素是否为要移除的,只管往左边赋值,这时我们不去判断
nums[left]是否仍为要去除的要素,因为下次循环会判断 left ;然后我们将 right左移
func removeElement(nums []int, val int) int {
left, right := 0, len(nums)-1
for left <= right { // “=”判断条件,其实可以理解为我们的 l、r 的初始值满足左闭右闭
if nums[left] != val {
left++
continue
}
nums[left] = nums[right]
right--
}
return left
}
错误代码:
// 我下面这段代码的问题,其实就是考虑太多,想着要是 right 指向的元素也为 target 怎么办,是否要先左移怎么的,然后就把自己搞糊涂了.下面的代码会无法通过测试用例[3,3]的, 可以通过[0,1,2,2,3,0,4,2]
// 想使用两个指针,左右指针进行操作,但是会存在一些问题的,思路还有点没想明白
func removeElement(nums []int, val int) int {
left, right := 0, len(nums)-1
for left <= right {
if nums[right] == val {
right--
}
if right < left {
return left
}
if nums[left] == val {
nums[left] = nums[right]
left++
right--
continue
}
left++
}
return left
}
总结
二分查找、双指针感觉都还是挺好理解的,但是像二分查找要先根据区间规则,这种思想,自己恐怕琢磨不出来.......