golang 提供了幾個簡單的容器供我們使用,本文在介紹幾種Golang 容器的基礎上,實現一個基於Golang 容器的LRU算法。
容器介紹
Golang 容器位於 container 包下,提供了三種包供我們使用,heap、list、ring. 下面我們分別學習。
heap
heap 是一個堆的實現。一個堆正常保證了獲取/彈出最大(最小)元素的時間為log n、插入元素的時間為log n.
golang的堆實現接口如下:
// src/container/heap.go
type Interface interface {
sort.Interface
Push(x interface{}) // add x as element Len()
Pop() interface{} // remove and return element Len() - 1.
}
heap 是基於 sort.Interface 實現的。
// src/sort/
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
因此,如果要使用官方提供的heap,需要我們實現如下幾個接口:
Len() int {} // 獲取元素個數
Less(i, j int) bool {} // 比較方法
Swap(i, j int) // 元素交換方法
Push(x interface{}){} // 在末尾追加元素
Pop() interface{} // 返回末尾元素
然后在使用時,我們可以使用如下幾種方法:
// 初始化一個堆
func Init(h Interface){}
// push一個元素倒堆中
func Push(h Interface, x interface{}){}
// pop 堆頂元素
func Pop(h Interface) interface{} {}
// 刪除堆中某個元素,時間復雜度 log n
func Remove(h Interface, i int) interface{} {}
// 調整i位置的元素位置(位置I的數據變更后)
func Fix(h Interface, i int){}
list 鏈表
list 實現了一個雙向鏈表,鏈表不需要實現heap 類似的接口,可以直接使用。
鏈表的構造:
// 返回一個鏈表對象
func New() *List {}
官方提供了豐富的方法供我們操作列表,方法如下:
// 返回鏈表的長度
func (l *List) Len() int {}
// 返回鏈表中的第一個元素
func (l *List) Front() *Element {}
// 返回鏈表中的末尾元素
func (l *List) Back() *Element {}
// 移除鏈表中的某個元素
func (l *List) Remove(e *Element) interface{} {}
// 在表頭插入值為 v 的元素
func (l *List) PushFront(v interface{}) *Element {}
// 在表尾插入值為 v 的元素
func (l *List) PushBack(v interface{}) *Element {}
// 在mark之前插入值為v 的元素
func (l *List) InsertBefore(v interface{}, mark *Element) *Element {}
// 在mark 之后插入值為 v 的元素
func (l *List) InsertAfter(v interface{}, mark *Element) *Element {}
// 移動e某個元素到表頭
func (l *List) MoveToFront(e *Element) {}
// 移動e到隊尾
func (l *List) MoveToBack(e *Element) {}
// 移動e到mark之前
func (l *List) MoveBefore(e, mark *Element) {}
// 移動e 到mark 之后
func (l *List) MoveAfter(e, mark *Element) {}
// 追加到隊尾
func (l *List) PushBackList(other *List) {}
// 將鏈表list放在隊列前
func (l *List) PushFrontList(other *List) {}
我們可以通過 Value 方法訪問 Element 中的元素。除此之外,我們還可以用下面方法做鏈表遍歷:
// 返回下一個元素
func (e *Element) Next() *Element {}
// 返回上一個元素
func (e *Element) Prev() *Element {}
下面是隊列的遍歷的例子:
// l 為隊列,
for e := l.Front(); e != nil; e = e.Next() {
//通過 e.Value 做數據訪問
}
ring 循環列表
container 中的循環列表是采用鏈表實現的。
// 構造一個包含N個元素的循環列表
func New(n int) *Ring {}
// 返回列表下一個元素
func (r *Ring) Next() *Ring {}
// 返回列表上一個元素
func (r *Ring) Prev() *Ring {}
// 移動n個元素 (可以前移,可以后移)
func (r *Ring) Move(n int) *Ring {}
// 把 s 鏈接到 r 后面。如果s 和r 在一個ring 里面,會把r到s的元素從ring 中刪掉
func (r *Ring) Link(s *Ring) *Ring {}
// 刪除n個元素 (內部就是ring 移動n個元素,然后調用Link)
func (r *Ring) Unlink(n int) *Ring {}
// 返回Ring 的長度,時間復雜度 n
func (r *Ring) Len() int {}
// 遍歷Ring,執行 f 方法 (不建議內部修改ring)
func (r *Ring) Do(f func(interface{})) {}
訪問Ring 中元素,直接 Ring.Value 即可。
容器的使用
下面,我們通過map 和 官方包中的雙向鏈表實現一個簡單的lru 算法,用來熟悉golang 容器的使用。
LRU 算法 (Least Recently Used),在做緩存置換時用的比較多。逐步淘汰最近未使用的cache,而使我們的緩存中持續保持着最近使用的數據。
package main
import "fmt"
import "container/list"
// lru 中的數據
type Node struct {
K, V interface{}
}
// 鏈表 + map
type LRU struct {
list *list.List
cacheMap map[interface{}]*list.Element
Size int
}
// 初始化一個LRU
func NewLRU(cap int) *LRU {
return &LRU{
Size: cap,
list: list.New(),
cacheMap: make(map[interface{}]*list.Element, cap),
}
}
// 獲取LRU中數據
func (lru *LRU) Get(k interface{}) (v interface{}, ret bool) {
// 如果存在,則把數據放到鏈表最前面
if ele, ok := lru.cacheMap[k]; ok {
lru.list.MoveToFront(ele)
return ele.Value.(*Node).V, true
}
return nil, false
}
// 設置LRU中數據
func (lru *LRU) Set(k, v interface{}) {
// 如果存在,則把數據放到最前面
if ele, ok := lru.cacheMap[k]; ok {
lru.list.MoveToFront(ele)
ele.Value.(*Node).V = v // 更新數據值
return
}
// 如果數據是滿的,先刪除數據,后插入
if lru.list.Len() == lru.Size {
last := lru.list.Back()
node := last.Value.(*Node)
delete(lru.cacheMap, node.K)
lru.list.Remove(last)
}
ele := lru.list.PushFront(&Node{K: k, V: v})
lru.cacheMap[k] = ele
}
其他
- 上述的容器都不是goroutines 安全的
- 上面的lr 也不是goroutines 安全的
- Ring 中不建議在Do 方法中修改Ring 的指針,行為是未定義的