數據結構【九】- 遞歸【二】鏈表的天然遞歸性 / 用遞歸解決鏈表中刪除元素的問題 /遞歸算法的調式

 

 

一。鏈表的天然遞歸性

對於一個鏈表,我們可以把它看成:

  • 【0節點】+【一個更短的鏈表(少了0節點的其他節點)】
  • 【一個更短的鏈表(少了0節點的其他節點)】= 【1節點】+【一個更短的鏈表(少了0和1節點的其他節點)】
  •                         ................以此類推..................
  • 【一個更短的鏈表(少了0,1,2,3節點的其他節點)】= 【4節點】+【NUll】
  •  最後我們可以理解爲【NULL】本身也是一個鏈表:這裏的【NULL】就是那個最基礎的鏈表,也就是基本問題

       

二。遞歸刪除鏈表元數的思路分析

遞歸算法分爲兩部分:

  • 求解最基本的問題
  • 將原問題轉化爲更小的問題

1. 對於最原始的鏈表,我們可以理解爲:最原始的鏈表 = 【頭節點e】+【一個更短的鏈表】

         

2. 我們現在假定有一個函數:【可以刪除一個更短的鏈表中的所有相應元素】,刪除完之後,【一個更短的鏈表】就變成了圖中紅色的部分。這個紅色的部分,是經過刪除的函數後得到的結果。

            

3. 但是第二步中我們沒有考慮【頭節點e】的情況。

  • 如果【頭節點e】不是刪除的節點,最終【原問題的結果】=【頭節點e】+【子問題求得的紅色的鏈表】
  • 如果【頭節點e】是刪除的節點,最終【原問題的結果】=【子問題求得的紅色的鏈表】

     

 

三。removeElements方法

removeElements() 的宏觀語意就是:對一個鏈表中刪除值爲val的節點

1.求解最基本的問題的解:也就是當鏈表爲NULL時的解

2. 更小問題的解:在【少了當前頭節點】的鏈表中,刪除值爲val的節點。刪除的結果賦給一個新的變量res;

3. 處理【當前頭節點】

  • 如果【當前頭節點】的值滿足要擺刪除的條件的話,我們直接返回res。res就是分析思路時候提到的【紅色部分】,也就是【head後面的結果】完成了刪除任務之後得到的結果。
  • 如果【當前頭節點】的值不滿足要擺刪除的條件的話,也就是【當前頭節點】會保留下來,那麼將res賦給【當前頭節點】的下一個節點。

(一)寫法一 

    public ListNode removeElements(ListNode head, int val){
        //1.求解最基本的問題的解
        if (head == null){
            return null;
        }
        //2. 在【少了頭節點】的鏈表中,刪除值爲val的節點。刪除的結果賦給一個新的變量res;
        ListNode res = removeElements(head.next, val);
        //3. 處理【當前頭節點】
        if(head.val == val){
            //如果【當前頭節點】的值滿足要擺刪除的條件的話,我們直接返回res。res就是分析思路時候提到的【紅色部分】,也就是【head後面的結果】完成了刪除任務之後得到的結果。
            return res;
        }else{
            //如果【當前頭節點】的值不滿足要擺刪除的條件的話,也就是【當前頭節點】會保留下來,那麼將res賦給【當前頭節點】的下一個節點。
            head.next = res;
            return head;
        }
    }

(二)寫法二

將removeElements() 的結果直接存在head.next之中

    public ListNode removeElements(ListNode head, int val){
        if (head == null){
            return null;
        }
        //將removeElements() 的結果直接存在head.next之中,
        head.next = removeElements(head.next, val);
        if(head.val == val){
            return head.next;
        }else{
            return head;
        }
    }

(三)寫法三

將【 if{}else{} 】的形式寫成一個三位運算符。

    public ListNode removeElements(ListNode head, int val){
        if (head == null){
            return null;
        }
        //將removeElements() 的結果直接存在head.next之中,
        head.next = removeElements(head.next, val);
        return head.val == val ? head.next : head;
    }

四。代碼 

1. LinkdeList_Recursion_Remove

package recursion;

import LinkedList.LinkedListRmove.ListNode;

/**
 * removeElements() 的宏觀語意就是:對一個鏈表中刪除值爲val的節點
 * */
public class LinkdeList_Recursion_Remove {
    public ListNode removeElements(ListNode head, int val){
        //1.求解最基本的問題的解
        if (head == null){
            return null;
        }
        //2. 在【少了頭節點】的鏈表中,刪除值爲val的節點。刪除的結果賦給一個新的變量res;
        ListNode res = removeElements(head.next, val);
        //3. 處理【當前頭節點】
        if(head.val == val){
            //如果【當前頭節點】的值滿足要擺刪除的條件的話,我們直接返回res。res就是分析思路時候提到的【紅色部分】,也就是【head後面的結果】完成了刪除任務之後得到的結果。
            return res;
        }else{
            //如果【當前頭節點】的值不滿足要擺刪除的條件的話,也就是【當前頭節點】會保留下來,那麼將res賦給【當前頭節點】的下一個節點。
            head.next = res;
            return head;
        }
    }
    public static void main(String[] args){
        int[] nums = {1,2,6,3,5,6};
        ListNode head = new ListNode(nums);
        System.err.println(head);
        ListNode res = (new LinkdeList_Recursion_Remove()).removeElements(head, 6);
        System.err.println(res);
    }
}

2. ListNode

package LinkedList.LinkedListRmove;

public class ListNode {
    int val;
    ListNode next;
    public ListNode(int val) {
        this.val = val;
    }
    //使用arr爲參數,創建一個鏈表,當前的ListNode爲鏈表頭節點
    public ListNode(int[] arr) {
        if(arr == null || arr.length ==0 ){
            throw new IllegalArgumentException("arr can not be empty");
        }
        this.val = arr[0];
        ListNode curr = this;
        for(int i=1;i<arr.length;i++){
            curr.next = new ListNode(arr[i]);
            curr = curr.next;
        }
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        ListNode curr = this;
        while (curr != null){
            res.append(curr.val + "->");
            curr = curr.next;
        }
        res.append("NULL");
        return res.toString();
    }
}

3. 測試結果

1->2->6->3->5->6->NULL
1->2->3->5->NULL

五。遞歸算法的調式

(一)調試一般有兩種方式:

  • 打印輸出
  • 單步跟蹤

(二)調試過程

  • 對於遞歸函數來說,有一個概念:遞歸深度,每個函數在內部調用自己,我們可以理解爲遞歸深度+1;在輸出可視化的過程中,遞歸深度能幫助我們理解遞歸過程的變量。
  • 初始depth爲0,每進行一次遞歸調用,給depth加1.
 
public class LinkdeList_Recursion_Remove {

    public ListNode removeElements(ListNode head, int val, int depth) {
        String depthString = generateDepthString(depth);
        System.err.print(depthString);
        System.err.println("Call: remove " + val + " in " + head);
        if (head == null) {
            System.err.print(depthString);
            System.err.println("Return: " + head);
            return null;
        }
        //初始depth爲0,每進行一次遞歸調用,給depth加1.
        ListNode res = removeElements(head.next, val, depth + 1);
        System.err.print(depthString);
        System.err.println("After: remove " + val + " : " + res);

        ListNode ret;
        if (head.val == val) {
            ret = res;
        } else {
            head.next = res;
            ret = head;
        }
        System.err.print(depthString);
        System.err.println("Return: "+ ret);

        return ret;
    }

    private String generateDepthString(int depth) {
        StringBuilder res = new StringBuilder();
        for(int i=0;i<depth;i++) {
            res.append("--");
        }
        return res.toString();
    }

    public static void main(String[] args){
        int[] nums = {1,2,6,3,5,6};
        ListNode head = new ListNode(nums);
        System.err.println(head);
        ListNode res = (new LinkdeList_Recursion_Remove()).removeElements(head, 6, 0);
        System.err.println(res);
    }
}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

對於以下結果的解釋:

0. 同一【--】代表同一遞歸深度,【--】越多表示深度越深。

1.  第一步:爲了解決【Call: remove 6 in 1->2->6->3->5->6->NULL (在1->2->6->3->5->6->NULL鏈表中刪除元素6的結果)】,要解決這個問題,我們需要遞歸調用【--Call: remove 6 in 2->6->3->5->6->NULL】,看它的結果。

2. 第二步:爲了解決【--Call: remove 6 in 2->6->3->5->6->NULL】,要解決這個問題,我們需要遞歸調用----Call: remove 6 in 6->3->5->6->NULL】,看它的結果。

。。。。以此類推

4. 第四步:爲了解決【--------Call: remove 6 in 5->6->NULL】,遞歸調用要看【----------Call: remove 6 in 6->NULL】中的結果。

5. 第五步:爲了解決【----------Call: remove 6 in 6->NULL】,遞歸調用需要看【------------Call: remove 6 in null】,也就是看【NULL】中刪除6的結果。這個已經是基本問題了,對於一個【NULL】鏈表刪除元素後,結果仍然爲空。所以返回了【------------Return: null】

      wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

6. 第六步:【----------After: remove 6 : null】此時【--】變短,說明我們回到了上一層的調用。也就是回到了【----------Call: remove 6 in 6->NULL】這一層。上一步我們已經知道了對子鏈表【NULL】刪除6得到的結果爲【Return: null】。此時我們需要看6節點是要刪除的節點,是要刪除的結果,所以6被刪除了,所以【----------Call: remove 6 in 6->NULL】得到的結果爲【----------Return: null】。

7. 第七步:又回到了上一次調用,也就是【--------Call: remove 6 in 5->6->NULL】。上一步我們已經知道了對子鏈表【6->NULL】刪除6得到的結果爲【Return: null】。此時我們需要看6節點是要刪除的節點,不是要刪除的結果,所以【--------Call: remove 6 in 5->6->NULL】得到的結果爲【--------Return: 5->NULL】。

     

8. 以此類推

1->2->6->3->5->6->NULL
Call: remove 6 in 1->2->6->3->5->6->NULL
--Call: remove 6 in 2->6->3->5->6->NULL
----Call: remove 6 in 6->3->5->6->NULL
------Call: remove 6 in 3->5->6->NULL
--------Call: remove 6 in 5->6->NULL
----------Call: remove 6 in 6->NULL
------------Call: remove 6 in null
------------Return: null
----------After: remove 6 : null
----------Return: null
--------After: remove 6 : null
--------Return: 5->NULL
------After: remove 6 : 5->NULL
------Return: 3->5->NULL
----After: remove 6 : 3->5->NULL
----Return: 3->5->NULL
--After: remove 6 : 3->5->NULL
--Return: 2->3->5->NULL
After: remove 6 : 2->3->5->NULL
Return: 1->2->3->5->NULL
1->2->3->5->NULL
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

六。遞歸函數的“微觀”解讀

1. 遞歸調用方法

       對於鏈表 【6 -> 7 -> 8 -> null】,刪除7

       wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

2. 遞歸運行過程:

    將不同的代碼寫成編號【1】、【2】和【3】。便於理解過程。

    遞歸調用的過程就是【子過程】一步一步的向下調用,然後【子過程】計算出結果後再一步一步的返回上層調用。節點真正的刪除發生在【3】中。

第一步: 進入方法removeElements(head, 7),此時【head的值是6】。也就是我將【6】爲頭節點的鏈表傳入removeElements中。這時針對鏈表 【6 -> 7 -> 8 -> null】刪除7.

  •  執行【1】,不符合。
  •  執行【2】,【head.next = removeElements(head.next, val);】,它的意思是:將【head.next】=【再進行一個遞歸調用removeElements(節點7, 7)後的結果】,但是這個removeElements(head.next, 7)的結果我不知道,我要進行調用求出來結果,放入圖中的【?】中。
  • 這個時候,【原始結果】=【節點6】-->【removeElements(節點7,7)】;

              wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

第二步:進入方法removeElements(head, 7),此時【head的值是7】。也就是我將【7】爲頭節點的鏈表傳入removeElements中。這時針對 【7 -> 8 -> null】來刪除元素7.

  •  執行【1】,不符合。
  •  執行【2】,【head.next = removeElements(head.next, val);】,將【head.next】=【再進行一個遞歸調用removeElements(節點8, 7)後的結果】,但是這個removeElements(head.next, 7)的結果我不知道,我要進行調用求出來結果,放入圖中的【?】中。調用removeElements(節點8, 7)
  • 這個時候,【removeElements(節點7,7)】=【節點7】-->【removeElements(節點8,7)】;

           wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

第三步:進入方法removeElements(head, 7),此時【head的值是8】。也就是我將【8】爲頭節點的鏈表傳入removeElements中。這時針對 【 8 -> null】來刪除元素7.

  •  執行【1】,不符合。
  •  執行【2】,【head.next = removeElements(head.next, val);】,將【head.next】=【再進行一個遞歸調用removeElements(null, 7)後的結果】,但是這個removeElements(head.next, 7)的結果我不知道,我要進行調用求出來結果,放入圖中的【?】中。調用removeElements(null節點, 7)
  • 這個時候,【removeElements(節點8,7)】=【節點8】-->【removeElements(節點null,7)】;

           wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

第四步:進入方法removeElements(head, 7),此時【head的值是null】。也就是我將【null】爲頭節點的鏈表傳入removeElements中。這時針對 【null】來刪除元素7.

  •  執行【1】,符合條件。【return null】,也就是說【removeElements(節點null,7)是null】。

         wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

第五步:將removeElements(節點null,7)的結果return到上一次調用中。

進入【2】,此時第三步【removeElements(節點8,7)】=【節點8】--> NULL。第三步中的【?】就是  NULL。

進入【3】,判斷當前的head.val(8)不爲7,返回head;這裏的head就是【節點8】--> NULL。將這個結果返回到上一步。

       wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

 第六步:將removeElements(節點8,7)的結果【節點8】--> NULL return到上一次調用中。

進入【2】,此時第二步【removeElements(節點7,7)】=【節點7】-->【節點8】--> NULL。第二步中的【?】就是【節點8】--> NULL。

進入【3】,判斷當前的head.val(7)不爲7,返回head.next;這裏的head是【節點7】-->【節點8】--> NULL;那麼removeElements(節點7,7)的返回結果應該是【節點8】--> NULL;

          wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

 第七步:將removeElements(節點7,7)的結果【節點8】--> NULL return到上一次調用中。

進入【2】,此時第一步【原始結果】=【節點6】-->【removeElements(節點7,7)】;第一步中的【?】就是【節點8】--> NULL。

進入【3】,判斷當前的head.val(7)不爲7,返回head;這裏的head是【節點6】-->【節點8】--> NULL;至此,整個邏輯結束。【原始結果】=【節點6】-->【節點8】--> NULL;

         wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

 

                                    以上所有內容都是通過"慕課網"聽"liuyubobobo"的《玩轉數據結構》課程後總結

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