算法進階面試題06——實現LFU緩存算法、計算帶括號的公式、介紹和實現跳錶結構

接着第四課的內容,主要講LFU、表達式計算和跳錶node

 

第一題

上一題實現了LRU緩存算法,LFU也是一個著名的緩存算法面試

自行了解以後實現LFU中的set 和 get算法

要求:兩個方法的時間複雜度都爲O(1)數組

 

LFU根據get、set操做次數決定的優先級。緩存

 

 

一樣次數,最不常常訪問的先出去。app

實現思路:創建一個次數鏈,每一個次數再連接上一個雙向鏈。(兩個雙鏈表less

 

 

Put和Get的時候,先檢查是否存在dom

若是沒有,put就存在1的鏈表下,get就返回null。函數

若是有,找到屬於哪一個頭,而後分離出來,查看頭部的下一個是否次數+1的關係,有就插入,沒有就建出來。每次掛都是掛在小鏈表的頭部。測試

size滿了,就刪最左邊底下的head。

看代碼...(代碼是把最近操做的放在頭部)

  

public class Code_03_LFU {
    //小鏈表的節點
    public static class Node {
        public Integer key;
        public Integer value;
        public Integer times;
        public Node up;
        public Node down;

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

    public static class LFUCache {

        //把次數相同的節點連在一塊兒的鏈表
        public static class NodeList {
            //本鏈的頭尾
            public Node head;
            public Node tail;
            //前一個和後一個
            public NodeList last;
            public NodeList next;

            public NodeList(Node node) {
                head = node;
                tail = node;
            }

            public void addNodeFromHead(Node newHead) {
                newHead.down = head;
                head.up = newHead;
                head = newHead;
            }

            public boolean isEmpty() {
                return head == null;
            }

            //其中的任何節點均可能刪,由於次數增長也要調整節點位置
            //把節點從本環境中分離
            public void deleteNode(Node node) {
                if (head == tail) {
                    head = null;
                    tail = null;
                } else {
                    if (node == head) {//
                        head = node.down;
                        head.up = null;
                    } else if (node == tail) {//
                        tail = node.up;
                        tail.down = null;
                    } else {//其中
                        node.up.down = node.down;
                        node.down.up = node.up;
                    }
                }
                node.up = null;
                node.down = null;
            }
        }

        private int capacity;//容量
        private int size;//當前大小
        //經過key找Node
        private HashMap<Integer, Node> records;
        //找到Node的當前所在鏈表
        private HashMap<Node, NodeList> heads;
        private NodeList headList;

        public LFUCache(int capacity) {
            this.capacity = capacity;
            this.size = 0;
            this.records = new HashMap<>();
            this.heads = new HashMap<>();
            headList = null;
        }

        public void set(int key, int value) {
            if (records.containsKey(key)) {//存在
                Node node = records.get(key);
                node.value = value;
                node.times++;
                NodeList curNodeList = heads.get(node);
                move(node, curNodeList);//幫node找新家
            } else {
                if (size == capacity) {//騰出空間
                    Node node = headList.tail;
                    headList.deleteNode(node);
                    modifyHeadList(headList);//檢查是否要調整(有可能刪光了)
                    records.remove(node.key);
                    heads.remove(node);
                    size--;
                }
                Node node = new Node(key, value, 1);
                if (headList == null) {//第一次加
                    headList = new NodeList(node);
                } else {
                    //檢查是否存在專屬的次數鏈表
                    if (headList.head.times.equals(node.times)) {
                        headList.addNodeFromHead(node);
                    } else {//沒有就建
                        NodeList newList = new NodeList(node);
                        newList.next = headList;
                        headList.last = newList;
                        headList = newList;
                    }
                }
                //記錄信息
                records.put(key, node);
                heads.put(node, headList);
                size++;
            }
        }

        private void move(Node node, NodeList oldNodeList) {
            oldNodeList.deleteNode(node);//先從老家搬出
            //搬出後老家有可能由於無住戶而被拆除,因此前指向要判斷下
            NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.last
                    : oldNodeList;
            NodeList nextList = oldNodeList.next;
            if (nextList == null) {//新家不存在,建一個
                NodeList newList = new NodeList(node);
                if (preList != null) {//老家還在
                    preList.next = newList;
                }
                newList.last = preList;
                //應對1---3的狀況,刪了1,後面沒2,天然新建的2會變爲新頭
                //   ^a---^b
                if (headList == null) {
                    headList = newList;
                }
                heads.put(node, newList);//加入記錄
            } else {
                //新家合適,是老家+1的配套
                if (nextList.head.times.equals(node.times)) {
                    nextList.addNodeFromHead(node);
                    heads.put(node, nextList);
                } else {//新家不合適,建一個
                    NodeList newList = new NodeList(node);
                    if (preList != null) {//保證前一個節點不爲空,否則下面代碼報錯
                        preList.next = newList;
                    }
                    newList.last = preList;
                    newList.next = nextList;
                    nextList.last = newList;
                    if (headList == nextList) {//判斷頭鏈表是否有變化
                        headList = newList;
                    }
                    heads.put(node, newList);//加入記錄
                }
            }
        }

        // return whether delete this head
        private boolean modifyHeadList(NodeList nodeList) {
            if (nodeList.isEmpty()) {//若是房子沒有住戶,要進行拆除
                if (headList == nodeList) {//若是是頭節點
                    headList = nodeList.next;
                    if (headList != null) {//尾部的狀況
                        headList.last = null;
                    }
                } else {//普通節點
                    nodeList.last.next = nodeList.next;
                    if (nodeList.next != null) {//尾部的話就是null的
                        nodeList.next.last = nodeList.last;
                    }
                }
                return true;
            }
            return false;
        }

        public int get(int key) {
            if (!records.containsKey(key)) {
                return -1;
            }
            Node node = records.get(key);
            node.times++;
            NodeList curNodeList = heads.get(node);
            move(node, curNodeList);//幫Node找新家
            return node.value;
        }

    }
}

 

 

打完這代碼,其餘什麼鏈表操做都是你孫子。

等同於面對真正對手前,練的打木樁和鐵砂掌。

 

面試經驗:

面試官問若是測試代碼你會怎麼測?

要把對數器的思路說上。

考察你是否想到一些極端的邊界狀況。

解決方法:

在寫代碼的過程當中,出錯點、代碼邊界點、須要討論的東西,記下來,養成一種思惟習慣。

寫代碼的時候,遇到特殊狀況,紙筆記錄下來,面試官問的時候,你都已經記錄好了。

 

題目二:

給定一個字符串str,str表示一個公式,公式裏可能有整數、加減乘除符號和左右括號,返回公式的計算結果。

【舉例】

str="48*((70-65)-43)+8*1",返回-1816。

str="3+1*4",返回7。 str="3+(1*4)",返回7。

【說明】

1.能夠認爲給定的字符串必定是正確的公式,即不須要對str作公式有效性檢查。

2.若是是負數,就須要用括號括起來,好比"4*(-3)"。但若是負數做爲公式的開頭或括號部分的開頭,則能夠沒有括號,好比"-3*4"和"(-3)*4"都是合法的。

3.不用考慮計算過程當中會發生溢出的狀況

 

須要好的代碼結構設計

 

思路:

分沒有小括號和有小括號的計算方法。

 

沒有小括號的狀況:不是 * / 直接往裏放,是就取出一個計算後再放回去。

最後所有放完只剩下加減運算。

 

 

至關於乘除所在區域合成一塊。

 

 

有小括號的狀況:

定義一個函數。

str從index開始計算公式,遇到)或str結尾,過程結束。

 

 

遇到左括號後,後面的東西就不算了,直接扔給調子過程,碰到右括號再返回[結果和算到哪一個位置]。

 

 

看到code

 

public class Code_07_ExpressionCompute {

    public static int getValue(String str) {
        return value(str.toCharArray(), 0)[0];
    }

    //返回數組[計算結果,計算到哪一個位置]
    public static int[] value(char[] str, int i) {
        //雙端隊列
        LinkedList<String> que = new LinkedList<String>();
        int pre = 0;//收集數字
        int[] bra = null;
        while (i < str.length && str[i] != ')') {
            if (str[i] >= '0' && str[i] <= '9') {//把數字重組,注意有=
                pre = pre * 10 + str[i++] - '0';
            } else if (str[i] != '(') {// + - * /
                //收集數字和符號,數字+符號是一組
                addNum(que, pre);
                que.addLast(String.valueOf(str[i++]));
                pre = 0;//注意清0
            } else {// 當前i位置爲(
                bra = value(str, i + 1);//無論,直接遞歸
                pre = bra[0];
                i = bra[1] + 1;
            }
        }
        addNum(que, pre);
        return new int[] { getNum(que), i };
    }

    public static void addNum(LinkedList<String> que, int num) {
        if (!que.isEmpty()) {
            int cur = 0;
            String top = que.pollLast();
            if (top.equals("+") || top.equals("-")) {
                que.addLast(top);//放回去
            } else {
                cur = Integer.valueOf(que.pollLast());
                num = top.equals("*") ? (cur * num) : (cur / num);
            }
        }
        que.addLast(String.valueOf(num));
    }

    //計算加減
    public static int getNum(LinkedList<String> que) {
        int res = 0;
        boolean add = true;
        String cur = null;
        int num = 0;
        while (!que.isEmpty()) {
            cur = que.pollFirst();
            if (cur.equals("+")) {
                add = true;
            } else if (cur.equals("-")) {
                add = false;
            } else {
                num = Integer.valueOf(cur);
                res += add ? num : (-num);
            }
        }
        return res;
    }

    public static void main(String[] args) {
        String exp = "48*((70-65)-43)+8*1";
        System.out.println(getValue(exp));

        exp = "4*(6+78)+53-9/2+45*8";
        System.out.println(getValue(exp));

        exp = "10-5*3";
        System.out.println(getValue(exp));

        exp = "-3*4";
        System.out.println(getValue(exp));

        exp = "3+1*4";
        System.out.println(getValue(exp));

    }

}

 

判斷一棵二叉樹是不是搜索二叉樹

判斷一棵二叉樹是不是徹底二叉樹(基礎班講過)

按層遍歷(隊列)節點,

一、若是當前節點有右無左直接false

二、不違反1的狀況,當前節點左右不全,剩下的全都要葉子節點。

 

 

 

public class Code_07_IsBSTAndCBT {

    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }
    //Morris遍歷
    //左子樹小、右子樹大
    public static boolean isBST(Node head) {
        if (head == null) {
            return true;
        }
        boolean res = true;
        Node pre = null;
        Node cur1 = head;
        Node cur2 = null;
        while (cur1 != null) {
            cur2 = cur1.left;
            if (cur2 != null) {
                while (cur2.right != null && cur2.right != cur1) {
                    cur2 = cur2.right;
                }
                if (cur2.right == null) {
                    cur2.right = cur1;
                    cur1 = cur1.left;
                    continue;
                } else {
                    cur2.right = null;
                }
            }
            if (pre != null && pre.value > cur1.value) {
                res = false;
            }
            pre = cur1;
            cur1 = cur1.right;
        }
        return res;
    }

    public static boolean isCBT(Node head) {
        if (head == null) {
            return true;
        }
        Queue<Node> queue = new LinkedList<Node>();
        boolean leaf = false;
        Node l = null;
        Node r = null;
        queue.offer(head);
        while (!queue.isEmpty()) {
            head = queue.poll();
            l = head.left;
            r = head.right;
            if ((leaf && (l != null || r != null)) || (l == null && r != null)) {
                return false;
            }
            if (l != null) {
                queue.offer(l);
            }
            if (r != null) {
                queue.offer(r);
            } else {//若是右子樹爲空,左子樹不爲空,接下來的都要是葉子節點
                leaf = true;
            }
        }
        return true;
    }

    // for test -- print tree
    public static void printTree(Node head) {
        System.out.println("Binary Tree:");
        printInOrder(head, 0, "H", 17);
        System.out.println();
    }

    public static void printInOrder(Node head, int height, String to, int len) {
        if (head == null) {
            return;
        }
        printInOrder(head.right, height + 1, "v", len);
        String val = to + head.value + to;
        int lenM = val.length();
        int lenL = (len - lenM) / 2;
        int lenR = len - lenM - lenL;
        val = getSpace(lenL) + val + getSpace(lenR);
        System.out.println(getSpace(height * len) + val);
        printInOrder(head.left, height + 1, "^", len);
    }

    public static String getSpace(int num) {
        String space = " ";
        StringBuffer buf = new StringBuffer("");
        for (int i = 0; i < num; i++) {
            buf.append(space);
        }
        return buf.toString();
    }

    public static void main(String[] args) {
        Node head = new Node(4);
        head.left = new Node(2);
        head.right = new Node(6);
        head.left.left = new Node(1);
        head.left.right = new Node(3);
        head.right.left = new Node(5);

        printTree(head);
        System.out.println(isBST(head));
        System.out.println(isCBT(head));

    }
}

 

 

題目三:

回到第三課,講跳錶。

何爲跳錶?

完成功能(最大的k,比k小/大離他最近的key)和紅色樹、平衡搜索二叉樹同樣

代價也是longn,底層結構不是樹結構。

Redis按序組織就是跳錶結構。

 

每一個數進去前,先random,0/1,直到1,就知道這個數是多少層。

若是L層沒到最大層,從最高層開始找,若是最高的下一個大,往下走。若是小往右走。走到不能再走,往下走。

這個過程當中,若是到了L層,在往下走以前,先把屬於那個層的點先建上,而後往下走。依次建出屬於新值的全部層的點。

  

 

若是數據爲N,第一層不少點,可是逐層上去會愈來愈少,在查詢和創建的過程當中,是從高層開始找的,一跨會跨過很是多的數。

用1/2機率這東西來優化效率。

 

從最高層開始找這個數(例如50萬)該待的位置,只向下和向右那就跨過不少數了,每層都會跨過必定量的數(由於每一個數的層數是1/2隨機的),每層一次越過的位置,其實在底層看已經越過至關多的位置了,因此管他叫「跳錶」。

 

怎麼插入,就怎麼查。

 

public class Code_02_SkipList {

    public static class SkipListNode {
        public Integer value;
        //長度爲10,說明有10層,nextNodes[1]表明在1層上他的下一個節點是什麼
        //0層指向null,1指向第一層他的下一個結點是誰,以此類推
        //從高層到下
        public ArrayList<SkipListNode> nextNodes;

        public SkipListNode(Integer value) {
            this.value = value;
            nextNodes = new ArrayList<SkipListNode>();
        }
    }

    public static class SkipListIterator implements Iterator<Integer> {
        SkipList list;
        SkipListNode current;

        public SkipListIterator(SkipList list) {
            this.list = list;
            this.current = list.getHead();
        }

        public boolean hasNext() {
            return current.nextNodes.get(0) != null;
        }

        public Integer next() {
            current = current.nextNodes.get(0);
            return current.value;
        }
    }

    public static class SkipList {
        private SkipListNode head;//巨小,層數是最高的
        private int maxLevel;
        private int size;//加進來了多少個key
        private static final double PROBABILITY = 0.5;

        public SkipList() {
            size = 0;
            maxLevel = 0;
            head = new SkipListNode(null);
            head.nextNodes.add(null);
        }

        public SkipListNode getHead() {
            return head;
        }

        public void add(Integer newValue) {
            if (!contains(newValue)) {
                size++;
                int level = 0;
                while (Math.random() < PROBABILITY) {
                    level++;
                }
                while (level > maxLevel) {
                    head.nextNodes.add(null);//頭增長區域到最大層數
                    maxLevel++;
                }
                SkipListNode newNode = new SkipListNode(newValue);
                SkipListNode current = head;//從頭部往下移動
                int levelAll = maxLevel;//從最高層開始找
                do {
                    current = findNext(newValue, current, levelAll);
                    if (levelAll <= level){//達到應該加入節點的層
                        //先後環境接上
                        //當前層,創建指向恰好比本身大的節點的聯繫
                        //例如一共5層,因爲是從高層開始插入,先把第五層加入0位置
                        //接着第四層的時候也是加到0位置,那就把本來第五層的擠到1位置
                        //...以此類推,加到最後一層就會是正常的0位置
                        // 最後一層也已經被擠到最後的位置上
                        newNode.nextNodes.add(0, current.nextNodes.get(level));
                        //把恰好比他小的節點,指向他。例如:7--->10 加入8 變成7--->8--->10
                        current.nextNodes.set(level, newNode);
                        level--;
                    }

                } while (levelAll-- > 0);//當前層小了往右,大了往下
            }
        }

        public void delete(Integer deleteValue) {
            if (contains(deleteValue)) {
                SkipListNode deleteNode = find(deleteValue);
                size--;
                int level = maxLevel;
                SkipListNode current = head;
                do {
                    current = findNext(deleteNode.value, current, level);
                    if (deleteNode.nextNodes.size() > level) {
                        current.nextNodes.set(level, deleteNode.nextNodes.get(level));
                    }
                } while (level-- > 0);
            }
        }

        // Returns the skiplist node with greatest value <= e
        private SkipListNode find(Integer e) {
            return find(e, head, maxLevel);
        }

        // Returns the skiplist node with greatest value <= e
        // Starts at node start and level
        private SkipListNode find(Integer e, SkipListNode current, int level) {
            do {
                current = findNext(e, current, level);
            } while (level-- > 0);
            return current;
        }

        // Returns the node at a given level with highest value less than e
        private SkipListNode findNext(Integer e, SkipListNode current, int level) {
            //得到當前節點所在層中鏈接的下一個節點(例如cur在第七層中的下一個)
            SkipListNode next = current.nextNodes.get(level);
            while (next != null) {
                Integer value = next.value;
                if (lessThan(e, value)) { // e < value
                    break;//若是下一個數比新增值大了,就找到接入位置。
                    //cur就是這一層中,最後一個小於當前數的值。
                }
                //向右動
                current = next;
                next = current.nextNodes.get(level);
            }
            return current;
        }

        public int size() {
            return size;
        }

        public boolean contains(Integer value) {
            SkipListNode node = find(value);
            return node != null && node.value != null && equalTo(node.value, value);
        }

        public Iterator<Integer> iterator() {
            return new SkipListIterator(this);
        }

        /******************************************************************************
         * Utility Functions *
         ******************************************************************************/

        private boolean lessThan(Integer a, Integer b) {
            return a.compareTo(b) < 0;
        }

        private boolean equalTo(Integer a, Integer b) {
            return a.compareTo(b) == 0;
        }

    }

    public static void main(String[] args) {

    }

}

 

 

add操做圖解

 

 

證實算法複雜度:

能夠研究下他的分佈,無論輸入規律是什麼,基本上就是一顆二叉樹的樣子,機率是0.5,那下一層基本是本層數量的兩倍,因此是logn的代價,一跳,跳過不少節點,一共跳多少層就是代價,邏輯概念能夠等同於一顆二叉樹,可是他是以機率完成的。

 

add方法存在問題,應該是從最高層開始找的,如今是從random出來的level開始找。應該是從最高層向下向右找到相應的位置,達到level層後再開始加入。(例如random出了第一層,若是從第一層開始遍歷,就不是longn的算法了)(上面展現的代碼已經修正)

 

相關文章
相關標籤/搜索