算法解題技巧總結

專欄原創出處:github-源筆記文件 github-源碼 ,歡迎 Star,轉載請附上原文出處鏈接和本聲明。

1. 解題思路

拿到題目,開始考慮暴力解法,藉助暴力方法我們可以在過程上繼續優化,循序漸進的優化複雜度。

  • 尋找關鍵字

    提取題目關鍵字,比如給定一個「二叉樹」與給定一個「完美二叉樹」解題思路可能完全不一樣。案例 力扣 116 / 力扣 117

  • 畫圖分析

    許多問題依靠大腦不能很好的立馬想出方案,尤其涉及鏈表相關問題。推薦採用畫圖模式分析解題過程,這樣在轉換代碼時更爲流暢。

  • 注意邊界、重複、溢出問題

    一定考慮數據邊界問題,尤其是 >= > < <= null 空頭、空尾... 之類的問題,很多 BUG 都是因爲邊界處理不周全導致的。注意遞歸重複計算問題,返回值溢出問題。

  • 優先考慮原地算法

    先不考慮藉助外部數據結構嘗試原地算法 ,當然如果本身數據不允許修改本身時只能藉助外部數據臨時存儲。

  • 藉助外部數據結構

    分析題目過程中的一些特點,是否執行替換操作、當前操作是否依賴歷史數據、過程數據的流向。
    一般情況,如果涉及替換、反轉類操作考慮指針(雙指針)。如果涉及歷史數據時考慮指針記錄是否滿足,不滿足時使用數據結構。比如先進後出、先進先出、KV 映射哈希表。
    在使用數據結構時,儘量減少空間複雜度。比如我們的一個數組,可以記錄一個下標,前半部分用於刪除後半部分用於修改。

2. 雙指針技巧

2.1 對撞指針-兩個指針從兩端向中間迭代

一個指針從始端開始,而另一個指針從末端開始。

關鍵詞:反轉字符串、排序數組、原地算法、首尾替換

// 案例:將輸入的字符串反轉過來
// 輸入:["h","e","l","l","o"] 輸出:["o","l","l","e","h"]
class Solution {
    public void reverseString(char[] s) {
        int min = 0; // 開始位置
        int max = s.length - 1; // 末尾位置
        while (min < max) { // 向中間迭代
            char minVal = s[min];
            s[min++] = s[max];
            s[max--] = minVal;
        }
    }
}

2.2 窗口指針-兩個指針保持一定距離

使用兩個指針,兩個指針保持一定的窗口距離進行迭代。

關鍵詞:刪除倒數第 N 個問題

// 案例:刪除鏈表的倒數第 N 個節點
// 給定一個鏈表,刪除鏈表的倒數第 n 個節點,並且返回鏈表的頭結點。
public ListNode removeNthFromEnd(ListNode head, int n) {
   ListNode sentinel = new ListNode(0); // 哨兵節點,避免頭節點 null 問題
   sentinel.next = head;

   ListNode first = sentinel; // 第一個指針
   ListNode second = sentinel;// 第二個指針

   // 第二個指針先前進 n 步
   for (int i = 0; i < n; i++) second = second.next;
   
   while (second.next != null) { // 同步向後迭代,第二個指針到末尾即尋找結束
       first = first.next;
       second = second.next;
   }
   first.next = first.next.next; // 第一個指針指向的節點,即爲移除節點
   return sentinel.next;
}

2.3 快慢指針

使用兩個指針,一個快指針和一個慢指針。指針的移動策略取決於條件本身。

關鍵詞:原地刪除、尋找環形鏈表環形入口點(環形鏈表 II)、兩個單鏈表相交的起始節點(相交鏈表)

// 案例:移除元素
// 給定一個數組 nums 和一個值 val,你需要原地移除所有數值等於 val 的元素,返回移除後數組的新長度。
public int removeElement(int[] nums, int val) {
    int i = 0; // 快指針
    int k = nums.length - 1; // 慢指針
    while (i <= k) {
        if (nums[i] == val) { // 不相等時將值交換到末尾
            nums[i] = nums[k--];
        } else {
            i++;
        }
    }
    return k + 1;
}

// 案例:判斷單鏈表是否是環形鏈表(鏈表中有一個環,其尾部連接到前面的節點。)
// 雙指針,每次移動慢指針一步,而移動快指針兩步。每一次迭代,快速指針將額外移動一步。
// 如果環的長度爲 M,經過 M 次迭代後,快指針肯定會多繞環 N 周,並趕上慢指針。
 public boolean hasCycle(ListNode head) {
     ListNode slow = head; // 慢指針
     ListNode fast = slow; // 快指針
     do {
         if (fast == null || fast.next == null) {
             return false;
         }
         slow = slow.next;
         fast = fast.next.next;
     } while (fast != slow);
     return true;
}

3. 哨兵節點技巧

我們在移除單鏈表的元素時,如果移除的是中間的元素,直接使用「curr」「prev」兩個指針來處理。
但是如果移除的是頭部節點時,「prev」爲空。使用了哨兵節點後我們避免對 「null」 處理

哨兵節點廣泛應用於樹和鏈表中,如僞頭、僞尾、標記等,它們是純功能的,通常不保存任何數據,其主要目的是使鏈表標準化,如使鏈表永不爲空、永不無頭、簡化插入和刪除。

// 案例:刪除鏈表中等於給定值 val 的所有節點。
public ListNode removeElements1(ListNode head, int val) {
    ListNode sentinel = new ListNode(0); // 哨兵節點
    sentinel.next = head; // 本身的頭節點掛在到哨兵節點
    
    ListNode curr = head; // 當前節點
    ListNode prev = sentinel; // 當前節點的上一個節點,默認爲哨兵節點
    
    while (curr != null) {
        if (curr.val == val) { // 相等,移除當前節點 curr
            // 上一個節點直接指向下下一個節點。如果不用哨兵節點需要判斷 null 重置頭節點
            prev.next = curr.next; 
        } else prev = curr; // 不能移除時,當前節點更新爲上一個節點
        curr = curr.next; // 繼續尋找下一個節點
    }
    return sentinel.next; // 移除哨兵節點返回真實的頭節點
}

4. 求和(進位、最終進位)

求和問題需要考慮進位問題,最終結果考慮是否加 1 。

99+199 + 1 運算,通常我們會考慮進位問題,有時也會忽略了處理最終進位時錯誤的輸出 00,百位的 1 可能丟失。

// 案例:兩數相加 https://leetcode-cn.com/problems/add-two-numbers/
輸入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
輸出:7 -> 0 -> 8
原因:342 + 465 = 807

輸入:(9 -> 9 -> 9) + (1)
輸出:1 -> 0 -> 0 -> 0
原因:999 + 1 = 1000

5. 返回根節點(頭節點)

一般在鏈表或者二叉樹中,題目要求返回根節點,本質上我們返回的是一個重新構造出來的新結構,構造順序爲由下到上的過程。

這一點的理解有助於你在解類似題目時思維的擴散性,其實本質上是遞歸由下到上時返回後計算的過程。可參考 如何輕而易舉的寫出遞歸函數

// 案例:合併兩個有序的鏈表並返回合併後的頭節點
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) return l2; // 尾節點
    if (l2 == null) return l1; // 尾節點

    if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2); // 當前 L1 爲頭,鏈接子節點
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next); // 當前 L2 爲頭,鏈接子節點
        return l2;
    }
}

參考

更多相關專欄內容彙總:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章