Go map 源码分析

哈希表

哈希表的查询效率贼高,时间复杂度是 O(1),这是咋实现的呢?

Go 的哈希表结构就是 map,这次我们就来探索一下它底层是咋回事~

3,2,1,上源码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const (
// Maximum number of key/value pairs a bucket can hold.
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
)
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed

buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)

extra *mapextra // optional fields
}

// mapextra holds fields that are not present on all maps.
type mapextra struct {
// If both key and elem do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and elem do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap
oldoverflow *[]*bmap

// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}

// A bucket for a Go map.
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}

这个就是 Go map 底层结构的部分源码了,业务代码中的每一个 map 底层对应的就是一个 hmap

hmap 有个字段 buckets,指向了 bmap 数组,bmap 里放的就是存进 map 里的 key value 了,每个 bmap 里都可以保存 bucketCnt 个键值对

啥?你说这个 bmap 里不就一个 tophash 字段,哪有什么 key value?

bmap 里 key value 在哪

1
2
3
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))

v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))

key value 是这样通过指针计算获取的,key value 数据存在 tophash 的后边(编译阶段会扩充 bmap 结构)

怎么知道数据放在哪个 bmap

下一行源码是 map 在决定数据放到哪个 bucket 时用的

1
bucket := hash & bucketMask(h.B)

hash

1
hash := alg.hash(key, uintptr(h.hash0))

Go 用 hmap.hash0(创建 map 时生成的随机数) 和存进 map 的 key 进行哈希运算,相等的 key 和 hash0 每次运算得出的 hash 值也相等

bucketMask()

1
2
3
4
// bucketMask returns 1<<b - 1, optimized for code generation.
func bucketMask(b uint8) uintptr {
return bucketShift(b) - 1
}

bucketMask 做的操作就是把 1 按二进制左移 b 位后再减去 1,比如当传进来的 b 是 4,1 用二进制表示是 0b1,左移 4 位后就是 0b10000,再减去 1 就是 0b1111,名符其实,就是掩码

h.B

h.B 就是 hmap 里的 B 字段,记录了 buckets 数组的长度信息。hmap.buckets 数组的长度是 2 的 hmap.B 次方

如果 B 是 4,那么 buckets 数组的长度是 2 的 4 次方就是 16

与掩码做 & 运算的结果只会落在 0 ~ 掩码这个闭区间里,在这个例子中也就是 0 ~ 15,这恰好是 buckets 数组下标的取值范围。这就是掩码的作用,相当于取模

就这样,从 key 可以映射到 buckets 下标位置,也就知道数据该存放到哪个 bmap 了,正是因为这个映射的存在,所以不需要遍历每一个 bucket 去比较,才把时间复杂度给打成了 O(1)

bmap 里的 tophash 干啥用

tophash 是一个数组,一个 bmap 里可以放多个键值对,所以当有多个数据放进同一个 bmap 时就需要挨个比较了,看到底是哪个数据

下面是从 map 查数据的一段源码中截出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if t.key.equal(key, k) {
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e
}
}

tophash 数组元素里存的是 hash 高位的一部分,就是用于快速比较,因为小,所以快

当然比较完 tophash 还是得再比较一遍 key 来确保是要取的值,因为不同 key 的 hash 值有小概率的可能是相等的(何况更短的 tophash)

好了,看到这里已经知道 map 基本的数据存取是咋做的了。

但是如果数据越来越多,现有的 bmap 放不下了该怎么办?

这就得讲 map 的扩容机制了

扩容

先看下 bmap 中还藏着的一个值

1
2
3
func (b *bmap) overflow(t *maptype) *bmap {
return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-goarch.PtrSize))
}

这个 overflow 也是像 key value 那样通过指针计算拿到的,实际位置是放在 key value 的后边,是 bmap 的最后一部分

overflow 是一个 bmap 指针,它就是为数据溢出做准备的

当要新增一个键值对,而它的目标 bmap 已经满了的时候,就会把一个新的 bmap 通过 overflow 与旧 bmap “连”起来,将新键值对放进新 bmap 里

1
2
3
4
5
// The current bucket and all the overflow buckets connected to it are full, allocate a new one.
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))

取数据时,遍历完当前 bmap 没找到的话要继续通过 overflow 去取溢出 bmap,就像遍历链表一样

1
2
3
4
5
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
...
}
}

到这就有问题了,要是数据一直这么增长下去,overflow bmap 链表越来越长,map 的查询时间复杂度岂不是和遍历链表一样是 O(n)

所以当数据太多时,需要一个把 buckets 数组变长的机制,让 hash 发挥更大作用,将时间复杂度继续打下来,维持在 O(1)

以下这段是从 map 存数据源码中截取出,目的就是检测当前是否需要扩容

1
2
3
4
5
6
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}

翻倍扩容

overLoadFactor 指示数据是否太多,需要翻倍扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const (
// Maximum average load of a bucket that triggers growth is 6.5.
// Represent as loadFactorNum/loadFactorDen, to allow integer math.
loadFactorNum = 13
loadFactorDen = 2
)
// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
// bucketShift returns 1<<b, optimized for code generation.
func bucketShift(b uint8) uintptr {
// Masking the shift amount allows overflow checks to be elided.
return uintptr(1) << (b & (goarch.PtrSize*8 - 1))
}

hmap.count 是 map 中实际存储数据的数量,当 count 超过 buckets 数组长度的 6.5 倍后,就会触发翻倍扩容

还有一种情况会导致查询效率下降,而且数据量不大无法触发翻倍扩容

先是新增数据导致生成许多 overflow bmap 后又删除了很多数据,导致各个 bmap 中有很多空位置,数据密度下降了,查询时在一个 bmap 里没两个数据就得访问下一个 overflow bmap,这时候就得重新整理一下数据,提高数据密度

等量扩容

tooManyOverflowBuckets 指示 overflow bmap 是否太多,需要整理(等量扩容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tooManyOverflowBuckets reports whether noverflow buckets is too many for a map with 1<<B buckets.
// Note that most of these overflow buckets must be in sparse use;
// if use was dense, then we'd have already triggered regular map growth.
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
// If the threshold is too low, we do extraneous work.
// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
// "too many" means (approximately) as many overflow buckets as regular buckets.
// See incrnoverflow for more details.
if B > 15 {
B = 15
}
// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
return noverflow >= uint16(1)<<(B&15)
}

hmap.noverflow 是大约有多少 overflow bmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// incrnoverflow increments h.noverflow.
// noverflow counts the number of overflow buckets.
// This is used to trigger same-size map growth.
// See also tooManyOverflowBuckets.
// To keep hmap small, noverflow is a uint16.
// When there are few buckets, noverflow is an exact count.
// When there are many buckets, noverflow is an approximate count.
func (h *hmap) incrnoverflow() {
// We trigger same-size map growth if there are
// as many overflow buckets as buckets.
// We need to be able to count to 1<<h.B.
if h.B < 16 {
h.noverflow++
return
}
// Increment with probability 1/(1<<(h.B-15)).
// When we reach 1<<15 - 1, we will have approximately
// as many overflow buckets as buckets.
mask := uint32(1)<<(h.B-15) - 1
// Example: if h.B == 18, then mask == 7,
// and fastrand & 7 == 0 with probability 1/8.
if fastrand()&mask == 0 {
h.noverflow++
}
}

B 小于 16 时,noverflow 是确切值,大于等于 16 时就是通过随机数按概率控制每次是否自增 1,为啥要搞这么麻烦呢?

为了降低 hmap 的内存占用量,noverflow 定为了 uint16 类型,为啥是 uint16 不是 uint32?要省这么点内存?因为 uint16 恰好能内存对齐

B 小于 16 时,当 noverflow 大于等于 buckets 数组(2 的 B 次方)长度就会触发等量扩容

B 大于等于 16 时,当 noverflow 大于等于 2 的 15 次方就会触发等量扩容

以上我们知道了 map 有两个扩容触发机制,那么触发后具体是怎么做的呢?

渐进式扩容

触发后会调用 hashGrow() 开始扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func hashGrow(t *maptype, h *hmap) {
// If we've hit the load factor, get bigger.
// Otherwise, there are too many overflow buckets,
// so keep the same number of buckets and "grow" laterally.
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) {
bigger = 0
h.flags |= sameSizeGrow
}
oldbuckets := h.buckets
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// commit the grow (atomic wrt gc)
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0

if h.extra != nil && h.extra.overflow != nil {
// Promote current overflow buckets to the old generation.
if h.extra.oldoverflow != nil {
throw("oldoverflow is not nil")
}
h.extra.oldoverflow = h.extra.overflow
h.extra.overflow = nil
}
if nextOverflow != nil {
if h.extra == nil {
h.extra = new(mapextra)
}
h.extra.nextOverflow = nextOverflow
}

// the actual copying of the hash table data is done incrementally
// by growWork() and evacuate().
}

这里做的是扩容的初始化步骤,根据扩容要求(等量扩容还是翻倍扩容)确定 B 是否加 1,将 hmap.oldbuckets 指针指向老的 buckets 数组,hmap.buckets 则指向崭新创建的新数组

实际的 bmap 拷贝操作是在后续的 growWork() 和 evacuate() 中做的

hmap.nevacuate 标记的是下一个要迁移的 oldbucket 下标位置,所以被初始化置为 0

因为 buckets 已经是崭新的数组了(没有实际数据),所以 hmap.noverflow 也被重置为 0

growWork() 会在 hmap 赋值和删除 key 时被调用,具体时机是在 hash 值计算完确定好是哪个目标 bucket 但未做实际操作前,调用前先判断当前 hmap 是否正在扩容

1
2
3
4
5
...
if h.growing() {
growWork(t, h, bucket)
}
...
1
2
3
4
5
6
7
8
9
10
func growWork(t *maptype, h *hmap, bucket uintptr) {
// make sure we evacuate the oldbucket corresponding
// to the bucket we're about to use
evacuate(t, h, bucket&h.oldbucketmask())

// evacuate one more oldbucket to make progress on growing
if h.growing() {
evacuate(t, h, h.nevacuate)
}
}

evacuate() 调用一次只会迁移一个 oldbucket

growWork() 会先确保已经把当前要用的 oldbucket 迁移掉,然后再根据 nevacuate 多 evacuate 一个 oldbucket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// evacDst is an evacuation destination.
type evacDst struct {
b *bmap // current destination bucket
i int // key/elem index into b
k unsafe.Pointer // pointer to current key storage
e unsafe.Pointer // pointer to current elem storage
}

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
newbit := h.noldbuckets()
if !evacuated(b) {
// TODO: reuse overflow buckets instead of using new ones, if there
// is no iterator using the old buckets. (If !oldIterator.)

// xy contains the x and y (low and high) evacuation destinations.
var xy [2]evacDst
x := &xy[0]
x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
x.k = add(unsafe.Pointer(x.b), dataOffset)
x.e = add(x.k, bucketCnt*uintptr(t.keysize))

if !h.sameSizeGrow() {
// Only calculate y pointers if we're growing bigger.
// Otherwise GC can see bad pointers.
y := &xy[1]
y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
y.k = add(unsafe.Pointer(y.b), dataOffset)
y.e = add(y.k, bucketCnt*uintptr(t.keysize))
}

for ; b != nil; b = b.overflow(t) {
k := add(unsafe.Pointer(b), dataOffset)
e := add(k, bucketCnt*uintptr(t.keysize))
for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
top := b.tophash[i]
if isEmpty(top) {
b.tophash[i] = evacuatedEmpty
continue
}
if top < minTopHash {
throw("bad map state")
}
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
var useY uint8
if !h.sameSizeGrow() {
// Compute hash to make our evacuation decision (whether we need
// to send this key/elem to bucket x or bucket y).
hash := t.hasher(k2, uintptr(h.hash0))
if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
// If key != key (NaNs), then the hash could be (and probably
// will be) entirely different from the old hash. Moreover,
// it isn't reproducible. Reproducibility is required in the
// presence of iterators, as our evacuation decision must
// match whatever decision the iterator made.
// Fortunately, we have the freedom to send these keys either
// way. Also, tophash is meaningless for these kinds of keys.
// We let the low bit of tophash drive the evacuation decision.
// We recompute a new random tophash for the next level so
// these keys will get evenly distributed across all buckets
// after multiple grows.
useY = top & 1
top = tophash(hash)
} else {
if hash&newbit != 0 {
useY = 1
}
}
}

if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
throw("bad evacuatedN")
}

b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
dst := &xy[useY] // evacuation destination

if dst.i == bucketCnt {
dst.b = h.newoverflow(t, dst.b)
dst.i = 0
dst.k = add(unsafe.Pointer(dst.b), dataOffset)
dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))
}
dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
if t.indirectkey() {
*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
} else {
typedmemmove(t.key, dst.k, k) // copy elem
}
if t.indirectelem() {
*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
} else {
typedmemmove(t.elem, dst.e, e)
}
dst.i++
// These updates might push these pointers past the end of the
// key or elem arrays. That's ok, as we have the overflow pointer
// at the end of the bucket to protect against pointing past the
// end of the bucket.
dst.k = add(dst.k, uintptr(t.keysize))
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
// Unlink the overflow buckets & clear key/elem to help GC.
if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
// Preserve b.tophash because the evacuation
// state is maintained there.
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}

if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}

evacuate() 比较长,其主要做的就是迁移一个 oldbucket 和它后面的所有 overflow bmap 到新的 buckets 目标位置,并清理掉旧的 overflow bmap

如果是翻倍扩容的话,oldbucket 的新位置就有两种可能(举个例子:比如原来 B 是 3,现在要迁移的 oldbucket 是 1,那么原先放在 oldbucket 的 key 的 hash 值末 3 位就是 0b001 ,现在 B 是 4 了,那 hash 就要多比较 1 位,多出来的这位要么是 1 要么是 0,也就是说现在的 buckets 下标要么是 0b0001 要么是 0b1001)

根据 nevacuate 迁移后还要更新 nevacuate 的值,指向下一个非空的 oldbucket,遇到空的 oldbucket 就跳过(为了保证当前操作的时间复杂度,还限制了最多只跳 1024 个 oldbucket)

还要判断迁移是否完成,迁移完了就做收尾工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
h.nevacuate++
// Experiments suggest that 1024 is overkill by at least an order of magnitude.
// Put it in there as a safeguard anyway, to ensure O(1) behavior.
stop := h.nevacuate + 1024
if stop > newbit {
stop = newbit
}
for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
h.nevacuate++
}
if h.nevacuate == newbit { // newbit == # of oldbuckets
// Growing is all done. Free old main bucket array.
h.oldbuckets = nil
// Can discard old overflow buckets as well.
// If they are still referenced by an iterator,
// then the iterator holds a pointers to the slice.
if h.extra != nil {
h.extra.oldoverflow = nil
}
h.flags &^= sameSizeGrow
}
}

好了 Go map 源码暂时就分析到这了,还有 makemap 和遍历过程没分析,有兴趣的话自己看源码去吧

LRU 缓存

背景

内存空间是有限的,当数据量超过空间的时候就得考虑淘汰哪部分数据,那按什么规则淘汰呢?

把最久没用到的数据淘汰了不就行了,最久没用的数据直觉上不就是不太会被访问的数据吗,既然不会被访问,那就先淘汰呗,LRU 算法就是这么来的

LRU(Least Recently Used)按字面意思理解就是最近最少使用,那该怎么知道哪个数据是最近最少使用的呢?

LRU 的数据结构

用链表实现!

用链表是个很自然就可以想到的办法。每次数据块在存取完后将其重新排到头节点上,这样当数据在头节点说明它最近才被使用过

数据块在尾节点说明它是最久没被用到的,当数据超出内存限制时将尾节点的数据块删掉就行了

但链表有个缺点

就是链表查询、更新、删除操作的时间复杂度都是 O(n),只有在头尾新增数据时才是 O(1),要是能把 O(n) 复杂度都降到 O(1) 岂不妙哉?

用哈希表呀!

哈希表查询数据的复杂度不就是 O(1) 嘛!

那我们该怎么把哈希表用进去呢?

首先数据在查询时肯定需要从哈希表查,这样才能发挥出哈希表查得快的作用

其次要把数据被使用的信息体现到链表里(也就是把刚用过的数据提到头节点),这样才可以在数据溢出时知道去淘汰哪个

话不多说,3,2,1,上代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package problems

type LRUCache struct {
LinkedList *DoubleLinkedList
LinkedNodeMap map[int]*DoubleLinkedNode
capacity int
}

func Constructor(capacity int) LRUCache {
if capacity == 0 {
panic("capacity should not be zero")
}
return LRUCache{
LinkedList: ConstructDoubleLinkedList(),
LinkedNodeMap: map[int]*DoubleLinkedNode{},
capacity: capacity,
}
}

func (cache *LRUCache) Get(key int) int {
node, ok := cache.LinkedNodeMap[key]
if !ok {
return -1
}
cache.LinkedList.MoveToTop(node)
return node.Value.(CacheNode).Value
}

func (cache *LRUCache) Put(key int, value int) {
newCacheNode := CacheNode{key, value}
node, ok := cache.LinkedNodeMap[key]

if ok {
node.Value = newCacheNode
cache.LinkedList.MoveToTop(node)
return
}

if len(cache.LinkedNodeMap) == cache.capacity {
delete(cache.LinkedNodeMap, cache.LinkedList.Tail.Pre.Value.(CacheNode).Key)
cache.LinkedList.Remove(cache.LinkedList.Tail.Pre)
}

newLinkedNode := &DoubleLinkedNode{Value: newCacheNode}
cache.LinkedNodeMap[key] = newLinkedNode
cache.LinkedList.AddToTop(newLinkedNode)
}

type DoubleLinkedList struct {
Head *DoubleLinkedNode
Tail *DoubleLinkedNode
}

func ConstructDoubleLinkedList() *DoubleLinkedList {
tail := &DoubleLinkedNode{}
head := &DoubleLinkedNode{}
tail.Pre = head
head.Next = tail
return &DoubleLinkedList{
Head: head,
Tail: tail,
}
}

func (list *DoubleLinkedList) MoveToTop(node *DoubleLinkedNode) {
list.Remove(node)
list.AddToTop(node)
}

func (list *DoubleLinkedList) Remove(node *DoubleLinkedNode) {
node.Pre.Next = node.Next
node.Next.Pre = node.Pre
}

func (list *DoubleLinkedList) AddToTop(node *DoubleLinkedNode) {
list.Head.Next.Pre = node
node.Next = list.Head.Next
node.Pre = list.Head
list.Head.Next = node
}

type CacheNode struct {
Key int
Value int
}

I have a 链表~, I have a 哈希表~, 嗯!?LRU Cache !

怎么样,是不是很好玩呀

深入浅出图解 KMP 算法思想

维基百科介绍

在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个字符串S内查找一个词W的出现位置。一个词在不匹配时本身就包含足够的信息来确定下一个匹配可能的开始位置,此算法利用这一特性以避免重新检查先前配对的字符。这个算法由高德纳和沃恩·普拉特在1974年构思,同年詹姆斯·H·莫里斯也独立地设计出该算法,最终三人于1977年联合发表。

核心思想

利用最长相等前后缀跳过相等字符,避免多余计算

核心步骤

  1. 计算模式串的最长相等前后缀数组(next 数组)
  2. 利用 next 数组在匹配字符串时跳过相等的字符,直到匹配结束

图解

Go 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package problems

func strStr(haystack string, needle string) int {
if len(needle) == 0 || len(haystack) == 0 {
return -1
}
j := 0
next := getNext(needle)
for i := 0; i < len(haystack); i++ {
for j > 0 && haystack[i] != needle[j] {
j = next[j-1]
}
if haystack[i] == needle[j] {
j++
}
if j == len(needle) {
return i - len(needle) + 1
}
}
return -1
}

func getNext(s string) []int {
j := 0
next := make([]int, len(s))
next[0] = 0
for i := 1; i < len(s); i++ {
for j > 0 && s[j] != s[i] {
j = next[j-1]
}
if s[j] == s[i] {
j++
}
next[i] = j
}
return next
}

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package problems

import (
"reflect"
"testing"
)

func Test_getNext(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want []int
}{
{
name: "",
args: args{
s: "benbenw",
},
want: []int{0, 0, 0, 1, 2, 3, 0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getNext(tt.args.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("getNext() = %v, want %v", got, tt.want)
}
})
}
}

func Test_strStr(t *testing.T) {
type args struct {
haystack string
needle string
}
tests := []struct {
name string
args args
want int
}{
{
name: "",
args: args{
haystack: "benbenbenw",
needle: "benbenw",
},
want: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := strStr(tt.args.haystack, tt.args.needle); got != tt.want {
t.Errorf("strStr() = %v, want %v", got, tt.want)
}
})
}
}

力扣相关题目

力扣 28 题:找出字符串中第一个匹配项的下标
力扣 459 题:重复的子字符串

图解快速排序

维基百科介绍

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),是一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序 n 个项目要 O(nlogn) 次比较。在最坏状况下则需要 O(n2) 次比较,但这种状况并不常见。事实上,快速排序 Θ(nlog⁡n) 通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。

核心步骤

  1. 选择基准:选择待排序数组中的一个数字作为基准
  2. 按基准分组:遍历数组元素,将小于基准值的数字移动到基准值左侧,大于的则移到右侧
  3. 递归:在基准值左侧与右侧数组重复 1、2、3 步,直到所有数据排序完成

核心步骤图解

C 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>

void EXCHANGE(int *left, int *right)
{
if (left == right)
return;
*left ^= *right;
*right ^= *left;
*left ^= *right;
}

int PARTITION(int A[], int left, int right)
{
int pivotValue = A[right];
int pivotDest = left;
for (int i = left; i < right; i++)
{
if (A[i] <= pivotValue)
{
EXCHANGE(&A[pivotDest], &A[i]);
pivotDest++;
}
}
EXCHANGE(&A[pivotDest], &A[right]);
return pivotDest;
}

void QUICKSORT(int A[], int left, int right)
{
if (left < right)
{
int pivot = PARTITION(A, left, right);
QUICKSORT(A, left, pivot - 1);
QUICKSORT(A, pivot + 1, right);
}
}

int main(int argc, char const *argv[])
{
int A[] = {7, 3, 9, 5, 8, 4, 2};
QUICKSORT(A, 0, 7);
for (int i = 0; i <= 7; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}

Go 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package sort

func QuickSort(a []int) []int {
partition := func(a []int, lo, hi int) int {
i, j := lo+1, hi
for {
for ; i < hi && a[i] <= a[lo]; i++ {
}
for ; j > lo && a[j] >= a[lo]; j-- {
}
if j <= i {
break
}
a[i], a[j] = a[j], a[i]
}
a[i-1], a[lo] = a[lo], a[i-1]
return i - 1
}

var sort func(a []int, lo, hi int)
sort = func(a []int, lo, hi int) {
if lo >= hi {
return
}
j := partition(a, lo, hi)
sort(a, lo, j-1)
sort(a, j+1, hi)
}
sort(a, 0, len(a)-1)
return a
}

改进

数组偏小时改用插入排序

1
2
3
4
if lo+M >= hi {
InsertionSort(a, lo, hi)
return
}

删除启动台 (Launchpad) 中无法去除的图标

应用删除后图标还在启动台?打开终端!

  1. 找到 Launchpad 存放数据的路径

    1
    find 2>/dev/null /private/var/folders -name com.apple.dock.launchpad
  2. 进入输出目录的db子目录

    1
    cd /private/var/folders/**/com.apple.dock.launchpad/db
  3. 删除数据库中title为@@@的数据,将@@@替换为应用名称(区分大小写)&&重启Dock

    1
    sqlite3 db "delete from apps where title='@@@';"&&killall Dock

Nginx 站点设置入门教程

Nginx是一个快速且强大的http和反向代理服务器,能够快捷方便地提供服务

安装

假设运行的系统是Ubuntu,在terminal输入如下命令安装nginx

1
apt-get install nginx

此时用浏览器访问你服务器的IP地址,你将会看见“welcome to nginx”页面

nginx的位置

所有nginx的配置文件都在**/etc/nginx目录下,cd到这个目录。本次教程要添加的配置文件会放在其中一个名为sites-enabled目录下,cd到该目录里,你会发现有个default文件在里面,就是这个文件使你看见“welcome to nginx”。touch一个test**在该目录下并用你喜欢的编辑器打开

设置静态服务器

Nginx的config文件有它自己的一套与css的类似的语法规则。如下是一个名为server的一级命名空间

1
2
3
server {

}

在这个命名空间内部,可以像css那样添加以分号结尾的键值对,也可以再添加一块子命名空间

键值对(key-value pairs)也称为指令(directives),命令有很多很多,本教程只用到其中一小部分,下面每个命令都加了一个指向该命令文档的链接,想要了解更多就看文档吧,文档是唯一的官方了解途径。如果你想要更加复杂高级的设置,也免不了要看文档。

listen指令用来设置服务器监听的端口号,默认为80

1
2
3
server {
listen 80;
}

由于80是默认值,可以不用写该指令,但为了表示清楚,写上也是极好的

server_name指令用来匹配url。如果你的站点在https://wongben.com,那根server_name就是wongben.com。如果还有一个在https://abc.com,可以再添加一个abc.com的server_name,各自的请求就会匹配到各自的站点里。

1
2
3
4
server {
listen 80;
server_name wongben.com;
}

root指令是设置静态网页的关键组成部分。如果你只是想要发布一些html和css文件,root指令描述了这些文件的存放位置。现在,创建目录 /var/www/example(也可以是其他任意位置),在此目录下,touch index.html,编辑该文件,存入一些类似hello world的文字,保存。回到刚才的配置文件,添加root指令

1
2
3
4
5
server {
listen 80;
server_name wongben.com;
root /var/www/example;
}

location指令有两个参数:1.字符串或正则 2.一个block(即命名空间namespace)。如果你想要给 wongben.com/whatever 指定页面,就用’whatever’作为uri。在这里,我们要匹配的是根目录,所以使用 / 作为uri

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name wongben.com;
root /var/www/example;

location / {

}
}

“**/**” 会匹配所有url,因为它被看成是一个正则。如果你想要单独匹配字符串,就加个等号

1
location = / { ... }

try_files指令根据文件名列表或模式在root指定的目录下寻找匹配的文件,在这里我们要做的很简单,匹配任何斜杠之后的字符,例如’whatever.html’,要是斜杠后没有任何字符,则匹配index.html

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name wongben.com;
root /var/www/example;

location / {
try_files $uri $uri/ /index.html;
}
}

发布

运行如下命令让nginx重新载入

1
service nginx reload

然后就可以在浏览器输入地址访问服务器啦

English