接着第四課的內容,主要講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的算法了)(上面展現的代碼已經修正)