玩转数据结构4-链表与递归

上一节课主要学习了一种具有真正动态数据结构的数据结构——链表,实现了链表基本的增删改查等操作,基于链表的操作特性,实现了栈的结构,并通过增加尾节点,进一步实现了队列这样一种数据结构。本节课从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(使用递归)

假设要删除下述链表中的某些元素:


image.png
  • 如果e需要删除,则删除e,并继续递归删除更短链表中相应的元素


    e.next=null
  • 如果e不需要删除,递归删除更短链表中相应的元素


    image.png
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,0)的第二行时,调用函数sum(arr,1):


    sum(arr,1)
  • 执行到sum(arr,1)的第二行时,调用函数sum(arr,2):


    sum(arr,2)
  • 返回sum(arr,1)的值,并继续执行sum(arr,0):


    sum(arr,0)

对链表删除元素也是一样的,模拟删除如下链表中的7:


LinkedList
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)
  • 对这个更小的链表,头节点不为空,调用removeElements(head.next,val)继续对下面更小的链表删除对应元素


    image.png
  • 重复这一过程,直到头节点为空


    image.png
  • 头节点为空时,返回空节点


    image.png
  • 判断头节点是否需要删除,如需要则直接返回子链表的删除结果,如不需要,则返回头节点


    image.png

    image.png

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. 总结

本节课主要介绍了递归的思想,结合链表的天然递归性,使用递归方法完成了链表元素的删除操作。递归的本质是把原问题转化为一个更小的同一问题,关键在于边界位置的处理以及如何进行转化。之后的课程将会涉及到大量的递归操作,多写多用,慢慢掌握其中技巧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容