[Go语言]训练算法的必备语法
[Go语言]训练算法的必备语法
数组【当然,go一搬训练slice】系统训练
go的slice的make函数的坑
#这里 只是创建了一个容量为 len+1,但长度为 0 的切片
make([]int, 0, len + 1)
#所以
myhash[v] = 1 #会 index out of range。解决方案
myhash := make([]int, lens + 1)它才是创建了1个长度为lens+1的切片
go初始化二维数组!基本上是dp或者bfs、dfs必备
func maximalSquare(matrix [][]byte) int {
// 1 <= m, n
lenX := len(matrix)
lenY := len(matrix[0])
// 语法!❤️❤️❤️❤️
dp := make([][]int, lenX) // 外部make一遍!❤️❤️❤️❤️
for i := range dp {
dp[i] = make([]int, lenY) // 内部再make一遍去初始化!❤️❤️❤️❤️
}
}1.必备常量
math.MinInt32
math.MaxInt32这个题目的数学证明,感觉麻烦,但是这种判断常量还是需要的
func reverse(x int) int {
// 先解决明显的特殊
if x == 0 {
return x
}
flag := 1
if x < 0 {
flag = -1
x = -x
}
// 常规的地方
cur := 0 // 截止当前累计的数
for x != 0 {
// 前置, 因为每次都要乘以10再加一个个位数,所以判断是否 *10 后会溢出
// 在更新 cur 之前预判是否会越界
if flag * cur < math.MinInt32/10 || flag * cur > math.MaxInt32/10 {
return 0
}
// step1:获取末尾的余数
remainder := x % 10
x = x / 10
// 伪代码逻辑【虽然对,但是cur本身可能加完后就超出范围是吧!
// 但是如果两边除10,那个remainder又被除掉了,那咋办?
// 解决方案:那就前置判断!
// 不变
// 累加到 cur(已经提前做过越界判断)
cur = cur * 10 + remainder
// step2:更新cur
// cur = cur * 10 + remainder
// step3:判断是否超出范围?
// if flag * cur < math.MinInt32 || flag * cur > math.MaxInt32 {
// return 0
// }
}
return flag * cur
}2.go没有C++那样的翻转字符串函数【一点点效率】
func rerverseStr(s string) string {
arr := []rune(s)
tmp := []rune{}
len := len(arr)
for i:= len-1; i>=0 ; i-- {
tmp = append(tmp, arr[i])
}
return string(tmp)
}
func isPalindrome(x int) bool {
mystr := strconv.Itoa(x)
tmp := rerverseStr(mystr)
return mystr == tmp
}3.go的for range从string获取的是rune,不是byte,所以这里要rune【巨坑】但是字符串索引是byte
是的,Go 的 for range 在遍历 string 时,会按 rune(Unicode 码点) 进行迭代,而不是按 byte 进行迭代
go的 字符串索引后是byte还是rune呢?
- Go 的字符串底层是只读的字节切片(
[]byte),存储的是 UTF-8 编码的字节序列。
- 用索引访问字符串(如
s[i])会返回字节(类型是byte,即uint8) - 如果要按 Unicode 字符(rune)处理,需要先转换成
[]rune或使用for range循环
package main
import "fmt"
func main() {
s := "Hello, 世界"
// 按字节索引访问
fmt.Println(s[0]) // 72(字节值,对应 'H' 的 ASCII 码)
fmt.Printf("%c\n", s[0]) // 'H'
// 当索引到非 ASCII 字符的字节时
fmt.Println(s[7]) // 228(世界"的"世"的第一个字节)
fmt.Printf("%c\n", s[7]) // 乱码(因为只取了一个字节)
// 正确遍历 Unicode 字符
for i, r := range s {
fmt.Printf("%d: %c\n", i, r) // i 是字节索引,r 是 rune
}
// 转换为 []rune 后按字符索引
runes := []rune(s)
fmt.Printf("%c\n", runes[7]) // '界'
}package main
import "fmt"
func main() {
s := "你好, Go!"
for i, r := range s { // 这里 r 是 rune,不是 byte
fmt.Printf("index: %d, rune: %c, unicode: %U\n", i, r, r)
}
}输出:
index: 0, rune: 你, unicode: U+4F60
index: 3, rune: 好, unicode: U+597D
index: 6, rune: ,, unicode: U+002C
index: 8, rune: , unicode: U+0020
index: 9, rune: G, unicode: U+0047
index: 10, rune: o, unicode: U+006F
index: 11, rune: !, unicode: U+0021关键点:
- 索引
**i**不是连续的
你好这两个汉字是 UTF-8 编码,每个占 3 个字节,所以好的索引是3,而不是1。
**r**是 rune(int32),不是 byte
r代表的是 Unicode 码点,而不是字节。
func romanToInt(s string) int {
tmp := []int{}
mp := map[rune]int {
'I' : 1,
'V' : 5,
'X' : 10,
'L' : 50,
'C' : 100,
'D' : 500,
'M' : 1000,
}
for _,v := range s {
tmp = append(tmp, mp[v])
}
len := len(s)
for i := 0; i < len-1; i++ {
if tmp[i] < tmp[i+1] {
// 只有相邻的,才可能是用减号
tmp[i] = -1 * tmp[i]
}
}
sum := 0
for _, v := range tmp {
sum = sum + v
}
return sum
}【必杀技1】有时候紧张【那么就写注释,典型的,其实问题简单】
func getLeftZeroIndex(nums []int) int {
len := len(nums)
ret := -1
for i := 0 ; i < len ; i++ {
if nums[i] == 0 {
ret = i
break
}
}
return ret
}
func moveZeroes(nums []int) {
// 注意:这就属于那种需要,短期理不清,那就写注释的情况
isContainZero := false
isContainNoZero := false // 包含非0的
for _, v := range nums {
if v == 0 {
isContainZero = true
} else {
isContainNoZero = true
}
}
// 没有0,不用管
if !isContainZero {
return
}
// 有0,但是是全0,不用管
if !isContainNoZero {
return
}
// 0和非零混杂
// leftZero指向从左边数第1个0的位置,每次需要计算、rightZero指向从右边数第1个非0的位置
// rightZero初始化为len-1
len := len(nums)
rightNoZeroIndex := len - 1
for i := len - 1 ; i >= 0 ; i-- {
if nums[i] != 0 {
rightNoZeroIndex = i
break
}
}
leftZeroIndex := getLeftZeroIndex(nums)
// 只有最后形如[1,0]才循环
// 因为可能存在[1,0]这样的情况,那么i
for leftZeroIndex < rightNoZeroIndex {
// 先将右边到rightNoZeroIndex全移动过去
for i := leftZeroIndex + 1 ; i <= rightNoZeroIndex ; i++ {
nums[i-1] = nums[i]
}
// 将0填充占位
nums[rightNoZeroIndex] = 0
// 更新rightNoZeroIndex的位置
for i := rightNoZeroIndex ; i>=0 ; i-- {
if nums[i] != 0 {
rightNoZeroIndex = i
break
}else {
// 【注意】之所以写这个,是因为可能出现没有0了啊
rightNoZeroIndex = i - 1 // 否则至少也是这个
}
}
// 更新 leftZeroIndex
leftZeroIndex = getLeftZeroIndex(nums)
}
}【必杀技2】精力不集中就念出声音强迫集中精力
附录1.【常用技巧】自定义排序
我以前写C++的时候,特别喜欢这样
本质
sort.Slice(数组,针对这个数组的处理函数)
就和C++的类似了
func insert(intervals [][]int, newInterval []int) [][]int {
// 它是一个幌子题目,故意换种提问方式,其实本质就是合并区间
var ret [][]int
// 1.将newInterval加入到intervals中
tmp := append(intervals, newInterval)
// 2.将tmp按照,start从小到大,如果start相同,那么end从小到大进行排序
// 这里有个坑!!func不能是func([][]arr, i , j int)就只能是下面这样!!!
// ❤️❤️❤️核心和C++一样的地方
cmpFunc := func(i int , j int) bool{
if tmp[i][0] == tmp[j][0] {
return tmp[i][1] < tmp[j][1]
}
return tmp[i][0] < tmp[j][0]
}
// ❤️❤️❤️核心和C++一样的地方
sort.Slice(tmp, cmpFunc)
// 3.遍历tmp进行合并区间,注意最开始ret是个空的哈,合并操作开始需要注意
for i := 0; i < len(tmp); i++ {
// 如果ret为空,或者当前区间的开始值大于ret中最后一个区间的结束值
// 无法合并,直接将当前区间加入结果
if len(ret) == 0 || tmp[i][0] > ret[len(ret)-1][1] {
ret = append(ret, tmp[i])
continue
}
// 去除ret的最后一个, 然后插入新的
newarr := []int{ret[len(ret)-1][0], getMax(ret[len(ret)-1][1], tmp[i][1])}
ret := ret[:len(ret)-1]
ret = append(ret, newarr)
}
return ret
}
func getMax(i, j int)int {
if i > j{
return i
}
return j
}改进-使用contain/list
我觉得,上面的,可能面试官挑错
func insert(intervals [][]int, newInterval []int) [][]int {
// 将newInterval加入到intervals中
tmp := append(intervals, newInterval)
// 将tmp按照,start从小到大,如果start相同,那么end从小到大进行排序
sort.Slice(tmp, func(i, j int) bool {
if tmp[i][0] == tmp[j][0] {
return tmp[i][1] < tmp[j][1]
}
return tmp[i][0] < tmp[j][0]
})
// ❤️❤️❤️核心和C++一样的地方
// 使用container/list作为双端队列
resultList := list.New()
// 遍历所有区间并进行合并
for i := 0; i < len(tmp); i++ {
current := tmp[i]
// 如果链表为空,直接添加当前区间
if resultList.Len() == 0 {
// ❤️❤️❤️核心和C++一样的地方
resultList.PushBack(current)
continue
}
// 获取链表最后一个元素
// ❤️❤️❤️核心和C++一样的地方
lastElement := resultList.Back()
// ❤️❤️❤️不和C++一样的地方,需要断言出来!!!
lastInterval := lastElement.Value.([]int)
// 判断是否可以合并
if current[0] > lastInterval[1] {
// 不能合并,直接添加到链表结尾
resultList.PushBack(current)
} else {
// 可以合并,更新链表最后一个元素的值
mergedInterval := []int{lastInterval[0], getMax(lastInterval[1], current[1])}
// ❤️❤️❤️不和C++一样的地方,需要用Remove!
resultList.Remove(lastElement)
resultList.PushBack(mergedInterval)
}
}
// 将链表中的元素转换为结果数组
result := make([][]int, 0, resultList.Len())
for e := resultList.Front(); e != nil; e = e.Next() {
result = append(result, e.Value.([]int))
}
return result
}
func getMax(i, j int) int {
if i > j {
return i
}
return j
}附录2.sort排序算法库的使用
该包实现了四种基本排序算法:
- 插入排序
- 归并排序
- 堆排序和
- 快速排序。
但是这四种排序方法是不公开的,它们只被用于 sort 包内部使用。
- 除此之外,为了方便对常用数据类型的操作,sort 包提供了对
[]int切片、[]float64切片和[]string切片完整支持,主要包括:- 对基本数据类型切片的排序支持
- 基本数据元素查找
- 判断基本数据类型切片是否已经排好序
- 对排好序的数据集合逆序
go语言标准库中sort包的设计理念是?
哲学1:以接口为核心,鼓励用户扩展
核心接口:sort.Interface
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}这个接口定义了排序的最小抽象单元,用户只要实现这三个方法,就可以使用标准库中的排序算法。这体现了 Go 一贯的接口驱动设计风格。
Go 语言标准库中的 sort 包设计理念体现了 简洁性、通用性与灵活性,核心遵循 Go 的设计哲学:接口优先、组合优于继承、避免过度抽象。
哲学2:将算法与数据结构解耦
Go 的 sort.Sort 并不关心你排序的是整数、字符串、结构体,还是其他类型,只要实现了 sort.Interface 接口,就可以排序。这种设计使得排序逻辑与数据结构完全解耦。
哲学3.提供常用类型的辅助实现
为了方便用户,sort 包内置了一些常见类型的封装,如:
sort.IntSlice([]int的封装)sort.StringSlice([]string的封装)sort.Float64Slice([]float64的封装)
这些封装类型已经实现了sort.Interface,可以直接传入sort.Sort使用,用户无需重复造轮子。
s := []int{5, 3, 2, 4, 1}
sort.Sort(sort.IntSlice(s)) // 或直接 sort.Ints(s)此外还提供便捷函数满足主流使用场景
例如:
sort.Ints(s []int)sort.Strings(s []string)sort.Float64s(s []float64)sort.Slice(slice, lessFunc):用于任意 slice 的自定义排序(Go 1.8+)
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})这符合 Go 的“提供工具函数简化常见操作”的理念。
其他的类似C++,性能优先:混合排序算法实现
Go 的排序算法底层使用了 优化过的快速排序、堆排序和插入排序的混合体(Timsort 思想),以确保在多数实际场景下具有良好的性能表现。
可能的疑惑:sort.Ints(s []int)函数和sort.IntSlice类型啥关系?
回答:一个是函数!一个是类型!
// sort.Ints()内部实现:
func Ints(x []int) {
sort.Sort(IntSlice(x)) // 其实就是包装了IntSlice
}
// 所以这两个是等价的:
sort.Ints(nums)
sort.Sort(sort.IntSlice(nums))为什么要设计两个?
设计哲学:灵活性 vs 便捷性
sort.Ints() - 函数(最简单的方式)
// 只是一个便捷函数
func Ints(x []int) {
sort.IntSlice(x).Sort()
}
// 使用:最简洁
nums := []int{3, 1, 4, 1, 5}
sort.Ints(nums) // nums现在是[1, 1, 3, 4, 5]sort.IntSlice - 类型(实现了sort.Interface这个接口)
// 是一个类型,实现了 sort.Interface
type IntSlice []int
// 使用:可以调用更多方法
nums := []int{3, 1, 4, 1, 5}
intSlice := sort.IntSlice(nums)
intSlice.Sort() // 排序
intSlice.Len() // 长度
intSlice.Less(i, j) // 比较
intSlice.Swap(i, j) // 交换为什么要设计两个?
// 场景1:简单排序(便捷性优先)
// 90%的使用场景只需要这个
nums := []int{3, 1, 4}
sort.Ints(nums) // 一行搞定
// 场景2:需要自定义排序逻辑(灵活性优先)
// 可以修改IntSlice的比较逻辑
type ReverseIntSlice struct {
sort.IntSlice
}
// 覆盖Less方法实现逆序
func (r ReverseIntSlice) Less(i, j int) bool {
return r.IntSlice.Less(j, i) // 反转比较逻辑
}
nums := []int{3, 1, 4}
reverseSlice := ReverseIntSlice{nums}
sort.Sort(reverseSlice) // nums现在是[4, 3, 1]
// 场景3:需要搜索(需要更多方法)
nums := []int{1, 3, 5, 7}
intSlice := sort.IntSlice(nums)
// 二分查找必须先排序
pos := intSlice.Search(5) // 返回索引2
// sort.Ints()无法直接搜索