上一节课主要学习了一种具有真正动态数据结构的数据结构——链表,实现了链表基本的增删改查等操作,基于链表的操作特性,实现了栈的结构,并通过增加尾节点,进一步实现了队列这样一种数据结构。本节课从leetcode的一个练习题出发,研究链表具有的一种天然属性——递归。
1. leetcode上的问题
问题描述:在链表中删除值为val的所有节点,,返回删除后的链表头节点
- 如1-->2-->6-->3-->4-->5-->6-->null,要删除值为6的节点
- 返回结果:1-->2-->3-->4-->5-->null
1.1 解题方案1(不使用虚拟头节点)
不使用虚拟头节点,从头节点出发:
- 如果头节点不为空,且头节点元素恰好是需要删除的节点,则删除头节点
head = head.next - 如果头节点为空,则直接返回空节点
- 如果头节点不为空,且不为待删除元素,则遍历余下的链表元素,删除所有满足条件的节点
public ListNode removeElements(ListNode head, int val) {
// 如果头节点不为空,且数据恰好为需要删除的元素,则删除头节点
// 使用while循环,处理前面多个节点均为待删除元素的情况
while(head!=null && head.val == val) {
ListNode delNode = head;
head = head.next;
delNode.next = null;
// head = head.next;
}
// 如果头节点为空,则直接返回空节点
if(head == null){
return null;
}
// 如果头节点不为空,且不为待删除元素,则遍历余下的链表元素
// 需找到待删除元素的前一节点
ListNode prev = head;
while(prev.next!=null) {
if(prev.next.val == val) {
ListNode delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
// prev.next = prev.next.next;
}
else {
prev = prev.next;
}
}
return head;
}
1.2 解决方案2(使用虚拟头节点)
同样地,使用虚拟头节点可以避免单独处理 1)头节点为空; 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) {
prev.next = prev.next.next;
}
else {
prev = prev.next;
}
}
return dummyHead.next;
}
1.3 测试解决方案
-
定义一个名义上的链表类ListNode
//Definition for singly-linked list. public class ListNode { public int val; public ListNode next; public ListNode(int x) { val = x; } }
-
为 ListNode添加一个构造函数,将一个数组初始化为链表
// 链表节点的构造函数 // 使用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 cur = this; for(int i=1;i<arr.length;i++) { cur.next = new ListNode(arr[i]); cur = cur.next; } }
-
重写ListNode的toString 函数,方便测试
@Override public String toString() { StringBuilder s = new StringBuilder(); ListNode cur = this; while(cur!=null) { s.append(cur.val+"->"); cur = cur.next; } s.append("NULL"); return s.toString(); }
-
测试解决方案是否满足题意
public static void main(String[] args) { int[] array = {1,2,6,3,4,5,6}; ListNode head = new ListNode(array); System.out.println(head); // 初始链表,1->2->6->3->4->5->6->NULL head = new Solution().removeElements(head,6); System.out.println(head); // 删除值为6后的链表,1->2->3->4->5->NULL }
2. 递归
2.1 递归的宏观语意
递归的本质是将原来的问题转化为更小的同一问题,以数组求和为例
- Sum(arr[0,1,...,n-1]) = arr[0] + Sum(arr[1,...,n-1]) // 更小的同一问题
- Sum(arr[1,...,n-1]) = arr[1] + Sum(arr[2,...,n-1]) // 更小的同一问题
- ...
- Sum(arr[n-1,n-1]) = arr[n-1] + Sum([]) // 最基本的问题
public class Sum {
public static int sum(int[] arr) {
return sum(arr,0);
}
// 计算arr[l...n)这个区间内所有数字的和
private static int sum(int[] arr,int l) {
if(l == arr.length)
return 0; // 求解最基本的问题
return arr[l] + sum(arr,l+1); // 把原问题转换为更小的同一问题
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println(sum(arr)); // 15
}
}
链表具有天然的递归性,一个链表完全可以用一个头节点和一个更短的链表来描述:
2.2 解决方案3(使用递归)
假设要删除下述链表中的某些元素:
-
如果e需要删除,则删除e,并继续递归删除更短链表中相应的元素
-
如果e不需要删除,递归删除更短链表中相应的元素
public ListNode removeElements(ListNode head, int val) {
// 如果头节点为空,返回空节点
if(head==null) {
return null;
}
// 如果头节点不为空,删除更短链表中对应的元素,记返回的链表头节点为res
ListNode res = removeElements(head.next,val);
// 如果头节点需要删除,直接返回删除更短链表中对应元素后的链表
if(head.val == val) {
return res;
}
// 如果头节点不需要删除,将更短链表中对应元素删除后的链表头节点赋为当前头节点的下一节点
else {
head.next = res;
return head;
}
// 简洁写法
// head.next = removeElements(head.next,val);
// return (head.val == val)? head.next:head;
}
2.3 递归的微观语意
递归的实质是函数的调用,只不过递归是函数调用函数本身,以数组求和为例:
-
对arr = [6,10]求和,首先调用函数sum(arr,0):
-
执行到sum(arr,0)的第二行时,调用函数sum(arr,1):
-
执行到sum(arr,1)的第二行时,调用函数sum(arr,2):
-
返回sum(arr,1)的值,并继续执行sum(arr,0):
对链表删除元素也是一样的,模拟删除如下链表中的7:
removeElements(head,int val){
if(head==null) return null;
head.next = removeElements(head.next,val);
return head.val == val? head.next:head;
}
-
头节点不为空,调用removeElements(head.next,val),对下面更小的链表删除对应的元素
-
对这个更小的链表,头节点不为空,调用removeElements(head.next,val)继续对下面更小的链表删除对应元素
-
重复这一过程,直到头节点为空
-
头节点为空时,返回空节点
-
判断头节点是否需要删除,如需要则直接返回子链表的删除结果,如不需要,则返回头节点
3. 递归程序的调试
对递归程序,一个可行的调试方法是用ide自带的Debug功能,跟踪程序运行的过程,观察运行过程中变量的变化情况。这里介绍一种通过打印一些输出信息来判断递归程序是否正常运行的方法。
这种调试方法的基本思想是,函数每调用自身一次,深度+1,函数每返回一次,深度-1,通过构造基于深度信息的特殊字符串,可以将处于同一深度的输入输出数据打印出来,观察递归执行过程中是否存在问题。
-
基于深度信息构造特殊字符串
private String generateDepthString(int depth) { StringBuilder res = new StringBuilder(); for(int i = 0;i<depth;i++) { res.append("--"); } return res.toString(); }
-
递归函数加入深度信息
public ListNode removeElements(ListNode head, int val,int depth) { String depthString = generateDepthString(depth); // 打印深度信息 System.out.print(depthString); // 打印函数作用 System.out.println("Call: remove " + val + " in " + head); if(head==null) { // 当头节点为空时,递归截止,开始返回,打印当前深度信息 System.out.print(depthString); System.out.println("Return: " + head); return null; } // 头节点不为空时,递归删除子链表中对应的元素,深度+1 ListNode res = removeElements(head.next,val,depth+1); // 子链表删除完成后,打印深度信息和子链表删除后的结果 System.out.print(depthString); System.out.println("After remove " + val + ": " + res); ListNode ret; if(head.val == val) { ret = res; } else { head.next = res; ret = head; } // 打印深度信息和当前链表删除对应元素后的结果 System.out.print(depthString); System.out.println("Return: " + ret); return ret; }
-
测试
public static void main(String[] args) { int[] array = {1,2,3,4,6,5,6}; ListNode head = new ListNode(array); System.out.println(head); head = new Solution().removeElements(head,6,0); System.out.println(head); // 1->2->3->4->6->5->6->NULL // Call: remove 6 in 1->2->3->4->6->5->6->NULL // --Call: remove 6 in 2->3->4->6->5->6->NULL // ----Call: remove 6 in 3->4->6->5->6->NULL // ------Call: remove 6 in 4->6->5->6->NULL // --------Call: remove 6 in 6->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: 5->NULL // ------After remove 6: 5->NULL // ------Return: 4->5->NULL // ----After remove 6: 4->5->NULL // ----Return: 3->4->5->NULL // --After remove 6: 3->4->5->NULL // --Return: 2->3->4->5->NULL // After remove 6: 2->3->4->5->NULL // Return: 1->2->3->4->5->NULL // 1->2->3->4->5->NULL }
4. 总结
本节课主要介绍了递归的思想,结合链表的天然递归性,使用递归方法完成了链表元素的删除操作。递归的本质是把原问题转化为一个更小的同一问题,关键在于边界位置的处理以及如何进行转化。之后的课程将会涉及到大量的递归操作,多写多用,慢慢掌握其中技巧。