引发思考的一道算法题
链接:组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
大致的思路就是递归+回溯:
- res [][]int存储所有可能的结果,temp []int存储一种可能的结果
- 首先确定递归参数和返回值。对于这道题目不需要有返回值,参数的话,传入n,k表示从n个数选出k个数,传入in表示当前处理的第几个数字.[由于闭包可以捕获变量所以n,k不需要作为参数传入]
- 递归边界就是,当切片temp中的元素个数到达k的时候,将其存储到res中
- 核心处理逻辑,从in–n中选择一个数字加入temp,然后递归,回溯
代码如下:
//这里使用到了闭包
func combine(n int, k int) [][]int {
var res [][]int
var tem []int
var dfs func (int)
dfs=func(in int){
if len(tem)==k{
res=append(res,tem)
}
for i:=in;i<=n;i++{
tem=append(tem,i)
dfs(i+1)
tem=tem[:len(tem)-1]
}
}
dfs(1)
return res
}
看一下运行结果
- 通过运行结果发现,结果中的值被覆盖掉了。要解释这个现象,还需要了解一下slice的底层构成。
slice
源码位置
src\runtime\slice.go
- slice其实是一个struct
type slice struct {
//指向底层数组的指针
array unsafe.Pointer
//数组长度
len int
//为数组分配的总空间大小
cap int
}
make初始化
slice:=make([]int,3,4)
利用"make"初始化一个slice会调用"makeslice函数"[src\runtime\slice.go]
func makeslice(et *_type, len, cap int) unsafe.Pointer {
//et.size表示的是切片内数据类型的大小,math.MulUinptr计算两个数的乘积,并返回这个乘积和一个表示是否溢出的bool型变量
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
//要么是用cap申请的内存溢出或者超出最大限制,要么是len和cap的关系有问题
if overflow || mem > maxAlloc || len < 0 || len > cap {
//再次计算用len申请的内存大小
mem, overflow := math.MulUintptr(et.size, uintptr(len))
//内存溢出。或者超出最大限制或者len<0
if overflow || mem > maxAlloc || len < 0 {
//panic("len out of range ")
panicmakeslicelen()
}
//panic("cap out of range ")len>cap的情况
panicmakeslicecap()
}
//如果申请cap*size大小的内存可以的话就申请cap*size大小的内存,不然的话申请len*size大小的内存
return mallocgc(mem, et, true)
}
如果make函数初始化一个很大的切片,该切片会逃逸到堆上。如果分配了一个比较小的切片会直接在栈中分配。没有发生逃逸调用的是"makeslice"函数,发生逃逸调用的是"makeslice64"函数
func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer {
//首先会判断要申请的内存大小是否超出了理论上系统可以分配的内存大小
len := int(len64)
if int64(len) != len64 {
panicmakeslicelen()
}
cap := int(cap64)
if int64(cap) != cap64 {
panicmakeslicecap()
}
//没有超出,理论上可以申请内存
return makeslice(et, len, cap)
}
看一下“mallogc”分配内存的函数,这个函数就不展开来看了,看看源代码上的解释
Allocate an object of size bytes.
Small objects are allocated from the per-P cache’s free lists.
Large objects (> 32 kB) are allocated straight from the heap.
小对象(<=32KB)可以直接分配在P拥有的cache的空闲链表中[这里的P是指GMP中的P,cache和go的内存管理有关,指的是mcache],大对象(>32KB)直接在堆上分配
需要注意的是每个版本的阈值可能不一样,我的版本是go version go1.20.8 windows/amd64
切片扩容原理
slice通过append进行扩容,先用代码演示一下
func main() {
//调用makesilice进行内存的分配
s := make([]int, 3, 4)
fmt.Println(len(s), cap(s)) //3 4
s = append(s, 1)
fmt.Println(len(s), cap(s))//4 4
s = append(s, 2)
fmt.Println(len(s), cap(s))//5 8
}
- 当插入元素时发现,没有可用的空间[len==cap],会先进行扩容,然后插入新的元素。
扩容的核心逻辑位于“slice.go\growslice”函数中
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
oldLen := newLen - num
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(oldPtr, uintptr(oldLen*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(oldPtr, uintptr(oldLen*int(et.size)))
}
if asanenabled {
asanread(oldPtr, uintptr(oldLen*int