二叉树与递归

递归思路

二叉树和递归。

  • 二叉树问题的思考方式

  • 为什么需要使用递归

  • 为什么这种写法一定能算出正确答案

  • 计算机如何执行递归

  • 另一种递归思路

举例子,计算一个二叉树的最大深度(leetcode 104),根节点到最远叶子节点的距离。一开始陷入二叉树的细节就不太好写代码了。先思考整棵树和左右子树的关系。

整棵树的最大深度 = max(左子树的最大深度, 右子树的最大深度) + 1

要计算子树的最大深度,和计算整个树的深度这个问题是一样的。

  • 原问题:计算整棵树的最大深度

  • 子问题:计算左右子树的最大深度

做循环计算,在使用同一份代码,那么这种相似问题,按道理也应该使用同一份代码计算。但这里的子问题需要把计算结果返回给上一级问题。子问题的规模比原问题小,不断向下(递),但总有尽头,即边界条件,直接返回答案(归)。

为什么这么做是对的?理论依据,数学归纳法证明。

#define MAX(a, b) (((a)>(b)) ? (a) : (b))

int maxDepth(struct TreeNode* root) {

    if (root == NULL)   return 0;
    return MAX(maxDepth(root->left), maxDepth(root->right)) + 1;
}

时间复杂度为 O(n),空间复杂度也是 O(n) 。return 怎么知道回到那个节点上去的,函数调用,栈帧。

另外一种思路,递归时除了把节点传下去,还可以把递归的层数传下去,同时维护一个全局变量,当层数大于最大值,就更新全局变量。这个思路在一些题目里也常用到。

#define MAX(a, b) (((a)>(b)) ? (a) : (b))

int result;

void dfs(struct TreeNode* root, int count)
{
    if (root == NULL)   return;

    count++;                        /* 递归层数 */
    result = MAX(count, result);    /* 记录最深递归层数 */
    dfs(root->left, count);         /* 理解返回的含义 */
    dfs(root->right, count); 
}

int maxDepth(struct TreeNode* root) 
{
    result = 0;
    dfs(root, 0);
    return result;
}

二叉树递归练习

二叉树最小深度(leetcode 111)

思路:递归遍历二叉树,同时判断是否为子叶,使用一个全局变量记录最小深度。

#define MIN(a, b) (((a)<(b)) ? (a) : (b))

int min;

void dfs(struct TreeNode* root, int count)
{
    if (root == NULL)   return;

    count++;

    if ( root->left == NULL && root->right == NULL)
        min = MIN(min, count);

    dfs(root->left, count);
    dfs(root->right, count);
}

int minDepth(struct TreeNode* root) {
    min = INT_MAX;
    dfs(root, 0);
    return (min == INT_MAX) ? 0 : min; 
}

路径总和 leetcode 112

关于递归函数参数与返回值的思考

int target;

bool dfs(struct TreeNode* root, int sum)
{
    if ( root == NULL)
        return false;
    sum += root->val;
    if ( root->left == NULL && root->right == NULL)
    {
        if ( sum == target )    return true;
        else                    return false;
    }
    return dfs(root->left, sum) || dfs(root->right, sum);
}

bool hasPathSum(struct TreeNode* root, int targetSum) {
    target = targetSum;
    return dfs(root, 0);
}

求根节点到叶节点数字之和 leetcode 129

统计二叉树中好节点的数目 leetcode 1448

思路:传入参数中有一个到当前层的最大值,然后维护一个全局变量记录符合要求的节点数。

int count;
void dfs(struct TreeNode* root, int max)
{
    if (root == NULL)       return;
    
    if (root->val >= max)
    {
        max = root->val;
        count++;
    }

    dfs(root->left, max);
    dfs(root->right, max);
}


int goodNodes(struct TreeNode* root){
    count = 0;
    dfs(root, INT_MIN);
    return count;
}

不使用全局变量?使用 return 返回数量呢?

int dfs(struct TreeNode* root, int max)
{
    if (root == NULL)   return 0;
    int ret = 0;
    if (root->val >= max)
    {
        max = root->val;
        ret = 1;
    }
    max = root->val >= max ? root->val : max;
    return ret + dfs(root->left, max) + dfs(root->right, max);
}

int goodNodes(struct TreeNode* root){
    return dfs(root, INT_MIN);
}

二叉树的垂序遍历 987

二叉树相同

判断相同二叉树(leetcode 100)

判断相同二叉树(leetcode 100)。根节点相同,然后左子树要相同,右子树也要相同。

如何判断根节点相同?节点的值相同的前提条件是两个节点都不是空节点,因此判空在先。如果两个节点都是空节点,那么返回 true 即相同,一个编程技巧,判断空节点时可以使用 p == q 来判断,只有都为 NULL 才相同,其他的地址是不可能相同的。这也是一个比较巧妙的边界条件。

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {

    if ( (p==NULL) || (q==NULL) )   return p == q;
    return (p->val == q->val) && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

判断对称二叉树 leetcode 101

如何思考?如果在没有上一题的的前提下,是否能想到这一步

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if ( p == NULL || q == NULL )   return p == q;
    return (p->val == q-> val) && isSameTree(p->left, q->right) && isSameTree(p->right, q->left);
}
bool isSymmetric(struct TreeNode* root) {
    if (root == NULL)   return true;
    return isSameTree(root->left, root->right);
}

判断平衡二叉树

#define MAX(a, b) (((a)>(b)) ? (a) : (b))
#define ABS(x)  (((x)>0) ? (x): (-(x)))

int max_depth(struct TreeNode* root)
{
    if (root == NULL)
        return 0;

    int l = max_depth(root->left);
    int r = max_depth(root->right);

    if ( l == -1 || r == -1)
        return -1;

    if (ABS(l-r) <= 1)
        return MAX(max_depth(root->left), max_depth(root->right)) + 1;
    else 
        return -1;
}

bool isBalanced(struct TreeNode* root) {

    if (max_depth(root) == -1)
        return false;
    return true;
}

二叉树的右视图 leetcode 199

#define MAX(a, b) (((a)>(b)) ? (a) : (b))
#define ABS(x)  (((x)>0) ? (x): (-(x)))

int max_depth;
int *result;

void find(struct TreeNode* root, int count)
{
    if (root == NULL)
        return;

    count++;
    
    if (count > max_depth)
    {
        result[max_depth] = root->val;
        max_depth = count;
    }

    find(root->right, count);
    find(root->left, count);
}


int* rightSideView(struct TreeNode* root, int* returnSize) {
    max_depth = 0;
    result = malloc(100*sizeof(int));

    find(root, 0);

    *returnSize = max_depth;
    return result;    
}

验证二叉搜索树

二叉树的遍历。前中后序遍历。

二叉树是不是二叉搜索树:对于一个节点来说,左子树节点值小于,右子树节点值大于。左子树右子树也必须是二叉搜索树。

方法一:前序遍历

区间分割。根据当前的节点值确定区间,然后递归左右子树。

先访问节点值,再递归左右子树的做法就是前序遍历(中左右)

bool dfs(struct TreeNode* root, long long int l, long long int r)
{
    if (root == NULL)
        return true;

    return  (root->val > l && root->val < r) && dfs(root->left, l, root->val) && dfs(root->right, root->val, r);
}

bool isValidBST(struct TreeNode* root) {
    return dfs(root, LLONG_MIN, LLONG_MAX);
}

在 dfs 函数中,return 后面的表达,隐含了先访问根节点,然后遍历左子树,遍历右子树。

时间复杂度为 O(n), 空间复杂度为 O(n)。

方法二:中序遍历

按照递归左子树,访问节点值,递归右子树这个顺序遍历,这就是中序遍历(左中右)。

二叉搜索树中序遍历的优秀性质:中序遍历访问节点值是严格递增的。

long long int pre = LLONG_MIN;

bool isValidBST(struct TreeNode* root) {
    if (root == NULL)
        return true;
    if (isValidBST(root->left) == false)
        return false;
    if (root->val <= pre )
        return false;
    pre = root->val;
    return isValidBST(root->right);
}

存在的问题:leetcode OJ 可能会在同一个进程里重复调用 isValidBST 函数,但是 pre 变量还没更新。同时使用 3 个测试用例,都只包含一个节点为0,但是第一个用例通过了,后面两个没过。

这个问题在一些题目里也遇到过,全局变量初始化后,运行测试用例会出问题,但是在核心代码里重新初始化就可以了。可以反向推断出 leetcode 的 OJ 是子啊一个进程里重复调用算法函数的。

可以通过 OJ 的实现

long long int pre;

bool dfs(struct TreeNode* root)
{
    if (root == NULL)
        return true;
    if (dfs(root->left) == false)
        return false;
    if (root->val <= pre )
        return false;
    pre = root->val;
    return dfs(root->right);    
}

bool isValidBST(struct TreeNode* root) {
    pre = LLONG_MIN;
    return dfs(root);
}

方法三

前序遍历的思路是把区间向下传递,还有一个思路是把节点的范围往上传。

最近公共祖先

struct TreeNode* lowestCommonAncestor(struct TreeNode* root, struct TreeNode* p, struct TreeNode* q) {

    if ( root == NULL || root == p || root == q )
        return root;
    
    struct TreeNode* left = lowestCommonAncestor(root->left, p, q);
    struct TreeNode* right = lowestCommonAncestor(root->right, p, q);

    if (left && right)
        return root;

    if (left)
        return left;

    return right;
}

层序遍历

广度优先搜索。递归从根节点出发一直往下走,层序遍历则是一行行遍历。

如何从根节点出发得到每层的值?

初始化一个 current 数组,把根节点放进去,然后开始循环。

还需要一个 next 数组,遍历 current,把左右儿子都记录到 next 中。遍历 current 的同时,记录节点值,遍历结束后添加答案。

使用两个数组实现。

#define MAX_RESULT  100000

int** result;       /* 保存结果的数组 */
int result_len;     /* 结果个数 */
int *column_size;   /* 每个结果的长度 */

struct TreeNode* current[1000];
int cur_len;
struct TreeNode* next[1000];
int nex_len;

int path[1000] = {0}; /* 单个结果 */
int path_len;       /* 单个结果的长度 */

void save_result(void)
{
    result[result_len] = malloc(path_len * sizeof(int));
    column_size[result_len] = path_len;

    for (int i=0; i<path_len; i++)
        result[result_len][i] = path[i];

    result_len++;
}

int** levelOrder(struct TreeNode* root, int* returnSize, int** returnColumnSizes) {
    result = malloc(MAX_RESULT * sizeof(int *));
    result_len = 0;
    column_size = malloc(MAX_RESULT * sizeof(int));

    cur_len = 0;

    if (root != NULL)
        current[cur_len++] = root;

    while ( cur_len != 0)
    {
        path_len = 0;
        nex_len = 0;
        for (int i=0; i<cur_len; i++)
        {
            if ( current[i]->left != NULL)
                next[nex_len++] = current[i]->left;
            if ( current[i]->right != NULL)
                next[nex_len++] = current[i]->right; 
            path[path_len++] = current[i]->val;
        }

        save_result();

        for (int i=0; i<nex_len; i++)
        {
            current[i] = next[i];
        } 

        cur_len = nex_len;
    }

    *returnSize = result_len;
    *returnColumnSizes = column_size;  /* 不是 &column_size */
    return result;
}

两个数组可以合并为一个数组,下一层的节点可以跟在当前层节点后面,这就是队列。

此外还有一题为二叉树的锯齿形层序遍历,和这个题差不多。切入的角度:在保存遍历结果时,反转数组。

类似的 leetcode 513 ,找树左下角的值,思路:返回层序遍历最后一层的第一个元素即可。

二叉树遍历节点

前序遍历的定义就是用递归定义的,二叉树本身也是用递归定义的。关于前中后,指的是中间节点的位置。

void preorder(struct TreeNode* root, int* ret, int* returnSize)
{
    if (root==NULL)
        return;

    ret[*returnSize] = root->val;
    (*returnSize)++;

    preorder(root->left, ret, returnSize);
    preorder(root->right, ret, returnSize);
}


int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    int* ret = (int*)malloc(sizeof(int) * 100);
    *returnSize = 0;
    preorder(root, ret, returnSize);
    return ret;
}

后序遍历。

void back_order(struct TreeNode* root, int *size, int *out)
{
    if (root == NULL)
        return;

    back_order(root->left, size, out);
    back_order(root->right, size, out);

    out[(*size)] = root->val;
    (*size) ++;
}

int* postorderTraversal(struct TreeNode* root, int* returnSize) {

    int *ret_array = (int *)malloc(100*sizeof(int));
    *returnSize = 0;

    back_order(root, returnSize, ret_array);
    return ret_array;
}

最后更新于