7 道高頻面試算法題,你都會了嗎?「矩陣 + 位運算 + LRU」

Attention

秋招接近尾聲,我總結了 牛客WanAndroid 上,有關筆試面經的帖子中出現的算法題,結合往年考題寫了這一系列文章,全部文章均與 LeetCode 進行覈對、測試。歡迎食用css


本文將覆蓋 「二進制」 + 「位運算」 和 Lru 方面的面試算法題,文中我將給出:java

  1. 面試中的題目
  2. 解題的思路
  3. 特定問題的技巧和注意事項
  4. 考察的知識點及其概念
  5. 詳細的代碼和解析

開始以前,咱們先看下會有哪些重點案例:

圖片

爲了方便你們跟進學習,我在 GitHub 創建了一個倉庫

倉庫地址:超級乾貨!精心概括視頻、歸類、總結,各位路過的老鐵支持一下!給個 Star !
android

如今就讓咱們開始吧!git



矩陣


矩陣



螺旋矩陣


  • 給定一個包含 m x n 個要素的矩陣,(m 行, n 列),按照螺旋順序,返回該矩陣中的全部要素。github

  • 示例 :面試

輸入:
[
  [1, 2, 3, 4],
  [5, 6, 7, 8],
  [9,10,11,12]
]
輸出: [1,2,3,4,8,12,11,10,9,5,6,7]
複製代碼

解題思路

  • 咱們定義矩陣的第 k 層是到最近邊界距離爲 k 的全部頂點。例如,下圖矩陣最外層元素都是第 1 層,次外層元素都是第 2 層,而後是第 3 層的。
[[1, 1, 1, 1, 1, 1, 1],
 [1, 2, 2, 2, 2, 2, 1],
 [1, 2, 3, 3, 3, 2, 1],
 [1, 2, 2, 2, 2, 2, 1],
 [1, 1, 1, 1, 1, 1, 1]]
複製代碼
  • 對於每層,咱們從左上方開始以順時針的順序遍歷全部元素,假設當前層左上角座標是 \text{(r1, c1)},右下角座標是 \text{(r2, c2)}
  1. 首先,遍歷上方的全部元素 (r1, c),按照 c = c1,...,c2 的順序。
  2. 而後遍歷右側的全部元素 (r, c2),按照 r = r1+1,...,r2 的順序。
  3. 若是這一層有四條邊(也就是 r1 < r2 而且 c1 < c2 ),咱們如下圖所示的方式遍歷下方的元素和左側的元素。

螺旋矩陣

public List<Integer> spiralOrder(int[][] matrix) {
    ArrayList<Integer> rst = new ArrayList<Integer>();
    if(matrix == null || matrix.length == 0) {
        return rst;
    }
    
    int rows = matrix.length;
    int cols = matrix[0].length;
    int count = 0;
    while(count * 2 < rows && count * 2 < cols){
        for (int i = count; i < cols - count; i++) {
            rst.add(matrix[count][i]);
        }
        
        for (int i = count + 1; i < rows - count; i++) {
            rst.add(matrix[i][cols - count - 1]);
        }
        
        if (rows - 2 * count == 1 || cols - 2 * count == 1) { // 若是隻剩1行或1列
            break;
        }
            
        for (int i = cols - count - 2; i >= count; i--) {
            rst.add(matrix[rows - count - 1][i]);
        }
            
        for (int i = rows - count - 2; i >= count + 1; i--) {
            rst.add(matrix[i][count]);
        }
        
        count++;
    }
    return rst;
}
複製代碼


判斷數獨是否合法


  • 請斷定一個數獨是否有效。該數獨可能只填充了部分數字,其中缺乏的數字用 . 表示。算法

  • 維護一個HashSet用來記同一、同一、同一九宮格是否存在相同數字數據庫

判斷數獨是否合法

示例 :編程

輸入:
[
  ["8","3",".",".","7",".",".",".","."],
  ["6",".",".","1","9","5",".",".","."],
  [".","9","8",".",".",".",".","6","."],
  ["8",".",".",".","6",".",".",".","3"],
  ["4",".",".","8",".","3",".",".","1"],
  ["7",".",".",".","2",".",".",".","6"],
  [".","6",".",".",".",".","2","8","."],
  [".",".",".","4","1","9",".",".","5"],
  [".",".",".",".","8",".",".","7","9"]
]

輸出: false
解釋: 除了第一行的第一個數字從 5 改成 8 之外,空格內其餘數字均與 示例1 相同。
     但因爲位於左上角的 3x3 宮內有兩個 8 存在, 所以這個數獨是無效的。
複製代碼
  • 說明:
  1. 一個有效的數獨(部分已被填充)不必定是可解的。
  2. 只須要根據以上規則,驗證已經填入的數字是否有效便可
  3. 給定數獨序列只包含數字 1-9 和字符 '.'
  4. 給定數獨永遠是 9x9 形式的。`

解題思路

  • 一次迭代緩存

  • 首先,讓咱們來討論下面兩個問題:

  • 如何枚舉子數獨? 可使用 box_index = (row / 3) * 3 + columns / 3,其中 / 是整數除法。

一次迭代

  • 如何確保行 / 列 / 子數獨中沒有重複項? 能夠利用 value -> count 哈希映射來跟蹤全部已經遇到的值。

  • 如今,咱們完成了這個算法的全部準備工做:

  1. 遍歷數獨。
  2. 檢查看到每一個單元格值是否已經在當前的行 / 列 / 子數獨中出現過:
  3. 若是出現重複,返回 false
  4. 若是沒有,則保留此值以進行進一步跟蹤。
  5. 返回 true

確保行 / 列 / 子數獨中沒有重複項

public boolean isValidSudoku(char[][] board) {
    Set seen = new HashSet();
    for (int i=0; i<9; ++i) {
        for (int j=0; j<9; ++j) {
            char number = board[i][j];
            if (number != '.')
                if (!seen.add(number + " in row " + i) ||
                    !seen.add(number + " in column " + j) ||
                    !seen.add(number + " in block " + i / 3 + "-" + j / 3))
                    return false;
        }
    }
    return true;
}
複製代碼


旋轉圖像


  • 給定一個N×N的二維矩陣表示圖像,90度順時針旋轉圖像。

  • 示例 :

輸入: [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
輸出: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
解釋: 首先翻轉每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];
     而後反轉圖片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
複製代碼
  • 說明:
1 <= A.length = A[0].length <= 20
0 <= A[i][j] <= 1
複製代碼

解題思路

  • 咱們先來看看每一個元素在旋轉的過程當中是如何移動的:

如何移動

  • 這提供給咱們了一個思路,將給定的矩陣分紅四個矩形而且將原問題劃歸爲旋轉這些矩形的問題。

旋轉這些矩形

  • 如今的解法很直接 -- 能夠在第一個矩形中移動元素而且在 長度爲 4 個元素的臨時列表中移動它們。

變化過程

public void rotate(int[][] matrix) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
        return;
    }

    int length = matrix.length;

    for (int i = 0; i < length / 2; i++) {
        for (int j = 0; j < (length + 1) / 2; j++){
            int tmp = matrix[i][j];
            matrix[i][j] = matrix[length - j - 1][i];
            matrix[length -j - 1][i] = matrix[length - i - 1][length - j - 1];
            matrix[length - i - 1][length - j - 1] = matrix[j][length - i - 1];
            matrix[j][length - i - 1] = tmp;
        }
    }   
}
複製代碼


二進制 / 位運算


二進制位運算

優勢:

特定狀況下,計算方便,速度快,被支持面廣
若是用算數方法,速度慢,邏輯複雜
位運算不限於一種語言,它是計算機的基本運算方法
複製代碼

知識點預熱


在這裏插入圖片描述



只出現一次的數字


  • 給出 2 * n + 1個數字,除其中一個數字以外其餘每一個數字均出現兩次,找到這個數字。

異或運算具備很好的性質,相同數字異或運算後爲0,而且具備交換律和結合律,故將全部數字異或運算後便可獲得只出現一次的數字。

示例 :

輸入: [4,1,2,1,2]
輸出: 4
複製代碼

解題思路

  • 若是咱們對 0 和二進制位作 XOR 運算,獲得的仍然是這個二進制位 a \oplus 0 = a a⊕0=a

  • 若是咱們對相同的二進制位作 XOR 運算,返回的結果是 0 a \oplus a = 0 a⊕a=0

  • XOR 知足交換律和結合律 a \oplus b \oplus a = (a \oplus a) \oplus b = 0 \oplus b = ba⊕b⊕a=(a⊕a)⊕b=0⊕b=b

  • 因此咱們只須要將全部的數進行 XOR 操做,獲得那個惟一的數字。

public int singleNumber(int[] A) {
    if(A == null || A.length == 0) {
        return -1;
    }
    int rst = 0;
    for (int i = 0; i < A.length; i++) {
        rst ^= A[i];
    }
    return rst;
}
複製代碼

複雜度分析

  • 時間複雜度: O(n) 。咱們只須要將 \text{nums} 中的元素遍歷一遍,因此時間複雜度就是 \text{nums} 中的元素個數。
  • 空間複雜度:O(1)


格雷編碼

  • 格雷編碼是一個二進制數字系統,在該系統中,兩個連續的數值僅有一個二進制的差別。給定一個非負整數 n ,表示該代碼中全部二進制的總數,請找出其格雷編碼順序。一個格雷編碼順序必須以 0 開始,並覆蓋全部的 2n 個整數。例子——輸入:2;輸出:[0, 1, 3, 2];解釋: 0 - 001 - 013 - 112 - 10

解題思路

  • 格雷碼生成公式:G(i) = i ^ (i >> 2)
public ArrayList<Integer> grayCode(int n) {
    ArrayList<Integer> result = new ArrayList<Integer>();
    for (int i = 0; i < (1 << n); i++) {
        result.add(i ^ (i >> 1));
    }
    return result;
}
複製代碼


其餘


其餘



整數反轉


  • 將一個整數中的數字進行顛倒,當顛倒後的整數溢出時,返回 0 (標記爲 32 位整數)。

  • 示例 :

輸入: -123
輸出: -321
複製代碼

解題思路

  • 利用除 10 取餘的方法,將最低位和最高倒序輸出便可
public int reverseInteger(int n) {
    int reversed_n = 0;
    
    while (n != 0) {
        int temp = reversed_n * 10 + n % 10;
        n = n / 10;
        if (temp / 10 != reversed_n) {
            reversed_n = 0;
            break;
        }
        reversed_n = temp;
    }
    return reversed_n;
}
複製代碼


LRU緩存策略


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

  • 獲取數據 get(key) - 若是密鑰 (key) 存在於緩存中,則獲取密鑰的值(老是正數),不然返回 -1。

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

示例:

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
複製代碼

解題思路

解法一:

  • 自定義數據結構:
  1. 實現一個鏈表用於記錄緩存,並處理調用使用頻率
  2. 定義一個 HashMap 用於記錄緩存內容
public class LRUCache {
    private class Node{
        Node prev;
        Node next;
        int key;
        int value;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
            this.prev = null;
            this.next = null;
        }
    }

    private int capacity;
    private HashMap<Integer, Node> hs = new HashMap<Integer, Node>();
    private Node head = new Node(-1, -1);// 頭
    private Node tail = new Node(-1, -1);// 尾

    public LRUCache(int capacity) {
        this.capacity = capacity;
        tail.prev = head;
        head.next = tail;
    }

    public int get(int key) {
        if( !hs.containsKey(key)) {    		//key找不到
            return -1;
        }

        // remove current
        Node current = hs.get(key);
        current.prev.next = current.next;
        current.next.prev = current.prev;

        // move current to tail
        move_to_tail(current);			//每次get,使用次數+1,最近使用,放於尾部

        return hs.get(key).value;
    }

    public void set(int key, int value) {			//數據放入緩存
        // get 這個方法會把key挪到最末端,所以,不須要再調用 move_to_tail
        if (get(key) != -1) {
            hs.get(key).value = value;
            return;
        }

        if (hs.size() == capacity) {		//超出緩存上限
            hs.remove(head.next.key);		//刪除頭部數據
            head.next = head.next.next;
            head.next.prev = head;
        }

        Node insert = new Node(key, value);		//新建節點
        hs.put(key, insert);
        move_to_tail(insert);					//放於尾部
    }

    private void move_to_tail(Node current) {    //移動數據至尾部
        current.prev = tail.prev;
        tail.prev = current;
        current.prev.next = current;
        current.next = tail;
    }
}
複製代碼

解法二:

  • 題目要求實現 LRU 緩存機制,須要在 O(1)時間內完成以下操做:
  1. 獲取鍵 / 檢查鍵是否存在
  2. 設置鍵
  3. 刪除最早插入的鍵
  4. 前兩個操做能夠用標準的哈希表在 O(1) 時間內完成。
  • 有一種叫作有序字典的數據結構,綜合了哈希表鏈表,在 Java 中爲 LinkedHashMap

  • 下面用這個數據結構來實現。

class LRUCache extends LinkedHashMap<Integer, Integer>{
    private int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity; 
    }
}
複製代碼

複雜度分析

  • 時間複雜度:對於 put 和 get 操做複雜度是 O(1),由於有序字典中的全部操做:
  • get/in/set/move_to_end/popitem(get/containsKey/put/remove)均可以在常數時間內完成。 空間複雜度:O(capacity),由於空間只用於有序字典存儲最多 capacity + 1 個元素。


Attention


  • 爲了提升文章質量,防止冗長乏味

下一部分算法題

  • 本片文章篇幅總結越長。我一直以爲,一片過長的文章,就像一堂超長的 會議/課堂,體驗很很差,因此我打算再開一篇文章

  • 在後續文章中,我將繼續針對鏈表 隊列 動態規劃 矩陣 位運算 等近百種,面試高頻算法題,及其圖文解析 + 教學視頻 + 範例代碼,進行深刻剖析有興趣能夠繼續關注 _yuanhao 的編程世界

  • 不求快,只求優質,每篇文章將以 2 ~ 3 天的週期進行更新,力求保持高質量輸出



相關文章


歡迎關注_yuanhao的掘金!




爲了方便你們跟進學習,我在 GitHub 創建了一個倉庫


倉庫地址:超級乾貨!精心概括視頻、歸類、總結,各位路過的老鐵支持一下!給個 Star !

請點贊!由於你的鼓勵是我寫做的最大動力!

android
相關文章
相關標籤/搜索