社招面經總結——算法題篇

面試結果

總結下最近的面試:node

  • 頭條後端:3面技術面掛
  • 螞蟻支付寶營銷-機器學習平臺開發: 技術面經過,年後被通知只有P7的hc
  • 螞蟻中臺-機器學習平臺開發: 技術面經過, 被螞蟻HR掛掉(脈脈上好多人遇到這種狀況,一個是今年大環境很差,另外一個,面試儘可能不要遇上阿里財年年末,這算是一點tips吧)
  • 快手後端: 拿到offer
  • 百度後端: 拿到offer

最終拒了百度,去快手了, 一心想去阿里, 我的有點阿里情節吧,緣分差點。 總結下最近的面試狀況, 因爲面了20多面, 就按照題型分類給你們一個總結。推薦你們每一年都要抽出時間去面一下,不必定跳槽,可是須要知道本身的不足,必定要你的工齡匹配上你的能力。好比就我我的來講,經過面試我知道數據庫的知識不是很懂,再加上因爲所在組對數據庫接觸較少,這就是短板,做爲一個後端工程師對數據庫說不太瞭解是很可恥的,在選擇offer的時候就能夠適當有偏向性。下面分專題把最近的面試總結和你們總結一下。過度簡單的就不說了,好比打印一個圖形啥的, 還有的我不太記得清了,好比快手一面好像是一道動態規劃的題目。固然,可能有的解決方法不是很好,你們能夠在手撕代碼羣裏討論。最後一篇我再談一下一些面試的技巧啥的。麻煩你們點贊轉發支持下~面試

股票買賣(頭條)

Leetcode 上有三題股票買賣,面試的時候只考了兩題,分別是easy 和medium的難度算法

Leetcode 121. 買賣股票的最佳時機

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。數據庫

若是你最多隻容許完成一筆交易(即買入和賣出一支股票),設計一個算法來計算你所能獲取的最大利潤。後端

注意你不能在買入股票前賣出股票。數組

示例 1:緩存

輸入: [7,1,5,3,6,4]
輸出: 5
複製代碼

解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 5 天(股票價格 = 6)的時候賣出,最大利潤 = 6-1 = 5 。 注意利潤不能是 7-1 = 6, 由於賣出價格須要大於買入價格。 示例 2:bash

輸入: [7,6,4,3,1]
輸出: 0
複製代碼

解釋: 在這種狀況下, 沒有交易完成, 因此最大利潤爲 0。數據結構

題解

紀錄兩個狀態, 一個是最大利潤, 另外一個是遍歷過的子序列的最小值。知道以前的最小值咱們就能夠算出當前天可能的最大利潤是多少機器學習

class Solution {
    public int maxProfit(int[] prices) {
        // 7,1,5,3,6,4
        int maxProfit = 0;
        int minNum = Integer.MAX_VALUE;
        for (int i = 0; i < prices.length; i++) {
            if (Integer.MAX_VALUE != minNum && prices[i] - minNum > maxProfit) {
                maxProfit = prices[i] - minNum;
            }

            if (prices[i] < minNum) {
                minNum = prices[i];
            }
        }
        return maxProfit;
    }
}
複製代碼

Leetcode 122. 買賣股票的最佳時機 II

此次改爲股票能夠買賣屢次, 可是你必需要在出售股票以前把持有的股票賣掉。 示例 1:

輸入: [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能得到利潤 = 5-1 = 4 。
     隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能得到利潤 = 6-3 = 3 。
複製代碼

示例 2:

輸入: [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能得到利潤 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接連購買股票,以後再將它們賣出。
     由於這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉以前的股票。
複製代碼

示例 3:

輸入: [7,6,4,3,1]
輸出: 0
解釋: 在這種狀況下, 沒有交易完成, 因此最大利潤爲 0。
複製代碼

題解

因爲能夠無限次買入和賣出。咱們都知道炒股想掙錢固然是低價買入高價拋出,那麼這裏咱們只須要從次日開始,若是當前價格比以前價格高,則把差值加入利潤中,由於咱們能夠昨天買入,今日賣出,若明日價更高的話,還能夠今日買入,明日再拋出。以此類推,遍歷完整個數組後便可求得最大利潤。

class Solution {
    public int maxProfit(int[] prices) {
        // 7,1,5,3,6,4
        int maxProfit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (i != 0 && prices[i] - prices[i-1] > 0) {
                maxProfit += prices[i] - prices[i-1];
            }
        }
        return maxProfit;
    }
}
複製代碼

LRU cache (頭條、螞蟻)

這道題目是頭條的高頻題目,甚至我懷疑,頭條這個面試題是題庫裏面的必考題。看脈脈也是好多人遇到。第一次我寫的時候沒寫好,可能因爲這個掛了。

轉自知乎:zhuanlan.zhihu.com/p/34133067

題目

運用你所掌握的數據結構,設計和實現一個 LRU (最近最少使用) 緩存機制。它應該支持如下操做: 獲取數據 get 和 寫入數據 put 。

獲取數據 get(key) - 若是密鑰 (key) 存在於緩存中,則獲取密鑰的值(老是正數),不然返回 -1。 寫入數據 put(key, value) - 若是密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據以前刪除最近最少使用的數據值,從而爲新的數據值留出空間。

進階:

你是否能夠在 O(1) 時間複雜度內完成這兩種操做?

LRUCache cache = new LRUCache( 2 /* 緩存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 該操做會使得密鑰 2 做廢
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 該操做會使得密鑰 1 做廢
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4
複製代碼

題解

這道題在今日頭條、快手或者硅谷的公司中是比較常見的,代碼要寫的還蠻多的,難度也是hard級別。

最重要的是LRU 這個策略怎麼去實現, 很容易想到用一個鏈表去實現最近使用的放在鏈表的最前面。 好比get一個元素,至關於被使用過了,這個時候它須要放到最前面,再返回值, set同理。 那如何把一個鏈表的中間元素,快速的放到鏈表的開頭呢? 很天然的咱們想到了雙端鏈表。

基於 HashMap 和 雙向鏈表實現 LRU 的

總體的設計思路是,可使用 HashMap 存儲 key,這樣能夠作到 save 和 get key的時間都是 O(1),而 HashMap 的 Value 指向雙向鏈表實現的 LRU 的 Node 節點,如圖所示。

image.png

LRU 存儲是基於雙向鏈表實現的,下面的圖演示了它的原理。其中 head 表明雙向鏈表的表頭,tail 表明尾部。首先預先設置 LRU 的容量,若是存儲滿了,能夠經過 O(1) 的時間淘汰掉雙向鏈表的尾部,每次新增和訪問數據,均可以經過 O(1)的效率把新的節點增長到對頭,或者把已經存在的節點移動到隊頭。

下面展現了,預設大小是 3 的,LRU存儲的在存儲和訪問過程當中的變化。爲了簡化圖複雜度,圖中沒有展現 HashMap部分的變化,僅僅演示了上圖 LRU 雙向鏈表的變化。咱們對這個LRU緩存的操做序列以下:

save("key1", 7)
save("key2", 0)
save("key3", 1)
save("key4", 2)
get("key2")
save("key5", 3)
get("key2")
save("key6", 4)
複製代碼

相應的 LRU 雙向鏈表部分變化以下:

image.png

總結一下核心操做的步驟:

save(key, value),首先在 HashMap 找到 Key 對應的節點,若是節點存在,更新節點的值,並把這個節點移動隊頭。若是不存在,須要構造新的節點,而且嘗試把節點塞到隊頭,若是LRU空間不足,則經過 tail 淘汰掉隊尾的節點,同時在 HashMap 中移除 Key。

get(key),經過 HashMap 找到 LRU 鏈表節點,由於根據LRU 原理,這個節點是最新訪問的,因此要把節點插入到隊頭,而後返回緩存的值。

private static class DLinkedNode {
        int key;
        int value;
        DLinkedNode pre;
        DLinkedNode post;
    }

    /**
     * 老是在頭節點中插入新節點.
     */
    private void addNode(DLinkedNode node) {

        node.pre = head;
        node.post = head.post;

        head.post.pre = node;
        head.post = node;
    }

    /**
     * 摘除一個節點.
     */
    private void removeNode(DLinkedNode node) {
        DLinkedNode pre = node.pre;
        DLinkedNode post = node.post;

        pre.post = post;
        post.pre = pre;
    }

    /**
     * 摘除一個節點,而且將它移動到開頭
     */
    private void moveToHead(DLinkedNode node) {
        this.removeNode(node);
        this.addNode(node);
    }

    /**
     * 彈出最尾巴節點
     */
    private DLinkedNode popTail() {
        DLinkedNode res = tail.pre;
        this.removeNode(res);
        return res;
    }

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

    public LRUCache(int capacity) {
        this.count = 0;
        this.capacity = capacity;

        head = new DLinkedNode();
        head.pre = null;

        tail = new DLinkedNode();
        tail.post = null;

        head.post = tail;
        tail.pre = head;
    }

    public int get(int key) {

        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1; // cache裏面沒有
        }

        // cache 命中,挪到開頭
        this.moveToHead(node);

        return node.value;
    }


    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);

        if (node == null) {

            DLinkedNode newNode = new DLinkedNode();
            newNode.key = key;
            newNode.value = value;

            this.cache.put(key, newNode);
            this.addNode(newNode);

            ++count;

            if (count > capacity) {
                // 最後一個節點彈出
                DLinkedNode tail = this.popTail();
                this.cache.remove(tail.key);
                count--;
            }
        } else {
            // cache命中,更新cache.
            node.value = value;
            this.moveToHead(node);
        }
    }
    
複製代碼

二叉樹層次遍歷(頭條)

這個題目以前也講過,Leetcode 102題。

題目

給定一個二叉樹,返回其按層次遍歷的節點值。 (即逐層地,從左到右訪問全部節點)。

例如: 給定二叉樹: [3,9,20,null,null,15,7],

3
   / \
  9  20
    /  \
   15   7
複製代碼

返回其層次遍歷結果:

[
  [3],
  [9,20],
  [15,7]
]
複製代碼

題解

咱們數據結構的書上教的層序遍歷,就是利用一個隊列,不斷的把左子樹和右子樹入隊。可是這個題目還要要求按照層輸出。因此關鍵的問題是: 如何肯定是在同一層的。 咱們很天然的想到: 若是在入隊以前,把上一層全部的節點出隊,那麼出隊的這些節點就是上一層的列表。 因爲隊列是先進先出的數據結構,因此這個列表是從左到右的。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new LinkedList<>();
        if (root == null) {
            return res;
        }

        LinkedList<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> currentRes = new LinkedList<>();
            // 當前隊列的大小就是上一層的節點個數, 依次出隊
            while (size > 0) {
                TreeNode current = queue.poll();
                if (current == null) {
                    continue;
                }
                currentRes.add(current.val);
                // 左子樹和右子樹入隊.
                if (current.left != null) {
                    queue.add(current.left);
                }
                if (current.right != null) {
                    queue.add(current.right);
                }
                size--;
            }
            res.add(currentRes);
        }
        return res;
    }
}
複製代碼

這道題可不能夠用非遞歸來解呢?

遞歸的子問題:遍歷當前節點, 對於當前層, 遍歷左子樹的下一層層,遍歷右子樹的下一層

遞歸結束條件: 當前層,當前子樹節點是null

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new LinkedList<>();
        if (root == null) {
            return res;
        }
        levelOrderHelper(res, root, 0);
        return res;
    }

    /**
     * @param depth 二叉樹的深度
     */
    private void levelOrderHelper(List<List<Integer>> res, TreeNode root, int depth) {
        if (root == null) {
            return;
        }
        
        if (res.size() <= depth) {
            // 當前層的第一個節點,須要new 一個list來存當前層.
            res.add(new LinkedList<>());
        }
        // depth 層,把當前節點加入
        res.get(depth).add(root.val);
        // 遞歸的遍歷下一層.
        levelOrderHelper(res, root.left, depth + 1);
        levelOrderHelper(res, root.right, depth + 1);
    }
}
複製代碼

二叉樹轉鏈表(快手)

這是Leetcode 104題。 給定一個二叉樹,原地將它展開爲鏈表。

例如,給定二叉樹

1
   / \
  2   5
 / \   \
3   4   6
複製代碼

將其展開爲:

1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6
複製代碼

這道題目的關鍵是轉換的時候遞歸的時候記住是先轉換右子樹,再轉換左子樹。 因此須要記錄一下右子樹轉換完以後鏈表的頭結點在哪裏。注意沒有新定義一個next指針,而是直接將right 當作next指針,那麼Left指針咱們賦值成null就能夠了。

class Solution {
    private TreeNode prev = null;

    public void flatten(TreeNode root) {
        if (root == null)  return;
        flatten(root.right); // 先轉換右子樹
        flatten(root.left); 
        root.right = prev;  // 右子樹指向鏈表的頭
        root.left = null; // 把左子樹置空
        prev = root; // 當前結點爲鏈表頭
    }
}
複製代碼

用遞歸解法,用一個stack 記錄節點,右子樹先入棧,左子樹後入棧。

class Solution {
    public void flatten(TreeNode root) {
        if (root == null) return;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode current = stack.pop();
            if (current.right != null) stack.push(current.right);
            if (current.left != null) stack.push(current.left);
            if (!stack.isEmpty()) current.right = stack.peek();
            current.left = null;
        }
    }
}
複製代碼

二叉樹尋找最近公共父節點(快手)

Leetcode 236 二叉樹的最近公共父親節點

題解

從根節點開始遍歷,若是node1和node2中的任一個和root匹配,那麼root就是最低公共祖先。 若是都不匹配,則分別遞歸左、右子樹,若是有一個 節點出如今左子樹,而且另外一個節點出如今右子樹,則root就是最低公共祖先. 若是兩個節點都出如今左子樹,則說明最低公共祖先在左子樹中,不然在右子樹。

public class Solution {  
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {  
        //發現目標節點則經過返回值標記該子樹發現了某個目標結點  
        if(root == null || root == p || root == q) return root;  
        //查看左子樹中是否有目標結點,沒有爲null  
        TreeNode left = lowestCommonAncestor(root.left, p, q);  
        //查看右子樹是否有目標節點,沒有爲null  
        TreeNode right = lowestCommonAncestor(root.right, p, q);  
        //都不爲空,說明作右子樹都有目標結點,則公共祖先就是自己  
        if(left!=null&&right!=null) return root;  
        //若是發現了目標節點,則繼續向上標記爲該目標節點  
        return left == null ? right : left;  
    }  
}
複製代碼

數據流求中位數(螞蟻)

面了螞蟻中臺的團隊,二面面試官根據彙報層級推測應該是P9級別及以上,在美國面我,面試風格偏硅谷那邊。題目是hard難度的,leetcode 295題。 這道題目是Leetcode的hard難度的原題。給定一個數據流,求數據流的中位數,求中位數或者topK的問題咱們一般都會想用堆來解決。 可是面試官又進一步加大了難度,他要求內存使用很小,沒有磁盤,可是壓榨空間的同時能夠忍受必定時間的損耗。且面試官不只要求說出思路,要寫出完整可通過大數據檢測的production code。

先不考慮內存

不考慮內存的方式就是Leetcode 論壇上的題解。 基本思想是創建兩個堆。左邊是大根堆,右邊是小根堆。 若是是奇數的時候,大根堆的堆頂是中位數。

例如:[1,2,3,4,5] 大根堆創建以下:

3
     / \
    1   2
複製代碼

小根堆創建以下:

4
     / 
    5   
複製代碼

偶數的時候則是最大堆和最小堆頂的平均數。

例如: [1, 2, 3, 4]

大根堆創建以下:

2
     / 
    1   
複製代碼

小根堆創建以下:

3
     / 
    4   
複製代碼

而後再維護一個奇數偶數的狀態便可求中位數。

public class MedianStream {
    private PriorityQueue<Integer> leftHeap = new PriorityQueue<>(5, Collections.reverseOrder());
    private PriorityQueue<Integer> rightHeap = new PriorityQueue<>(5);

    private boolean even = true;

    public double getMedian() {
        if (even) {
            return (leftHeap.peek() + rightHeap.peek()) / 2.0;
        } else {
            return leftHeap.peek();
        }
    }

    public void addNum(int num) {
        if (even) {
            rightHeap.offer(num);
            int rightMin = rightHeap.poll();
            leftHeap.offer(rightMin);
        } else {
            leftHeap.offer(num);
            int leftMax = leftHeap.poll();
            rightHeap.offer(leftMax);
        }
        System.out.println(leftHeap);
        System.out.println(rightHeap);
        // 奇偶變換.
        even = !even;
    }
}
複製代碼

壓榨內存

可是這樣作的問題就是可能內存會爆掉。若是你的流無限大,那麼意味着這些數據都要存在內存中,堆必需要可以建無限大。若是內存必須很小的方式,用時間換空間。

  • 流是能夠重複去讀的, 用時間換空間;
  • 能夠用分治的思想,先讀一遍流,把流中的數據個數分桶;
  • 分桶以後遍歷桶就能夠獲得中位數落在哪一個桶裏面,這樣就把問題的範圍縮小了。
public class Median {
    private static int BUCKET_SIZE = 1000;

    private int left = 0;
    private int right = Integer.MAX_VALUE;

    // 流這裏用int[] 代替
    public double findMedian(int[] nums) {
        // 第一遍讀取stream 將問題複雜度轉化爲內存可接受的量級.
        int[] bucket = new int[BUCKET_SIZE];
        int step = (right - left) / BUCKET_SIZE;
        boolean even = true;
        int sumCount = 0;
        for (int i = 0; i < nums.length; i++) {
            int index = nums[i] / step;
            bucket[index] = bucket[index] + 1;
            sumCount++;
            even = !even;
        }
        // 若是是偶數,那麼就須要計算第topK 個數
        // 若是是奇數, 那麼須要計算第 topK和topK+1的個數.
        int topK = even ? sumCount / 2 : sumCount / 2 + 1;

        int index = 0;
        int indexBucketCount = 0;
        for (index = 0; index < bucket.length; index++) {
            indexBucketCount = bucket[index];
            if (indexBucketCount >= topK) {
                // 當前bucket 就是中位數的bucket.
                break;
            }
            topK -= indexBucketCount;
        }
        
        // 劃分到這裏其實轉化爲一個topK的問題, 再讀一遍流.
        if (even && indexBucketCount == topK) { 
            left = index * step;
            right = (index + 2) * step;
            return helperEven(nums, topK);
            // 偶數的時候, 剛好劃分到在左右兩個子段中.
            // 左右兩段中 [topIndex-K + (topIndex-K + 1)] / 2.
        } else if (even) {
            left = index * step;
            right = (index + 1) * step;
            return helperEven(nums, topK);
            // 左邊 [topIndex-K + (topIndex-K + 1)] / 2 
        } else {
            left = index * step;
            right = (index + 1) * step;
            return helperOdd(nums, topK);
            // 奇數, 左邊topIndex-K
        }
    }
}
複製代碼

這裏邊界條件咱們處理好以後,關鍵仍是helperOdd 和 helperEven這兩個函數怎麼去求topK的問題. 咱們仍是轉化爲一個topK的問題,那麼求top-K和top(K+1)的問題到這裏咱們是否是能夠用堆來解決了呢? 答案是不能,考慮極端狀況。 中位數的重複次數很是多

eg:
[100,100,100,100,100...] (1000億個100)
複製代碼

你的劃分剛好落到這個桶裏面,內存一樣會爆掉。

再用時間換空間

假如咱們的劃分bucket大小是10000,那麼最大的時候區間就是20000。(對應上面的偶數且落到兩個分桶的狀況) 那麼既然劃分到某一個bucket了,咱們直接用數數字的方式來求topK 就能夠了,用堆的方式也能夠,更高效一點,其實這裏問題規模已經劃分到很小了,兩種方法均可以。 咱們選用TreeMap這種數據結構計數。而後分奇數偶數去求解。

private double helperEven(int[] nums, int topK) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] >= left && nums[i] <= right) {
                if (!map.containsKey(nums[i])) {
                    map.put(nums[i], 1);
                } else {
                    map.put(nums[i], map.get(nums[i]) + 1);
                }
            }
        }

        int count = 0;
        int kNum = Integer.MIN_VALUE;
        int kNextNum = 0;
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            int currentCountIndex = entry.getValue();
            if (kNum != Integer.MIN_VALUE) {
                kNextNum = entry.getKey();
                break;
            }
            if (count + currentCountIndex == topK) {
                kNum = entry.getKey();
            } else if (count + currentCountIndex > topK) {
                kNum = entry.getKey();
                kNextNum = entry.getKey();
                break;
            } else {
                count += currentCountIndex;
            }
        }

        return (kNum + kNextNum) / 2.0;
    }

    private double helperOdd(int[] nums, int topK) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] >= left && nums[i] <= right) {
                if (!map.containsKey(nums[i])) {
                    map.put(nums[i], 1);
                } else {
                    map.put(nums[i], map.get(nums[i]) + 1);
                }
            }
        }
        int count = 0;
        int kNum = Integer.MIN_VALUE;
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            int currentCountIndex = entry.getValue();
            if (currentCountIndex + count >= topK) {
                kNum = entry.getKey();
                break;
            } else {
                count += currentCountIndex;
            }
        }

        return kNum;
    }
複製代碼

至此,我以爲算是一個比較好的解決方案,leetcode社區沒有相關的解答和探索,歡迎你們交流。

熱門閱讀

Leetcode名企之路
相關文章
相關標籤/搜索