算法学习笔记Day7——二叉树全解

一、入门篇

三、动归/DFS/回溯算法

动归/DFS/回溯算法都可以看做二叉树问题的扩展,只是它们的关注点不同:

  • 动态规划算法专注于把问题拆分成更小的子问题
  • 回溯算法专注于节点之间的移动
  • DFS 算法专注于一个点要做的事情

例子1:分解问题的思路

int count(TreeNode* root){
    if(root == nullptr){
        return 0;
    }
    int left = count(root->left);
    int right = count(root->right);
    return left + right + 1;
}

 这就是动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」。

例子2:写一个打印出遍历这棵二叉树过程的函数

void traverse(TreeNode* root){
    if(root == nullptr){
        return;
    }
    printf("从节点%s进入到节点%s", root, root->left);
    traverse(root->left);
    printf("从节点%s回到到节点%s", root->left, root);

    printf("从节点%s进入到节点%s", root, root->right);
    traverse(root->right);
    printf("从节点%s进入到节点%s", root->left, root);
}

上述代码进阶到多叉树:

void traverse(Node* root) {
    if (root == nullptr) return;
    for (Node* child : root->children) {
        printf("从节点 %s 进入节点 %s", root, child);
        traverse(child);
        printf("从节点 %s 回到节点 %s", child, root);
    }
}

 再回来看回溯算法框架

// 回溯算法核心部分代码
void backtrack(int[] nums) {
    // 回溯算法框架
    for (int i = 0; i < nums.length; i++) {
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);

        // 进入下一层回溯树
        backtrack(nums);

        // 取消选择
        track.removeLast();
        used[i] = false;
    }
}

这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,做选择和撤销选择,类比到二叉树上就是「树枝」。

例子3:把这棵二叉树上的每个节点的值都加一

void traverse(TreeNode* root) {
    if (root == nullptr) return;
    // 遍历过的每个节点的值加一
    root->val++;
    traverse(root->left);
    traverse(root->right);
}

这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」。

DFS 算法和回溯算法非常类似,只是在细节上有所区别

这个细节上的差别是什么呢?其实就是「做选择」和「撤销选择」到底在 for 循环外面还是里面的区别,DFS 算法在外面,回溯算法在里面。

// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面
void dfs(Node* root) {
    if (root == NULL) return;
    // 做选择
    printf("我已经进入节点 %p 啦\n", root);
    for (Node* child : root->children) {
        dfs(child);
    }
    // 撤销选择
    printf("我将要离开节点 %p 啦\n", root);
}

// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面
void backtrack(Node* root) {
    if (root == NULL) return;
    for (Node* child : root->children) {
        // 做选择
        printf("我站在节点 %p 到节点 %p 的树枝上\n", root, child);
        backtrack(child);
        // 撤销选择
        printf("我将要离开节点 %p 到节点 %p 的树枝上\n", child, root);
    }
}

关于两者详细可见:

DFS 算法解决岛屿题目

回溯算法解决排列-组合-子集问题

四、层序遍历

二叉树题型主要是用来培养递归思维的,而层序遍历属于迭代遍历

// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode* root) {
    if (root == nullptr) return;
    queue<TreeNode*> q;
    q.push(root);

    // 从上到下遍历二叉树的每一层
    while (!q.empty()) {
        int sz = q.size();
        // 从左到右遍历每一层的每个节点
        for (int i = 0; i < sz; i++) {
            TreeNode* cur = q.front();
            q.pop();
            // 将下一层节点放入队列
            if (cur->left != nullptr) {
                q.push(cur->left);
            }
            if (cur->right != nullptr) {
                q.push(cur->right);
            }
        }
    }
}

 这里面 while 循环和 for 循环分管从上到下和从左到右的遍历:

五、例题讲解

例题1: 二叉树的最大深度

思路一:遍历一遍

为什么前序,因为每次进入二叉树节点就可以算出深度了,而不需要左右子树的信息

class Solution {
public:
    int deepest, present;
    void traverse(TreeNode* root){
        if(root == nullptr){
            return;
        }
        present++;
        if(root->left == nullptr && root->right == nullptr){
            deepest = max(present, deepest);
        }
        traverse(root->left);
        traverse(root->right);
        present--;
        return;
    }
    int maxDepth(TreeNode* root) {
        traverse(root);
        return deepest;
    }
};

思路2:分解问题

因为需要用到子树的信息,所以业务代码在递归后面

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(root == nullptr){
            return 0;
        }
        int ret = max(maxDepth(root->left), maxDepth(root->right));
        return ret+1;
    }
};

例题2:二叉树的前序遍历

前序遍历的顺序:根节点,左子树,右子树,对应ans向量

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ans;
        if(root == nullptr){
            return ans;
        }
        ans.push_back(root->val);
        vector<int> left = preorderTraversal(root->left);
        ans.insert(ans.end(), left.begin(), left.end());
        vector<int> right = preorderTraversal(root->right);
        ans.insert(ans.end(), right.begin(), right.end());
        return ans;
    }
};

例题3: 二叉树的直径

设置一个外部变量,然后遍历二叉树,遍历过程中更新结果即可。

class Solution {
public:
    int best;
    int depth(TreeNode* root){
        if(root == nullptr){
            return 0;
        }
        int left = depth(root->left);
        int right = depth(root->right);
        best = max(best, left + right);
        return max(left ,right)+1;
    }    
    int diameterOfBinaryTree(TreeNode* root) {
        depth(root);
        return best;
    }
};

二、思维篇

1. 介绍

二叉树解题的思维模式分两类:

1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现,这叫「遍历」的思维模式。

2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。

无论使用哪种思维模式,你都需要思考:

如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。

拿到一道题,需要思考:如果单独考虑二叉树节点,在什么位置(前中后)做?不用考虑其他节点,递归函数会在所有节点上执行相同的操作。

2. 例题讲解

例题1:翻转二叉树

分析:遍历的思路:只需要每个节点把它的左右节点互换,然后递归处理左右节点即可。

代码

class Solution {
public:
    TreeNode* tmp;
    void traverse(TreeNode* root){
        if(root == nullptr){
            return;
        }
        tmp = root->right;
        root->right = root->left;
        root->left = tmp;
        traverse(root->left);
        traverse(root->right);
    }
    TreeNode* invertTree(TreeNode* root) {
        traverse(root);
        return root;
    }
};

能不能用分解的思路解决呢?

class Solution {
public:
    TreeNode* Inverse(TreeNode* root){
        if(root == nullptr){
            return root;
        }
        TreeNode* left = Inverse(root->left);
        TreeNode* right = Inverse(root->right);
        root->right = left;
        root->left = right;
        return root;
    }
    TreeNode* invertTree(TreeNode* root) {
        return Inverse(root);
    }
};

这种「分解问题」的思路,核心在于你要给递归函数一个合适的定义,然后用函数的定义来解释你的代码;如果你的逻辑成功自恰,那么说明你这个算法是正确的。

例题2:填充每个节点的下一个右侧节点指针

代码

思路1:while循环

class Solution {
public:
    Node* connect(Node* root) {
        if(root == nullptr){
            return root;
        }
        Node* left,* head;
        head = root;
        while(root->left){
            left = root->left;
            while(root != nullptr){
                root->left->next = root->right;
                if(root->next){
                    root->right->next = root->next->left;
                }
                root = root->next;
            }
            root = left;
        }
        return head;
    }
};

思路2:递归遍历思路

class Solution {
public:
    Node* connect(Node* root) {
        if(!root){
            return nullptr;
        }
        if(root->left){
            root->left->next = root->right;
            if(root->next){
                root->right->next  = root->next->left;
            }
        }
        connect(root->left);
        connect(root->right);
        return root;
    }
};

上面两个方法是等价的,大部分递归都可以用while循环写出来 

例题3:二叉树展开为链表

代码

分解问题,先把子树排好,然后排序子树

class Solution {
public:
    TreeNode* tmp;
    TreeNode* traverse(TreeNode* root){
        if(!root){
            return nullptr;
        }
        TreeNode* left = traverse(root->left);
        TreeNode* right = traverse(root->right);
        root->left = nullptr;
        root->right = left;
        tmp = root;
        while(tmp->right != nullptr){
            tmp = tmp->right;
        }
        tmp->right = right;
        return root;
    }
    void flatten(TreeNode* root) {
        traverse(root);
    }
};

三、构造篇

例题1:最大二叉树

分析:每个二叉树节点都可以认为是一棵子树的根节点,对于根节点,首先要做的当然是把想办法把自己先构造出来,然后想办法构造自己的左右子树。

代码:

class Solution {
public:
    TreeNode* traverse(vector<int>& nums, int a, int b){
        if(a >= b){
            return nullptr;
        }
        TreeNode* root = new TreeNode();
        auto maxn = max_element(nums.begin()+a, nums.begin()+b) - nums.begin();
        root->val = nums[maxn];
        root->left = traverse(nums, a, maxn);
        root->right = traverse(nums, maxn+1, b);
        return root;
    }
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        TreeNode* root = traverse(nums,0, nums.size());
        return root;
    }
};

例题2*:从前序与中序遍历序列构造二叉树

分析:

代码:

 

四、后序篇

五、序列化篇

六、二叉搜索树篇

前中后序的区别

i)前序位置的代码在刚刚进入一个二叉树节点的时候执行;

ii)中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。(很多多叉树没有中序位置,因为一个节点没有唯一的中序遍历位置。)

iii)后序位置的代码在将要离开一个二叉树节点的时候执行;后序和前序的区别在于,后序会用到子树的信息,一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了

七、总结

i)C++没有截取vector的函数,只能够传起始位置进去。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值