資料結構-遞迴

小墨魚3發表於2020-01-31

連結串列與遞迴

簡單介紹

前面我們已經介紹了連結串列, 然而這個主題主要是用於擴充套件學習連結串列的, 以及遞迴的基本使用。 連結串列練習主要以LeetCode為主, 然後給大家認識一下基礎的遞迴使用以及呼叫流程等。

LeetCode連結串列問題

203. 移除連結串列元素

刪除連結串列中等於給定值 val 的所有節點
示例:
輸入: 1->2->6->3->4->5->6, val = 6 <>
輸出: 1->2->3->4->5
複製程式碼

然後, LeetCode給出的Node程式碼如下:

public class ListNode {
  int val;
  ListNode next;
  ListNode(int x) { val = x; }
}
複製程式碼

使用非虛擬頭結點解決方法:


public class Solution {
    public ListNode removeElements(ListNode head, int val) {

        // 刪除頭結點, 需要優先判斷是否為空
        while (head != null && head.val == val) {
            head = head.next;
        }

        if (head == null) {
            return null; // 如果全部都是要刪除的節點資料, 則中間資料不需要處理了
        }

        ListNode prev = head ;
        // 處理中間資料集
        while (prev.next != null) {
            if (prev.next.val == val) {
                prev.next = prev.next.next;
            } else {
                prev = prev.next;
            }
        }

        return head;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        ListNode node1 = new ListNode(3);
        ListNode node2 = new ListNode(3);
        ListNode node3 = new ListNode(3);
        ListNode node4 = new ListNode(3);

        node1.next = node2;
        node2.next = node3;
        node3.next = node4;

        s.removeElements(node1, 3);
    }
}
複製程式碼

虛擬頭結點版本

public ListNode removeElements(ListNode head, int val) {

        // 由於虛擬節點永遠不會被訪問到, 所以給個負數
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;


        // 應為有了虛擬頭結點, 所以不需要關心頭節點問題, 直接遍歷
        ListNode prev = dummyHead ;
        // 處理中間資料集

        while (prev.next != null) {
            if (prev.next.val == val) {
                prev.next = prev.next.next;
            } else {
                prev = prev.next;
            }
        }

        // 虛擬節點是不會外暴露的, 需要需要next
        return dummyHead.next;
}
複製程式碼

遞迴

遞迴簡介

是指在函式的定義中使用函式自身的方法, 即自己呼叫自己。
本質上, 將原來的問題轉換為更小的同一問題。

遞迴的優缺點

  • 優點

    • 大問題化為小問題,可以極大的減少程式碼量
    • 用有限的語句來定義物件的無限集合
    • 程式碼更簡潔清晰,可讀性更好
  • 缺點

    • 遞迴呼叫函式,浪費空間
    • 遞迴太深容易造成堆疊的溢位
參考:
  https://blog.csdn.net/acmman/article/details/80547512
  https://blog.csdn.net/laoyang360/article/details/7855860
複製程式碼

第一個遞迴例子

現在我們有一組陣列, 我們如何通過遞迴的形式進行累加並返回結果呢?

看看遞迴是如何呼叫的(後面還會詳細介紹遞迴呼叫流程)

avatar


/**
  遞迴返回陣列中的總和
*/
public class ArrayElementSum {

    public static int sum(int[] arr) {
        return sum(arr, 0) ;
    }

    private static int sum(int[] arr, int l) {
        if (l == arr.length)
            return 0; // 遞迴出口, 問題已經最小無法在分解了
        return arr[l] + sum(arr, ++ l); // 遞迴將問題更小化
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4, 5, 6, 7, 8} ;
        System.out.println(sum(nums));
    }
}
複製程式碼

遞迴函式執行流程

我們在學習棧的應用的時候, 就是程式呼叫的系統棧。在一個函式中呼叫一個子函式就會壓入一個系統棧, 當子函式執行完之後就會從系統棧彈出 然後回到上次指定的地方並繼續。

其實遞迴呼叫也就是這麼一個過程, 只不過就是呼叫的還是這個函式本身而已。

透過下圖, 我們更加直觀的瞭解上面的遞迴例子的執行流程把。

avatar

假設現在陣列中只有數字[6, 10] 方便進行除錯。陣列長度為2。

sum函式執行過程為:

  1. 判斷當前位置是否等於陣列長度, 如果等於則返回, 否則往下執行
  2. 繼續呼叫sum函式, 並傳入對應的位置
  3. 當第一步成立後, 獲取到函式返回值並加上當前位置陣列的值。
  4. 返回累加後的結果

可能會迷糊的一些點:

  1. sum函式在呼叫sum函式的時候, 是新開闢的空間, 對應的引數引數值都是不同的,所以裡面的變數是相互不會影響的。
  2. 千萬不要被sum函式給迷惑了, 其本質上就是相當於A函式呼叫B函式, B函式呼叫C函式, 只不過現在是sum呼叫sum呼叫sum而已。我們可以編個號sum$0呼叫sum$1,sum$1呼叫sum$2, 然後sum$2的把結果返回給了sum$1, sum$1進行餘下操作把結果sum$0, sum$0做完餘下操作返回最終的結果值。這樣是不是會好一點理解呢?



上面的例子可能會比較簡單一點, 那下面我們來稍微複雜一點的, 比如刪除連結串列的元素。

avatar

removeElements執行流程只有三步

  1. 判斷當前頭結點是否為null, 如果為null則返回
  2. 當前頭結點的下一個節點為誰呢? 現在還不知道, 一直重複步驟1和2, 知道步驟1滿足為止
  3. 如果當前頭結點是要被刪除的節點, 則返回的是他的下一個節點資料, 否則返回當前頭結點資料

avatar

  1. 當我們以6為頭結點執行第一步判斷不為空則進入第二步"6"的下一個節點當前還不可知
  2. 當我們以7為頭結點執行第一步判斷不為空則進入第二步"7"的下一個節點當前還不可知
  3. 當我們以8為頭結點執行第一步判斷不為空則進入第二步"8"的下一個節點當前還不可知
  4. 當我們傳入NULL後

下圖

avatar

  1. 返回NULL值
  2. 頭結點為8的next值為null, 判斷當前頭結點是否需要被刪除, 當前節點不被刪除返回 8 -> NULL
  3. 頭結點為7的next值為8 -> NULL, 判斷是否需要被刪除, 需要刪除則返回下一個節點即: 8 -> NULL
  4. 頭結點為6的next值為8 -> NULL, 判斷是否需要被刪除, 不需要則返回 6 -> 8 -> NULL返回資料

到這裡就已經結束了整個呼叫流程了。

avatar

相關文章