常见递归模式

常见递归模式

    • 递归模式
      • 遍历二叉树模式
      • 回溯模式
      • 子问题分解模式

 


递归模式

常见递归模式:

  • 遍历二叉树模式
  • 回溯模式
  • 子问题分解模式
     

遍历二叉树模式

只要涉及递归的问题,都是树的问题,或者说树的遍历。

void traverse(TreeNode root) { // 遍历二叉树
    if (root == null) return;  
    print(root.val)            // 注意:【前序位置,在进入左节点前】,输出当前节点
    traverse(root.left);       // 进入左子树
    print(root.val)            // 注意:【中序位置,在进入右节点前】,输出当前节点
    traverse(root.right);      // 进入右子树
    print(root.val)            // 注意:【后序位置,离开右节点后】,输出当前节点
}

这个代码的关键在于,时机。下图是可视化过程:
常见递归模式_第1张图片

  • 前序位置,放一个函数:在进入左节点前,做某事
  • 中序位置,放一个函数:在离开左节点后,进入右节点前,做某事
  • 后序位置,放一个函数:在离开右节点后,做某事

这里主要强调前序、后序的区别。

  • 前序能解决的问题:遍历二叉树直接计算出来
  • 后序能解决的问题:遍历二叉树直接计算出来,及遍历完子树之后才能计算出来。

任何递归函数,本质上都是在遍历一棵(递归)树。

  • 设计每一个节点的模式(遍历二叉树模式、回溯模式、分解子问题模式下)
  • 设计需要在什么时候(前\中\后序位置)做某事。

递归函数 = 遍历二叉树模式/回溯模式/分解子问题模式 + 前/中/后序位置 + 做某事

比如归并排序是,遍历二叉树模式 + 后序位置 + 做某事(合并数组merge)。

void merge_sort(int a[], int l, int r) {
    if( l >= r ) return;             // 只有一个元素,或者没有元素,不用排序
    int mid = (l + r) / 2;           // 分界点
    merge_sort(a, l, mid);           // 分治,对前半部分排序
    merge_sort(a, mid+1, r);         // 分治,对后半部分排序
	merge(a, l, mid, r);             // 【后序位置】合并,对俩个有序数组排序
}

比如快速排序是,遍历二叉树模式 + 前序位置 + 做某事(中枢值划分partition)。

void QuickSort(int arr[], int l, int r) {
	if( l >= r ) return;
	int p = partition(arr, l, r);    // 【前序位置】p 是划分中枢值的下标
	QuickSort(arr, l, p-1);          // 小于 p
	QuickSort(arr, p+1, r);          // 大于 p
}

 


回溯模式

使用场景:问题可通过遍历一棵二叉树得到答案。

ans = []
void recall( 路径,[选择列表] )
	if 满足结束条件:
		ans.add( 路径 )
		return
		
	for 选择 in [选择列表]:
		做选择
		recall( 路径,[选择列表] )
		撤销选择

回溯框架,本质是遍历一颗决策树。

  • 路径:已经做出的选择
  • 选择列表:当前可以做的选择
  • 结束条件:到了决策树底层,无法再做选择


核心在于 for 循环里面的递归,在递归之前做选择,在递归之后撤销选择。

  • for 循环,如果可视化就是在遍历一颗 N 叉树

问题是,选择和撤销选择是在这颗树上做什么呢?

  • 选择:是在这棵树上做前序遍历
  • 撤销选择:是在这颗树上做后序遍历


选择是,在进入树的某一节点前执行。

撤销选择是,在离开树的某一节点后执行。

做选择:在进入节点前,从选择列表拿出一个选择,将它放入路径。

撤销选择:在离开节点后,从路径中拿出一个选择,将它恢复到选择列表中。
 


子问题分解模式

使用场景:可通过子问题/子树的答案推导出原问题的答案。

原问题,分解成当前节点 + 左右子树的子问题。

int dp(TreeNode root) {        // 二叉树版本
    if (root == null) 
		return 0;
		
    // 分解(子问题的规模为n/2,求出前半部分的最值,和后半部分的最值)
    int left = dp(root.left);
	int right = dp(root.right);
 
    // 合并(在把前半部分的最值和后半部分的最值做个比较,相当于求整个大数组的最值)
    return 最值(left, right);
}

int dp(int arr[], 某状态) {    // 多叉树版本
    for a in arr:
        res = 最值(res, dp(arr, 某状态));
    return res;
}

因为位置的原因,前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。

一旦发现问题和子树有关,我们用后序位置 + 给函数设置返回值,可以简化代码。

你可能感兴趣的:(算法,决策树,算法)