LinkedHashMap实现LRU算法--高频面试题

LinkedHashMap实现LRU算法–高频面试题

至于LRU算法是什么,相信大家都已经有所了解了,那么在这里就不作描述了。

一、什么是LinkedHashMap?

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

不难从源码中看出,实现了Map接口,是HashMap的子类。LinkedHashMap 相对于 HashMap 的源码比,是很简单的。它继承了 HashMap,仅重写了几个方法,以改变它迭代遍历时的顺序。

整体结构

直接上图,直观的了解一下它的结构:简单来看就是使用双向链表将hashmap中的节点根据插入顺序进行首尾连接。但是后面插入和查找数据的时候会将顺序进行细微的改动。后面会提到。

结构图

构造方法

 public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    
 public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
    
 public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    
  public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    
  public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

基本和hashmap没有什么太大差别。其中的accessOrder是什么呢?

/**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

使用了一个boolean类型值来控制迭代时的节点顺序。当accessOrder设置为false时,会按照插入顺序进行排序,当accessOrder为true是,会按照访问顺序,也就是插入还是访问时会将当前节点放置到尾部,尾部代表的是最近访问的数据。注意:JDK1.6是反过来的,jdk1.6头部是最近访问的。

Node

LinkedHashMap 的节点 Entry<K,V> 继承自 HashMap.Node<K,V>

    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

这里的before“指向”上一个Node节点,after“指向”下一个Node节点。

新增部分

/**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

不难理解就是一个是整个双向链表的头节点,另一个是整个双向链表的尾节点。

新增方法

重写了HashMap中的构建新节点的方法:newNode(); 在每次插入数据时都会构建新节点,通过linkNodeLast(LinkedHashMap.Entry<K,V> p) 将新节点链接在内部双向链表的尾部。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

// link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

查找方法

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;   //注意!!!
        }
    }

LinkedHashMap的改动是:在accessOrder为true时,会去调用void afterNodeAccess(Node<K,V> e),将这个节点移动到整个双向链表的末尾。

注意:调用void afterNodeAccess(Node<K,V> e)后,最后会修改modCount的值。这会导致:如果在将整个双向链表进行迭代输出的时候去查询节点数据的话(执行void afterNodeAccess(Node<K,V> e)),就会触发 fail-fast 机制。也就是快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。顺带一提ArrayList在使用迭代器遍历时如果使用remove删除元素,也会触发fail-fast机制。

删除元素

对remove(); 并没有做改动。但是重写了:

void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

也就是说,删除节点的话还会把结点从双向链表删除。也就是将删掉节点头尾节点进行连接。

补充

  • 对于containsValue(),因为双向链表的缘故,只需要遍历双向链表去判断value是否相等就好了。而原来的hashmap中则需要进行两次遍历。
//hashmap
public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {                   //遍历Node数组,tab所存储元素为每个链表的第一个元素
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {  //循环链表,判断值是否相同
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

//LinkedHashMap
public boolean containsValue(Object value) {
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {  //双向链表遍历
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }
  • 双链表节点的顺序在 LinkedHashMap 的增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出。

二、使用LinkedHashMap实现LRU算法

public static int[] LRU (int[][] operators, int k) {
        // write code here
    	//注意初始化的时候需要定义好是否需要按照访问顺序来进行,如果这里默认是false就不会将get操作的值放至双向链表末尾
        LinkedHashMap<Integer,Integer> linkedHashMap = new LinkedHashMap(k,0.75f,true);
        ArrayList<Integer> list = new ArrayList<>();
        int flag = 0;
        //开始读取操作
        for (int[] operator : operators) {
            //如果是set操作
            if (operator[0] == 1){
                //优先判断是否超出预定大小
                if(linkedHashMap.size() >= k){
                    //获取双向链表中的第一个entry
                    Map.Entry<Integer, Integer> first = linkedHashMap.entrySet().iterator().next();
                    System.out.println(first.getKey()+" "+first.getValue());
                    //将这个entry删除
                    linkedHashMap.remove(first.getKey());
                }
                //最后加入值
                linkedHashMap.put(operator[1],operator[2]);
            }
            //如果是get操作
            if (operator[0] == 2){
                //判断是否存在
                if (linkedHashMap.containsKey(operator[1])){
        		   //存在值就存入到list中
                    list.add(linkedHashMap.get(operator[1]));
                }else{
                    //不存在就存-1到list中
                    list.add(-1);
                }
            }
            //为了更加直观看出整个过程,在每次操作一次后都会遍历一边。正式环境中要删除,因为会导致超时
            for(Map.Entry<Integer, Integer> entry : linkedHashMap.entrySet()) {
                System.out.println("key:" + entry.getKey() + "   value:" + entry.getValue());
            }
            System.out.println("=============================");
        }
    	//将其转换成数组格式,一开始定义数组如果有空间没有用到会是0值
        int[] array = new int[list.size()];
        for (Integer item : list) {
            array[flag++] = item;
        }
        return array;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值