一。鏈表的天然遞歸性
對於一個鏈表,我們可以把它看成:
- 【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);
}
}
對於以下結果的解釋:
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】。
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
六。遞歸函數的“微觀”解讀
1. 遞歸調用方法
對於鏈表 【6 -> 7 -> 8 -> null】,刪除7
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)】;
第二步:進入方法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)】;
第三步:進入方法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)】;
第四步:進入方法removeElements(head, 7),此時【head的值是null】。也就是我將【null】爲頭節點的鏈表傳入removeElements中。這時針對 【null】來刪除元素7.
- 執行【1】,符合條件。【return null】,也就是說【removeElements(節點null,7)是null】。
第五步:將removeElements(節點null,7)的結果return到上一次調用中。
進入【2】,此時第三步中【removeElements(節點8,7)】=【節點8】--> NULL。第三步中的【?】就是 NULL。
進入【3】,判斷當前的head.val(8)不爲7,返回head;這裏的head就是【節點8】--> NULL。將這個結果返回到上一步。
第六步:將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;
第七步:將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;
以上所有內容都是通過"慕課網"聽"liuyubobobo"的《玩轉數據結構》課程後總結