关于用golang实现有序map与lru的一些思考

428 阅读3分钟

连续两天被问到lru算法

第一次被问到的时候又惊又喜 毕竟之前在LeetCode上刷到过

放链接:力扣算法题地址

但是第一次答没完美答出来

导致睡觉的时候lru算法像放电影似的在脑子里放映......😭

这个算法我所了解的使用场景有

  • redis的缓存淘汰策略
  • mysql的buffer pool页面置换算法

重要肯定是相当重要 毕竟在很多重要的组件中都有其身影

这边附上算法实现(golang)版本

package main

/*
 * @lc app=leetcode.cn id=146 lang=golang
 *
 * [146] LRU 缓存机制
 */

// @lc code=start
type LRUCache struct {
	size       int
	capacity   int
	cache      map[int]*DLinkedNode
	head, tail *DLinkedNode
}

type DLinkedNode struct {
	key, value int
	prev, next *DLinkedNode
}

func initDLinkedNode(key, value int) *DLinkedNode {
	return &DLinkedNode{
		key:   key,
		value: value,
	}
}

func Constructor_(capacity int) LRUCache {
	l := LRUCache{
		cache:    map[int]*DLinkedNode{},
		head:     initDLinkedNode(0, 0),
		tail:     initDLinkedNode(0, 0),
		capacity: capacity,
	}

	l.head.next = l.tail
	l.tail.prev = l.head

	return l
}

func (this *LRUCache) Get(key int) int {

	if _, ok := this.cache[key]; !ok {
		return -1
	}

	node := this.cache[key]
	this.moveToHead(node)
	return node.value
}

func (this *LRUCache) Put(key int, value int) {
	if _, ok := this.cache[key]; !ok {
		node := initDLinkedNode(key, value)
		this.cache[key] = node
		this.addToHead(node)
		this.size++
		if this.size > this.capacity {
			removed := this.removeTail()
			delete(this.cache, removed.key)
			this.size--
		}
	} else {
		node := this.cache[key]
		node.value = value
		this.moveToHead(node)
	}
}

func (this *LRUCache) addToHead(node *DLinkedNode) {
	node.prev = this.head
	node.next = this.head.next
	this.head.next.prev = node
	this.head.next = node
}

func (this *LRUCache) removeNode(node *DLinkedNode) {
	node.prev.next = node.next
	node.next.prev = node.prev
}

func (this *LRUCache) moveToHead(node *DLinkedNode) {
	this.removeNode(node)
	this.addToHead(node)
}

func (this *LRUCache) removeTail() *DLinkedNode {
	node := this.tail.prev
	this.removeNode(node)
	return node
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * obj := Constructor(capacity);
 * param_1 := obj.Get(key);
 * obj.Put(key,value);
 */
// @lc code=end

需要解决两个矛盾:如何高效的读(get)与写(write)

高效意味着时间度复杂度为o(1)

需要使得数据结构当自身容量满了的情况下能把最近最不常使用的那个键给剔除掉

核心思想就是

  • 每次读或者写一个键,就相应的把改键往前挪,当容量满且需要新添加元素的时候删除最后那个元素(表示最久不曾使用过),同时将新添加的元素添加至队头

思考:相当于一个数组或者链表,但是由于数组挪动元素不方便,需要搬运大量的元素。所以采用链 表的方式,最新操作的永远放在表头。但是链表有缺点:查找元素很慢。为了解决查找元素慢的痛 点,需要给结构体添加一个map结构,map进行键值定位时间复杂度为o(1)。这样就可以使链表能实 现o(1)时间复杂度的读跟写

关键点来了........
我突然想起了两年前面试中面试官问我: 如果让你去实现一个有序的golang map,你会怎么设计
其实代码可以完全复用,核心思想一模一样!只不过每次在map中insert一个元素,是往队尾插...

总结:map+双向链表

  • 新加元素往队头插 可以实现lru
  • 新加元素往队尾插 实现的就是一个有序的map

我愿称之为黄金组合!
特此发个帖子记录一下

希望能对一些朋友有所启发, 也不知道是不是因为我太菜了才会因为把两个东西联系到一起才这么激动- -