写递归,其实只要两步

递归函数的思路

很多人觉得递归函数很绕,不知道怎么写,其实只是没掌握思路,一旦你了解了递归函数的思路,写递归就是一件水到渠成的事情了。
我自己写递归函数一般分两步:

  • 缩小问题的规模,并且把规模缩小到一个单位的规模,并且解决掉第一个单位
  • 增加检查条件
    其实就是这么简单的两步,多说无益,直接看题。

    1. 累加

    1+2+3+····+n
  • 累加就是一个非常简单的递归例子
  • 首先把问题缩小到一个单位的规模,这里就是一个数,并且选择第一个单位,就是1
public int sum(int n) {
    // 把问题缩小到一个单位的规模
    if(n == 1){
        return 1;
    }
    // 调用递归
    return sum(n - 1) * n;
}
  • 测试用例
    /**
     * 测试用例
     * @param args
     */
    public static void main(String[] args) {
        Demo demo = new Demo();
        System.out.println(demo.sum(5));
        System.out.println(demo.mulSum(5));
    }
输出:
15
120

2. 根据list创建链表

  • 首先我们先写一个链表的数据结构
/**
 * 节点类
 *
 */
public class Node {
    private final int value; // 本结点的值
    private Node next; // 下一个结点
    public Node(int value, Node next) {
        this.value = value;
        this.next = null;
    }
    public Node getNext() {
        return next;
    }
    public void setNext(Node next) {
        this.next = next;
    }
    public int getValue() {
        return value;
    }
    /**
     * 打印方法
     * @param head
     */
    public static void printLinkedList(Node head) {
        while(head != null) {
            System.out.print(head.getValue());
            System.out.print(" ");
            head = head.getNext();
        }
        System.out.println();
    }
}
  • 接下来就是递归来根据 list 生成链表
  • 思路还是一样的,先把问题规模缩小到一个单位,就是链表的一个结点,并且解决掉第一个单位,就是解决掉链表的第一个结点
    /**
     * 生成链表
     * @param data
     * @return 返回头结点
     */
    public Node createLinkedList(List<Integer> data) {
        // 把问题缩小到链表的一个结点,在解决掉链表的第一个结点
        // 这里链表的第一个结点值就是 list 第一个值,就是data.get(0),调用构造函数去生成第一个结点
        Node first = new Node(data.get(0), null);
        // 递归生成接下来的所有结点
        Node headOfSubList = createLinkedList(data.subList(1, data.size()));
        // 将第一个结点的引用指向下一个结点
        first.setNext(headOfSubList);
        return first;
    }
  • 做完第一步其实就已经是完成递归了,接下来就是增加条件检查,这里调用了data.get(0),所以应该去对data为空的情况下,做一个检查
    /**
     * 生成链表
     * @param data
     * @return 返回头结点
     */
    public Node createLinkedList(List<Integer> data) {
        // 检查 data 是否为空
        if(data.isEmpty()) {
            return null;
        }
        Node first = new Node(data.get(0), null);
        Node headOfSubList = createLinkedList(data.subList(1, data.size()));
        first.setNext(headOfSubList);
        return first;
    }
  • 测试用例
    /**
     * 测试用例
     * @param args
     */
    public static void main(String[] args) {
        Recursion rec = new Recursion();
        Node.printLinkedList(rec.createLinkedList(new ArrayList<>()));
        Node.printLinkedList(rec.createLinkedList(Arrays.asList(1)));
        Node.printLinkedList(rec.createLinkedList(Arrays.asList(1, 2, 3, 4, 5)));
    }
输出:

1 
1 2 3 4 5 

3. 递归删除指定结点

  • 假设我们有一条链表,我们传一个参数进去,利用递归删除掉这个结点。就是12345,传个2,就变成1345的链表
  • 思路还是一样,首先我们应该把问题规模缩小到一个单位,也就是链表的头结点,然后对头结点进行操作,假如这个结点不是我们要删除的结点,我们就保留它,假如是我们要删除的,我们就把头结点赋值给下一个结点
    /**
     * 递归删除指定结点
     * @param head 头结点
     * @param N 要删除的值
     * @return 头结点
     */
    public Node deleteNode(Node head, int N) {
        // 可能连续几个结点都是我们要删除的值,所以我们这里用一个 while 循环,而不是一个 if 判断
        // 当头结点就是我们要删除的结点,我们就把下一个结点赋值给头结点
        while(head.getValue() == N) { 
            head = head.getNext();
        }
        // 递归生成所有结点
        head.setNext(deleteNode(head.getNext(), N));
        return head;
    }
  • 接下来,就是增加条件判断,代码中调用了 head.getValue() 和 head.getNext(),所以我们应该对 head 做一个非空判断
    /**
     * 递归删除指定结点
     * @param head
     * @param N
     * @return
     */
    public Node deleteNode(Node head, int N) {
        if(head == null) {
            return null;
        }
        while(head.getValue() == N) {
            head = head.getNext();
            if(head == null)
                return null;
        }
        head.setNext(deleteNode(head.getNext(), N));
        return head;
    }

-- 测试用例

    /**
     * 测试用例
     * @param args
     */
    public static void main(String[] args) {
        Recursion rec = new Recursion();
        Node.printLinkedList(rec.deleteNode(rec.createLinkedList(Arrays.asList(2, 2, 1, 2, 2, 3, 4, 5)), 2));
                Node.printLinkedList(rec.deleteNode(rec.createLinkedList(new ArrayList<>()), 2));
    }
输出:
1 3 4 5 

看完这几个例子,其实就不难发现,只要理解的递归的思路,其实递归函数是非常容易写的,并且相当,简单粗暴。

递归的缺点

递归函数简单易理解,也相对暴力,所以也有显而易见的缺点,简单来讲,就是我们每次调用一个方法,其实就像向虚拟机栈里面压一个栈帧,所以如果我们如果递归调用的层数太多,像是我们要生成一个一百万个结点的链表,这时候就是报异常了,就是 StackOutFlowError 堆栈溢出异常。
为了解决这个问题,我们也可以根据实际的情况,将递归函数改造成 while 循环的方法,这样就只调用一次方法,就不会有 StackOutFlowError 的情况了。这里举个例子

循环删除链表的结点

问题还是删除链表的结点,只是这里用 while 来代替递归函数。我这里用 curNode 作为一个游标,来表示现在检查到的结点,如果我们检查到 curNode 的下一个结点是我们要删除的,我们就把 next 指向再下一个结点。

    /**
     * while 循环删除结点
     * @param head 头结点
     * @param value 要删除的值
     * @return 头结点
     */
    public Node deleteNodeByLoop(Node head, int value) {
        if(head == null) {
            return null;
        }
        // 处理头结点重复的情况
        while(head.getValue() == value) {
            head = head.getNext();
            if(head == null){
                return null;
            }
        }
        // 处理头结点之后的结点
        Node curNode = head;
        while(curNode.getNext() != null) {
            if(curNode.getNext().getValue() == value) {
                curNode.setNext(curNode.getNext().getNext());
            } else {
                curNode = curNode.getNext();
            }
        }
        return head;
    }

写在最后

源码在这里
还写了链表反转的递归写法和 while 写法,可以了解一下

猜你喜欢

转载自www.cnblogs.com/wengzp/p/9333646.html