【数据结构】LRU的两种实现--链表和哈希表

1.单链表(O(n))

单向链表,从head出,tail进入(理解成队列,左进(tail)右出(head)。

* 查找:
  1.查找:遍历得到这个数据对应的结点
  2.移动:将其从原来的位置删除,然后再插入到链表的尾部
* 添加:查找 + (移动/删除)
  - 缓存中已存在:移动到队尾
  - 缓存中不存在:
    - 缓存未满,则将此结点直接插入到链表的尾部
    - 缓存已满,则链表**头结点删除**,将新的数据结点插入链表的尾部
* 删除:
  1.查找:遍历找到要删除节点
  2.删除:用前驱节点删除

这三个操作都要涉及“查找”操作。所以,如果单纯地采用链表的话,时间复杂度只能是 O(n)
PS:关于LinkedList的源码分析可以参考 【Java容器源码】LinkedList源码分析

// 直接继承Java的LinkedList,就不用再自己实现链表
public class LinkedLRU extends LinkedList<String> {
	
    private int capcity;

    public LinkedLRU (int capcity) {
        this.capcity = capcity;
    }
    
    @Override
    public boolean add(String str) {
    	// 如果队列中包含要添加元素
    	if (contains(str)) {
    		// 删除,然后再插入到队尾
            remove(str);
        // 如果队列中不包含要添加元素
        } else {
        	// 如果队列已经满乐,就将元素队首删除
            if (size() == capcity) {
                removeFirst();
            }
        }
        // 尾插
        return super.add(str);
    }
	
    @Override
    public String get(int index) {
    	// 查出结果
        String item = super.get(index);
        // 将当前元素删除
        remove(index);
        // 然后再插入到尾部
        add(item);
        return item;
    }
}

测试如下:

public class TestLRU {

    public static void main(String[] args) {
    	// 设置容量大小为3,并加入3个元素
        LinkedLRU lru = new LinkedLRU(3);
        lru.add("a");
        lru.add("b");
        lru.add("c");
        System.out.println("还未做任何操作:" + lru);
		// 访问其中一个元素
		// 按照lru的逻辑,被访问的元素要被挪动到队尾
        lru.get(0);
        System.out.println("访问已存在元素(a):" + lru);
        // 添加一个缓存中已经有了的元素
        // 按照lru的逻辑,该元素会被挪到队尾
        lru.add("b");
        System.out.println("添加一个已存在的元素(b):" + lru);
		// 添加一个缓存没有的元素,但此时队列已经满了
		// 按照lru的逻辑,队首的元素会被删除
        lru.add("d");
        System.out.println("超过容量添加一个元素:" + lru);
    }
}

结果如下:
【数据结构】LRU的两种实现--链表和哈希表

2.散列表 + 双向链表

【数据结构】LRU的两种实现--链表和哈希表

* 查找:
  1.查找:散列表中查找数据的时间复杂度接近 O(1),所以通过散列表,我们可以很快地在缓存中找到一个数据。
  2.移动:当找到数据之后,我们还需要将它移动到双向链表的尾部。
* 添加:找 + (移动/删除)
  - 缓存中存在:将此节点移动到双向链表的尾部;
  - 缓存中不存在:
    - 缓存已满:将双向链表头部的结点删除,然后再将数据放到链表的尾部;
    - 缓存未满:直接将数据新添到链表的尾部。
* 删除:
  1.查找:借助散列表,我们可以在 O(1) 时间复杂度里找到要删除的结点。
  2.删除:因为我们的链表是双向链表, 双向链表可以通过前驱指针 O(1) 时间复杂度获取前驱结点,所以在双向链表中,删除结点 只需要 O(1) 的时间复杂度。

这整个过程涉及的查找操作都可以通过散列表来完成。所以其他的操作,比如删除头结点、链表尾部插入数据等,都可以在 O(1) 的时间复杂度内完成。所以,这三个操作的时间复杂度都 是 O(1)
PS:关于LinkedHashMap源码可以参考 【Java容器源码】LinkedHashMap源码分析

// 通过LInkedListMap来实现(Map + List)
class LRUCache extends LinkedHashMap<Integer, Integer>{
    private int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        /*
        	if ((v = get(k)) == null)  return -1;
        	else return v;
        */
        // 没找到就返回默认
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
    	// put方法实际上是调用HashMap的,但是LinkedHashMap重写了afterNodeAccess方法,所以无论要添加元素是否存在,都会被移到队尾
        super.put(key, value);
    }
	
    // 重写removeEldestEntry来定义什么时候进行删除操作
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity; 
    }
}

至此,实现了一个高效的、支持 LRU 缓存淘汰算法的缓存系统原型。

上一篇:操作系统页面置换算法(最佳置换算法,FIFO,LRU,Clock)


下一篇:java之LRU缓存实现