力扣刷题笔记(1)--面试150数组部分

力扣数组刷题技巧总结

这些题目是力扣150中的内容,题目的代码是我想出或者根据题解思路复现的。里面的一些方法和技巧,是我根据自己做题中的一些不懂或者不熟练的内容,让AI生成的总结技巧。这个教程适合对于go语言有一定使用或者纯新手的快速了解go语言算法题的技巧和特性,如果你可以看完,我相信你对数组和切片这部分的大多数算法一定有了不错的了解(没有针对解法做一个总结,因为我觉得如果刷或者浏览一部分题目之后,看到某些特征就有一定的基础思路,看到复杂度就会有一定的优化想法)。如果有不足或者需要改进的地方,欢迎大家在评论区批评指正。

后续的150内容我立一个flag在这周之内出完,希望大家可以多多支持。

合并两个有序数组(9.15.1)

方法1 追加之后排序

这个方法针对无序数组也可以使用

func merge(nums1 []int, m int, nums2 []int, n int)  {
   
   
    //使用数组处理方法直接处理
    copy(nums1[m:],nums2)//追加切片
    sort.Ints(nums1)//对整形数组的排序方法
}

方法2 双指针法

func merge(nums1 []int, m int, nums2 []int, n int)  {
   
   
    //创建结果数组并且进行赋值
    result := make([]int,0,m+n)
    //创建左右指针
    p1 := 0
    p2 := 0
    //赋值
    for {
   
   
        //防止nums中有空切片  并且顺带可以简化for循环
        if p1==m{
   
   
            result=append(result,nums2[p2:]...)
            break
        }
        if p2==n{
   
   
            result=append(result,nums1[p1:]...)
            break
        } 

        if nums1[p1]>=nums2[p2]{
   
   
            result = append(result,nums2[p2])
            p2++
            continue
        }else{
   
   
            result = append(result,nums1[p1])
            p1++
            continue
        }
    }
    copy(nums1,result)
}    

注意

result := make([]int,m+n)//这样写再加上append方法,会造成前面m+n个元素全为0

知识点

make方法

make 是 Go 语言中用于初始化内建引用类型(slice、map、channel) 的函数,它分配内存并初始化相应的数据结构,返回的是类型本身(而不是指针)。

用于切片 (Slice) - 力扣最常用

切片是动态数组,是解决数组类题目的核心数据结构。

语法:

// 语法1: 指定长度和容量
slice := make([]T, length, capacity)

// 语法2: 省略容量,则长度 == 容量
slice := make([]T, length)
  • T: 切片元素的类型 (如 int, string, byte)
  • length: 切片的当前长度(已有元素个数)
  • capacity: 切片的总容量(底层数组能容纳的元素个数)

力扣实战技巧:

  1. 预分配内存以避免频繁扩容:这是最重要的性能优化点。如果你能预估结果切片的大致大小,使用 make 预先分配足够的 capacity 可以避免 append 操作时底层数组的多次重新分配和拷贝,极大提升性能。

    • 例子 (力扣 509. 斐波那契数):你知道要计算前 N 个斐波那契数。
      func fib(n int) int {
             
             
          if n <= 1 {
             
             
              return n
          }
          // 预分配一个长度为 n+1 的切片,避免后续 append 的扩容开销
          dp := make([]int, n+1)
          dp[0], dp[1] = 0, 1
          for i := 2; i <= n; i++ {
             
             
              dp[i] = dp[i-1] + dp[i-2]
          }
          return dp[n]
      }
      
  2. 创建空切片但有容量:当你需要一个初始为空的切片,但又知道即将添加大量元素时。

    • 例子:收集二叉树遍历的结果。
      // 假设你知道树节点大约有1000个
      result := make([]int, 0, 1000)
      // 然后使用 result = append(result, value) 添加元素,效率极高
      
  3. 创建具有初始长度的切片:当你需要直接通过索引 dp[i] 赋值,而不是使用 append 时,必须指定足够的 length

    • 动态规划 (DP) 问题:几乎所有的 DP 题都需要先创建一个 dp 数组(切片)。
      // 力扣 70. 爬楼梯
      func climbStairs(n int) int {
             
             
          if n <= 2 {
             
             
              return n
          }
          dp := make([]int, n+1) // 创建长度为 n+1 的切片
          dp[1] = 1
          dp[2] = 2
          for i := 3; i <= n; i++ {
             
             
              dp[i] = dp[i-1] + dp[i-2] // 直接按索引赋值
          }
          return dp[n]
      }
      
用于映射 (Map) - 也很常用

Map 用于存储键值对,解决需要快速查找、计数的题目。

语法:

m := make(map[KeyType]ValueType)
m := make(map[KeyType]ValueType, initialCapacity) // 推荐指定初始容量
  • initialCapacity: 提示 Go 运行时预先分配大约能存储 initialCapacity 个元素的空间。虽然不是严格的限制,但指定一个合理的值可以避免初期的内存重整,提升性能。

力扣实战技巧:

  1. 用于计数:统计元素出现的频率。

    • 例子 (力扣 1. 两数之和):虽然最优解是一遍哈希,但也能体现 map 的创建。
      func twoSum(nums []int, target int) []int {
             
             
          // 创建一个map,key是数字,value是索引
          numMap := make(map[int]int, len(nums)) // 建议指定容量为nums的长度
          for i, num := range nums {
             
             
              complement := target - num
              if idx, ok := numMap[complement]; ok {
             
             
                  return []int{
             
             idx, i}
              }
              numMap[num] = i
          }
          return nil
      }
      
  2. 用于记录状态:记录访问过的节点或状态,避免重复处理(如图的遍历、回溯算法)。

    • 例子:克隆图(力扣 133)、避免回溯中的重复选择。
用于通道 (Channel) - 并发题目中使用

Channel 用于 goroutine 之间的通信,在并发类的题目中可能会用到。

语法:

ch := make(chan T)    // 无缓冲通道
ch := make(chan T, n) // 带缓冲通道,缓冲区大小为 n
  • 在力扣的算法题中,单纯使用 channel 的情况较少,除非题目明确要求并发。
总结与对比 (make vs new)
特性 make new
适用类型 仅用于内建引用类型:slice, map, channel 用于任何类型(包括值类型和自定义结构体)
返回值 类型 T 本身 (e.g., []int, map[int]int) 指向该类型的指针 (*T)
初始化 会初始化内存(置零值),并设置好内部数据结构(如 slice 的 len/cap) 只分配内存,并将内存置为零值(nil for references, 0 for numbers, "" for strings),返回指向这块零值内存的指针

错误示例:

var s *[]int = new([]int) // s 现在是一个指向 nil 切片的指针
*s = make([]int, 10)      // 必须这样使用:先 new 出一个指针,再让指针指向 make 出来的切片
// 通常我们直接写更简单: s := make([]int, 10)
力扣题刷技巧
  1. 看到数组、字符串、动态规划题:第一反应想到 make([]T, len, cap) 来创建你的切片(DP 表、结果集)。
  2. 看到需要查找、去重、计数的题:第一反应想到 make(map[K]V) 来创建你的映射。
  3. 养成预分配容量的习惯:在知道或能估算数据规模时,始终为 make 函数指定 capacity 参数。这是一个简单且有效的性能优化手段,能让你的解法跑得更快。

append方法

追加单个元素
numbers := []int{
   
   1, 2, 3}
numbers = append(numbers, 4)  // [1, 2, 3, 4]
追加多个元素
numbers := []int{
   
   1, 2, 3}
numbers = append(numbers, 4, 5, 6)  // [1, 2, 3, 4, 5, 6]
追加另一个切片
slice1 := []int{
   
   1, 2, 3}
slice2 := []int{
   
   4, 5, 6}
slice1 = append(slice1, slice2...)  // [1, 2, 3, 4, 5, 6]
下面内容了解一下即可

重要特性

  1. 自动扩容:当切片容量不足时,append 会自动分配一个新的底层数组,通常是原容量的2倍(当长度小于1024时)。

  2. 返回值append 总是返回一个新的切片,必须将返回值赋给原切片变量。

  3. 零值切片:可以向 nil 切片追加元素:

    var s []int
    s = append(s, 1)  // [1]
    

性能考虑

  • 频繁的 append 操作可能导致多次内存分配和复制

  • 如果知道最终大小,可以预先分配容量:

    s := make([]int, 0, 100)  // 长度为0,容量为100
    

常见错误

s := []int{
   
   1, 2, 3}
_ = append(s, 4)  // 错误:没有使用返回值
// s 仍然是 [1, 2, 3]

append 是 Go 中处理动态数组增长的主要方式,理解它的工作原理对于编写高效的 Go 代码非常重要。

copy方法

copy 是 Go 语言中用于复制切片内容的内置函数,常用于将一个切片的内容复制到另一个切片中。

copy(dst, src []T) int
  • 返回实际复制的元素个数(取 dst 和 src 长度的较小值)
用法
nums1 := make([]int, 10)  // 假设 nums1 有足够容量
nums2 := []int{
   
   4, 5, 6}
m := 5  // 从 nums1 的第 m 个位置开始复制

n := copy(nums1[m:], nums2)  // 将 nums2 复制到 nums1[m:] 的位置
// n 是实际复制的元素数
以下内容了解一下即可
  1. 不会自动扩容,目标切片必须有足够空间
  2. 可以部分复制(取决于目标切片的剩余空间)
  3. 常用于合并切片或覆盖切片部分内容

示例:合并两个有序切片

nums1 := make([]int, len(nums1Original)+len(nums2)
copy(nums1, nums1Original)
copy(nums1[len(nums1Original):], nums2)

sort包

sort 包是 Go 标准库中用于排序和搜索的包,提供了对切片和用户定义集合进行排序的功能。

基本类型排序方法
  1. 整型排序
func Ints(x []int)                  // 对 int 切片进行升序排序
func IntsAreSorted(x []int) bool    // 检查 int 切片是否已排序

sort.Ints(nums)//升序
sort.Sort(sort.Reverse(sort.IntSlice(nums)))//降序
  1. 浮点数排序
func Float64s(x []float64)          // 对 float64 切片进行升序排序
func Float64sAreSorted(x []float64) bool  // 检查 float64 切片是否已排序
  1. 字符串排序
func Strings(x []string)            // 对 string 切片进行升序排序(按字典序)
func StringsAreSorted(x []string) bool  // 检查 string 切片是否已排序
通用排序接口
  1. Slice 排序 (Go 1.8+)
func Slice(x interface{
   
   }, less func(i, j int) bool)  // 根据 less 函数对切片排序
func SliceStable(x interface{
   
   }, less func(i, j int) bool)  // 稳定排序
func SliceIsSorted(x interface{
   
   }, less func(i, j int) bool) bool  // 检查是否已排序
  1. 自定义排序接口
type Interface interface {
   
   
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}
func Sort(data Interface)          // 排序
func Stable(data Interface)       // 稳定排序
func IsSorted(data Interface) bool // 检查是否已排序
搜索方法
func SearchInts(a []int, x int) int      // 在已排序的 int 切片中搜索 x
func SearchFloat64s(a []float64, x float64) int  // 在已排序的 float64 切片中搜索
func SearchStrings(a []string, x string) int  // 在已排序的 string 切片中搜索
func Search(n int, f func(int) bool) int  // 通用二分搜索
逆序排序

Go 1.21+ 引入了新的逆序排序方式:

// 对整型切片降序排序
sort.Sort(sort.Reverse(sort.IntSlice(nums)))

// Go 1.21+ 更简洁的方式
slices.SortFunc(nums, func(a, b int) int {
   
   
    return b - a  // 降序排序
})
实用示例
  1. 自定义结构体排序
type Person struct {
   
   
    Name string
    Age  int
}

people := []Person{
   
   
    {
   
   "Alice", 25},
    {
   
   "Bob", 20},
}

// 按年龄排序
sort.Slice(people, func(i, j int) bool {
   
   
    return people[i].Age < people[j].Age
})
  1. 搜索示例
nums := []int{
   
   1, 3, 5, 7, 9}
x := 5
pos := sort.SearchInts(nums, x)  // 返回 2
  1. 检查切片是否已排序
if sort.IntsAreSorted(nums) {
   
   
    fmt.Println("切片已排序")
}
性能提示
  1. 对于基本类型,优先使用 sort.Ints 等特定函数,它们比通用 sort.Slice 更快
  2. 需要保持相等元素原始顺序时,使用 Stable 系列方法
  3. 对大型数据集,考虑使用 sort.Slice 而非实现 sort.Interface 接口,代码更简洁
  4. sort 包提供了强大而灵活的排序功能,是 Go 语言中处理有序数据的核心工具之一。

移除元素(9.15.2)

方法1:前后指针法

func removeElement(nums []int, val int) int {
   
   
    //创建前后指针以及最终元素个数的记录
    follow := 0
    result := 0
    for front,_ := range nums{
   
   
        if nums[front] != val{
   
   
            nums[follow]=nums[front]
            result++
            front++
            follow++
        }else{
   
   
            front++
        }
    }
    return result
}

知识点

快慢指针写法

func removeElement(nums []int, val int) int {
   
   
    slow := 0 // 慢指针,指向下一个要放置非val元素的位置
    
    for fast := 0; fast < len(nums); fast++ {
   
   
        if nums[fast] != val {
   
   
            nums[slow] = nums[fast]
            slow++
        }
    }
    
    return slow
}

range方法

好的!为您总结Go语言range方法在力扣题目中的使用要点,特别针对算法题目的特点:

for index, value := range collection {
   
   
    // 循环体
}
数组/切片(最常用)
nums := []int{
   
   1, 2, 3, 4, 5}

// 方式1:获取索引和值(推荐)
for i, num := range nums {
   
   
    fmt.Printf("nums[%d] = %d\n", i, num)
}

// 方式2:只获取值
for _, num := range nums {
   
   
    fmt.Printf("值: %d\n", num)
}

// 方式3:只获取索引
for i := range nums {
   
   
    fmt.Printf("索引: %d, 值: %d\n", i, nums[i])
}
字符串(字符遍历)
s := "hello"

// 遍历字符串(按rune遍历,支持中文)
for i, char := range s {
   
   
    fmt.Printf("位置%d: %c (Unicode: %U)\n", i, char, char)
}
Map(无序遍历)
m := map[string]int{
   
   "a": 1, "b": 2, "c": 3}

// 遍历map(顺序随机)
for key, value := range m {
   
   
    fmt.Printf("%s: %d\n", key, value)
}
力扣题目中的实用技巧
  1. 原地修改数组

    // 错误:修改的是副本,不影响原数组
    for _, num := range nums {
         
         
        num = num * 2  // 无效!
    }
    
    // 正确:通过索引修改原数组
    for i := range nums {
         
         
        nums[i] = nums[i] * 2  // 有效!
    }
    
  2. 双指针技巧中的range

    func twoSum(nums []int, target int) []int {
         
         
        // 使用range遍历,配合map快速查找
        seen := make(map[int]int)
        for i, num := range nums {
         
         
            complement := target - num
            if idx, exists := seen[complement]; exists {
         
         
                return []int{
         
         idx, i}
            }
            seen[num] = i
        }
        return nil
    }
    
  3. 字符串处理

    func countCharacters(s string) int {
         
         
        count := 0
        for _, char := range s {
         
         
            if char != ' ' {
         
         
                count++
            }
        }
        return count
    }
    
  4. 矩阵遍历(二维数组)

    matrix := [][]int{
         
         {
         
         1,2,3}, {
         
         4,5,6}, {
         
         7,8,9}}
    
    // 遍历二维数组
    for i, row := range matrix {
         
         
        for j, value := range row {
         
         
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, value)
        }
    }
    
常见陷阱与注意事项
  1. 值拷贝问题

    nums := []int{
         
         1, 2, 3}
    for _, num := range nums {
         
         
        num++  // 这只是修改副本,原数组不变!
    }
    // nums仍然是[1, 2, 3]
    
  2. 性能考虑

    // 对于大数组,直接使用索引可能更快
    bigArray := make([]int, 1000000)
    
    // 较慢:每次迭代都有值拷贝
    for _, value := range bigArray {
         
         
        _ = value
    }
    
    // 较快:直接通过索引访问
    for i := range bigArray {
         
         
        _ = bigArray[i]
    }
    
  3. 边界情况处理

    // 空数组/切片不会panic
    var empty []int
    for i, num := range empty {
         
         
        fmt.Println(i, num)  // 不会执行
    }
    
    // nil切片也不会panic
    var nilSlice []int = nil
    for i, num := range nilSlice {
         
         
        fmt.Println(i, num)  // 不会执行
    }
    
力扣实战示例
  1. 移除元素

    func removeElement(nums []int, val int) int {
         
         
        slow := 0
        // 使用range遍历,但通过索引修改原数组
        for _, num := range nums {
         
         
            if num != val {
         
         
                nums[slow] = num
                slow++
            }
        }
        return slow
    }
    
  2. 反转字符串

    func reverseString(s []byte) {
         
         
        // 使用range获取索引,进行双指针交换
        for i := range s[:len(s)/2] {
         
         
            j := len(s) - 1 - i
            s[i], s[j] = s[j], s[i]
        }
    }
    
  3. 寻找重复元素

    func findDuplicate(nums []int) int {
         
         
        seen := make(map[int]bool)
        for _, num := range nums {
         
         
            if seen[num] {
         
         
                return num
            }
            seen[num] = true
        }
        return -1
    }
    
总结
  1. 优先使用索引:在需要修改原数组时,使用for i := range形式
  2. 值遍历只读for _, value := range中的value是副本,不能修改原数据
  3. map遍历无序:不要依赖map的遍历顺序
  4. 性能敏感时:大数组考虑直接使用索引访问
  5. 安全遍历:range对nil和空集合是安全的

(须回看)删除有序数组中的重复项(9.15.3)

快慢指针法

func removeDuplicates(nums []int) int {
   
   
    //异常情况
    length:=len(nums)
    if length == 0{
   
   
        return 0
    }
    
    //慢指针
    slow := 0

    //快指针起始遍历
    for fast:=1;fast<length;fast++{
   
   
        if nums[fast]!=nums[slow]{
   
   
            slow++
            nums[slow]=nums[fast]
        }
    }

    return slow+1
}

注意1

下面这段代码存在问题:

对于输入 [1,1,2],当 fast = 1(指向第二个1)时:

  • nums[fast] == nums[fast+1]1 == 2 → false
  • 进入 else 分支:fast++fast = 2
  • 现在 fast = 2,但数组长度是3(索引0,1,2)
  • 下一次循环:if fast == length-12 == 2,应该跳出循环

但是,如果数组是 [1,1,1]

  • 内层循环会一直 fast++,直到 fast = 2
  • 然后检查 nums[2] != nums[3] → 访问 nums[3] 导致越界
func removeDuplicates(nums []int) int {
   
   
    //慢指针
    fast,slow := 0,0
    length := len(nums)
    
    for {
   
   
        //弹出条件
        if fast == length-1{
   
   
            break
        }

        if nums[fast]==nums[fast+1]{
   
   
            for{
   
   
                if nums[fast]!=nums[fast+1]{
   
   
                    break
                }
                fast++
            }
            fast++
            slow++
            nums[slow]=nums[fast]
        }else{
   
   
            fast++
            slow++
            nums[slow]=nums[fast]
        }

    }
    return slow+1
}

注意2

return slow++  // 错误!
  1. 语法错误:Go 的 return 语句不能包含表达式,只能返回值
  2. 逻辑错误:即使语法正确,slow++ 是后置自增,会先返回 slow 的值,然后再自增

因此要写成:

return slow+1

哈希表(如果不是非严格递增就要用到)

基本操作

// 创建哈希表
m := make(map[keyType]valueType)

// 字面量初始化
m := map[string]int{
   
   "a": 1, "b": 2}

// 基本操作
m["key"] = value      // 插入/更新
value := m["key"]     // 读取
delete(m, "key")      // 删除
len(m)                // 获取大小

力扣常用哈希表模式

频率统计(最常用)
// 统计元素频率
func countFrequency(nums []int) map[int]int {
   
   
    freq := make(map[int]int)
    for _, num := range nums {
   
   
        freq[num]++
    }
    return freq
}

// 字符串字符统计
func charCount(s string) map[rune]int {
   
   
    count := make(map[rune]int)
    for _, char := range s {
   
   
        count[char]++
    }
    return count
}
快速查找存在性
// 检查重复元素
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值