目錄
一、鏈表的使用: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;至此,整個函數執行結束。
注意:遞歸調用是有代價的 ——> 函數調用+系統棧空間(遞歸深度)