IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 一看就懂的哈希表(下) -> 正文阅读

[数据结构与算法]一看就懂的哈希表(下)

之前在写链表那一篇文章的时候写过基于链表来实现LRU缓存淘汰算法,

这是链接https://mp.csdn.net/mp_blog/creation/editor/120435048

下面来看一看LinkedHashMap中的哈希表和链表是如何组合实现LRU缓存淘汰算法的。

LRU缓存淘汰算法

先来回顾一下基于链表实现的

我们维护一个有序的链表,越靠近链表尾部的结点存储越早访问的数据(头结点存储最新访问的数据,尾结点存储最早访问的数据),并且记录头指针和尾指针,分别指向链表的头结点和尾结点。因为缓存大小有限,当缓存空间不够,需要淘汰数据的时候,我们就直接将链表尾部的结点删除。

当要缓存某个数据的时候,现在链表中查找这个元素,如果没找到,则直接将数据放到链表的头部。如果找到了,就直接放到链表的头部。此时因为要遍历查找数据,时间复杂度就会为O(n)。

概况的说,一个缓存系统主要包含下面几个操作

在缓存中添加一个数据

在缓存中删除一个数据

在缓存中查找一个数据

这三个操作都涉及在链表中查找数据,但是如果适用链表的话,时间复杂度就会为O(n),我们可不可以这样,在原有链表的基础上假设一个哈希表作为索引,哈希表中的每个结点额外存储一个指向有序链表的指针。通过哈希表就能快速查找到需要删除的有序链表的结点。如图。

?下面通过代码来看看如何在缓存中查找,删除,添加一个数据。

package com.example.demo;

import java.util.HashMap;
import java.util.Map;

public class LRUCache {
    private class DLinkedNode{
        public int key;
        public int value;
        public DLinkedNode pre;
        public DLinkedNode next;

        public DLinkedNode(int key,int value){
            this.key = key;
            this.value = value;
        }
    }

    private Map<Integer,DLinkedNode> cache = new HashMap<>();
    private int size;
    private int capacity;
    private DLinkedNode head;
    private DLinkedNode tail;

    public LRUCache(int capacity){
        this.size = 0;
        this.capacity = capacity;
        this.head = new DLinkedNode(-1,-1);
        this.tail = new DLinkedNode(-1,-1);
        this.head.next = tail;
        this.tail.pre = head;
        this.head.pre = null;
        this.tail.next = null;
    }

    private void removeNode(DLinkedNode node){
        node.next.pre = node.pre;
        node.pre.next = node.next;
    }

    private void addNodeAtHead(DLinkedNode node){
        node.next = head.next;
        tail.pre = node;
        head.next = node;
        node.pre = head;
    }

    //查找数据,并移到表头
    public int get(int key){
        if(size == 0){
            return -1;//缓存中没有数据
        }
        //要查找结点
        DLinkedNode dLinkedNode = cache.get(key);
        if(dLinkedNode == null){
            return -1;
        }
        removeNode(dLinkedNode);
        addNodeAtHead(dLinkedNode);
        return dLinkedNode.value;
    }

    //删除
    public void remove(int key){
        DLinkedNode dLinkedNode = cache.get(key);
        if(dLinkedNode != null){
            removeNode(dLinkedNode);
            cache.remove(key);
            return;
        }
    }
    //插入
    public void put(int key,int value){
        DLinkedNode dLinkedNode = cache.get(key);
        //如果数据已经存在,直接移到到表头
        if(dLinkedNode != null){
            dLinkedNode.value = value;
            removeNode(dLinkedNode);
            addNodeAtHead(dLinkedNode);
            return;
        }
        //大小等于哈希表容量时,删除尾结点
        if(size == capacity){
            cache.remove(tail.pre.key);
            removeNode(tail.pre);
            size--;
        }
        DLinkedNode dLinkedNode1 = cache.get(key);
        addNodeAtHead(dLinkedNode1);
        cache.put(key,dLinkedNode1);
        size++;
    }
}

Java LinkedHashMap

前面我们讲了两个散列表和链表结合的例子,现在我们再来看另外一个,Java 中LinkedHashMap 这种容器。
我们之前讲过,HashMap 底层是通过散列表这种数据结构实现的。而 LinkedHashMap 前面比 HashMap 多了一 个“Linked”,这里的“Linked”是不是说,LinkedHashMap 是一个通过链表法解决散列冲突的散列表呢?
实际上,LinkedHashMap 并没有这么简单,其中的“Linked”也并不仅仅代表它是通过链表法解决散列冲突的。
我们先来看一段代码。你觉得这段代码会以什么样的顺序打印 3,1,5,2 这几个 key
呢?原因又是什么呢?
?
1 HashMap<Integer, Integer> m = new LinkedHashMap<>();
2 m.put(3, 11);
3 m.put(1, 12);
4 m.put(5, 23);
5 m.put(2, 22);
6
7 for (Map.Entry e : m.entrySet()) {
8 System.out.println(e.getKey());
9 }
我先告诉你答案,上面的代码会按照数据插入的顺序依次来打印,也就是说,打印的顺序就
是 3,1,5,2。你有没有觉得奇怪?散列表中数据是经过散列函数打乱之后无规律存储
的,这里是如何实现按照数据的插入顺序来遍历打印的呢?
你可能已经猜到了,LinkedHashMap 也是通过散列表和链表组合在一起实现的。实际
上,它不仅支持按照插入顺序遍历数据,还支持按照访问顺序来遍历数据。你可以看下面这 段代码:
?
// 10 是初始大小,0.75 是装载因子,true 是表示按照访问时间排序
2 HashMap<Integer, Integer> m = new LinkedHashMap<>(10, 0.75f, true);
3 m.put(3, 11);
4 m.put(1, 12);
5 m.put(5, 23);
6 m.put(2, 22);
7
8 m.put(3, 26);
9 m.get(5);
10
11 for (Map.Entry e : m.entrySet()) {
12 System.out.println(e.getKey());
13 }
这段代码打印的结果是 1,2,3,5。我来具体分析一下,为什么这段代码会按照这样顺序
来打印。
每次调用 put() 函数,往 LinkedHashMap 中添加数据的时候,都会将数据添加到链表的
尾部,所以,在前四个操作完成之后,链表中的数据是下面这样:

?

在第 8 行代码中,再次将键值为 3 的数据放入到 LinkedHashMap 的时候,会先查找这个
键值是否已经有了,然后,再将已经存在的 (3,11) 删除,并且将新的 (3,26) 放到链表的尾
部。所以,这个时候链表中的数据就是下面这样:

?

当第 9 行代码访问到 key 为 5 的数据的时候,我们将被访问到的数据移动到链表的尾部。
所以,第 9 行代码之后,链表中的数据是下面这样:

?

所以,最后打印出来的数据是 1,2,3,5。从上面的分析,你有没有发现,按照访问时间
排序的 LinkedHashMap 本身就是一个支持 LRU 缓存淘汰策略的缓存系统?实际上,它们
两个的实现原理也是一模一样的。我也就不再啰嗦了。
我现在来总结一下,实际上, LinkedHashMap 是通过双向链表和散列表这两种数据结构
组合实现的。LinkedHashMap 中的“Linked”实际上是指的是双向链表,并非指用链表
法解决散列冲突
部分引用《数据结构与算法之美》
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-10-03 17:18:25  更:2021-10-03 17:20:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 12:12:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码