數據結構之鏈表與遞歸

1、提起鏈表,有一塊非常重要的內容,就是遞歸,這是因爲鏈表本身具有天然的遞歸性,同時,鏈表也是一種結構非常簡單的數據結構,使得鏈表是一種非常好的來學習和研究遞歸這種邏輯機制的數據結構。

2、使用一個簡單的案例,數組求和,使用遞歸算法進行計算。案例,如下所示:

 1 package com.array;
 2 
 3 /**
 4  * 數組求和,使用遞歸算法進行計算。
 5  * <p>
 6  * 遞歸算法的基本原則。
 7  * 1、第一部分,求解最基本的問題。
 8  * 例如,return 0;遞歸算法就是將原問題變成了一個更小的問題,
 9  * 更小的問題變成一個更更小的問題,以此類推,直到變成了一個最基本的問題,這個最基本的問題是不能自動求解的,
10  * 是需要我們編寫邏輯進行求解的。這裏對數組求和的算法就表現在if (left == arr.length) { return 0;},先判斷
11  * 一下,我們當前真的是一個最基本的問題,是的話,直接返回retun 0;
12  * 2、第二部分,把原問題轉化成更小的問題的這樣一個過程。
13  * 通常,對於遞歸算法來說,最基本的問題都是極其簡單的,甚至基本上都是這樣的一種形式,直接return一個數就行了。
14  * 最基本的問題,一眼就可以看出答案是多少了,但是難得是如何把原問題轉化成更小的問題呢,所謂的轉化爲更小的問題,
15  * 不是求一個更小的問題的答案就好了,是根據更小的問題的答案構建出原問題的答案。
16  * 這裏面的構建就是讓arr[left] + 更小的一段數組中所有的元素的和,那麼,這個構建方式非常簡單。
17  */
18 public class ArraySum {
19 
20     /**
21      * 數組求和,用戶調用的公開方法。
22      *
23      * @param arr
24      * @return
25      */
26     public static int sum(int[] arr) {
27         // 思路,一點一點縮小數組的大小,就是,數組從那裏一直到數組的最後對數組進行求和。這些規模在一直減小。
28         // 調用私有的sum方法,將數組arr傳入參數1,索引0位置傳入參數2。
29         // 參數2傳入的是0,是遞歸的初始調用,計算的是從0一直到n-1這些元素所有的和。
30         return sum(arr, 0);
31     }
32 
33     /**
34      * 遞歸算法,對用戶屏蔽的私有方法。
35      * <p>
36      * 私有的函數,計算arr[left...n)這個區間內所有數字的和。即left到n-1這些索引區間的和。
37      *
38      * @param arr  數組
39      * @param left 左邊界的點,其實是一個索引
40      * @return
41      */
42     private static int sum(int[] arr, int left) {
43         // 首先判斷,left等於數組長度的時候,說明遞歸到了最後
44         if (left == arr.length) {
45             // 此時,整個數組爲空的時候,直接返回0即可
46             return 0;
47         } else {
48             // 否則,返回arr[left] + sum(arr, left + 1)。
49             // 取出,現在要計算出從left到n-1這些索引的所有元素的和。
50             // 把左邊的元素單獨拿出來,就是arr[left],再加上sum,對arr的從left + 1這個索引,一直到n-1這個索引。
51             // 這些範圍裏面的所有元素進行求和操作。sum(arr, left + 1)就是遞歸調用的過程。
52             return arr[left] + sum(arr, left + 1);
53             // 我們計算從left到n這個區間範圍內的所有元素的和,變成了計算從left+1到n所有數字的和。
54             // 我們解決的這個問題,規模變小了,直到最終left和數組的長度相等的時候,也就是要求一個空數組的和。
55         }
56     }
57 
58     public static void main(String[] args) {
59         int[] arr = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9};
60         int sum = ArraySum.sum(arr);
61         System.out.println("數組之和: " + sum);
62     }
63 
64 }

3、鏈表天然的遞歸性。鏈表就是一個節點一個節點鏈接起來就是一個鏈表。鏈表也可以當作如下看待,現在的鏈表可以想象成是0這個節點後面又掛了一個鏈表。

4、使用鏈表遞歸解決,刪除鏈表中等於給定值val的所有節點。

實現代碼,如下所示:

 1 package com.leetcode;
 2 
 3 /**
 4  * 刪除鏈表中等於給定值 val 的所有節點。
 5  * <p>
 6  * Definition for singly-linked list.
 7  * <p>
 8  * public class ListNode {
 9  * int val;
10  * ListNode next;
11  * ListNode(int x) { val = x; }
12  * }
13  */
14 public class RemoveLinkedList3 {
15 
16     /**
17      * 刪除鏈表中等於給定值 val 的所有節點。
18      * 輸入: 1->2->6->3->4->5->6, val = 6
19      * 輸出: 1->2->3->4->5
20      *
21      * @param head
22      * @param val
23      * @return
24      */
25     public ListNode removeElements(ListNode head, int val) {
26         // 使用鏈表的遞歸解決刪除鏈表中等於給定值 val 的所有節點。
27         // 對於遞歸的解決,只有兩個部分。
28         // 第一個部分,就是對於那種最基本的情況,也就是問題規模最小的那種情況,它的解是什麼。
29         // 頭部節點後面跟着一個小的短的鏈表,走到最底的情況,就是鏈表的頭部節點head等於空。也就是整個鏈表爲空。
30         if (head == null) {
31             // 此時,不需要任何邏輯。
32             return head;// 此時,return返回head還是return null都是一樣的。
33         }
34 
35         // 第二部分,把原問題轉化成更小的問題的這樣一個過程。
36         // 知道removeElements這個函數,這個模塊,把它當作一個子模塊,它的宏觀語意,
37         // 它做的事情就是對一個鏈表中刪除掉值爲val這樣的的節點。
38         // 所以對head接的這個鏈表中值爲val的節點進行刪除即可。
39 //        ListNode removeElements = removeElements(head.next, val);
40 
41         // 也可以寫成如下寫法,讓返回值直接存儲到head的下一個節點中。
42         head.next = removeElements(head.next, val);
43         // removeElements存儲的就是應該是我們將頭節點後面跟的那個鏈表中所有的值爲val的節點刪除後剩餘的節點。
44         // removeElements就是將指定的val刪除掉以後剩餘的節點。
45 
46         // 寫法一
47         // 此時,進行處理,head這個節點的值是怎麼樣的。
48 //        if (head.val == val) {
49 //            // 如果此時,head這個節點的值是待刪除的val值。此時,把head節點刪除了,
50 //            // removeElements存儲的就是head之後跟的那個鏈表,完成了這個任務之後,得到的結果給它返回。
51 //            return removeElements;
52 //        } else {
53 //            // 讓head的下一個節點指向了removeElements這個節點。將head節點連接到最前面。
54 //            // head節點後面跟head後面的那個鏈表刪除了val之後得到的那個結果的鏈表給它接上。
55 //            head.next = removeElements;
56 //            return head;
57 //        }
58 
59 
60         // 寫法二
61 //        if (head.val == val) {
62 //            // 如果此時,head這個節點的值是待刪除的val值。此時,把head節點刪除了,
63 //            // removeElements存儲的就是head之後跟的那個鏈表,完成了這個任務之後,得到的結果給它返回。
64 //            return head.next;
65 //        } else {
66 //            // 讓head的下一個節點指向了removeElements這個節點。將head節點連接到最前面。
67 //            // head節點後面跟head後面的那個鏈表刪除了val之後得到的那個結果的鏈表給它接上。
68 //            return head;
69 //        }
70 
71         // 寫法三
72         // 直接寫成三目運算符,更加方便。
73         return head.val == val ? head.next : head;
74     }
75 
76 
77     public static void main(String[] args) {
78         int[] arr = new int[]{1, 2, 6, 3, 4, 5, 6};
79         // 創建ListNode
80         ListNode head = new ListNode(arr);
81         // 打印head,注意,這裏的head雖然是一個頭部節點,但是覆蓋了toString方法,這裏的head是以head作爲頭部節點的整個鏈表對應的字符串。
82         System.out.println(head);
83         // 創建本來對象
84         RemoveLinkedList3 removeLinkedList = new RemoveLinkedList3();
85         // 調用方法,刪除待刪除元素的節點
86         ListNode removeElements = removeLinkedList.removeElements(head, 6);
87         // 打印輸出即可
88         System.out.println(removeElements);
89     }
90 
91 }

5、遞歸函數的微觀解讀。遞歸函數的調用,本質就是函數調用,和普通函數的調用沒有區別,只不過調用的函數是自己而已。

5.1、數組求和,使用遞歸算法進行計算。遞歸調用的函數微觀解讀。

5.2、使用鏈表遞歸解決,刪除鏈表中等於給定值val的所有節點,微觀層面的步驟解析。

總結,遞歸調用是有代價的,函數調用(需要時間的開銷,需要記錄當前的邏輯執行到那裏了,當前的局部變量都是怎麼樣的)+  系統棧空間(遞歸調用消耗系統棧的空間的)。

6、遞歸算法的調試,可以根據打印輸出或者開發工具的debug進行調試即可。

  1 package com.company.linkedlist;
  2 
  3 /**
  4  * @ProjectName: dataConstruct
  5  * @Package: com.company.linkedlist
  6  * @ClassName: RemoveLinkedList
  7  * @Author: biehl
  8  * @Description: ${description}
  9  * @Date: 2020/3/9 9:48
 10  * @Version: 1.0
 11  */
 12 public class RemoveLinkedList {
 13 
 14     // 內部類
 15     class ListNode {
 16 
 17         int val;
 18         ListNode next;
 19 
 20         ListNode(int x) {
 21             val = x;
 22         }
 23 
 24         /**
 25          * 將數組轉換爲鏈表結構
 26          * <p>
 27          * 鏈表節點的構造函數,使用arr爲參數,創建一個鏈表,當前的ListNode爲鏈表頭節點。
 28          * <p>
 29          * 將整個鏈表創建完成以後,這個構造函數是一個節點的構造函數,我們最終呢,
 30          * 相當於把我們的當前構造的這個節點作爲頭節點。
 31          *
 32          * @param arr
 33          */
 34         public ListNode(int[] arr) {
 35             // 首先,判斷數組arr是否合法,數組不能爲空,必須包含元素
 36             if (arr == null || arr.length == 0) {
 37                 throw new IllegalArgumentException("arr can not be empty.");
 38             }
 39 
 40             // 讓數組索引爲0的元素即第一個元素賦值給存儲鏈表節點元素的val。
 41             this.val = arr[0];
 42             // 遍歷數組,將數組中的每一個元素都創建成新的ListNode節點。鏈接到前一個節點上,形成這樣的一個鏈表。
 43             // 創建一個節點,從this開始,將之後的節點都鏈接到此節點的後面。
 44             ListNode current = this;
 45             for (int i = 1; i < arr.length; i++) {
 46                 // 讓從this開始,將之後的節點都鏈接到此節點的後面。
 47                 current.next = new ListNode(arr[i]);
 48                 // 讓current這個幾點。每次循環都向後移動一個位置,將後面的節點都依次進行掛接。
 49                 current = current.next;
 50             }
 51             // 最後,this就是我們用上面的循環創建的鏈表相對應的頭部節點head。
 52         }
 53 
 54         /**
 55          * 爲了方便在main函數中觀察鏈表。
 56          * <p>
 57          * 返回的是以當前節點爲頭部節點的鏈表信息字符串。
 58          * <p>
 59          * 注意,這裏的ListNode是一個節點哈,不是一個鏈表結構。
 60          *
 61          * @return
 62          */
 63         @Override
 64         public String toString() {
 65             StringBuilder stringBuilder = new StringBuilder();
 66             // 從自身開始循環
 67             ListNode current = this;
 68             // 循環遍歷,只要current不爲空,就進行操作
 69             while (current != null) {
 70                 // 將當前節點的val值進行拼接
 71                 stringBuilder.append(current.val + "->");
 72                 // 將current向下一個節點進行移動
 73                 current = current.next;
 74             }
 75             // 表示達到了鏈表的尾部。
 76             stringBuilder.append("NULL");
 77             return stringBuilder.toString();
 78         }
 79     }
 80 
 81     /**
 82      * 根據遞歸深度生成一個深度字符串。
 83      *
 84      * @param depth
 85      * @return
 86      */
 87     private String generateDepthString(int depth) {
 88         StringBuilder stringBuilder = new StringBuilder();
 89         for (int i = 0; i < depth; i++) {
 90             // 每次循環添加兩個中劃線,方便觀察遞歸效果。深度越深,中劃線越多。
 91             stringBuilder.append("--");
 92         }
 93         return stringBuilder.toString();
 94     }
 95 
 96 
 97     /**
 98      * @param head  以head爲頭部節點的鏈表。
 99      * @param val   待刪除元素。
100      * @param depth 遞歸深度,每一個函數在內部調用一次自己,可以理解爲遞歸深度多了一。
101      *              遞歸深度幫助理解遞歸的一個變量。
102      * @return
103      */
104     public ListNode removeElements(ListNode head, int val, int depth) {
105         // 根據遞歸深度生成一個深度字符串。
106         String depthString = generateDepthString(depth);
107         // 首先打印一下深度字符串。打印的是在這個遞歸深度下。
108         System.out.print(depthString);
109         // 複用了ListNode的toString方法。表示,在head爲頭部節點的鏈表刪除val這個元素。
110         System.out.println("Call: remove " + val + " in " + head);
111 
112         if (head == null) {
113             System.out.print(depthString);
114             System.out.println("Return: " + head);
115             // 此時,不需要任何邏輯。
116             return head;// 此時,return返回head還是return null都是一樣的。
117         }
118 
119         // 也可以寫成如下寫法,讓返回值直接存儲到head的下一個節點中。遞歸深度每次加一。
120         ListNode res = removeElements(head.next, val, depth + 1);
121         System.out.print(depthString);
122         System.out.println("After: remove " + val + " : " + res);
123 
124         ListNode ret;
125         if (head.val == val) {
126             // 刪除頭部節點
127             ret = res;
128         } else {
129             // 不刪除頭部節點
130             head.next = res;
131             ret = head;
132         }
133         System.out.print(depthString);
134         System.out.println("Return: " + res);
135 
136         return ret;
137     }
138 
139 
140     public static void main(String[] args) {
141         int[] arr = new int[]{1, 2, 6, 3, 4, 5, 6};
142         // 創建ListNode
143         ListNode head = new RemoveLinkedList().new ListNode(arr);
144         // 打印head,注意,這裏的head雖然是一個頭部節點,但是覆蓋了toString方法,這裏的head是以head作爲頭部節點的整個鏈表對應的字符串。
145         System.out.println(head);
146         // 創建本來對象
147         RemoveLinkedList removeLinkedList = new RemoveLinkedList();
148         // 調用方法,刪除待刪除元素的節點。遞歸深度,默認是0
149         ListNode removeElements = removeLinkedList.removeElements(head, 6, 0);
150         // 打印輸出即可
151         System.out.println(removeElements);
152     }
153 
154 }

運行效果,如下所示:

7、關於遞歸,鏈表具有天然的遞歸結構,近乎和鏈表相關的所有操作,都可以使用遞歸的形式來完成,比如,可以使用遞歸對鏈表進行增加,刪除,修改和查詢操作的。

7.1、雙鏈表的結構。

7.2、循環鏈表的結構。

7.3、數組鏈表的結構。

 

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