链表回顾:
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 }
使用虚拟头节点:
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 }
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 }
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 }
原本只有四行的代码,被我们整成了 这么多行, 它说明 牛逼的算法可能是需要上百上千的代码才可以透彻的理解的。才能潇洒的写出那四行代码的。
更多的 和 链表相关的话题:
链表的形态了解:
1,双链表:
我们之前的问题,在对队列尾端删除的时候,即使有tail ,也需要O(n) 的时间复杂度。
其实,我们可以用双链表来 解决这个事情。
不过,这样带给我们的代价是,由于有两个指针,所以维护起来会比较麻烦。
当然,双链表也可以通过使用 虚拟头节点 去使得处理逻辑统一。
2,循环链表:
进一步,有循环链表(双向),它可避免使用tail 指针了就。
链表也可以用数组来实现:
3,数组链表:
至此,之前的内容都是关于线性的。下面将进入非线性。首先的就是二分搜索树。