「面試原題 + 圖文詳解 + 實例代碼」二叉搜索樹-雙指針-貪心 面試題彙總

面試


本文將覆蓋 「字符串處理」 + 「動態規劃」 方面的面試算法題,文中我將給出:html

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

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

知識點

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

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

java

如今就讓咱們開始吧!node



二叉搜索樹

二叉搜索樹(Binary Search Tree),它或者是一棵空樹,或者是具備下列性質的二叉樹:android

  1. 若它的左子樹不空,則左子樹上全部結點的值均小於它的根結點的值;
  2. 若它的右子樹不空,則右子樹上全部結點的值均大於它的根結點的值;
  3. 它的左、右子樹也分別爲二叉搜索樹。

二叉搜索樹





驗證二叉搜索樹

給定一個二叉樹,判斷其是不是一個有效的二叉搜索樹。git

假設一個二叉搜索樹具備以下特徵:github

  1. 節點的左子樹只包含小於當前節點的數。
  2. 節點的右子樹只包含大於當前節點的數。
  3. 全部左子樹和右子樹自身必須也是二叉搜索樹。

示例 :面試

輸入:
    5
   / \
  1   4
     / \
    3   6
輸出: false
解釋: 輸入爲: [5,1,4,null,null,3,6]。
     根節點的值爲 5 ,可是其右子節點值爲 4 。

解題思路

乍一看,這是一道很簡單的題。只須要遍歷整棵樹,檢查 node.right.val > node.val 和
node.left.val < node.val 對每一個結點是否成立。算法

問題是,這種方法並不老是正確。不只右子結點要大於該節點,整個右子樹的元素都應該大於該節點。例如:這意味着咱們須要在遍歷樹的同時保留結點的上界與下界,在比較時不只比較子結點的值,也要與上下界比較。編程

上述思路能夠用遞歸法實現:數組

  • 首先將結點的值與上界和下界(若是有)比較。而後,對左子樹和右子樹遞歸進行該過程。

視頻

視頻講解和源碼-驗證二叉搜索樹

public boolean isValidBST(TreeNode root) {
    return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}

private boolean isValidBST(TreeNode root, long min, long max){
    if (root == null) {
        return true;
    }
    
    if (root.val <= min || root.val >= max){
        return false;
    }
    
    return isValidBST(root.left, min, root.val) && isValidBST(root.right, root.val, max);
}





二叉搜索樹中第K小的元素

給定一個二叉搜索樹,編寫一個函數 kthSmallest 來查找其中第 k 個最小的元素。

說明:
你能夠假設 k 老是有效的,1 ≤ k ≤ 二叉搜索樹元素個數。

示例 :

輸入: root = [5,3,6,2,4,null,null,1], k = 3
       5
      / \
     3   6
    / \
   2   4
  /
 1
輸出: 3

解題思路

  1. 增長 getCount 方法來獲取傳入節點的子節點數(包括本身)
  2. 從 root 節點開始判斷k值和子節點數的大小決定遞歸路徑是往左仍是往右。
public int kthSmallest(TreeNode root, int k) {
    if (root == null) {
        return 0;
    }
    
    int leftCount = getCount(root.left);
    if (leftCount >= k) {
        return kthSmallest(root.left, k);
    } else if (leftCount + 1 == k) {
        return root.val;
    } else {
        //注(1)
        return kthSmallest(root.right, k - leftCount - 1);
    }
}

private int getCount(TreeNode root) {
    if (root == null) {
        return 0;
    }
    
    return getCount(root.left) + getCount(root.right) + 1;
}

注:
(1)爲何是 k - leftCount - 1 而不是 k ,咱們能夠把當前的二叉樹當作左右兩部分。在執行到這個條件的時候,很明顯,左邊 leftCount 個數,加上根節點,都小於所要求的元素。接着,如今要從右子樹搜索,很明顯,搜索是往下的,不可能往上(原根節點的方向)搜索,故,以前 leftCount + 1 個數做廢,因此所傳入 k - leftCount - 1





數組 / 雙指針

所謂雙指針

指的是在遍歷對象的過程當中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向或者相反方向的指針進行掃描,從而達到相應的目的。

換言之,雙指針法充分使用了數組有序這一特徵,從而在某些狀況下可以簡化一些運算。

加一

給定一個非負數,表示一個數字數組,在該數的基礎上+1,返回一個新的數組。該數字按照數位高低進行排列,最高位的數在列表的最前面。

示例 :

輸入: [4,3,2,1]
輸出: [4,3,2,2]
解釋: 輸入數組表示數字 4321。

解題思路

只須要判斷有沒有進位並模擬出它的進位方式,如十位數加 11 個位數置爲 00,如此循環直到判斷沒有再進位就退出循環返回結果。

而後還有一些特殊狀況就是當出現 999九、999999 之類的數字時,循環到最後也須要進位,出現這種狀況時須要手動將它進一位。

加一

視頻

給定一個由整數組成的非空數組所表示的非負整數,在該數的基礎上加一

public int[] plusOne(int[] digits) {
        for (int i = digits.length - 1; i >= 0; i--) {
            digits[i]++;
            digits[i] = digits[i] % 10;
            if (digits[i] != 0) return digits;
        }
        digits = new int[digits.length + 1];
        digits[0] = 1;
        return digits;
    }





刪除元素

給定一個數組和一個值,在原地刪除與值相同的數字,返回新數組的長度。

解題思路

  1. 定義一個 index 用於記錄新數組下標,遍歷數組
  2. 若是與傳入值不一樣,則其應存在於新數組中 index++ 並存入
  3. 若是與傳入值相同,說明重複,則直接跳過該數
  4. 最後返回 index 便可
public int removeElement(int[] A, int elem) {
    if (A == null || A.length == 0) {
        return 0;
    }
    
    int index = 0;
    for (int i = 0; i < A.length; i++) {
        if (A[i] != elem) {
            A[index++] = A[i];
        } 
    }
    
    return index;
}





刪除排序數組中的重複數字

在原數組中「刪除」重複出現的數字,使得每一個元素只出現一次,而且返回「新」數組的長度。

示例 :

給定 nums = [0,0,1,1,1,2,2,3,3,4],

函數應該返回新的長度 5, 而且原數組 nums 的前五個元素被修改成 0, 1, 2, 3, 4。

你不須要考慮數組中超出新長度後面的元素。

解題步驟

  1. 數組完成排序後,咱們能夠放置兩個指針 size 和 i,其中 size 是慢指針,而 i 是快指針。
  2. 只要 nums[size] = nums[i] ,咱們就增長 i 以跳太重複項。
  3. 當咱們遇到 nums[i] =nums[size] 時,跳太重複項的運行已經結束
  4. 所以咱們必須把它(nums[i])的值複製到 nums[size+1]。
  5. 而後遞增 i 接着咱們將再次重複相同的過程,直到 size 到達數組的末尾爲止。

刪除排序數組中的重複數字

public int removeDuplicates(int[] A) {
    if (A == null || A.length == 0) {
        return 0;
    }
    
    int size = 0;
    for (int i = 0; i < A.length; i++) {
        if (A[i] != A[size]) {
            A[++size] = A[i];
        }
    }
    // (1)
    return size + 1;
}

注:由於 size 爲下標,因此返回長度要加一





個人日程安排表 I

實現MyCalendar類來存儲活動。若是新添加的活動沒有重複,則能夠添加。類將有方法book(int start,int end)。這表明左閉右開的間隔[start,end)有了預約,範圍內的實數x,都知足start <= x < end,返回true。 不然,返回false,而且事件不會添加到日曆中。

示例 :

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(15, 25); // returns false
MyCalendar.book(20, 30); // returns true
解釋: 
第一個日程安排能夠添加到日曆中.  第二個日程安排不能添加到日曆中,由於時間 15 已經被第一個日程安排預約了。
第三個日程安排能夠添加到日曆中,由於第一個日程安排並不包含時間 20 。

解題步驟

  1. TreeMap 是一個有序的key-value集合,它經過 紅黑樹 實現,繼承於AbstractMap,因此它是一個Map,即一個key-value集合。
  2. TreeMap能夠查詢小於等於某個值的最大的key,也可查詢大於等於某個值的最小的key。
  3. 元素的順序能夠改變,而且對新的數組不會有影響。
  • floorKey(K key) 方法用於返回小於或等於給定的鍵的全部鍵中,的最大鍵,或null,若是不存在這樣的鍵

  • ceilingKey(K key) 方法用於返回大於或等於返回到給定的鍵中,的最小鍵,或null,若是不存在這樣的鍵

class MyCalendar {
    TreeMap<Integer, Integer> calendar;

    MyCalendar() {
        calendar = new TreeMap();
    }

    public boolean book(int start, int end) {
        Integer previous = calendar.floorKey(start), next = calendar.ceilingKey(start);
        if ((previous == null || calendar.get(previous) <= start) && (next == null || end <= next)) {
            calendar.put(start, end);
            return true;
        }
        return false;
    }
}





合併兩個有序數組

合併兩個排序的整數數組A和B變成一個新的數組。能夠假設A具備足夠的空間去添加B中的元素。

說明:

初始化 A 和 B 的元素數量分別爲 m 和 n。
你能夠假設 A 有足夠的空間(空間大小大於或等於 m + n)來保存 B 中的元素。

示例:

輸入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6],       n = 3

輸出: [1,2,2,3,5,6]

解題思路

  • 雙指針 / 從後往前
  • 這裏的指針 p 用於追蹤添加元素的位置。

圖一
圖二

public void mergeSortedArray(int[] A, int m, int[] B, int n) {
    int i = m - 1, j = n - 1, index = m + n - 1;
    while (i >= 0 && j >= 0) {
        if (A[i] > B[j]) {
            A[index--] = A[i--];
        } else {
            A[index--] = B[j--];
        }
    }
    while (i >= 0) {
        A[index--] = A[i--];
    }
    while (j >= 0) {
        A[index--] = B[j--];
    }
}





貪心

顧名思義,貪心算法老是做出在當前看來最好的選擇。也就是說貪心算法並不從總體最優考慮,它所做出的選擇只是在某種意義上的局部最優選擇。固然,但願貪心算法獲得的最終結果也是總體最優的。雖然貪心算法不能對全部問題都獲得總體最優解,但對許多問題它能產生總體最優解。如單源最短路經問題,最小生成樹問題等。在一些狀況下,即便貪心算法不能獲得總體最優解,其最終結果倒是最優解的很好近似。

視頻

貪心算法 - 2 理論基礎

買賣股票的最佳時機

假設有一個數組,它的第i個元素是一支給定的股票在第i天的價格。若是你最多隻容許完成一次交易(例如,一次買賣股票),設計一個算法來找出最大利潤。

注意:

  • 你不能在買入股票前賣出股票。

示例 :

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

若是將測試範例 [7, 1, 5, 3, 6, 4]

繪製成圖,咱們發現:

買賣股票的最佳時機

  1. 咱們須要找到最小的谷以後的最大的峯。
  2. 咱們能夠維持兩個變量 —— min 和 profit,它們分別對應迄今爲止所獲得的最小的谷值和最大的利潤(賣出價格與最低價格之間的最大差值)。
public int maxProfit(int[] prices) {
    if (prices == null || prices.length == 0) {
        return 0;
    }

    int min = Integer.MAX_VALUE;  //記錄最低的價格
    int profit = 0;
    for (int price : prices) {
        min = Math.min(price, min);
        profit = Math.max(price - min, profit);
    }

    return profit;
}





買賣股票的最佳時機 II

給定一個數組 prices 表示一支股票天天的價格。能夠完成任意次數的交易, 不過不能同時參與多個交易,設計一個算法求出最大的利潤。

解題思路

貪心:

  1. 只要相鄰的兩天股票的價格是上升的,
  2. 咱們就進行一次交易, 得到必定利潤。

在買賣股票的最佳時機 II

視頻

買賣股票的最佳時機 II by 代碼會說話

public int maxProfit(int[] prices) {
        int profit = 0;
        for(int i = 0 ; i < prices.length -1; i++) {
            if(prices[i + 1] > prices[i]) {
                profit += prices[i + 1] - prices[i];
            }
        }
        return profit;
    }





最大子數組

給定一個整數數組,找到一個具備最大和的子數組,返回其最大和。

示例:

輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。

解題思路

最大子數組

public int maxSubArray(int[] nums) {
        if(nums == null || nums.length == 0) {
            return 0;
        }
        int max = Integer.MIN_VALUE;
        int sum = 0;
        for (int num : nums) {
            sum += num;
            max = Math.max(sum, max);
            sum = Math.max(sum, 0);
        }
        return max;
    }





主元素

給定一個整型數組,找出主元素,它在數組中的出現次數嚴格大於數組元素個數的二分之一(能夠假設數組非空,且數組中老是存在主元素)。

解題思路

  1. 重點在於:主元素數量大於數組全部元素的二分之一
  2. 因此咱們要作的是,選出一個出現次數大於其餘全部數,出現次數和的數便可
  3. 設一個計數器 currentMajor 候選數 和 一個 count 用於記錄次數
  4. 每當當前數和 currentMajor 值相同時, count 值 +1
  5. 每當當前數和 currentMajor 值不一樣時, count 值 -1
  6. 每次 count 等於 0 時,說明在以前訪問的數組裏 currentMajor 的數量小於或等於一半
  7. 則將 currentMajor 賦值爲當前數,繼續尋找。
public int majorityNumber(List<Integer> nums) {
    int currentMajor = 0;
    int count = 0;

    for(Integer num : nums) {
        if(count == 0) {
            currentMajor = num;
        }
        
        if(num == currentMajor) {
            count++;
        } else {
            count--;
        }
    }
    return currentMajor;
}





Attention

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

下一部分算法題

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

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

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




# 相關文章

面試高頻算法題彙總「圖文解析 + 教學視頻 + 範例代碼」之 二分 + 哈希表 + 堆 + 優先隊列 合集
🔥面試必備:高頻算法題彙總「圖文解析 + 教學視頻 + 範例代碼」必知必會 排序 + 二叉樹 部分!🔥
每一個人都要學的圖片壓縮終極奧義,有效解決 Android 程序 OOM
Android 讓你的 Room 搭上 RxJava 的順風車 從重複的代碼中解脫出來
ViewModel 和 ViewModelProvider.Factory:ViewModel 的建立者
單例模式-全局可用的 context 對象,這一篇就夠了
縮放手勢 ScaleGestureDetector 源碼解析,這一篇就夠了
Android 屬性動畫框架 ObjectAnimator、ValueAnimator ,這一篇就夠了
看完這篇再不會 View 的動畫框架,我跪搓衣板
看完這篇還不會 GestureDetector 手勢檢測,我跪搓衣板!





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

請點贊





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

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

相關文章
相關標籤/搜索