注:参考的某算法训练营的计划
在 Golang(和大多数主流语言,如 C/C++)中,二维数组按行访问的效率更高。因为它符合 Go 的内存连续存储结构,能提高 CPU Cache 命中率,减少内存跳跃带来的开销。
力扣题目链接(opens new window)
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件。
要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
因为定义target在[left, right]区间,所以有如下两点:
func search(nums []int, target int) int {
left,right :=0,len(nums)-1
for left<=right{ // 当left==right,[left,right]区间依然有效
mid := left + (right-left)/2
if nums[mid]==target{
return mid
}else if nums[mid]
在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。
有如下两点:
left
开始,到 不包含 right。
func search(nums []int, target int) int {
left,right :=0,len(nums)
for left
区间写法 | 循环条件 | mid 计算 | 优势 |
---|---|---|---|
[left, right] |
left <= right |
mid = (left+right)/2 |
更直观,初学者容易理解 |
[left, right) |
left < right |
mid = (left+right)/2 |
更清晰、不易越界,更适合语言的 slice 习惯 |
✅ 数组是有序的
✅ 没有重复元素 → 可以使用标准二分查找
❓ 如果有重复元素呢?还能用二分吗?
答案是:
✅ 可以用二分查找,但是需要改进策略,以应对重复元素带来的“多个目标值下标”的情况。
在有重复元素的前提下,使用二分查找依然可行,但要明确查找目标是哪种:
查找目标 | 举例描述 |
---|---|
找到任意一个等于 target 的下标 | 只要找到一个就行(最简单) |
找到第一个等于 target 的下标 | 返回最左边的那个 |
找到最后一个等于 target 的下标 | 返回最右边的那个 |
查找 target 的个数 | 可用右边界 - 左边界 + 1 计算出现次数 |
代码同上
func leftBound(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2 // 防止整数溢出
if nums[mid] >= target { // 如果中间值大于等于目标,收缩右边界
right = mid - 1
} else { // 中间值小于目标,收缩左边界
left = mid + 1
}
}
// 循环结束后,left 是“第一个大于等于 target 的位置”
// 此时需要确认该位置是否真的等于 target
if left < len(nums) && nums[left] == target {
return left
}
return -1 // 没找到
}
func rightBound(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] <= target { // 如果中间值小于等于目标,继续往右找
left = mid + 1
} else { // 中间值大于目标,缩小右边界
right = mid - 1
}
}
// 循环结束后,right 是“最后一个小于等于 target 的位置”
// 此时需要确认该位置是否真的等于 target
if right >= 0 && nums[right] == target {
return right
}
return -1 // 没找到
}
出现次数 = rightBound - leftBound + 1
力扣题目链接(opens new window)
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。
示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
你不需要考虑数组中超出新长度后面的元素。
// 暴力法
// 时间复杂度 O(n^2)
// 空间复杂度 O(1)
func removeElement(nums []int, val int) int {
size := len(nums)
for i := 0; i < size; i ++ {
if nums[i] == val {
for j := i + 1; j < size; j ++ {
nums[j - 1] = nums[j]
}
i --
size --
}
}
return size
}
// 快慢指针法
// 时间复杂度 O(n)
// 空间复杂度 O(1)
func removeElement(nums []int, val int) int {
// 初始化慢指针 slow
slow := 0
// 通过 for 循环移动快指针 fast
// 当 fast 指向的元素等于 val 时,跳过
// 否则,将该元素写入 slow 指向的位置,并将 slow 后移一位
for fast := 0; fast < len(nums); fast++ {
if nums[fast] == val {
continue
}
nums[slow] = nums[fast]
slow++
}
return slow
}
简洁写法
func removeElement(nums []int, val int) int {
left :=0
for _,v:= range nums{
if v!= val{
nums[left]=v
left++
}
}
return left
}
使用双指针 left
:
遇到 ≠ val
的元素,就放到 nums[left]
位置
每次放完 left++
最后返回 left
即为新数组长度(无需额外空间)
力扣题目链接(opens new window)
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
示例 2:
func sortedSquares(nums []int) []int {
for i,val := range nums{
nums[i]*=val
}
sort.Ints(nums)
return nums
}
时间复杂度:O(n+nlogn)
空间复杂度:O(1)
数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,i指向起始位置,j指向终止位置。
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。
如果A[i] * A[i] < A[j] * A[j]
那么result[k--] = A[j] * A[j];
。
如果A[i] * A[i] >= A[j] * A[j]
那么result[k--] = A[i] * A[i];
。
func sortedSquares(nums []int) []int {
n := len(nums)
i,j,k := 0,n-1,n-1
res := make([]int,n)
for i<=j{
i2,j2 := nums[i]*nums[i],nums[j]*nums[j]
if i2>=j2{
res[k] = i2
k--
i++
}else{
res[k]=j2
k--
j--
}
}
return res
}
时间复杂度和空间复杂度都为O(n)