leetcode-二叉树

  1. B树和B+树的区别
    • B树,也即balance树,是一棵多路自平衡的搜索树。它类似普通的平衡二叉树,不同的一点是B树允许每个节点有更多的子节点。
  • B+树内节点不存储数据,所有关键字都存储在叶子节点上。
  • B树:
  • B+树:

    二叉树理论基础:

    1.种类:满二叉树、完全二叉树、二叉搜索树、平衡二叉树。

    完全二叉树是最后一行从左到右连续但不一定全满。

    二叉搜索树,必须有一定顺序。查询和添加都是O(logn),因为添加就是查询的过程。

    平衡二叉搜索树:左右子树高度差的绝对值不超过1。map,set,multimap,multiset底层都是平衡二叉搜索树(是红黑树,红黑树是一种平衡二叉搜索树。)

    2.存储方式:链表、数组。

    链表,就是val,Treenode* left, Treenode *right

    数组,一开始从0开始,左孩子就是2k+1,右孩子就是2k+2

    3.遍历方式:

    深度优先搜索:前序(中左右)、中序(左中右)、后序(左右中)。一般用递归实现,也可以迭代实现。

    广度优先搜索:层序遍历是其中一种。

    图的深度优先搜索就对应树的前中后序,图的广度优先搜索就对应树的层序遍历。

    1. 二叉树的前,中,后序遍历 - 递归 leetcode144.94.145 20231026

    代码随想录又卡了,栈与队列最后那题打着C++已实现的优先队列的旗号实际上是堆,而堆本身又是完全二叉树.....优先队列那题还不是直接拿的priority_queue去实现的,还自定义了它的比较规则,这又引出一个函数对象的概念,总之是无从下手,遂转战二叉树。今天看了看二叉树的理论知识,感觉还行,结果写题的时候又被递归摆了一道,完全忘了return和题干给的函数有什么用。

    总之,题干给的preorderTraversal没动,自己重新实现了一个函数,调用之即可。下面两题是类似的。注意在preorder函数中,不能再写一个while循环了,这是写完迭代之后顺手写的错误。

    class Solution {
    public:
        void preorder(TreeNode* cur, vector<int>& vct) {
            if(cur == nullptr){
                return;
            }
            vct.push_back(cur->val);
            preorder(cur->left,vct);
            preorder(cur->right,vct);
        }
        vector<int> preorderTraversal(TreeNode* root){
            vector<int> vct;
            preorder(root,vct);
            return vct;
        }
    
    };

    2. 二叉树的前,中,后序遍历 - 迭代leetcode144.94.145 20231027

    迭代分为前后,中两种,理解起来其实还是很困难的,看代码貌似记住了,自己写对了,但是再过几天让我写是绝对写不出来的

    前后之所以说是“一种”,因为后序可以由前序倒一下左右,再reverse一下数组就能得到。他们遍历和处理的顺序都是一样的,而中序就不一样了。

    下面来看一下具体的代码~

    前序,后序在这里就只放前序了:

    
    class Solution {
    public:
        vector<int> preorderTraversal(TreeNode* root) {
            stack<TreeNode*> stk;
            vector<int> vct;
    
            if(root == nullptr)
                return vct;
            
            stk.push(root);
    
            while(!stk.empty()){
                TreeNode* cur = stk.top();
                stk.pop();
                vct.push_back(cur->val);
    
                if(cur->right !=nullptr)
                    stk.push(cur->right);
                if(cur->left !=nullptr)
                    stk.push(cur->left);
            }
    
            return vct;
        }
    };

    定义一个stk用于模拟递归,一个vct用户返回数组。因为要先push root进去,所以先得判断一下是否为空。

    root被push进去以后,只要栈不为空,就pop栈顶出来,再把刚出的栈顶push到 vct 里面去,然后先往栈里push右边,再往栈里push左边。这样的话,之后vct就会从栈顶开始出,就会先被push_back到vct里面去,而后又是往复的右左栈入,中(栈里元素)左右vct出。

    注意,这里push左指针和右指针的时候我都进行了非空判断。后来我又写了一次,忘了加判断,直接往stk里push了空指针,但是用if(cur!=nullptr){}把vct的push_back和两个stk的push括起来了,照样通过,不加则不行。说明可以往栈里面push一个空指针,且stk.top()返回的就是这个空指针。

    中序

    class Solution {
    public:
        vector<int> inorderTraversal(TreeNode* root) {
            vector<int> vct;
            stack<TreeNode*> stk;
            TreeNode* cur = root;
            while(cur != nullptr || !stk.empty()){
                if(cur != nullptr){
                    stk.push(cur);
                    cur = cur->left;//入栈,然后cur一直到最左边
                }
                else{
                    cur = stk.top();//已经为空了,就取栈头的成为现在的cur
                    stk.pop();
                    vct.push_back(cur->val);
                    cur = cur->right;
                }
            }
            return vct;
        }
    };

    而迭代法中序遍历的进栈和处理顺序是不一样的,简单来说,我们要从最左下角的节点开始处理,但是进栈肯定是从root进栈。这样就导致我们需要进root->进root的左节点->一直进左节点,这就是while里面第一个if条件的生成,也就是说,只要这个节点不为空,就进栈,然后现节点变成它的左节点,这个现节点是可能为空的,可能就是一个空节点进入下一个while;而else分支则是为空了,要么是空左节点,要么是空右节点,这个时候如果是空左节点,那么就应该处理中和后了。中现在在栈顶,所以用于处理的cur指针指到stack的top,这个时候执行出栈、vct的push_back操作,然后再把用于处理节点的cur指到右节点;如果是空右节点,栈顶实际上是它的父节点的父节点,也应该指到栈顶,继续处理中和后。(此时,完成了一整个循环,右节点此时相当于还没处理,只是cur指到了,下次再进while循环的时候就会重新判断它是否为空,如果非空是否有左节点,如果为空就回到栈top开始处理中和右)

    这一段核对了一下逻辑应该没有大的问题,自己也手写了三四遍递归和迭代了。

    3.二叉树的层序遍历

    队列,queue进第一层,记录size=1,一个一个进一维vct,queue出第一层,进第二层,一维vct进二维result。

    class Solution {
    public:
        vector<vector<int>> levelOrder(TreeNode* root) {
            vector<vector<int>> result;
            if(root == nullptr)
                return result;
            queue<TreeNode*> que;
            que.push(root);
            while(!que.empty()){
                int size = que.size();// 现在是1
                vector<int> vct;
                while(size--){
                    //一个一个将que里本层的记录到一个vct里面去,然后删掉que里本层的东西,加入下一层的东西
                    TreeNode* cur = que.front();
                    vct.push_back(cur->val);
                    que.pop();
    
                    if(cur->left != nullptr)
                        que.push(cur->left);
                    if(cur->right != nullptr)
                        que.push(cur->right);
                }
                //把这个vct push_back到二维result里去
                result.push_back(vct);
            }
            return result;
        }
    };
    

    23/11/11更新:
    经过一段时间的学习,我发现树的大多数题就是深度优先(递归、迭代)、广度优先(层序)。而深度优先里面,递归是方便人能理解的,迭代需要我们使用额外的数据结构(比如栈)才能实现,也有可能需要使用队列。广度优先,也就是层序,需要我们使用que实现,有的题也能用栈。
    递归,大部分的题就是让我们在另外一个函数A里面把主要思想实现,比如求某个节点的高度,而主函数里面就是调用这个函数A。而我们在函数A里面,需要根据不同情况使用前序、中序、后序遍历,其中求高度需要用后序,求深度需要用前序,而求深度和高度其实也可以相互转化。

    下面,是我觉得有必要在之前3个代码之后做的题。

    第一个是对称二叉树,这一题让我知道可以传2个参数进去递归,很新鲜

    4. 对称二叉树

    这个一开始以为跟翻转二叉树一样,但是自己也知道应该是两棵树的比较,具体代码还是看了随想录才知道递归应该传两个参进去。

    /**
     * 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:
        bool isSym(TreeNode* left, TreeNode* right) {
            if(left == nullptr && right != nullptr) return false;
            else if(left != nullptr && right == nullptr)  return false;
            else if(left==nullptr&& right==nullptr) return true;
            else if (left->val != right->val) return false;
    
            bool r1 = isSym(left->left,right->right);
            bool r2 = isSym(left->right,right->left);
            return r1 && r2;
            
        }
        bool isSymmetric(TreeNode* root) {
            if(root == nullptr)
                return true;
            return isSym(root->left, root->right);
        }
    };
    

    5.二叉树的所有路径

    第二个是二叉树的所有路径,这一题的回溯真的很有意思,大家都说这一题不像个简单题,我也觉得(毕竟隐藏在里面的回溯真的很难搞)。
    我在没看答案之前写出了一版跟隐藏所有细节后差不多的代码,但是完全不会处理回溯问题,纠结的点在于vector参数和string参数是不是需要引用,哪里需要加->,->是不是要自己手动回退(pop_back),string里加上的节点值是否要手动回退。
    后来看了随想录,解决了我的大部分疑问,但是最后一句“每次都是复制赋值,不用使用引用,否则就无法做到回溯的效果。”还是让我有点懵。后来自己又重新思路。
    下面是自己最后的思路:

    1. 首先看主函数,traversal传参root,path,result,之后return的result必须得修改过,所以函数traversal里面对应result的那个形参必须是引用类型。实际上,vector也不回溯,所以不纠结。
    2. 然后看traversal函数。这个函数有2种实现方式,一种是传string做path,一种是传vector做path。如果是传vector做path,又分为传path的引用和不传path的引用。如果是传string做path,又分为传path的引用和不传path的引用,底下再分回溯精简和手动实现回溯。
    3. 在第二点中,如果我们选择path为vector,并且是引用格式,那么逻辑为:path加入本节点->(中)如果是叶子节点则通过一个for循环把这些path串起来to_string。变成一个最终的string类型往vector里面push_back,然后开始(左)向左执行traversal函数,如果执行完了,path得pop_back对应的值再(右)向右执行traversal函数,如果执行完了,path要pop_back对应的值。这样的话,->是不用考虑的。那pop_back逻辑是否正确呢?假设有一个树如下所示,首先path=[1],然后在这个函数里面向左path=[1,2],然后又去了右分支变成了[1,2,5],然后vector里面加了这么一个[1,2,5]。而执行完毕后path这时是引用返回,所以path依然是[1,2,5],但是返回上一级函数之后path立刻里面被pop_back了,所以又变成了[1,2],再返回上一级,依旧是[1,2],又被pop_back了,变成了1,此时再执行右分枝变成[1,2,3],然后vector加上这个[1,2,3]之后return了,此时真正return,函数结束。
      在这里插入图片描述
    class Solution {
    private:
    
        void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) {
            path.push_back(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中 
            // 这才到了叶子节点
            if (cur->left == NULL && cur->right == NULL) {
                string sPath;
                for (int i = 0; i < path.size() - 1; i++) {
                    sPath += to_string(path[i]);
                    sPath += "->";
                }
                sPath += to_string(path[path.size() - 1]);
                result.push_back(sPath);
                return;
            }
            if (cur->left) { // 左 
                traversal(cur->left, path, result);
                path.pop_back(); // 回溯
            }
            if (cur->right) { // 右
                traversal(cur->right, path, result);
                path.pop_back(); // 回溯
            }
        }
    
    public:
        vector<string> binaryTreePaths(TreeNode* root) {
            vector<string> result;
            vector<int> path;
            if (root == NULL) return result;
            traversal(root, path, result);
            return result;
        }
    };
    

    如果我们不使用引用,就不用写明面上的回溯了,直接删掉引用,删掉两个pop_back。这是为什么呢?代码随想录讲的是“传值”带来的好处,但是没有具体细说。接下来,继续模拟一下这个过程:
    在这里插入图片描述
    path=[1]
    traversal(左,path,result)进入之后,path=[1,2]
    进入traversal(右,path, result),进入之后,path=[1,2,5]
    开始return。return到的地点是path=[1,2]的函数体的if(cur->right){}里面。关键点在于理解递归返回后 path 的状态:

    当返回到 if (cur->right) 语句块内部(即在处理右子节点 5 后),此时的 path 就会是 [1, 2]了。因为我们传递的是值而非引用,每次递归调用都是在操作自己的 path 副本,所以在 5 的父节点 2 的层级上,path 保持不变。
    一旦退出了这个 if 块 ,就意味着path=[1,2]的递归也走到尽头,接下来回到了第一个节点的right调用部分,path=[1,3]
    

    总而言之,因为 path 是按值传递的,所以每个递归层级都有自己的 path 副本,它们彼此独立。这意味着在递归返回时,不需要担心 path 的状态,因为它自然会恢复到适当的状态,这就是为什么不需要 pop_back()。

    // 版本二
    class Solution {
    private:
    
        void traversal(TreeNode* cur, vector<int> path, vector<string>& result) {
            path.push_back(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中 
            // 这才到了叶子节点
            if (cur->left == NULL && cur->right == NULL) {
                string sPath;
                for (int i = 0; i < path.size() - 1; i++) {
                    sPath += to_string(path[i]);
                    sPath += "->";
                }
                sPath += to_string(path[path.size() - 1]);
                result.push_back(sPath);
                return;
            }
            if (cur->left) { // 左 
                traversal(cur->left, path, result);
            }
            if (cur->right) { // 右
                traversal(cur->right, path, result);
            }
        }
    
    public:
        vector<string> binaryTreePaths(TreeNode* root) {
            vector<string> result;
            vector<int> path;
            if (root == NULL) return result;
            traversal(root, path, result);
            return result;
        }
    };
    

    (混淆:至于全局变量因为局部函数修改而变掉,那是因为从头到尾全局变量的存放只在一个地方,修改都是对一个内存地址的修改)让我们看看以下代码,理清思路

    #include <iostream>
    
    // 定义全局变量x
    int x = 10;
    
    void modifyGlobalX(int y) {
        std::cout << "Inside function, before modifying y: " << y << std::endl; // y的初始值
        y = 50;  // 修改参数y
        std::cout << "Inside function, after modifying y: " << y << std::endl;  // y被修改后的值
    
        std::cout << "Before modifying x: " << x << std::endl; // 修改全局变量x之前
        x = 20;  // 修改全局变量x
        std::cout << "After modifying x: " << x << std::endl;  // 修改全局变量x之后
    }
    
    int main() {
        int y = 30;
        std::cout << "Initial global x: " << x << std::endl; // 输出全局变量x的初始值
        std::cout << "Initial y in main: " << y << std::endl; // 输出主函数中y的初始值
    
        // 调用函数,传递y作为参数
        modifyGlobalX(y);
    
        // 输出全局变量x和主函数中y的值,看看它们是否被修改
        std::cout << "Global x after function call: " << x << std::endl;
        std::cout << "y in main after function call: " << y << std::endl;
    
        return 0;
    }
    
    Result
    
    ['Initial global x: 10',
     'Initial y in main: 30',
     'Inside function, before modifying y: 30',
     'Inside function, after modifying y: 50',
     'Before modifying x: 10',
     'After modifying x: 20',
     'Global x after function call: 20',
     'y in main after function call: 30']#主要看这里--本题讨论的值传递就是这个亚子!
    
    1. 在第二点中,如果我们选择path为string
      分为传path的引用和不传path的引用——跟上面一样,传引用就要手动pop,但是string类型没法简单的pop一个数字(可能两位数,可能n位数),不传引用就好办,反正还是按值传递,每个递归是每个递归的副本。所以这个地方我们选择不传path的引用。
      底下再分回溯精简和手动实现回溯——第一个回溯精简,也就是省略掉了很多东西,也就是我自己刚开始写出来的类似如下的代码。这个代码中,path按值传递所以不需要管,参数是path+"->"所以path更是一点也没变,最终就变成了很简单的样子:
    class Solution {
    private:
    
        void traversal(TreeNode* cur, string path, vector<string>& result) {
            path += to_string(cur->val); // 中
            if (cur->left == NULL && cur->right == NULL) {
                result.push_back(path);
                return;
            }
            if (cur->left) traversal(cur->left, path + "->", result); // 左
            if (cur->right) traversal(cur->right, path + "->", result); // 右
        }
    
    public:
        vector<string> binaryTreePaths(TreeNode* root) {
            vector<string> result;
            string path;
            if (root == NULL) return result;
            traversal(root, path, result);
            return result;
    
        }
    };
    

    第二个手动实现回溯,也就是说,把->放出来看看究竟path做了什么变化。

    if (cur->left) {
        path += "->";
        traversal(cur->left, path, result); // 左
        path.pop_back(); // 回溯 '>'
        path.pop_back(); // 回溯 '-'
    }
    

    这个时候,path每一层递归都有变化,所以最后返回的时候要想保持原样,当然要把进去之前加上的->给删掉~!因为进一层递归就+一个->,所以出递归自然要减一个!

    6.左叶子之和

    第三题是404.左叶子之和
    这道题我以为自己用的是层序遍历,随想录说其实是迭代——fine。
    用迭代法,在写判断“为左节点”的时候犯了难。因为我们无法通过当前节点获取父结点的信息,所以无法知道自己是什么节点。后来经过帮助,直到可以直接在if(node->left)里面进一步判断if(node->left->left&&node->left->right),只要左节点为叶结点,那么在这里就加上左节点的值。
    而这一题让我加进来的意义在于这个递归,本来以为遵循传统的后序遍历肯定可以,但是这一题的规则让代码不太好写

    /**
     * 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:
    //再看看深度优先怎么做。
    
        int sumOfLeftLeaves(TreeNode* root) {
            if(root == nullptr)
                return 0;
            //没关系,真正的左节点为空的情况已经在下面被考虑到了
            if(!root->left && !root->right) return 0;
            int leftsum = sumOfLeftLeaves(root->left);
            //这里考虑到了,值在这里被加上,父节点的时候就已经把下面左叶子节点值加上了
            if(root->left && !root->left->left && !root->left->right)
                leftsum = root->left->val;
            
            int rightsum = sumOfLeftLeaves(root->right);
            int sum = leftsum + rightsum;
            return sum;
        }
    };
    

    7.从中序与后序遍历序列构造二叉树

    这个题主要困难在于不熟悉题就写不出来第一次递归。其实看过一遍答案之后还是比较好理解的。
    首先,主函数是判断为空则返回,不空则进行递归,并且由于这个递归函数是有返回值的,所以直接返回这个递归函数。
    然后,考虑递归函数的写法。
    递归函数最终返回的一定是一个TreeNode* 类型的根节点,所以我们在其中递归来递归去,其实就是往这个根节点的左右挂靠子树。而子树的诞生需要的依然是这个递归函数,参数依然是两个vector——一个中序vector,一个后序vector。左子树需要两个vector(root->left = traversal(vct1,vct2)),右子树也需要两个vector(root->right = traversal(vct3,vct4)),所以一个递归函数中我们需要产生4个vector,这就需要我们通过后序序列的末尾元素去切割。切割的具体方法很简单,就是去中序里遍历查有没有跟后序末尾元素相等的,有就记录一下位置,然后申请4个vector,分别存中序前部分,中序后部分,后序前部分,后序后部分。
    其中细节就是vct.size() ==0、==1时的处理,以及返回值(那肯定是middle)

    class Solution {
    public:
        TreeNode* traversal(vector<int>& inorder, vector<int>& postorder){//&????????????
            if(inorder.size() == 0)
                return nullptr;
            
            // 提取后序的最后一个元素
            int back_ele_postorder = postorder[postorder.size() - 1];
            TreeNode* middle = new TreeNode(back_ele_postorder);
    
            if(inorder.size() == 1)// 叶子结点
                return middle;
            
            // temp 为 中序的根节点的序号
            int temp;
            for (int i = 0; i < inorder.size(); ++i){
                if(back_ele_postorder == inorder[i]){
                    temp = i;
                    break;
                }
            }
    
            // 找中序进行切割
            vector<int> inorder_front(inorder.begin(), inorder.begin() + temp);
            vector<int> inorder_back(inorder.begin() + temp + 1, inorder.end());
    
            // 找后序进行切割
            vector<int> postorder_front(postorder.begin(), postorder.begin() + temp);
            vector<int> postorder_back(postorder.begin() + temp, postorder.end() - 1);
    
            middle -> left = traversal(inorder_front, postorder_front); 
            middle -> right = traversal(inorder_back, postorder_back); 
    
            return middle;
    
        }
        TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
            if(inorder.size() == 0)
                return nullptr;
            return traversal(inorder, postorder);
        }
    };
    

    二叉树部分完结,但是仍然感觉有些地方并不熟练。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值