go 数据结构与算法
go 数据结构与算法
1.1 链表
链表是一种数据结构,单向列表每个元素后面都有一个next指向下一个元素,双向列表还有一个prev指向上一个元素
我们定义一个链表:
func main() {
list := List{}
list.Add(1)
list.Add(8)
list.Add(4)
list.Add(3)
list.Add(5)
list.Travs()
}
//链表的一个元素
type Node struct{
Info int
Next *Node //自己套自己,形成链表
}
//链表
type List struct{
Head *Node
Len int
}
// 1 > 8 > 4 > 3 > 5
func (list *List) Add (ele int) {
node := &Node{Info: ele,Next: nil} //取指针,因为需要放到List里面去的
//如果长度为0,那么说明加的是第一个元素,直接用node赋值
if list.Len == 0 {
list.Head = node
} else {
head := list.Head
for i := 1;i < list.Len;i++ {
head = head.Next
}
head.Next = node
}
list.Len += 1
}
func (list List) Travs() {
if list.Len == 0 {
return
}
head := list.Head
for i := 1;i < list.Len;i++{
fmt.Printf("%d ",head.Info)
head = head.Next
}
fmt.Printf("%d ",head.Info)
fmt.Println()
}
链表的应用案例
链表的一个应用案例。LRU(Least Recently Used, 最近最少使用)缓存淘汰的总体思路:缓存的key放到链表中,头部的元素表示最近刚使用。
如果命中缓存,从链表中找到对应的key,移到链表头部。
如果没命中缓存:
- 如果缓存容量没超,放入缓存,并把key放到链表头部。
- 如果超出缓存容量,删除链表尾部元素,再把key放到链表头部。
//类似redis缓存,最新访问的元素放到最前面
// 1 > 8 > 7 >5
func (list *List) Visit (ele int) {
//如果我们Visit查询一个元素,长度为0,说明缓存为0,直接return
if list.Len == 0 {
return
}
head := list.Head
for {
if head == nil{
break
}
//如果不是我们要查的哪个ele
if head.Info != ele {
//就跳转到下一个head
head = head.Next
} else {
break
}
}
//这时候可能在列表中查到ele了,也可能没查到
//如果没查到head应该是nil,这里我们假如查的数字是7
if head == nil {
return
} else {
post := head.Next //5 7的下一个元素就是5
pre := head.Prev //8 7的上一个元素是8
pre.Next = post //让8的下一个元素指向5,相当于把7移除了
post.Prev = pre //让5的上一个元素指向8
head.Next = list.Head //把7的下一个元素指向list.Head ,也就是指向开头的1
list.Head.Prev = head //让1的前一个元素变成7 head=7
list.Head = head //把list.Head改成7
}
}
注:上面是我们自己写的一个双向链表,体验了他的工作状态,那我们实际工作中,可以调用go自带的双向链表完成上述功能
//官方自带的双向链表功能
package main
//官方自带的双向链表
import (
"container/list"
"fmt"
)
func main() {
//用官方自带创建一个空的双向链表
lst := list.New()
//Pushback往链表末尾追加
lst.PushBack(4)
lst.PushBack(6)
lst.PushBack(2)
TravList(lst)
lst.PushFront(3)
TravList(lst)
}
func TravList(lst *list.List) {
//我们上面的例子用的Head结构体,这里是Front
head := lst.Front()
for head.Next() != nil {
//Value对应上面的Info,这个value是个interface
fmt.Printf("%v ",head.Value)
head = head.Next()
}
fmt.Printf("%v ",head.Value)
fmt.Println()
}
RING回环链表
ring的应用:基于滑动窗口的统计。比如最近100次接口调用的平均耗时、最近10笔订单的平均值、最近30个交易日股票的最高点。ring的容量即为滑动窗口的大小,把待观察变量按时间顺序不停地写入ring即可。
func RingDaemon() {
//定义单向链表回环的容量为10
ring := ring.New(10)
//插入100个元素,只会记录最后10个
for i := 0;i < 100;i++ {
ring.Value = i
ring = ring.Next()
}
sum := 0
//ring.Do遍历整个循环,对每个元素采取的处理措施由回调函数func(i interface{}) {}来定义
ring.Do(func(i interface{}) {
fmt.Printf("%v ",i)
num := i.(int)
sum += num
})
fmt.Println()
fmt.Println(sum)
}
2.1 栈
栈是一种先进后出的数据结构,push把元素压入栈底,pop弹出栈顶的元素。编程语言的编译系统也用到了栈的思想
go自带的List已经包含了栈的功能,这里实现一个线程安全的栈。
这里可以用双向列表就可以实现了,比如斐波那契函数,用go写,使用栈和不使用栈,可以减少重复计算的部分几百倍
3.1 堆
堆是一棵二叉树。大根堆即任意节点的值都大于等于其子节点。反之为小根堆。堆得底层就是数组
用数组来表示堆,下标为 i 的结点的父结点下标为(i-1)/2,其左右子结点分别为 (2i + 1)、(2i + 2)。
如图, 21+1就是3 21+2就是4 1的子节点是3和4
小根堆。父节点比子节点小
大根堆,父节点比子节点大
给我一个数组,把它调整为小根堆,这个过程称为堆的构建过程,构建过程遵循从右向左,从下到上
每当有元素调整下来时,要对以它为父节点的三角形区域进行调整。
删除堆顶
元素个数和层数有关系, 假如有x层, 元素个数是y, 2^x-1=y,例如有三层,那么就是 8-1=7个元素,相反 层数x=log2^n+1
下面讲几个堆的应用。
堆排序
- 构建堆O(N)。
- 不断地删除堆顶O(NlogN)。
求集合中最大的K个元素 - 用集合的前K个元素构建小根堆。
- 逐一遍历集合的其他元素,如果比堆顶小直接丢弃;否则替换掉堆顶,然后向下调整堆。
把超时的元素从缓存中删除 - 按key的到期时间把key插入小根堆中。随时刷新
- 周期扫描堆顶元素,如果它的到期时间早于当前时刻,则从堆和缓存中删除,然后向下调整堆。 ??golang中的container/heap实现了小根堆,但需要自己定义一个类,实现以下接口:
- Len() int
- Less(i, j int) bool
- Swap(i, j int)
- Push(x interface{})
- Pop() x interface{}
func main() {
testPriorityQueue()
}
type Item struct {
Value string
priority int //优先级,数字越大,优先级越高
}
type PriorityQueue []*Item
func (pq PriorityQueue) Len() int {
return len(pq)
}
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].priority > pq[j].priority //golang默认提供的是小根堆,而优先队列是大根堆,所以这里要反着定义Less
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
//往slice里append,需要传slice指针
func (pq *PriorityQueue) Push(x interface{}) {
item := x.(*Item)
*pq = append(*pq, item)
}
//让slice指向新的子切片,需要传slice指针
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1] //数组最后一个元素
*pq = old[0 : n-1] //去掉最一个元素
return item
}
func testPriorityQueue() {
pq := make(PriorityQueue,0,10) //定义一个数组
pq.Push(&Item{"A",3}) //往数组中添加元素
pq.Push(&Item{"B",9}) //往数组中添加元素
pq.Push(&Item{"C",6}) //往数组中添加元素
heap.Init(&pq) //根据数组中的元素构建堆,这里默认是大根堆
heap.Push(&pq,&Item{"D",5}) //通过heap添加元素
for pq.Len() > 0 {
fmt.Println(heap.Pop(&pq)) //通过heap删除堆顶元素
}
}
4.1 代码总结
4.1.1 用map和链表实现LRU缓存
func main() {
testUrl()
}
type UrlCache struct{
cache map[int]int
lst list.List
Cap int //容量上限
}
//初始化函数
func NewUrlCache(cap int) *UrlCache {
lru := new(UrlCache)
lru.Cap = cap
lru.cache = make(map[int]int,cap)
lru.lst = list.List{}
return lru
}
//开始写代码之前,我们要考虑,缓存我们需要增加和获取
func (url *UrlCache) Add(key,value int) {
//先判断长度是否大于容量cap上限
if len(url.cache) < url.Cap {
url.cache[key] = value
//添加到数组首位
url.lst.PushFront(key)
} else {
//如果长度等于cap的时候,先从缓存中淘汰一个,再把key,value加到缓存中去
//获取末尾最后一个map
back := url.lst.Back()
delete(url.cache,back.Value.(int))
url.lst.Remove(back)
//然后把新增的key,value加到map里面去
url.cache[key] = value
//添加到数组首位
url.lst.PushFront(key)
}
}
func (url *UrlCache) Get(key int) (int, bool) {
value,exists := url.cache[key]
ele := url.Find(key)
if ele != nil {
url.lst.MoveToFront(ele)
}
return value,exists
}
func (url *UrlCache) Find(key int) *list.Element {
if url.lst.Len() == 0 {
return nil
}
head := url.lst.Front()
for {
if head == nil {
break
}
if head.Value.(int) == key {
return head
} else {
head = head.Next()
}
}
return nil
}
func testUrl() {
//初始化一个容量为10的链表
url := NewUrlCache(10)
//for循环添加10个元素
for i := 0;i < 10;i++{
url.Add(i,i) //9 8 7 6 5 4 3 2 1
}
//for循环读取下
for i := 0;i < 10;i += 2{
url.Get(i) // 8 6 4 2 0 9 7 5 3 1
fmt.Printf("Get:%d\n",i)
}
//for循环再插入10个
for i := 10;i < 15;i++{
fmt.Printf("Add %d : %d\n",i,i)
url.Add(i,i) //14 13 12 11 10 8 6 4 2 0 最后5个没了 9 7 5 3 1
}
//for循环再读10个,这次可能有不存在的元素了
for i := 0;i < 10;i ++{
_,exists := url.Get(i)
fmt.Printf("key %d exists %t\n",i,exists) //9 7 5 3 1 不存在了,因为最后5个被删除了
}
}
4.1.2 用map和堆实现超时缓存
package main
import (
"container/heap"
"fmt"
"time"
)
func main() {
testTimeoutCache()
}
type HeapNode struct{
value int //对应map里面的key
deadline int //到期时间戳,精确到秒
}
type Heap []*HeapNode
func (heap Heap) Len() int{
return len(heap)
}
//默认是小根堆,和我们的需求一致
func (heap Heap) Less(i,j int) bool{
return heap[i].deadline < heap[j].deadline
}
//交换i和j的元素
func (heap Heap) Swap(i,j int){
heap[i],heap[j] = heap[j],heap[i]
}
//添加元素
func (heap *Heap) Push(x interface{}){
//node转换成HeapNode类型
node := x.(*HeapNode)
*heap = append(*heap,node)
}
func (heap *Heap) Pop() (x interface{}){
//获取总长度
n :=len(*heap)
//获取最后一个元素
last := (*heap)[n-1]
//重新定义切片,和python一样,是包前不包后的
*heap = (*heap)[0:n-1]
//返回最后一个元素
return last
}
type TimeoutCache struct{
cache map[int]interface{}
hp Heap
}
func NewTimeoutCache(cap int) *TimeoutCache {
tc := new(TimeoutCache)
tc.cache = make(map[int]interface{},cap)
//初始化
tc.hp = Heap{}
heap.Init(&tc.hp) //包装升级,从一个常规的切片升级为堆
return tc
}
func (tc *TimeoutCache) Add(key int,value interface{},life int) {
//直接把key value加入map
tc.cache[key] = value
//计算deadline,然后把key和deadline放入堆
deadline := int(time.Now().Unix()) + life
tc.cache[key] = value
node := &HeapNode{value:key,deadline:deadline}
heap.Push(&tc.hp,node)
fmt.Printf("heap len is %d\n",tc.hp.Len())
}
func (tc TimeoutCache) Get(key int) (interface{},bool) {
value,exists := tc.cache[key]
return value,exists
}
func (tc *TimeoutCache) Taotai() {
for {
if tc.hp.Len() == 0 {
time.Sleep(100 * time.Millisecond)
continue
}
now := int(time.Now().Unix())
top := tc.hp[0]
fmt.Printf("top value is %d\n",top.value)
fmt.Println(top.deadline)
fmt.Println(now)
if top.deadline < now {
fmt.Printf(">>>>>>>>>>>>>>>>>")
heap.Remove(&tc.hp,0) //移除第i个元素
//heap.Pop(&tc.hp)
delete(tc.cache,top.value)
} else { //堆顶还没有到期的
time.Sleep(3 * time.Second) //休息1秒,避免耗尽cpu的一核
fmt.Println("<<<<<<<<<<<<<<<<<<<<<")
}
}
}
func testTimeoutCache() {
tc := NewTimeoutCache(10)
go tc.Taotai() //在子协程里面执行,不影响主进程
tc.Add(1,'1',1)
tc.Add(2,'2',2)
tc.Add(3,'3',3)
time.Sleep(1)
for _,key := range []int{1,2,3} {
_,exists := tc.Get(key)
fmt.Printf("key %d exists %t\n",key,exists)
}
fmt.Printf("head last len is %d",tc.hp.Len())
}