連結串列與遞迴
簡單介紹
前面我們已經介紹了連結串列, 然而這個主題主要是用於擴充套件學習連結串列的, 以及遞迴的基本使用。 連結串列練習主要以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
複製程式碼
第一個遞迴例子
現在我們有一組陣列, 我們如何通過遞迴的形式進行累加並返回結果呢?
看看遞迴是如何呼叫的(後面還會詳細介紹遞迴呼叫流程)
/**
遞迴返回陣列中的總和
*/
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));
}
}
複製程式碼
遞迴函式執行流程
我們在學習棧的應用的時候, 就是程式呼叫的系統棧。在一個函式中呼叫一個子函式就會壓入一個系統棧, 當子函式執行完之後就會從系統棧彈出 然後回到上次指定的地方並繼續。
其實遞迴呼叫也就是這麼一個過程, 只不過就是呼叫的還是這個函式本身而已。
透過下圖, 我們更加直觀的瞭解上面的遞迴例子的執行流程把。
假設現在陣列中只有數字[6, 10] 方便進行除錯。陣列長度為2。
sum函式執行過程為:
- 判斷當前位置是否等於陣列長度, 如果等於則返回, 否則往下執行
- 繼續呼叫sum函式, 並傳入對應的位置
- 當第一步成立後, 獲取到函式返回值並加上當前位置陣列的值。
- 返回累加後的結果
可能會迷糊的一些點:
- sum函式在呼叫sum函式的時候, 是新開闢的空間, 對應的引數引數值都是不同的,所以裡面的變數是相互不會影響的。
- 千萬不要被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做完餘下操作返回最終的結果值。這樣是不是會好一點理解呢?
上面的例子可能會比較簡單一點, 那下面我們來稍微複雜一點的, 比如刪除連結串列的元素。
removeElements執行流程只有三步
- 判斷當前頭結點是否為null, 如果為null則返回
- 當前頭結點的下一個節點為誰呢? 現在還不知道, 一直重複步驟1和2, 知道步驟1滿足為止
- 如果當前頭結點是要被刪除的節點, 則返回的是他的下一個節點資料, 否則返回當前頭結點資料
- 當我們以6為頭結點執行第一步判斷不為空則進入第二步"6"的下一個節點當前還不可知
- 當我們以7為頭結點執行第一步判斷不為空則進入第二步"7"的下一個節點當前還不可知
- 當我們以8為頭結點執行第一步判斷不為空則進入第二步"8"的下一個節點當前還不可知
- 當我們傳入NULL後
下圖
- 返回NULL值
- 頭結點為8的next值為null, 判斷當前頭結點是否需要被刪除, 當前節點不被刪除返回 8 -> NULL
- 頭結點為7的next值為8 -> NULL, 判斷是否需要被刪除, 需要刪除則返回下一個節點即: 8 -> NULL
- 頭結點為6的next值為8 -> NULL, 判斷是否需要被刪除, 不需要則返回 6 -> 8 -> NULL返回資料
到這裡就已經結束了整個呼叫流程了。