01玩转数据结构_05_链表和递归

链表回顾:

leetcode 题目(No.203 移除链表元素):

不使用虚拟头节点:

 1 class Solution {
 2     public ListNode removeElements(ListNode head, int val) {
 3         //1,head.val 就是应该删除的节点
 4 //        if(head != null && head.val == val ){
 5         while(head != null && head.val == val ){ //之所以使用while是因为可能有多个。
 6             ListNode delNode = head;
 7             head = delNode.next;
 8             delNode.next = null;
 9         }
10         //2,head.val 不是应该删除的节点
11         if(head ==null){
12             return null;
13         }
14         ListNode prePtr = head;   //删除时应该要知道前一个节点。
15         while (prePtr.next != null){
16             if(prePtr.next.val == val){
17                 ListNode delNode = prePtr.next;
18                 prePtr.next = delNode.next;
19                 delNode.next = null;
20             }else{
21                 prePtr = prePtr.next;
22             }
23         }
24         return head;
25     }
26 }
View Code

使用虚拟头节点:

 1 class Solution {
 2     public ListNode removeElements(ListNode head, int val) {
 3         //使用虚拟头节点  将代码处理逻辑统一。
 4         ListNode dummyNode = new ListNode(0);
 5         dummyNode.next = head;  
 6         ListNode prePtr = dummyNode;
 7         while(prePtr.next != null){
 8             if(prePtr.next.val == val){
 9                 ListNode delNode = prePtr.next; 
10                 prePtr.next = delNode.next; 
11                 delNode.next = null;     
12             }else{
13                 prePtr = prePtr.next;  
14             }
15         }
16         return dummyNode.next;  
17         
18      
19     }
20 }
使用虚拟头节点 将代码处理逻辑统一。

链表和递归:

递归是一个很重要的概念,它甚至是 初级程序员和高级程序员 拉开距离的分水岭。

递归基础和递归的宏观语意

它的本质

将原来的问题,转化为更小的同一问题

下面我们用递归来实现一下 数组求和。

 1 package cn.zcb.demo01;
 2 
 3 public class MySum {
 4     public static void main(String[] args) {
 5         int [] arr = {1,2,3,4,5,6,7,8,9,10};
 6         int ret = sum(arr);
 7         System.out.println(ret);
 8 
 9     }
10     public static int sum(int []arr){
11         //为用户设计
12         return sum(arr,0); // 初始索引是0 
13     }
14     private static int sum(int []arr,int leftIdx){
15         //真正的递归函数,它额外需要一个左边界的索引 。
16         //终止条件
17         if(leftIdx == arr.length ){
18             return 0;
19         }
20         //递归链条
21         return arr[leftIdx] + sum(arr,leftIdx+1);
22     }
23 }
递归 进行数组求和。

上面递归分两处:

1,终止条件。终止条件一般比较简单,

2,递归链条。一般难的是下面的递归链表的创建(即 把原问题 转化成更小的问题)。

其实有的时候,我们不必关系递归的具体每一步骤,

而仅仅知道 该函数是干嘛的就行了(即递归函数的宏观语意)。然后,围绕它来构建终止条件  和 递归链条。  

总结:宏观语意很重要,可以直接将 函数中调用自身的函数当做是已经解决的函数。

链表的天然递归结构:

 所以,对于链表来说 ,绝大多数都是可以通过递归来进行解决的。

下面使用递归来解决上面的LeetCode题目。

 1 class Solution {
 2     public ListNode removeElements(ListNode head, int val) {
 3         //终止条件
 4         if(head == null)
 5             return null;
 6         
 7         //递归链条 
 8         ListNode res = removeElements(head.next,val);  //假设 res 是已经清理好的链表。
 9         if(head.val == val){ //如果head 需要清理 直接返回 res
10             return res;  
11         }else{
12             head.next = res;
13             return head; 
14         }
15     }
16 }
把函数中调用自己函数的返回值当做是 已经解决好的答案。
 1 package cn.zcb.demo01;
 2 class Solution {
 3     public ListNode removeElements(ListNode head, int val) {
 4         //终止条件
 5         if(head == null)
 6             return null;
 7 
 8         //递归链条
 9         head.next  = removeElements(head.next,val); //假设函数的返回值是 已经清理好的链表。
10         if(head.val == val){ //如果head 需要清理 直接返回head.next   
11             return head.next;
12         }else{
13             return head;
14         }
15     }
16 }
version better
 1 package cn.zcb.demo01;
 2 class Solution {
 3     public ListNode removeElements(ListNode head, int val) {
 4         //终止条件
 5         if(head == null)
 6             return null;
 7 
 8         //递归链条
 9         head.next  = removeElements(head.next,val); //假设函数的返回值是 已经清理好的链表。
10         return head.val == val?head.next:head;  
11     }
12 }
再次优化。更加简洁,不过可读性就降低了

递归运行的机制:递归的“微观”解读:

就解决问题本身而言,我们只需要掌握 “宏观”语意,基本上问题都能解决。 

下面我们也看下微观发生了什么!

另一题:

递归的代价:

对于线性结构来说,我们是可以直接使用循环来解决的。所以,我们不能发现递归的很多好处。

但是对于非线性结构(树啊,图啊之类的)来说,我们就会发现递归的好处。它的书写逻辑是清晰的。而且是简单的。

递归算法的调试(程序中调试):

上面是在图纸上写的。对于平时写程序来说,如果每个都写在纸上也挺浪费时间,虽然它的学习效果是最好的。

其实,我们是完全可以通过打印输出进行递归调试 的 。

 1 package cn.zcb.demo01;
 2 class Solution {
 3     public ListNode removeElements(ListNode head, int val,int depth) { //depth 是递归的深度
 4         //终止条件
 5         //1,一进来就输出个 关于深度的信息。 这里用- 表示, - 表示一个深度。
 6         String depthString = generateDepthString(depth); //专门用来生成深度信息的函数
 7         System.out.print(depthString);
 8         System.out.println("Call: remove " + val + " in "+head);
 9 
10         if(head == null) {
11             //2, 结束递归时候的输出。
12             System.out.print(depthString);
13             System.out.println("0Return: "+head);
14             return null;
15         }
16         //递归链条
17         ListNode res = removeElements(head.next,val,depth+1);
18         //3 处理完小问题之后输出一下。
19         System.out.print(depthString);
20         System.out.println("After remove "+val +": "+res);
21 
22         ListNode ret;
23         if(head.val == val){
24             ret = res;
25         }else{
26             head.next = res;
27             ret = head;
28         }
29         System.out.print(depthString);
30         System.out.println("1Return: "+ret);
31         return ret;
32     }
33     private String generateDepthString(int depth){
34         StringBuilder builder = new StringBuilder();
35         for (int i=0;i<depth;i++){
36             builder.append("-");
37         }
38         return builder.toString();
39 
40 
41     }
42     public static void main(String[] args) {
43         int [] nums = {1,2,6,3,4,5,6};
44         ListNode head = new ListNode(nums);
45         System.out.println(head);
46 
47         //构建链表完毕,下面为测试 removeElements() 代码
48         ListNode res =  (new Solution()).removeElements(head,6,0);
49         System.out.println(res);
50     }
51 
52 }
Solution.java
 1 package cn.zcb.demo01;
 2 
 3 public class ListNode {
 4     public int val;
 5     public ListNode next;
 6     public ListNode(int val){
 7         this.val = val;
 8     }
 9     //int [] 数组作为参数的构造器
10     public ListNode(int [] arr){
11         if(arr == null || arr.length ==0 ){
12             throw new IllegalArgumentException("数组不能为空 !");
13         }
14         this.val  = arr[0];
15 
16         ListNode temp = this;
17         for (int i=1;i<arr.length;i++){
18             temp.next = new ListNode(arr[i]);
19             temp = temp.next;
20         }
21     }
22     //重写toString()
23     @Override
24     public String toString(){
25         StringBuilder builder = new StringBuilder();
26         ListNode temp =this;
27         while (temp != null){
28             builder.append(temp.val +"->");
29             temp = temp.next;
30         }
31         builder.append("null");
32         return builder.toString();
33     }
34 }
ListNode.java

原本只有四行的代码,被我们整成了 这么多行, 它说明 牛逼的算法可能是需要上百上千的代码才可以透彻的理解的。才能潇洒的写出那四行代码的。

更多的 和 链表相关的话题:

链表的形态了解:

1,双链表:

我们之前的问题,在对队列尾端删除的时候,即使有tail ,也需要O(n) 的时间复杂度。

其实,我们可以用双链表来 解决这个事情。

不过,这样带给我们的代价是,由于有两个指针,所以维护起来会比较麻烦。

当然,双链表也可以通过使用 虚拟头节点 去使得处理逻辑统一。

2,循环链表:

进一步,有循环链表(双向),它可避免使用tail 指针了就。

链表也可以用数组来实现:

3,数组链表:

至此,之前的内容都是关于线性的。下面将进入非线性。首先的就是二分搜索树。

猜你喜欢

转载自www.cnblogs.com/zach0812/p/11872101.html