20230511 递归

参考资料

基础

选择数组尾部、链表头部作为栈顶

  • 可以保证在操作栈时时间复杂度为 O(1)
  • 选择数组头部、链表尾部作为栈顶,时间复杂度是 O(n)

函数调用系统栈

  • 函数参数
  • 局部变量
  • 返回地址:函数执行完成后(本函数调用其他函数返回后),下一个指令的地址

示例:用汇编指令讲解函数调用

示例:用递归的方式打印数组

public static void main(String[] args) {
    int[] arr = {1, 2, 3, 4};

    print(arr, 0, arr.length - 1);
}

public static void print(int[] arr, int start, int end) {
    // 退出条件
    if (start > end) {
        return;
    }
    System.out.println(arr[start]);
    print(arr, start + 1, end);
}

理解递归

递归的本质就是函数调用,分为两个过程:递、归,两个过程都可以做一些操作

  • 递的过程:划分子问题
  • 归的过程:合并结果

递归需要更多的时间和空间开销

为什么使用递归:

  • 对于线性数据结构问题,一般使用迭代解决
  • 对于非线性数据结构(比如树、图等),使用递归来解决更加简单、更容易理解

什么场景下,可以使用递归?满足以下3个特点的问题可以使用递归

  • 大问题可以拆解成若干个子问题
  • 大问题和子问题的求解方法是一样的
  • 存在结果已知的最小子问题

对于递归的代码,没必要一步步去跟踪它的微观执行步骤,只需要抓住宏观语义即可

宏观语义:递归函数是做什么的

尾递归

函数调用在最后一行,称为尾调用

尾调用可以在编译时优化,被尾调用的函数可以和调用函数使用相同栈帧

public static void main(String[] args) {
    System.out.println(factorial(5));
    System.out.println(factorialTailRecursive(5, 1));
}

/**
    * 计算阶乘
    */
public static int factorial(int n) {
    if (n == 1) {
        return 1;
    }
    // 因为递归计算后,还有一步乘以n,所以不是尾递归
    return n * factorial(n - 1);
}

/**
    * 计算阶乘
    * 尾递归
    */
public static int factorialTailRecursive(int n, int sum) {
    if (n == 1) {
        return sum;
    }

    return factorialTailRecursive(n - 1, n * sum);
}

在常见数据结构中使用递归

数组、链表

递归一般不用于处理线性结构,一般使用迭代,这里是因为线性结构简单,方便理解

示例:删除链表中的指定节点

/**
 * 删除链表中的指定节点
 */
public class Test3 {
    public static void main(String[] args) {
        ListNode n76 = new ListNode(6);
        ListNode n65 = new ListNode(5, n76);
        ListNode n54 = new ListNode(4, n65);
        ListNode n43 = new ListNode(3, n54);
        ListNode n36 = new ListNode(6, n43);
        ListNode n22 = new ListNode(2, n36);
        ListNode n11 = new ListNode(1, n22);

        ListNode head = deleteNode(n11, 6);
        while (head != null) {
            System.out.println(head.val);
            head = head.next;
        }

    }

    /**
     * 删除链表中值为val的节点,并返回删除后新的头节点
     *
     * @param head
     * @param val
     * @return
     */
    public static ListNode deleteNode(ListNode head, int val) {
        if (head == null) {
            return null;
        }

        // 递的过程中,分解成子问题
        head.next = deleteNode(head.next, val);

        // 归的过程中,进行操作
        return head.val == val ? head.next : head;
    }


    private static class ListNode {
        int val;
        ListNode next;

        public ListNode(int val) {
            this.val = val;
        }

        public ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }
}

示例:反转链表

/**
    * 反转关系,并返回头节点
    */
public static ListNode reverse(ListNode head) {
    if (head.next == null) {
        return head;
    }

    ListNode newHead = reverse(head.next);

    // 将next节点的next指针指向自身
    head.next.next = head;
    // 将自身的next节点设置为null
    head.next = null;

    return newHead;
}

二叉树

三种遍历方式:前、中、后

DFS 可以解决二叉树 98% 的问题,98% 的二叉树问题本质上都是遍历问题

掌握DFS,可以解决90%的图的问题

实际应用

  • 快速排序:pivot,大小分区,两个指针,大小交换,在递的过程中进行排序
  • 归并排序:二分分区,两个指针,左右比较,临时数组,在归的过程中进行排序
  • 汉诺塔问题:讲解的非常好,p23
  • 回溯算法思想:示例-找到二叉树的所有路径,
  • 全排列:回溯代码框架,穷举,剪枝
  • 动态规划:斐波那契数列p26(不是动态规划,只是为了引入记忆化搜索),自底而上的解法是动态规划的经典模板,不断优化斐波那契数列解法
posted @ 2023-06-20 11:22  流星<。)#)))≦  阅读(8)  评论(0编辑  收藏  举报