今日頭條後端面試記錄

目錄

前言

最近參加了一下頭條後端工程師的面試, 很慘, 一面就掛掉了.java

回來以後也對面試過程作了一些總結,就不夾帶私貨了,這篇文章主要對面試過程當中的技術問題作一個覆盤.node

1. 求二叉樹的最遠節點的距離

這是一道 LeetCode 原題, 原題連接面試

求二叉樹的最遠距離節點間的節點. 首先總結幾個規律:redis

  1. 一棵樹的直徑要麼徹底在其左子樹中,要麼徹底在其右子樹中, 要麼路過根節點.
  2. 一棵樹的直徑 = 以該樹中每個節點爲根節點, 求 路過根節點的最大直徑, 全部節點的最大值就是這棵樹的直徑.
  3. 求通過根節點的直徑, 能夠分解爲: 求左子樹的最深葉子 + 右子樹的最深葉子.

因此咱們要作:後端

  1. 遞歸的求 每個節點的 路過根節點的直徑(求當前節點的左子樹最深葉子和右子樹的最深葉子),取其最大值.
  2. 求某一個節點的最深葉子, 就等於 他的 左子樹最深葉子 + 1 和 右子樹最深葉子 + 1 的較大值.
  3. 在求最深葉子的時候, 實際上是求出了當前節點直徑的(左邊最深葉子 + 右邊最深葉子 + 2), 爲了不重複計算, 咱們在遞歸求最深葉子的時候, 把直徑也記錄下來.)

代碼以下:安全

public int diameterOfBinaryTree(TreeNode root) {
       AtomicReference<Integer> ret = new AtomicReference<>(0);
        find(root, ret);
       return ret.get();
   }

   private int find(TreeNode node, AtomicReference<Integer> result) {
       if (node == null) return 0;
       int left = 0, right = 0;
       if (node.left != null) left = find(node.left,result) + 1;
       if (node.right != null) right = find(node.right,result) + 1;
       int tmp = Math.max(result.get(), left + right);
       result.set(tmp);
       return Math.max(left, right);
   }
複製代碼

代碼很簡單, 就是遞歸的求節點的左子樹最遠葉子和右子樹最遠葉子. 而後在 計算過程當中, 將 當前節點的直徑 做爲一個備選項存儲,最後求最大直徑便可.服務器

2. Java的裝箱與拆箱

Java在1.5添加了自動裝箱和拆箱機制. 總的來講基本就是基本類型和對應的包裝類型之間的自動轉換.微信

以下面的代碼中:併發

public class BoxTest {
    public static void main(String [] args){
        Integer a = 10; // 裝箱
        int b = a; // 拆箱
    }
}
複製代碼

咱們將代碼編譯以後進行反編譯, 能夠看到函數

2019-10-24-14-32-50

很明顯在 代碼中的 #2 ,#3 處進行了裝箱和拆箱.

分別調用了Integer的 valueOf 方法和intValue 方法.

3. CMS垃圾收集器的收集過程當中,何時會暫停用戶線程?

這裏不對全部的垃圾收集器展開講解, 有興趣的朋友們能夠移步 JVM的數據區域與垃圾收集.

衆所周知, CMS的垃圾收集過程以下:

2019-08-10-21-19-28

因此在初始標記和從新標記兩個階段仍是須要暫停用戶線程的.

4. ConcurrentHashMap在讀取的過程當中爲何不須要加鎖?

查看ConcurrentHashMap 的源碼能夠發現, Node節點的定義是:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
}
複製代碼

能夠看到, 裏面定義了幾個屬性, 分別以下:

  1. final修飾的hash值,初始化後不能再次改變.
  2. final修飾的key,初始化後不能再次改變.
  3. volatile 修飾的值
  4. volatile 修飾的下一節點指針

get(Ojbect)方法的調用過程當中.

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //獲取hash值
    int h = spread(key.hashCode());
    //經過tabat獲取hash桶, tabAt是一個線程安全的操做, 有UnSafe來保證的.
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //若是該hash桶的第一個節點就是查找結果,則返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //第一個節點是樹的根節點,按照樹的方式進行遍歷查找
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        //第一個節點是鏈表的根節點,按照鏈表的方式遍歷查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
複製代碼

在這個過程當中,

  1. 獲取hash桶的根節點, 經過 tabAt 來操做, 線程安全.
  2. 遍歷的時候用到了node的next屬性, 因爲其與volatile修飾的, 因此線程間可見,出現併發問題.
  3. 返回時讀取node的volatile屬性val.

因此 get 過程當中不用加鎖也能夠正確的獲取對象.

5. Redis的字典rehash和JDK中hashmap等rehash有什麼不一樣?

這個問題比較寬泛,我我的的理解有如下兩點.

  1. hashmap rehash的時候的 另一張table是臨時建立的. 而 redis 是時刻保持兩張表的引用的. 只是在須要rehash的時候才分配足夠的空間.
  2. hashmap rehash是一次性的,集中的完成rehash過程, 而redis是漸進式hash.

hashmap的rehash過程想必你們都是瞭解的, 那麼這麼稍微說一下redis的漸進式hash.

首先, rehash是要將原來表中的全部數據從新hash一遍,存放到新的表格中, 以進行擴容.

而redis是一個單線程的高性能的服務, 若是一個hash表中有幾億條數據, rehash 花費的時間將比較長, 而在此期間, redis是沒法對外提供服務的, 這是不可接受的.

所以, redis實現了漸進式hash. 過程以下:

  1. 假如當前數據在ht[0]中, 那麼首先爲ht[1]分配足夠的空間.
  2. 在字典中維護一個變量, rehashindex = 0. 用來指示當前rehash的進度.
  3. 在rehash期間, 每次對 字典進行 增刪改查操做, 在完成實際操做以後, 都會進行 一次rehash操做, 將 ht[0] 在rehashindex 位置上的值rehash到ht[1]上. 將 rehashindex 遞增一位.
  4. 隨着不斷的執行, 原來的 ht[0] 上的數值總會所有rehash完成, 此時結束rehash過程.

在上面的過程當中有兩個問題沒有提到:

  1. 假如這個服務器很空餘呢? 中間幾小時都沒有請求進來, 那麼同時保持兩個 table, 豈不是很浪費內存?

解決辦法是: 在redis的定時函數裏, 也加入幫助rehash的操做, 這樣子若是服務器空閒, 就會比較快的完成rehash.

  1. 在保持兩個table期間, 該哈希表怎麼對外提供服務呢?

解決辦法: 對於添加操做, 直接添加到ht[1]上, 所以這樣才能保證ht[0]的數量只會減小不會增長,才能保證rehash過程能夠完結. 而刪除,修改, 查詢等操做會在ht[0]上進行, 若是得不到結果, 會去ht[1]再執行一遍.

漸進式hash帶來的好處是顯而易見的, 他採用了分而治之的思想, 將rehash操做分散到每個對該哈希表的操做上,避免了集中式rehash帶來的性能壓力.

與此同時,漸進式hash也帶來了一個問題, 那就是 在rehash的時間內, 須要保存兩個 hash表, 對內存的佔用稍大, 並且若是在redis服務器原本內存滿了的時候, 忽然進行rehash會形成大量的key被拋棄.

參考文章

《Redis設計與實現(第二版》

完。



ChangeLog

2019-05-19 完成

以上皆爲我的所思所得,若有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文連接。

聯繫郵箱:huyanshi2580@gmail.com

更多學習筆記見我的博客或關注微信公衆號 < 呼延十 > ------>呼延十

相關文章
相關標籤/搜索