基于递归理解二叉树

前言

  • 二叉树是算法的基础,很多经典算法(回溯算法、动态规划、分治算法)其实都是树的问题,而树的问题就永远逃不开树的递归遍历框架这几行破代码:
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    // 前序遍历
    traverse(root.left)
    // 中序遍历
    traverse(root.right)
    // 后序遍历
}
二叉树的重要性
  • 快速排序就是个二叉树的前序遍历
  • 归并排序就是个二叉树的后序遍历
快速排序的逻辑
  • 若要对 nums[lo..hi] 进行排序,我们先找一个分界点 p,通过交换元素使得 nums[lo..p-1] 都小于等于 nums[p],且 nums[p+1..hi] 都大于 nums[p],然后递归地去 nums[lo..p-1]nums[p+1..hi] 中寻找新的分界点,最后整个数组就被排序了。
void sort(int[] nums,int lo,int hi){
	/**前序遍历位置**/
	// 通过交换元素构建分界点p
	int p = partition(nums,lo,hi);
	/**************/
	
	sort(nums,lo,p-1);
	sort(nums,p+1,hi);
}
  • 先构造分界点,然后去左右子数组构造分界点,本质就是一个二叉树的前序遍历
归并排序的逻辑
  • 若要对 nums[lo..hi] 进行排序,我们先对 nums[lo..mid] 排序,再对 nums[mid+1..hi] 排序,最后把这两个有序的子数组合并,整个数组就排好序了。
void sort(int []nums,int lo,int hi){
	int mid = (lo+hi)/2;
	sort(nums,li,mid);
	sort(nums,mid+1,hi);
	
	/**后序遍历位置**/
	// 合并两个排好序的子数组
	merge(num,lo,mid,hi);
	/**************/
}
  • 左右子数组排序,然后合并(类似合并有序链表的逻辑),其实就类似二叉树的后序遍历框架,也可以启发分治算法

递归算法的秘诀

  • 写递归算法的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要跳入递归的细节

  • 例如计算二叉树有多少个节点

int count(TreeNode root){
	if(root == null){
		return 0;
	}
	return 1 + count(root.left)+count(root.right);
}

226.翻转二叉树(简单)

class Solution {
    public TreeNode invertTree(TreeNode root) {
		if(root==null){
			return null;
		}
		
		TreeNode tmp = root.left;
		root.left = root.right;
		root.right = tmp;
		
		invertTree(root.left);
		invertTree(root.right);
		
		return root;
    }
}
  • 这道题目比较简单,关键思路在于我们发现翻转整棵树就是交换每个节点的左右子节点,于是我们把交换左右子节点的代码放在了前序遍历的位置。值得一提的是,如果把交换左右子节点的代码放在后序遍历的位置也是可以的.

  • 首先讲这道题目是想告诉你,二叉树题目的一个难点就是,如何把题目的要求细化成每个节点需要做的事情

114.二叉树展开为链表(中等)

void flatten(TreeNode root) {
    // base case
    if (root == null) return;

    flatten(root.left);
    flatten(root.right);

    /**** 后序遍历位置 ****/
    // 1、左右子树已经被拉平成一条链表
    TreeNode left = root.left;
    TreeNode right = root.right;

    // 2、将左子树作为右子树
    root.left = null;
    root.right = left;

    // 3、将原先的右子树接到当前右子树的末端
    TreeNode p = root;
    while (p.right != null) {
        p = p.right;
    }
    p.right = right;
}
  • 这就是递归的魅力,你说 flatten 函数是怎么把左右子树拉平的?说不清楚,但是只要知道 flatten 的定义如此,相信这个定义,让 root 做它该做的事情,然后 flatten 函数就会按照定义工作。另外注意递归框架是后序遍历,因为我们要先拉平左右子树才能进行后续操作。

116.填充每个节点的下一个右侧节点指针(中等)

// 主函数
Node connect(Node root) {
    if (root == null) return null;
    connectTwoNode(root.left, root.right);
    return root;
}

// 辅助函数
void connectTwoNode(Node node1, Node node2) {
    if (node1 == null || node2 == null) {
        return;
    }
    /**** 前序遍历位置 ****/
    // 将传入的两个节点连接
    node1.next = node2;

    // 连接相同父节点的两个子节点
    connectTwoNode(node1.left, node1.right);
    connectTwoNode(node2.left, node2.right);
    // 连接跨越父节点的两个子节点
    connectTwoNode(node1.right, node2.left);
}
  • 要注意跨越父节点的两个子节点也要连接起来

总结

  • 递归算法的关键要明确函数的定义,相信这个定义,而不要跳进递归细节。

  • 写二叉树的算法题,都是基于递归框架的,我们先要搞清楚 root 节点它自己要做什么,然后根据题目要求选择使用前序,中序,后续的递归框架。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值