算法恢复训练-Part01-数组

注:参考的某算法训练营的计划

核心注意点

在 Golang(和大多数主流语言,如 C/C++)中,二维数组按行访问的效率更高。因为它符合 Go 的内存连续存储结构,能提高 CPU Cache 命中率,减少内存跳跃带来的开销。

704. 二分查找

力扣题目链接(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        

提示:

  • 你可以假设 nums 中的所有元素是不重复的。
  • n 将在 [1, 10000]之间。
  • nums 的每个元素都将在 [-9999, 9999]之间。

分析

前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件。

要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。

写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。

左闭右闭即[left, right]

因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
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]
  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)

左闭右开即[left, right)

在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。

有如下两点:

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的。表示查找的区间是:从 left 开始,到 不包含 right。
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
func search(nums []int, target int) int {
    left,right :=0,len(nums)
    for left
  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)

对比

区间写法 循环条件 mid 计算 优势
[left, right] left <= right mid = (left+right)/2 更直观,初学者容易理解
[left, right) left < right mid = (left+right)/2 更清晰、不易越界,更适合语言的 slice 习惯

如果有重复的怎么办

✅ 数组是有序的
✅ 没有重复元素 → 可以使用标准二分查找
如果有重复元素呢?还能用二分吗?

答案是:

可以用二分查找,但是需要改进策略,以应对重复元素带来的“多个目标值下标”的情况。

 

问题背景:有重复元素时,可能的需求变化

在有重复元素的前提下,使用二分查找依然可行,但要明确查找目标是哪种:

查找目标 举例描述
找到任意一个等于 target 的下标 只要找到一个就行(最简单)
找到第一个等于 target 的下标 返回最左边的那个
找到最后一个等于 target 的下标 返回最右边的那个
查找 target 的个数

可用右边界 - 左边界 + 1 计算出现次数

1. 找任意一个(标准二分查找)

 代码同上

2. 找第一个等于 target 的位置
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 // 没找到
}

3. 找最后一个等于 target 的位置
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 // 没找到
}
4.查找 target 的个数

出现次数 = rightBound - leftBound + 1

27. 移除元素

力扣题目链接(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 即为新数组长度(无需额外空间)

977.有序数组的平方

力扣题目链接(opens new window)

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

  • 输入:nums = [-4,-1,0,3,10]
  • 输出:[0,1,9,16,100]
  • 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]

示例 2:

  • 输入:nums = [-7,-3,2,3,11]
  • 输出:[4,9,9,49,121]

代码

暴力解法

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)

你可能感兴趣的:(算法恢复训练记录,算法,go)