鏈表與遞歸-代碼實現

目錄

一、鏈表的使用:LeetCode例題

1、解法一:不使用虛擬頭節點

2、解法二:使用虛擬頭節點

二、遞歸

1、鏈表的天然遞歸性

2、遞歸運行機制


一、鏈表的使用:LeetCode例題

刪除鏈表中等於給定值val的所有元素。

示例:給定 1->2->6->3->4->5->6,val = 6;返回:1->2->3->4->5

ListNode爲題中給出的鏈表代碼

public class ListNode {
    public int val;
    public ListNode next;

    public ListNode(int x){
        val = x;
    }
}

1、解法一:不使用虛擬頭節點

public ListNode removeElements(ListNode head, int val) {
        // 循環遍歷頭節點
        while (head != null && head.val == val) {
            ListNode delNode = head;
            // 繞過刪除節點,鏈接轉移
            head = head.next;
            delNode.next = null;
        }
        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;
    }

2、解法二:使用虛擬頭節點

使用虛擬頭節點,可以統一鏈表的操作,簡化實現邏輯。

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) {
                ListNode delNode = prev.next;
                prev.next = delNode.next;
            } else {
                // 循環到下一個
                prev = prev.next;
            }
        }
        return dummyHead.next;
    }

爲了能在本地進行測試,重新編寫了一下ListNode的相關代碼,自定義了ListNode構造函數,代碼入下:

public class ListNode {
    public int val;
    public ListNode next;

    public ListNode(int x){
        val = x;
    }

    // 鏈表節點的構造函數
    // 使用arr作爲參數,創建一個鏈表,當前的ListNode爲鏈表頭節點
    public ListNode(int[] arr){
        if(arr == null && arr.length == 0){
            throw new IllegalArgumentException("arr cannot be empty.");
        }
        // 頭節點
        this.val = arr[0];
        ListNode cur = this;
        for (int i = 1; i < arr.length; i++) {
            cur.next = new ListNode(arr[i]);
            // 不斷循環賦值
            cur = cur.next;
        }
    }

    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        ListNode cur = this;
        while(cur != null){
            sb.append(cur.val + "->");
            // 循環移動
            cur = cur.next;
        }
        // 表示已經到達了鏈表末尾
        sb.append("NULL");
        return sb.toString();
    }
}

二、遞歸

遞歸的本質就是把原來的問題轉化爲更小的同一問題

理解遞歸:本質無非是一個函數裏邊調用了另一個函數,只是所調用的函數是他本身而已。

舉例:數組求和

1、鏈表的天然遞歸性

如下是一個鏈表的基本構成圖示:

我們如果用遞歸的視角來看它,就是一個節點鏈接了一個更短的鏈表,如下:

利用上邊遞歸的思想,我們重新來實現上邊leetcode的例題,示例代碼如下:

public class Solution {
    public ListNode removeElements(ListNode head, int val) {
        // 從頭節點開始判斷起
        if(head == null){
            return null;
        }
        // 除頭節點的外,更短的鏈表
        ListNode res = removeElements(head.next,val);
        if(head.val == val){
            // 如果是刪除元素,丟棄頭節點
            return res;
        }else{
            // 如果非刪除元素,頭節點保留
            head.next = res;
            return head;
        }
    }
}

進一步簡化爲:

public class Solution {
    public ListNode removeElements(ListNode head, int val) {
        // 從頭節點開始判斷起
        if(head == null){
            return null;
        }
        // 除頭節點的外的,更短的鏈表
        head.next = removeElements(head.next,val);
        return head.val == val ? head.next : head;
    }
}

使用遞歸的核心思想是:使用基本問題的解去構建整體問題的解。圖示解如下:

2、遞歸運行機制

遞歸的運行機制,還是使用基本問題的解去構建整體問題的解,下邊爲了更加清楚的描述遞歸的運行機制,使用一個數組求和的簡單例子來做步驟分解:

求解:arr = [6,10] 數組元素的和,過程示例如下,n爲數組的長度,此處爲簡寫

上圖示例的過程可以理解爲:0 + 10 + 6;

1、遍歷到數組最後一個元素(l = n = 2),沒有相加的數據,所以函數執行返回0;

2、在上一個函數求解返回之後,函數在停頓後繼續執行,得到上一個調用計算的值 x = 0 ; 那麼 res = arr[1] + 0;即res = 10;

3、當前一個函數執行完後,得到計算的值 x = 10;函數繼續執行,那麼 res = arr[0] + 10;所以 res = 16;至此,整個函數執行結束。

注意:遞歸調用是有代價的 ——> 函數調用+系統棧空間(遞歸深度)

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