第5章:链表和递归密不可分的关系(Leetcode原题)

第五章 链表和递归

5-1 Leetcode中和链表相关的问题
5-2 测试自己的Leetcode链表代码
5-3 递归基础与递归的宏观语意
5-4 链表的天然递归结构性质
5-5 递归运行的机制:递归的微观解读
5-6 递归算法的调试
5-7 更多和链表相关的问题


5-1 Leetcode中和链表相关的问题
https://leetcode-cn.com/problems/remove-linked-list-elements/

题目中给出了ListNode的定义。接下来用两种方法进行链表中元素的删除:

//Definition for singly-linked list.
public class ListNode {
    public int val;
    public ListNode next;

    public ListNode(int x){
        val = x;
    }
}

解法一:没有虚拟头节点的情况
(1)删除头节点,可能需要连续删除,需要使用while循环(比如示例中有两个6需要删除)

ListNode delNode = head;
head = head.next;
delNode.next = null;

(2)删除中间的节点也需要使用while循环,循环条件是 prev.next != null; 删除条件是 prev.next.val == val.

ListNode delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;

注意元素删除后,不要立即将prev右移,因为可能新的prev.next也是需要删除的节点,这时候需要再次进入while循环。

class Solution1 {
    public ListNode removeElements(ListNode head, int val){
        while(head != null && head.val==val){
            // head = head.next; (下面三行代码可以简化为这一行代码)
            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; (下面三行代码可以简化为这一行代码)
                ListNode delNode = prev.next;
                prev.next = delNode.next;
                delNode.next = null;
            }
            else{
                prev = prev.next;
            }
        }
        return head;
    }
}

解法二:使用虚拟节点dummyhead的情况
使用dunmmyHead需要注意链表是否为空(详细的说明可以查看4-5)

class Solution2 {
    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;
    }
}

简单总结:

  • LeetCode上的原题,给定一个value让你删除其链表中所有相同值的节点,与上一个章节对链表的操作是完全相同的,唯一一点区别在于把if判断语句改成了while,是因为在一个链表中可能存在多个相同数值的节点。

5-2 测试上述Leetcode链表代码
通过一个数组构建一个链表 (使用arr为参数,创建一个链表,当前的ListNode为链表头节点),并且打印这个链表相应的字符串。

 // 链表节点的构造函数
    // 使用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 res = new StringBuilder();
        ListNode cur = this; // 从自身开始进行循环
        while(cur != null){
            res.append(cur.val + "->");
            cur = cur.next;
        }
        res.append("NULL");
        return res.toString();
    }

题目所给的测试用例的实现及结果如下:

 public static void main(String[] args){
        int[] nums = {1, 2, 6, 3, 4, 5, 6};
        ListNode head = new ListNode(nums);
        System.out.println(head);

        ListNode res = (new Solution1()).removeElements(head, 6);
        // ListNode res = (new Solution2()).removeElements(head, 6);
        System.out.println(res);
    }

1->2->6->3->4->5->6->NULL
1->2->3->4->5->NULL

5-3 递归基础与递归的宏观语义
递归:本质上,将原来的问题,转化为更小的同一问题

用代码来实现数组求和:

public class Sum {
    // 为用户设计使用的sum函数
    // 用户只需要传进来一个int型的arr数组
    public static int sum(int[] arr){
        return sum(arr, 0);
        // 0:递归的初始调用是从0到n-1这些元素所有的和
    }
    // 计算arr[l,...,n]这个区间内所有数字的和
    // 这个sum函数是真正的递归函数
    private static int sum(int[] arr, int l){
        if (l == arr.length)    
            return 0;
        return arr[l] + sum(arr, l+1);
        // 从计算[l,n]区间数字的和 -> 计算[l+1,n]区间数字的和
    }
    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4, 5, 6, 7, 8};
        System.out.println(sum(nums));
    }
}
  • 在写递归函数的时候,一定要注重递归函数本身的语义(递归函数的宏观语义)不用太纠结于里边具体的程序执行机制。本质上:函数A里边调用函数A,与函数A里边调用函数B是没有区别的。
  • 写递归算法的基本原则:第一个部分是求解最基本问题; 第二部分是把原问题转化成更小的问题。
  • 将一个问题分解成一个更小的问题,一个更小的问题再分解成一个更更小的问题…最后一直分解直至称为一个可以解决的最基本的问题,最基本的问题靠程序员自己编写逻辑求解。

举个栗子:在数组求和的这个例子中,待解决的问题是将数组中所有的数进行求和。将其分解为更小的一个问题就是从第一个元素求到最后一个元素的和,再分解为从第二个元素求到最后一个元素的和…这样分解直至只用求最后一个元素为止。那么求最后一个元素的值便是程序员所要解决的最基本的问题。

  • 把原问题转化为更小的问题不是简单地求一个更小的问题的答案,要根据更小的问题的答案构建出原问题的答案。(这里表现为arr[1] + 更小的数组中的元素的和)

对于一个复杂的递归算法来说,这个逻辑可能是非常复杂的。
难点在于如何把一个问题转化为一个更小的问题,并根据更小问题的答案构建出原问题的答案。

  1. 注意递归函数的“宏观”语义
  2. 递归函数就是一个函数,完成一个功能

5-4 链表的天然递归结构性质

对于这个链表可以想像成是0这个节点后边又挂了一个更短的链表(比整个链表少了一个节点),这里1就是这个更短链表的头节点,以此类推,直到最后可以理解为NULL本身也是一个链表,而NULL就是那个最基础、最平凡的链表。

解法三:用递归来解决链表中删除元素的问题:

代码实现如下:

public class Solution3 {
    // 使用递归方法来解决问题
    public ListNode removeElements (ListNode head, int val){
        if (head == null)
            return null;
        ListNode res = removeElements(head.next, val);
        // 宏观语义:对一个链表中删除值为val的节点
        // head后边接的那个更短的链表(head.next),将这个链表中值为val相应的元素删除
        // res中存储的就是:将头节点后边跟的那个链表中所有的值为val的节点删除后,剩下的这个链表
        if (head.val == val)
            return res;
        else{
            head.next = res;
            return head;
        }
    }
}

这里注意一下可以简化以上代码,四行代码就可以完美解决这个问题。

 // 使用递归方法来解决问题
    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;
        // 上一行代码可以解释为下面的代码:
//        if (head.val == val)
//            return head.next;
//        else{
//            return head;
        }
    }

运行一下这个代码,结果如下:

public static void main(String[] args){
        int[] nums = {1, 2, 6, 3, 4, 5, 6};
        ListNode head = new ListNode(nums);
        System.out.println(head);

        ListNode res = (new Solution3()).removeElements(head, 6);
        System.out.println(res);
    }

1->2->6->3->4->5->6->NULL
1->2->3->4->5->NULL

5-5 递归运行的机制:递归的微观解读

  • 递归函数的调用,本质就是函数调用,只不过调用的函数是自己而已。
    举个栗子:

  • 递归调用至最后一个子程序时,返回值会返回上一个子程序终止位置继续执行。

与程序调用系统栈原理一致。

  • 这里简单的表述一下:调用sum(arr, 2),返回值为0 (if(l == n) return 0),因此在调用sum(arr, 1)中,sum(arr, l+1)的值为0,因此,x=0, res = arr[1] + 0 = 10 (这里arr[1] = 10, x = 0)。上一步我们得到了调用sum(arr, 1)的返回值res = 10, 因此在sum(arr, 0)中,sum(arr, l+1)的值为10,因此x = 10, res = arr[0] + 10 = 16 (这里arr[0] = 6, x = 10)。最后我们得到的返回结果为:16

-> 用程序调用系统栈的方式进行一次解释:

  • 具体过程不再详细描述,可以自己先思考一下看是否能够理解。若不能理解,再次观看教学视频:
    https://coding.imooc.com/lesson/207.html#mid=13441
  • 递归的调用本质上和子过程的调用是没有区别的,A调用B,B调用C == A调用A,A调用A
  • 从代码的角度看,执行递归调用的本质是重新调用了以下逻辑,但此时是对一组新的参数调用了逻辑
  • 递归调用是有代价的: 函数调用 + 系统栈空间
  • 递归调用编写非线性数据结构(树结构、图结构)的代码逻辑更简单一些,线性结构由于结构简单使得递归调用的优势并不明显,使用常用的循环遍历即可解决大部分线性结构问题。

5-6 递归算法的调试
-> 使用一个打印输出的方式,来展现出一个递归函数调用的整个过程。

import java.util.List;

public class Test {
    public ListNode removeElements(ListNode head, int val, int depth){

        String depthString = generateDepthString(depth);

        System.out.println(depthString);
        System.out.println("Call: remove " + val + " in " + head);

        if (head == null){
            System.out.println(depthString);
            System.out.println("Return: " + head);
            return head;
        }

        ListNode res = removeElements(head.next, val, depth+1);
        System.out.println(depthString);
        System.out.println("After remove" + val + ": " + res);

        ListNode ret;
        if (head.val == val)
            ret = res;
        else {
            head.next = res;
            ret = head;
        }
        System.out.println(depthString);
        System.out.println("Return: " + ret);
        return ret;
    }

    private String generateDepthString(int depth){
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < depth; i ++)
            res.append("--");
        return res.toString();
    }

    public static void main(String[] args){
        int[] nums = {1, 2, 6, 3, 4, 5, 6};
        ListNode head = new ListNode(nums);
        System.out.println(head);

        ListNode res = (new Test()).removeElements(head, 6, 0);
        System.out.println(res);
    }
}

运行结果如下:

1->2->6->3->4->5->6->NULL
– Call: remove 6 in 1->2->6->3->4->5->6->NULL
---- Call: remove 6 in 2->6->3->4->5->6->NULL
------ Call: remove 6 in 6->3->4->5->6->NULL
-------- Call: remove 6 in 3->4->5->6->NULL
---------- Call: remove 6 in 4->5->6->NULL
------------ Call: remove 6 in 5->6->NULL
-------------- Call: remove 6 in 6->NULL
-------------- Call: remove 6 in null
------------ Return: null
------------ After remove6: null
---------- Return: null
---------- After remove6: null
-------- Return: 5->NULL
-------- After remove6: 5->NULL
------ Return: 4->5->NULL
------ After remove6: 4->5->NULL
---- Return: 3->4->5->NULL
---- After remove6: 3->4->5->NULL
– Return: 3->4->5->NULL
– After remove6: 3->4->5->NULL
Return: 2->3->4->5->NULL
After remove6: 2->3->4->5->NULL
Return: 1->2->3->4->5->NULL
1->2->3->4->5->NULL

5-7 更多和链表相关的问题

  • 近乎和链表相关的所有操作,都可以使用递归的形式完成
  • 建议同学们对链表的增、删、改、查,进行递归实现
  • Leetcode中链表/递归相关的题目可以看看(不要想着把所有的问题都解决了再进行下面的学习)
  • 双链表、循环链表、数组链表
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值