leetcode day07 二叉树和回溯算法

二叉树

先简单的复习一个二叉树的遍历方式。博主链接如下所示:二叉树遍历方法——前、中、后序遍历(图解)_二叉树遍历前序中序后序-CSDN博客

前序遍历的算法思路归纳就是:

若二叉树为空,什么都不做,否则:

        i、先访问根结点;

        ii、再前序遍历左子树;

        iii、最后前序遍历右子树;

中序遍历的算法思路归纳就是:

二叉树为空,什么也不做,否则:

        i、中序遍历左子树;

        ii、访问根结点;

        iii、中序遍历右子树

后序遍历的算法思路归纳就是:

若二叉树为空,什么也不做,否则:

        i、后序遍历左子树

        ii、后序遍历右子树

        iii、访问根结点

由于我之前都是用C语言写的二叉树,所以先从简单的题目来,练手C++。

94.二叉树的中序遍历

很简单,根据中序遍历的算法思路,有如下代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> res;
    vector<int> inorderTraversal(TreeNode* root) {
        if(!root) return res;
        inorderTraversal(root->left);
        if(root) res.push_back(root->val);
        inorderTraversal(root->right);
        return res;
    }
};

好接下来,再熟悉下回溯算法,这个我看好多题解上都有提到,但我不大记得了,所以先来复习一下。以下是参考代码随想录的讲解。

回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。(所以回溯函数即可称为递归函数)它的本质是穷举,穷举所有可能,然后选出我们想要的答案。如果要让效率再高一点,那么则可以结合剪枝。该算法一般适用于解决以下几类问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

回溯法解决的问题都可以抽象为树形结构。

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

其算法的模板框架如下所示:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}
组合问题
77.组合

组合问题是回溯法解决的经典问题,当n为100,k为50的话,直接想法就需要50层for循环。所以就需要引出回溯法来解决这种k层for循环嵌套的问题。然后进一步把回溯法的搜索过程抽象为树形结构,可以直观的看出搜索的过程。

             

接着用回溯法三部曲,逐步分析了函数参数、终止条件和单层搜索的过程。

  • 递归函数的返回值以及参数

在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。

  • 回溯函数终止条件

什么时候到达所谓的叶子节点了呢?path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。

  • 单层搜索的过程

回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。

   

class Solution {
    void backtrace(int i,int n,int k,vector<int> &combination,vector<vector<int>>& res){
        if(combination.size()==k){
            res.emplace_back(combination);
            return;
        }
        for(i=i+1;i<=n;++i){
            combination.emplace_back(i);
            backtrace(i,n,k,combination,res);
            combination.pop_back();
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> res;
        vector<int> combination; //表示当下的一行结果
        backtrace(0,n,k,combination,res);
        return res;
    }
};
17.电话号码的字母组合

由于每个数字对应多个字母,所以要先考虑数字和字母如何映射。这里我们定义一个二维数组,例如:string letterMap[10],来做映射。

其次还要考虑如何去解决n个for循环的问题。

 这里遍历的深度,就是输入“23”的长度,叶节点就是我们所要收集的结果,输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]。

根据回溯算法的思路,

首先,确定回溯函数参数:

我们要用一个字符串s来收集叶子结点的结果,然后用一个字符串数组result保存起来(两个变量定义为全局。)题目中参数指定是string digits,还要有一个int型的参数index来表示记录遍历第几个数字了,同时index也表示树的深度。

其次,确定终止条件:

例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。然后收集结果,结束本层递归。

最后,确定单层遍历逻辑:

首先要取index指向的数字,并找到对应的字符集,然后用for循环来处理这个字符集。

class Solution {
private:
    const string letterMap[10]={
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz",
    };
public:
    vector<string> result;
    void getCombination(const string& digits,int index,const string & s){
        if(index == digits.size()){
            result.push_back(s);
            return;
        }
        int digit = digits[index] - '0';
        string letters = letterMap[digit];
        for(int i=0;i<letters.size();i++){
            getCombination(digits,index+1,s+letters[i]);
        }
    }
    vector<string> letterCombinations(string digits) {
        result.clear();
        if(digits.size()==0){
            return result;
        }
        getCombination(digits,0,"");
        return result;
    }
};

这里的const string &s,使用了两个重要的C++概念:常量引用和传递引用。

1、常量引用中,const关键字表示引用的对象不能被修改,在这个函数中,digits 和 s 都是以 const string& 的形式传递的,这意味着函数保证不会改变这两个字符串的内容。

  • 对于 digits,这是因为电话号码组合的输入应该是不可变的,函数中只需要读取数字对应的字母组合,而不需要修改输入的数字字符串。
  • 对于 s,这是因为在递归过程中,每次递归调用都会在前一个字符串的基础上添加新的字母,如果 s 可以被修改,那么递归过程中的任何改变都会影响上一层递归的结果。

2、传递引用中,

  • 引用传递是一种高效的传递对象的方式,因为它不涉及对象的复制。在递归调用中,如果使用值传递,每次递归都会创建 s 的新副本,这会导致不必要的性能开销。使用引用传递可以避免复制,直接在原始字符串上进行操作。
  • s 在递归过程中逐渐构建,每次递归都会扩展字符串。如果使用值传递,每次递归都会创建一个新的字符串副本,这会导致性能问题。使用引用传递可以确保所有递归层级都共享同一个字符串,避免了不必要的复制。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值