1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        冷淡的面試官,讓我手寫(xiě)LRU緩存淘汰算法打發(fā)時(shí)間!

        共 17557字,需瀏覽 36分鐘

         ·

        2021-03-01 16:42

        背景

        在我們這個(gè)日益追求高效的世界,我們對(duì)任何事情的等待都顯得十分的浮躁,網(wǎng)頁(yè)頁(yè)面刷新不出來(lái),好煩,電腦打開(kāi)運(yùn)行程序慢,又是好煩!那怎么辦,技術(shù)的產(chǎn)生不就是我們所服務(wù)么,今天我們就聊一聊緩存這個(gè)技術(shù),并使用我們熟知的數(shù)據(jù)結(jié)構(gòu)--用鏈表實(shí)現(xiàn)LRU緩存淘汰算法

        在學(xué)習(xí)如何使用鏈表實(shí)現(xiàn)LRU緩存淘汰算法前,我們先提出幾個(gè)問(wèn)題,大家好好思考下,問(wèn)題如下:

        • 什么是緩存,緩存的作用?
        • 緩存的淘汰策略有哪些?
        • 如何使用鏈表實(shí)現(xiàn)LRU緩存淘汰算法,有什么特點(diǎn),如何優(yōu)化?

        好了,我們帶著上面的問(wèn)題來(lái)學(xué)進(jìn)行下面的學(xué)習(xí)。

        1、什么是緩存,緩存的作用是什么?

        緩存可以簡(jiǎn)單的理解為保存數(shù)據(jù)的一個(gè)副本,以便于后續(xù)能夠快速的進(jìn)行訪(fǎng)問(wèn)。以計(jì)算機(jī)的使用場(chǎng)景為例,當(dāng)cpu要訪(fǎng)問(wèn)內(nèi)存中的一條數(shù)據(jù)時(shí),它會(huì)先在緩存里查找,如果能夠找到則直接使用,如果沒(méi)找到,則需要去內(nèi)存里查找;

        同樣的,在數(shù)據(jù)庫(kù)的訪(fǎng)問(wèn)場(chǎng)景中,當(dāng)項(xiàng)目系統(tǒng)需要查詢(xún)數(shù)據(jù)庫(kù)中的某條數(shù)據(jù)時(shí),可以先讓請(qǐng)求查詢(xún)緩存,如果命中,就直接返回緩存的結(jié)果,如果沒(méi)有命中,就查詢(xún)數(shù)據(jù)庫(kù), 并將查詢(xún)結(jié)果放入緩存,下次請(qǐng)求時(shí)查詢(xún)緩存命中,直接返回結(jié)果,就不用再次查詢(xún)數(shù)據(jù)庫(kù)。

        通過(guò)以上兩個(gè)例子,我們發(fā)現(xiàn)無(wú)論在哪種場(chǎng)景下,都存在這樣一個(gè)順序:先緩存,后內(nèi)存;先緩存,后數(shù)據(jù)庫(kù)。但是緩存的存在也占用了一部分內(nèi)存空間,所以緩存是典型的以空間換時(shí)間,犧牲數(shù)據(jù)的實(shí)時(shí)性,卻滿(mǎn)足計(jì)算機(jī)運(yùn)行的高效性

        仔細(xì)想一下,我們?nèi)粘i_(kāi)發(fā)中遇到緩存的例子還挺多的。

        • 操作系統(tǒng)的緩存

        減少與磁盤(pán)的交互

        • 數(shù)據(jù)庫(kù)緩存

        減少對(duì)數(shù)據(jù)庫(kù)的查詢(xún)

        • Web服務(wù)器緩存

        減少對(duì)應(yīng)用服務(wù)器的請(qǐng)求

        • 客戶(hù)瀏覽器的緩存

        減少對(duì)網(wǎng)站的訪(fǎng)問(wèn)

        2、緩存有哪些淘汰策略?

        緩存的本質(zhì)是以空間換時(shí)間,那么緩存的容量大小肯定是有限的,當(dāng)緩存被占滿(mǎn)時(shí),緩存中的那些數(shù)據(jù)應(yīng)該被清理出去,那些數(shù)據(jù)應(yīng)該被保留呢?這就需要緩存的淘汰策略來(lái)決定。

        事實(shí)上,常用的緩存的淘汰策略有三種:先進(jìn)先出算法(First in First out FIFO);淘汰一定時(shí)期內(nèi)被訪(fǎng)問(wèn)次數(shù)最少的頁(yè)面(Least Frequently Used LFU);淘汰最長(zhǎng)時(shí)間未被使用的頁(yè)面(Least Recently Used LRU)

        這些算法在不同層次的緩存上執(zhí)行時(shí)具有不同的效率,需要結(jié)合具體的場(chǎng)景來(lái)選擇。

        2.1 FIFO算法

        FIFO算法即先進(jìn)先出算法,常采用隊(duì)列實(shí)現(xiàn)。在緩存中,它的設(shè)計(jì)原則是:如果一個(gè)數(shù)據(jù)最先進(jìn)入緩存中,則應(yīng)該最早淘汰掉

        FIFO算法
        • 新訪(fǎng)問(wèn)的數(shù)據(jù)插入FIFO隊(duì)列的尾部,隊(duì)列中數(shù)據(jù)由隊(duì)到隊(duì)頭按順序順序移動(dòng)
        • 隊(duì)列滿(mǎn)時(shí),刪除隊(duì)頭的數(shù)據(jù)

        2.2 LRU算法

        LRU算法是根據(jù)對(duì)數(shù)據(jù)的歷史訪(fǎng)問(wèn)次數(shù)來(lái)進(jìn)行淘汰數(shù)據(jù)的,通常使用鏈表來(lái)實(shí)現(xiàn)。在緩存中,它的設(shè)計(jì)原則是:如果數(shù)據(jù)最近被訪(fǎng)問(wèn)過(guò),那么將來(lái)它被訪(fǎng)問(wèn)的幾率也很高。

        LRU算法
        • 新加入的數(shù)據(jù)插入到鏈表的頭部
        • 每當(dāng)緩存命中時(shí)(即緩存數(shù)據(jù)被訪(fǎng)問(wèn)),則將數(shù)據(jù)移到鏈表頭部
        • 當(dāng)來(lái)鏈表已滿(mǎn)的時(shí)候,將鏈表尾部的數(shù)據(jù)丟棄

        2.3 LFU算法

        LFU算法是根據(jù)數(shù)據(jù)的歷史訪(fǎng)問(wèn)頻率來(lái)淘汰數(shù)據(jù),因此,LFU算法中的每個(gè)數(shù)據(jù)塊都有一個(gè)引用計(jì)數(shù),所有數(shù)據(jù)塊按照引用計(jì)數(shù)排序,具有相同引用計(jì)數(shù)的數(shù)據(jù)塊則按照時(shí)間排序。在緩存中,它的設(shè)計(jì)原則是:如果數(shù)據(jù)被訪(fǎng)問(wèn)多次,那么將來(lái)它的訪(fǎng)問(wèn)頻率也會(huì)更高。

        LFU算法
        • 新加入數(shù)據(jù)插入到隊(duì)列尾部(引用計(jì)數(shù)為1;
        • 隊(duì)列中的數(shù)據(jù)被訪(fǎng)問(wèn)后,引用計(jì)數(shù)增加,隊(duì)列重新排序;
        • 當(dāng)需要淘汰數(shù)據(jù)時(shí),將已經(jīng)排序的列表最后的數(shù)據(jù)塊刪除。

        3、如何使用鏈表實(shí)現(xiàn)緩存淘汰,有什么特點(diǎn),如何優(yōu)化?

        在上面的文章中我們理解了緩存的概念淘汰策略,其中LRU算法是筆試/面試中考察比較頻繁的,我秋招的時(shí)候,很多公司都讓我手寫(xiě)了這個(gè)算法,為了避免大家采坑,下面,我們就手寫(xiě)一個(gè)LRU緩存淘汰算法。

        我們都知道鏈表的形式不止一種,我們應(yīng)該選擇哪一種呢?

        思考三分鐘........

        好了,公布答案!

        事實(shí)上,鏈表按照不同的連接結(jié)構(gòu)可以劃分為單鏈表、循環(huán)鏈表雙向鏈表。

        • 單鏈表
        • 每個(gè)節(jié)點(diǎn)只包含一個(gè)指針,即后繼指針。
        • 單鏈表有兩個(gè)特殊的節(jié)點(diǎn),即首節(jié)點(diǎn)和尾節(jié)點(diǎn),用首節(jié)點(diǎn)地址表示整條鏈表,尾節(jié)點(diǎn)的后繼指針指向空地址null。
        • 性能特點(diǎn):插入和刪除節(jié)點(diǎn)的時(shí)間復(fù)雜度為O(1),查找的時(shí)間復(fù)雜度為O(n)。
        • 循環(huán)鏈表
        • 除了尾節(jié)點(diǎn)的后繼指針指向首節(jié)點(diǎn)的地址外均與單鏈表一致。
        • 適用于存儲(chǔ)有循環(huán)特點(diǎn)的數(shù)據(jù),比如約瑟夫問(wèn)題。
        • 雙向鏈表
        • 節(jié)點(diǎn)除了存儲(chǔ)數(shù)據(jù)外,還有兩個(gè)指針?lè)謩e指向前一個(gè)節(jié)點(diǎn)地址(前驅(qū)指針prev)和下一個(gè)節(jié)點(diǎn)地址(后繼指針next)

        • 首節(jié)點(diǎn)的前驅(qū)指針prev和尾節(jié)點(diǎn)的后繼指針均指向空地址。

        雙向鏈表相較于單鏈表的一大優(yōu)勢(shì)在于:找到前驅(qū)節(jié)點(diǎn)的時(shí)間復(fù)雜度為O(1),而單鏈表只能從頭節(jié)點(diǎn)慢慢往下找,所以仍然是O(n).而且,對(duì)于插入和刪除也是有優(yōu)化的。

        我們可能會(huì)有問(wèn)題:?jiǎn)捂湵淼牟迦雱h除不是O(1)嗎?

        是的,但是一般情況下,我們想要進(jìn)行插入刪除操作,很多時(shí)候還是得先進(jìn)行查找,再插入或者刪除,可見(jiàn)其實(shí)是先O(n),再O(1)。

        不熟悉鏈表解題的同學(xué)可以先看看我的上一篇算法解析文章刷了LeetCode鏈表專(zhuān)題,我發(fā)現(xiàn)了一個(gè)秘密

        因?yàn)槲覀冃枰獎(jiǎng)h除操作,刪除一個(gè)節(jié)點(diǎn)不僅要得到該節(jié)點(diǎn)本身的指針,也需要操作其它前驅(qū)節(jié)點(diǎn)的指針,而雙向鏈表能夠直接找到前驅(qū),保證了操作時(shí)間復(fù)雜度為O(1),因此使用雙向鏈表作為實(shí)現(xiàn)LRU緩存淘汰算法的結(jié)構(gòu)會(huì)更高效。

        算法思路

        維護(hù)一個(gè)雙向鏈表,保存所有緩存的值,并且最老的值放在鏈表最后面。

        • 當(dāng)訪(fǎng)問(wèn)的值在鏈表中時(shí):將找到鏈表中值將其刪除,并重新在鏈表頭添加該值(保證鏈表中 數(shù)值的順序是從新到舊)
        • 當(dāng)訪(fǎng)問(wèn)的值不在鏈表中時(shí):當(dāng)鏈表已滿(mǎn):刪除鏈表最后一個(gè)值,將要添加的值放在鏈表頭 當(dāng)鏈表未滿(mǎn):直接在鏈表頭添加

        3.1 LRU緩存淘汰算法

        極客時(shí)間王爭(zhēng)的《數(shù)據(jù)結(jié)構(gòu)與算法之美》給出了一個(gè)使用有序單鏈表實(shí)現(xiàn)LRU緩存淘汰算法,代碼如下:

        public class LRUBaseLinkedList<T{

            /**
             * 默認(rèn)鏈表容量
             */

            private final static Integer DEFAULT_CAPACITY = 10;

            /**
             * 頭結(jié)點(diǎn)
             */

            private SNode<T> headNode;

            /**
             * 鏈表長(zhǎng)度
             */

            private Integer length;

            /**
             * 鏈表容量
             */

            private Integer capacity;

            public LRUBaseLinkedList() {
                this.headNode = new SNode<>();
                this.capacity = DEFAULT_CAPACITY;
                this.length = 0;
            }

            public LRUBaseLinkedList(Integer capacity) {
                this.headNode = new SNode<>();
                this.capacity = capacity;
                this.length = 0;
            }

            public void add(T data) {
                SNode preNode = findPreNode(data);

                // 鏈表中存在,刪除原數(shù)據(jù),再插入到鏈表的頭部
                if (preNode != null) {
                    deleteElemOptim(preNode);
                    intsertElemAtBegin(data);
                } else {
                    if (length >= this.capacity) {
                        //刪除尾結(jié)點(diǎn)
                        deleteElemAtEnd();
                    }
                    intsertElemAtBegin(data);
                }
            }

            /**
             * 刪除preNode結(jié)點(diǎn)下一個(gè)元素
             *
             * @param preNode
             */

            private void deleteElemOptim(SNode preNode) {
                SNode temp = preNode.getNext();
                preNode.setNext(temp.getNext());
                temp = null;
                length--;
            }

            /**
             * 鏈表頭部插入節(jié)點(diǎn)
             *
             * @param data
             */

            private void intsertElemAtBegin(T data) {
                SNode next = headNode.getNext();
                headNode.setNext(new SNode(data, next));
                length++;
            }

            /**
             * 獲取查找到元素的前一個(gè)結(jié)點(diǎn)
             *
             * @param data
             * @return
             */

            private SNode findPreNode(T data) {
                SNode node = headNode;
                while (node.getNext() != null) {
                    if (data.equals(node.getNext().getElement())) {
                        return node;
                    }
                    node = node.getNext();
                }
                return null;
            }

            /**
             * 刪除尾結(jié)點(diǎn)
             */

            private void deleteElemAtEnd() {
                SNode ptr = headNode;
                // 空鏈表直接返回
                if (ptr.getNext() == null) {
                    return;
                }

                // 倒數(shù)第二個(gè)結(jié)點(diǎn)
                while (ptr.getNext().getNext() != null) {
                    ptr = ptr.getNext();
                }

                SNode tmp = ptr.getNext();
                ptr.setNext(null);
                tmp = null;
                length--;
            }

            private void printAll() {
                SNode node = headNode.getNext();
                while (node != null) {
                    System.out.print(node.getElement() + ",");
                    node = node.getNext();
                }
                System.out.println();
            }

            public class SNode<T{

                private T element;

                private SNode next;

                public SNode(T element) {
                    this.element = element;
                }

                public SNode(T element, SNode next) {
                    this.element = element;
                    this.next = next;
                }

                public SNode() {
                    this.next = null;
                }

                public T getElement() {
                    return element;
                }

                public void setElement(T element) {
                    this.element = element;
                }

                public SNode getNext() {
                    return next;
                }

                public void setNext(SNode next) {
                    this.next = next;
                }
            }

            public static void main(String[] args) {
                LRUBaseLinkedList list = new LRUBaseLinkedList();
                Scanner sc = new Scanner(System.in);
                while (true) {
                    list.add(sc.nextInt());
                    list.printAll();
                }
            }
        }

        這段代碼不管緩存有沒(méi)有滿(mǎn),都需要遍歷一遍鏈表,所以這種基于鏈表的實(shí)現(xiàn)思路,緩存訪(fǎng)問(wèn)的時(shí)間復(fù)雜度為 O(n)。

        3.2使用哈希表優(yōu)化LRU

        事實(shí)上,這個(gè)思路還可以繼續(xù)優(yōu)化,我們可以把單鏈表?yè)Q成雙向鏈表,并引入散列表。

        • 雙向鏈表支持查找前驅(qū),保證操作的時(shí)間復(fù)雜度為O(1)
        • 引入散列表記錄每個(gè)數(shù)據(jù)的位置,將緩存訪(fǎng)問(wèn)的時(shí)間復(fù)雜度降到O(1)

        哈希表查找較快,但是數(shù)據(jù)無(wú)固定的順序;鏈表倒是有順序之分。插入、刪除較快,但是查找較慢。將它們結(jié)合,就可以形成一種新的數(shù)據(jù)結(jié)構(gòu)--哈希鏈表(LinkedHashMap)

        哈希表+雙向鏈表

        力扣上146題-LRU緩存機(jī)制剛好可以拿來(lái)練手,題圖如下:

        題目:

        運(yùn)用你所掌握的數(shù)據(jù)結(jié)構(gòu),設(shè)計(jì)和實(shí)現(xiàn)一個(gè)  LRU (最近最少使用) 緩存機(jī)制 。

        • 實(shí)現(xiàn) LRUCache 類(lèi):

        LRUCache(int capacity) 以正整數(shù)作為容量 capacity 初始化 LRU 緩存 

        int get(int key) 如果關(guān)鍵字 key 存在于緩存中,則返回關(guān)鍵字的值,否則返回 -1 。

        void put(int key, int value) 如果關(guān)鍵字已經(jīng)存在,則變更其數(shù)據(jù)值;如果關(guān)鍵字不存在,則插入該組「關(guān)鍵字-值」。當(dāng)緩存容量達(dá)到上限時(shí),它應(yīng)該在寫(xiě)入新數(shù)據(jù)之前刪除最久未使用的數(shù)據(jù)值,從而為新的數(shù)據(jù)值留出空間。

        思路:

        我們的思路就是哈希表+雙向鏈表

        • 哈希表用于滿(mǎn)足題目時(shí)間復(fù)雜度O(1)的要求,雙向鏈表用于存儲(chǔ)順序
        • 哈希表鍵值類(lèi)型:<Integer, ListNode>,哈希表的鍵用于存儲(chǔ)輸入的key,哈希表的值用于存儲(chǔ)雙向鏈表的節(jié)點(diǎn)
        • 雙向鏈表的節(jié)點(diǎn)中除了value外還需要包含key,因?yàn)樵趧h除最久未使用的數(shù)據(jù)時(shí),需要通過(guò)鏈表來(lái)定位hashmap中應(yīng)當(dāng)刪除的鍵值對(duì)
        • 一些操作:雙向鏈表中,在后面的節(jié)點(diǎn)表示被最近訪(fǎng)問(wèn)
        • 新加入的節(jié)點(diǎn)放在鏈表末尾,addNodeToLast(node)
        • 若容量達(dá)到上限,去除最久未使用的數(shù)據(jù),removeNode(head.next)
        • 若數(shù)據(jù)新被訪(fǎng)問(wèn)過(guò),比如被get了或被put了新值,把該節(jié)點(diǎn)挪到鏈表末尾,moveNodeToLast(node)
        • 為了操作的方便,在雙向鏈表頭和尾分別定義一個(gè)head和tail節(jié)點(diǎn)。

        代碼

        class LRUCache {
            private int capacity;
            private HashMap<Integer, ListNode> hashmap; 
            private ListNode head;
            private ListNode tail;

            private class ListNode{
                int key;
                int val;
                ListNode prev;
                ListNode next;
                public ListNode(){  
                }
                public ListNode(int key, int val){
                    this.key = key;
                    this.val = val;
                }
            }

            public LRUCache(int capacity) {
                this.capacity = capacity;
                hashmap = new HashMap<>();
                head = new ListNode();
                tail = new ListNode();
                head.next = tail;
                tail.prev = head;
            }

            private void removeNode(ListNode node){
                node.prev.next = node.next;
                node.next.prev = node.prev;
            }

            private void addNodeToLast(ListNode node){
                node.prev = tail.prev;
                node.prev.next = node;
                node.next = tail;
                tail.prev= node;
            }

            private void moveNodeToLast(ListNode node){
                removeNode(node);
                addNodeToLast(node);
            }
            
            public int get(int key) {   
                if(hashmap.containsKey(key)){
                    ListNode node = hashmap.get(key);
                    moveNodeToLast(node);
                    return node.val;
                }else{
                    return -1;
                }
            }
            
            public void put(int key, int value) {
                if(hashmap.containsKey(key)){
                    ListNode node = hashmap.get(key);
                    node.val = value;
                    moveNodeToLast(node);
                    return;
                }
                if(hashmap.size() == capacity){
                    hashmap.remove(head.next.key);
                    removeNode(head.next);
                }

                ListNode node = new ListNode(key, value);
                hashmap.put(key, node);
                addNodeToLast(node);
            }
        }

        巨人的肩旁:

        [1]數(shù)據(jù)結(jié)構(gòu)與算法之美-王爭(zhēng)

        [2]力扣-LRU緩存機(jī)制(146題)

        [3]https://blog.csdn.net/yangpl_tale/article/details/44998423

        [4]https://leetcode-cn.com/problems/lru-cache/solution/146-lru-huan-cun-ji-zhi-ha-xi-biao-shuan-l3um/


        瀏覽 40
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产精品入口夜色视频大尺度 | 91搞黄 | 全网可操逼 | 国产成人视频在线观看 | 色老太老太色HD | 日韩一区二区三区在线观看 | 免费三级黄色 | 国产精品福利在线播放 | 痞子gay大猛一xnxx1 | 日批免费视频 |