【代码随想录】【LeetCode】自学笔记08 -二叉树

前言

【代】:避免递归!
“现场面试的时候,面试官看你顺畅的写出了递归,一般会进一步考察能不能写出相应的迭代。”
前中后序遍历:看中间节点,用递归方法

入门
【https://cloud.tencent.com/developer/article/1591357】

数据结构分为物理结构和逻辑结构,像数组这种数据结构就属于物理结构,人家是怎么样的就在内存中怎么存储,但是对于逻辑结构比如这里的二叉树,貌似就没法直接存了,不过逻辑结构的数据结构可以使用多种物理结构来存储。
一般来说,就是数组和链表,这俩万能啊,很多东东的底层貌似都有这俩货。所以对于二叉树也是一样的,可以使用链表和数组来存储。
一般我们都是用链式存储二叉树。

C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,
unordered_map、unordered_map底层实现是哈希表。
所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!

二叉树的定义代码:


 /* 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) {}
 };

从数组形式还原二叉树:
以下图为例,可见前序遍历第一个元素是根节点,后序遍历最后一个是根节点。

        A
    /       \
   D         C
  / \       / \
 #   E     B   #
    / \   / \
   #   # #   #
#:代表空结点
前序遍历:AD#E##CB###,ADECB
中序遍历:#D#E#A#B#C#,DEABC
后序遍历:###ED##B#CA,EDBCA
层序遍历:ADC#EB#####,ADCEB

二叉树与图论的关系:
二叉树主要有两种遍历方式:
深度优先遍历:先往深走,遇到叶子节点再往回走。
广度优先遍历:一层一层的去遍历。
这两种遍历是图论中最基本的两种遍历方式。

144. 二叉树的前中后序遍历-DFS

1.递归法:
有通用模板:void + vector< int >
2.迭代法:
普通写法:前后模板一致,空节点不入栈;中序遍历另说
统一写法:遇到中节点,再pushback一个NULL做标记。详见代随。
## 2.非递归法的前中后序遍历※都还没看※见代随

遍历目标:依次写入一个vector< int >:

//基本操作之:vector作参数要加&
//【1】-递归
 //回溯总得有个void。。
class Solution {
public:
    void traversal(TreeNode* root, vector<int>& vec){
        if(root==nullptr) return;///BC
        vec.push_back(root->val);///MID
        traversal(root->left, vec);///LEFT
        traversal(root->right, vec);///RIGHT
        //return;
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;        
        traversal(root, res);
        return res;
    }
};
//【2】-迭代
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root){
        vector<int> res;
        stack<TreeNode*> st;<*>
        if(root==NULL) return res;///res默认为空,但不能真的直接return空,因为只是到了叶子节点,前面的res已经记录了内容了
        st.push(root);
        while(!st.empty()){//注意这里面都是st.top() 对应的临时节点了,完全没有root的身影了!!!
            TreeNode* cur = st.top();                                       // 中
            st.pop();
            res.push_back(cur->val);
            if(cur->right) st.push(cur->right);WRONG root!!!           // 右(空节点不入栈)
            if(cur->left) st.push(cur->left);                              // 左(空节点不入栈)
        }
        return res;
    }
};

102.二叉树的层序遍历

【代】&【扣】推荐方法均为迭代+队列:

关键字:层次遍历
模式识别:(?)一旦出现树的层次遍历,都可以用队列作为辅助结构

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        vector<vector<int>> res;
        if(root==NULL) return res;
        else que.push(root);
        while(!que.empty()){
            int size = que.size();//清空每层的path
            vector<int> path;// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
            
            for(int i = 0; i<size; i++){
                TreeNode* node = que.front();
                que.pop();

                path.push_back(node->val);
                if(node->left) que.push(node->left);//层序遍历是先左再右的“正常”顺序!!!
                if(node->right) que.push(node->right);
            }
            res.push_back(path);
        }
        return res;
    }
};

226.翻转二叉树

前序后序都可以,中序不行,会导致反转两次
疑问:invertTree(root->left);///可以没有返回,直接这样写。。。。。(?因为最后有终止条件的,用最底层的return吗)

class Solution {
public:

    TreeNode* invertTree(TreeNode* root) {
        if(root==NULL) return root;
        //mid
        swap(root->left, root->right);
        invertTree(root->left);///可以没有返回,直接这样写。。。。。(?因为最后有终止条件的,用最底层的return吗)
        invertTree(root->right);
        return root;
    }
};

101.对称二叉树

/* 
写成两个函数,对这道题是优选,【力扣】也是如此,因为递归思路用两个变量合适,但题目只有一个参数root
 本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
自己写的是遍历两个左右子树,把元素存在数组里依次对比,这种是不行的,因为遇到[1,2,2,null,3,null,3]这样的,空节点就跳过了,但实际位置没有被记录。但如果去记录的话会导致无法收敛,无限循环。
 */
class Solution {
public:
    bool duibi(TreeNode* zuo, TreeNode* you){
        if(zuo==NULL && you !=NULL)return false;
        else if(zuo!=NULL && you == NULL) return false;
        else if(zuo== NULL && you==NULL) return true;
        else if(zuo->val != you->val) return false;
        //else{
         bool waimian=    duibi(zuo->left, you->right);
         bool limian=   duibi(zuo->right, you->left);
         //   if(zuo->val != you->val) return false;
         return waimian && limian? true : false;/WRONG ==!!!!
            
        //}
    }

    bool isSymmetric(TreeNode* root) {
        if (root==NULL) return true;
        return duibi(root->left, root->right);
    }

104.二叉树的最大深度

为什么自己写的(上面的部分)能通过?是看了代随思路之后写的,下面部分好复杂

后序遍历方便,不需要迭代,可以转化成根节点的高度。前序遍历需要回溯,因为是中左右。
 */
class Solution {
public:

    int maxDepth(TreeNode* root) {
        int res = 0;
        if(root ==NULL) return 0;
        int zuo = maxDepth(root->left);
        int you = maxDepth(root->right);
        res = max(zuo, you)+1;
        return res;

    }
};
class solution {
public:
    int result;
    void getdepth(treenode* node, int depth) {
        result = depth > result ? depth : result; // 中
        if (node->left == NULL && node->right == NULL) return ;
        if (node->left) {            getdepth(node->left, depth + 1);        }// 左 
        if (node->right) {           getdepth(node->right, depth + 1);        } // 右
        return ;
    }
    int maxdepth(treenode* root) {
        result = 0;
        if (root == 0) return result;
        getdepth(root, 1);
        return result;
    }
};

111.二叉树的最小深度

遍历顺序上依然是后序遍历(因为要比较递归返回之后的结果),
但在处理中间节点的逻辑上, 求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。

class Solution {
public:
    int minDepth(TreeNode* root) {
        if(root==NULL) return 0;
        //int depth;
        int zuo = minDepth(root->left);///左
        int you = minDepth(root->right);///右

        //中:分三种情况:只有左是叶子节点+只有右边+都不是,(可能有三层及以上)取最小
        if(root->left!=NULL && root->right==NULL) return 1+zuo;
        if(root->left==NULL && root->right!=NULL) return 1+you;
        return min(zuo, you)+1;

    }
};

222. 完全二叉树的节点个数

递归法,自己写的,走不通,为什么??非得像之前两个求高度、深度的题,左右分开计数?明明和之前递归模板里面除了vec一模一样??是因为int作返回值,从同一祖先的左孩子转到右孩子的时候会自动更新吗????? 为什么结果恒为0??????
层序遍历,能行,就是以上思路:

那么只要模板少做改动,加一个变量result,统计节点数量就可以了

void traversal (TreeNode* node, int tmp){
        if(node==nullptr) {
            //tmp = 0;
            return;
        }
        
        //if(node->left!=NULL)
        traversal(node->left, tmp);
        //if(node->right!=NULL)
        traversal(node->right, tmp);
        tmp=tmp+1;
        cout<<node->val<<"    "<<tmp<<endl;
    }
    int countNodes(TreeNode* root) {
        int res = 0;
        traversal(root, res);
        return res;
    }
};///input 1 output 11....with res = 0.....为什么???

110. 平衡二叉树

核心:
①比较每个节点的左右子树的高度,如果不符合当场返回,符合就取最大值+1;
②递归用int同时起到计数/标记不符合情况的作用:返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1
class Solution {
public:
    int panduan(TreeNode* node){
        if(node==NULL) return 0;
        int zuo = panduan(node->left);
        int you = panduan(node->right);
        if(zuo==-1 || you==-1) return -1;
        if(zuo>you && zuo-you >1 || you>zuo && you-zuo>1) return -1;
        return max(zuo, you)+1;

    }
    bool isBalanced(TreeNode* root) {
        int res = panduan(root);
        if(res == -1) return false;
        return true;

    }
};

257. 二叉树的所有路径

/*//回溯要和递归永远在一起,no世界上最遥远的距离是你在花括号里,而我在花括号外!
// //前序求的就是深度,使用后序呢求的是高度
// //什么是叶子节点,左右孩子都为空的节点才是叶子节点!
// 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
需要前序遍历
第一次涉及到回溯
 */
class Solution {
public:
//private:///?
    void traversal(TreeNode* root, vector<int>& path, vector<string>& result){
        path.push_back(root->val);///不需要判断是不是空节点,因为到叶子结点就return了不会继续往下走
        if(root->left==NULL && root->right==NULL){
            string str;
            
            for(int i = 0;i<path.size()-1; i++){
                str+= to_string(path[i]);
                str+= "->";
            }
            str += to_string(path[path.size()-1]);
            result.push_back(str);
            return;
        }
        if(root->left) {
            traversal(root->left, path, result);
            path.pop_back();
        }
        if(root->right) {
            traversal(root->right, path, result);
            path.pop_back();
        }
    }
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<int> path;
        vector<string> result;

        if(root==NULL) return result;
        traversal(root, path, result);
        return result;
    }

};

404.左叶子之和

典型题:利用二叉树的【属性】,获取递归函数的返回【值】(而非数组),来累加处理
答案做法:写到主函数里; 累加值这种的题必须要定义左值+右值+中值;重点看主函数的注释,记得是有return值的并且需要及时赋给result并最终return。

/**
递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。。
 */
class Solution {
public:
    int traversal(TreeNode*node, int res){
        if(node==NULL) return 0;

        int leftval =0, rightval = 0;
        if(node->left) leftval = traversal(node->left, res);
        if(node->right) rightval = traversal(node->right, res);

        int midval = 0;///
        if( node->left!=NULL && node->left->left==NULL && node->left->right==NULL){//node!=NULL &&
            midval = node->left->val;
            
        }
        
        res+= leftval+rightval+midval;
        return res;


    }
    int sumOfLeftLeaves(TreeNode* root) {
        int result = 0; 
        if(root==NULL) return 0;
        result = traversal(root, result);///如果没有result = ,这句话也能运行但是result始终是0!
        return result;
    }
};
答案做法见下:

// int sumOfLeftLeaves(TreeNode* root) {
//         if (root == NULL) return 0;

//         int leftValue = sumOfLeftLeaves(root->left);    // 左
//         int rightValue = sumOfLeftLeaves(root->right);  // 右
//                                                         // 中
//         int midValue = 0;
//         if (root->left && !root->left->left && !root->left->right) { // 中
//             midValue = root->left->val;
//         }
//         int sum = midValue + leftValue + rightValue;
//         return sum;
//     }

513. 找树左下角的值

如果需要遍历整棵树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!
但是上一题(404左叶子之和)就不是吧,,,遍历整个树吧?但有返回值

层序遍历固定套路,3+2个新建变量

int result = 0;
queue<TreeNode*> que;
int size = que.size();
int i = 0;
TreeNode* node = que.front();

【自己写的,主要看注释!!!!!】
class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*>que;/
        vector<vector<int>>res;///no need
        if(root==NULL) return 0;
        que.push(root);
        int result = 0;
                
        while(!que.empty()){
            int size = que.size();
            vector<int> path;///no need
            
            for(int i = 0; i<size; i++){i<que.size() WRONG!!!
                TreeNode* node = que.front();/node  is in for!!!
                que.pop();

                path.push_back(node->val);
                //if (i==0) result = node->val;
                if(node->left) que.push(node->left);//1、层序遍历是先左后右!!!
                if(node->right) que.push(node->right);//2、层序遍历while内【无】root!!!!!
                
                
            }
            res.push_back(path);
        }
        result = res[res.size()-1][0];
        return result;

    }
};
【代码随想录方式更简单】
    int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        int result = 0;
        while (!que.empty()) {
            int size = que.size();
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (i == 0) result = node->val;
                // 记录当前行第一个元素,自上而下遍历所有行,不断更新,直到最后一行 // 记录最后一行第一个元素
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result;
    }

112.路径总和

递归函数什么时候要有返回值,什么时候不能有返回值?

【如果需要遍历整棵树,递归函数(一般而言)就不能有返回值。】
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先中介绍)

【如果需要遍历某一条固定路线,递归函数就一定要有返回值!】
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
其实还有一种就是**【后序遍历需要根据左右递归的返回值推出中间节点的状态,这种需要有返回值】**,例如222/110

本题:
可以使用**深度优先遍历(前序)**的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。

【自己写的,
√ 使用左值右值(中值本题不需要)分别计数
× 存在多次重复计数(遇到叶子结点的mid处理,及主函数root处理)、修改代码时候没有一起修改导致重复计数】
    bool traversal(TreeNode* node, int targetSum, int Sum){
        //if(node==NULL) return false;
        int leftval, rightval;//=0, =0, midval=0;

        if( node->left==NULL && node->right==NULL){//node!=NULL &&
            //midval = node->val;
            //Sum = Sum + midval; //+ leftval + rightval;
            cout<<"node->val "<<node->val<<", Sum "<<Sum<<endl;
            if(targetSum == Sum) return true;
            else return false;
        }

        if(node->left) {
            leftval = node->left->val;
            Sum += leftval;
            if(traversal(node->left, targetSum, Sum)) return true;
            Sum -= leftval;
            //leftval = 0;
        }
        if(node->right){
            rightval = node->right->val;
            Sum += rightval;
            if(traversal(node->right, targetSum, Sum))return true;///Sum+rightval
            Sum -= rightval;
            //rightval = 0;
        }

        return false;
    }
    bool hasPathSum(TreeNode* root, int targetSum) {
        if(root==NULL) return false;
        return traversal(root, targetSum, root->val);///0

    }
    【代码随想录答案】
class Solution {
private:
    bool traversal(TreeNode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0,返回√
        if (!cur->left && !cur->right) return false; // 遇到叶子节点,计数不为0,返回×

        if (cur->left) { // 左
            count -= cur->left->val; // 递归,处理节点;
            if (traversal(cur->left, count)) return true;
            count += cur->left->val; // 回溯,撤销处理结果
        }
        if (cur->right) { // 右
            count -= cur->right->val; // 递归,处理节点;
            if (traversal(cur->right, count)) return true;
            count += cur->right->val; // 回溯,撤销处理结果
        }
        return false;
    }

public:
    bool hasPathSum(TreeNode* root, int sum) {
        if (root == NULL) return false;
        return traversal(root, sum - root->val);
    }
};

构造二叉树

构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。

优化: 每次分隔不用定义新的数组,而是通过下标索引直接在原数组上操作。
注意区间范围,一般是在左闭右开区间[ left, right ),构造二叉树,具体代码为构造一个privateTreeNode* traversal(vector<int>& nums, int left, int right) {...}来递归。

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

思路:以后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

来看一下一共分几步:
第一步:如果数组大小为零的话,说明是空节点了。
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
第五步:切割后序数组,切成后序左数组和后序右数组
第六步:递归处理左区间和右区间

/**
 左闭右开
 后序数组的切割点怎么找?没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。
 此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。
 */
class Solution {
private:
    TreeNode* traversal (vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& postorder, int postorderBegin, int postorderEnd) {
        //第一步:如果数组大小为零的话,说明是空节点了。
        if (postorderBegin == postorderEnd) return NULL;

        //第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
        int rootValue = postorder[postorderEnd - 1];
        TreeNode* root = new TreeNode(rootValue);

        //如果后序数组就一个元素,那这就是?(注意迭代函数是 treenode 格式)
        if (postorderEnd - postorderBegin == 1) return root;

        //第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
        int delimiterIndex;
        for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
            if (inorder[delimiterIndex] == rootValue) break;
        }

        // 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
        // 左中序区间,左闭右开[leftInorderBegin, leftInorderEnd)
        int leftInorderBegin = inorderBegin;
        int leftInorderEnd = delimiterIndex;
        // 右中序区间,左闭右开[rightInorderBegin, rightInorderEnd)
        int rightInorderBegin = delimiterIndex + 1;
        int rightInorderEnd = inorderEnd;

        // 第五步:切割后序数组,切成后序左数组和后序右数组:后序数组大小一定是和中序数组的大小相同的
        // 左后序区间,左闭右开[leftPostorderBegin, leftPostorderEnd)
        int leftPostorderBegin =  postorderBegin;
        int leftPostorderEnd = postorderBegin + delimiterIndex - inorderBegin; // 终止位置是 需要加上 中序区间的大小size
        // 右后序区间,左闭右开[rightPostorderBegin, rightPostorderEnd)
        int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin);
        int rightPostorderEnd = postorderEnd - 1; // 排除最后一个元素,已经作为节点了

        //第六步:递归处理左区间和右区间
        root->left = traversal(inorder, leftInorderBegin, leftInorderEnd,  postorder, leftPostorderBegin, leftPostorderEnd);
        root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd);

        return root;
    }
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return NULL;
        return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size());
    }
};

654. 最大二叉树

【自己写的,标注了错误】
public:

    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        int i;
        int maxi = 0;
        int maxnum = nums[0];
        for(i = 0; i< nums.size(); i++){//WRONG 新建数组的话可以。如果用下标索引法,i 应该从“left_i”开始,不是每次从0开始的
            maxnum = max(maxnum, nums[i]);///WRONG要求的是最大值,及,对应的下标
        }
        TreeNode* node = new TreeNode (nums[i]);
        vector<int> leftvector(nums.begin(), nums.begin()+i);///新建数组的话,格式是对的,但是不建议用此方法
        vector<int> rightvector(nums.begin()+i+1, nums.end());
        node->left = constructMaximumBinaryTree(leftvector);/缺边界条件(if),见下面解释
        node->right = constructMaximumBinaryTree(rightvector);
        return node;
    }
【代】答案:
class Solution {
private:
    // 在左闭右开区间[left, right),构造二叉树
    TreeNode* traversal(vector<int>& nums, int left, int right) {
        if (left >= right) return nullptr;

        // 分割点下标:maxValueIndex
        int maxValueIndex = left;
        for (int i = left + 1; i < right; ++i) {
            if (nums[i] > nums[maxValueIndex]) maxValueIndex = i;
        }

        TreeNode* root = new TreeNode(nums[maxValueIndex]);

        // 左闭右开:[left, maxValueIndex)
        root->left = traversal(nums, left, maxValueIndex);

        // 左闭右开:[maxValueIndex + 1, right)
        root->right = traversal(nums, maxValueIndex + 1, right);

        return root;
    }
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return traversal(nums, 0, nums.size());
    }
};

一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。
(参考此句出处对应的代码片)
个人认为以上两种情况是自己可选的,(至少对本题)与题意无关,只稍微节省时间空间。。。

617. 合并二叉树

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        TreeNode* node = new TreeNode;//(0);
        if(!root1 && !root2) return NULL;
        if(!root1 && root2) return root2;//node = root2;
        if(root1 && !root2) return root1;//node = root1;
        if(root1!=NULL && root2!=NULL) node->val = root1->val + root2->val;
        
        node->left = mergeTrees(root1->left, root2->left);///这里如果不指定node是否可能是空,就会报错
        node->right = mergeTrees(root1->right, root2->right);所以要在前面的代码里保证此时的node是非空的

        return node;
    }
};
        // 也可以修改t1的数值和结构用于返回,不再新构建一个树

二叉搜索树

二叉搜索树是一个有序树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

搜索树里不能有相同元素
二叉搜索树也可以为空!
遍历整棵搜索树简直是对搜索树的侮辱(701)

若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树;
搜索目标的左右位置是固定的(取决于其上每层的根节点) 这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。
注意判断目标应该在左在右的时候不要反了

常见思路:
1 - 把二叉搜索树转换成有序数组,然后遍历一遍数组;
2 - 标记一个pre作为BTS的最左端节点,标记一个INT_MIN / INT_MAX 作为比较起始点。
举例:98题

700.二叉搜索树中的搜索

注意:函数整体需要有一个return!哪怕根本用不到!(即使需要return的情况都在函数里的while里实现了)

复习旧知识点:

如果要搜索一条边,递归函数就要加返回值。
因为搜索到目标节点了,就要立即return了,这样才是找到节点就返回(搜索某一条边),如果不加return,就是遍历整棵树了。

class Solution{
    public:
     TreeNode* searchBST(TreeNode* root, int val){
         while (root){
             if (!root) return nullptr; 
             if (root->val == val) return root;
             if (val< root->val) root = root->left;
             else root = root->right;
         }
         return nullptr;
     }
};

迭代法:

    TreeNode* searchBST(TreeNode* root, int val) {
        while (root != NULL) {
            if (root->val > val) root = root->left;
            else if (root->val < val) root = root->right;
            else return root;
        }
        return NULL;
    }

因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。
对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。
而对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。

98.验证搜索二叉树

普通递归法:

中序遍历(左中右)下,输出的二叉搜索树节点的数值是有序序列
有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。
我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点。

class Solution{
    private:
    vector<int> vec;
    void traversal (TreeNode* root){
        //参考了之前的中序遍历,但是它的参数里面有包括了vector,所以这次vector要先在函数定义之前定义一下
        if (!root) return;
        traversal (root->left);
        vec.push_back(root->val);
        traversal(root->right);
    }
    public:
    bool isValidBST(TreeNode* root){
        traversal(root);
        for(int i = 0; i < vec.size()-1; i++){
            cout<<i<<vec[i]<<endl;
            if (vec[i]>=vec[i+1]) return false;
        }
        return true;
    }
};

直接搜索法:
注意学习以下如何标记BTS最左边的节点!!并将之作为前一个节点!!!

递归函数究竟什么时候需要返回值,什么时候不要返回值? 只有寻找某一条边(或者一个节点)的时候,递归函数会有bool类型的返回值。其实本题是同样的道理,我们在寻找一个不符合条件的节点,如果没有找到这个节点就遍历了整个树,如果找到不符合的节点了,立刻返回。
使用此方法可能存在的陷阱:1-最小值需要初始化,不能用INT_MIN而是LONG_MIN,最好是用最左边的节点值作为初值;2- 不能比较某个节点的左右是不是从小到大排列的,因为比较的是整个二叉树每个节点都符合BST,所以要一边遍历一边比较。

public:
    TreeNode* pre = NULL; // 用来记录前一个节点
    bool isValidBST(TreeNode* root) {
        if (root == NULL) return true;
        bool left = isValidBST(root->left);

        if (pre != NULL && pre->val >= root->val) return false;
        pre = root; // 记录前一个节点

        bool right = isValidBST(root->right);
        return left && right;
    }

530.二叉搜索树的最小绝对误差

用 pre 节点记录 cur 节点的前一个节点。(这种写法一定要掌握)

class Solution {
public:
    TreeNode* pre;
    int minnum = INT_MAX;//!!!!!min对应是MAX

    void traversal(TreeNode* cur){
        if(!cur) return;

        traversal(cur->left);
        
        if(!pre) {
            minnum = cur->val; 求差值最小,不应该把差值初始化为BTS最小值!!必须初始化为INT/LONG_MAX
        }
        else {
            minnum = min((cur->val - pre->val), minnum);//abs(pre-val - cur->val)
        }
        pre = cur;

        traversal(cur->right);

    }
    int getMinimumDifference(TreeNode* root) {
        if(root==NULL) return 0;
        traversal(root);
        return minnum;
    }
};

501. 二叉搜索树中的众数

对普通二叉树:
注意其中unordered_map赋值、unordered_map转vector< pair >、vector排序及其排序函数写法
本质上是因为本题需要对unordered_map的value排序,而map只能对key排序。

private:

void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍历
    if (cur == NULL) return ;
    map[cur->val]++; //map赋值———— 统计元素频率
    searchBST(cur->left, map);
    searchBST(cur->right, map);
    return ;
}
bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
    return a.second > b.second;
}
public:
    vector<int> findMode(TreeNode* root) {
        unordered_map<int, int> map; // key:元素,value:出现频率
        vector<int> result;
        if (root == NULL) return result;
        searchBST(root, map);
        vector<pair<int, int>> vec(map.begin(), map.end());//map转vector!!!!
        sort(vec.begin(), vec.end(), cmp); // vector排序中排序函数写法————给频率排个序
        result.push_back(vec[0].first);
        for (int i = 1; i < vec.size(); i++) {
            // 取最高的放到result数组中
            if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
            else break;
        }
        return result;
    }

对BST:

/**
 * 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:
    TreeNode* pre = NULL;
    vector<int> result;
    int count = 0; // 统计频率
    int Maxcount = 0;// 最大频率

    void traversal(TreeNode* cur){
        if(cur == NULL)return;

        traversal(cur->left);//可以不加 if(cur->left)  

        if(pre == NULL) count = 1;//
        else{
            if(pre->val == cur->val) count++;
            else count = 1;/// 0;
        }
        pre = cur;
        
        if(count == Maxcount) result.push_back(cur->val);
        if(count > Maxcount) {>=要分开处理!
                result.clear();///
                result.push_back(cur->val);
                Maxcount = count;
            }


        traversal(cur->right);///可以不加 if(cur->right) 
        ///return;
    }
    vector<int> findMode(TreeNode* root) {
        
        /*TreeNode* pre = NULL;
        result.clear();
        count = 0; // 统计频率
        Maxcount = 0;// 最大频率*/
        
        traversal(root);
        return result;
    }
};

236. 二叉树的最近公共祖先

总结:
1- 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式。
2- 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码3- 本题没有给出迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了。

如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢?

搜索一条边的写法:
if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;

搜索整个树写法:
left = 递归函数(root->left);
right = 递归函数(root->right);
left与right的逻辑处理;//也就是后序遍历中处理中间节点的逻辑(也是回溯)a

:
堆是完全二叉树排序的结合,而不是平衡二叉搜索树
是:堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 完全二叉树一定是平衡二叉树.
不是:堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == p || root==q || root==NULL) return root;

        TreeNode* lefttree = lowestCommonAncestor(root->left, p, q);
        TreeNode* righttree = lowestCommonAncestor(root->right, p, q);

        if(lefttree != NULL && righttree!=NULL) return root;
        else if(lefttree == NULL) return righttree;
        return lefttree;
        
    }
};

235. 二叉搜索树的最近公共祖先

本题是二叉搜索树,二叉搜索树是有序的,只要从上到下遍历的时候,cur节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。(注意这里是左闭右闭)
普通二叉树求最近公共祖先需要使用回溯,从底向上来查找,二叉搜索树就不用回溯了,因为搜索树有序(相当于自带方向),那么只要从上向下遍历就可以了。
那么我们可以采用前序遍历。(其实和顺序无关)
不需要遍历整棵树,找到结果直接返回!

此时不知道p和q谁大,所以两个都要判断,但不需要比较p和q谁大

本题就是标准的搜索一条边的写法

//依然是:创建左右,分别接住
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(!root) return NULL;

        if(root->val > p->val && root->val > q->val){
            TreeNode* leftTree = lowestCommonAncestor(root->left, p, q);
            if(leftTree != NULL) return leftTree;
        }
        if(root->val < p->val && root->val < q->val){
            TreeNode* rightTree = lowestCommonAncestor(root->right, p, q);
            if(rightTree != NULL) return rightTree;
        }

        return root;  
    }
};

迭代法(还没看):当【二叉搜索树】+【在自上而下遍历过程中】+【返回子树】时,代码非常简单:

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        while(root) {
            if (root->val > p->val && root->val > q->val) {
                root = root->left;
            } else if (root->val < p->val && root->val < q->val) {
                root = root->right;
            } else return root;
        }
        return NULL;
    }
};

701.二叉搜索树中的插入操作

和上一题类似,也是记录左右…
通过递归返回值来加入新节点
但是个人感觉主要看数学思路(或者靠背会。。)

class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        ///(虽然是BST但涉及公共祖先相关则)前序遍历,从上到下!
        if(root==NULL){
            TreeNode* cur = new TreeNode(val);
            return cur;
        }
        if(root->val > val) root->left = insertIntoBST(root->left, val);
        if(root->val < val) root->right = insertIntoBST(root->right, val);
        return root;
    }
};

一开始错写成了return insertIntoBST(root->left, val);这样的话无法修改root的值,只能返回node(一个节点),这和上一题、第700题有所区分(虽然也是迭代和中间return,但最终return的一个是T/F,一个是子树,而本题要求返回的是root),需要注意!

669. 修剪二叉搜索树

前序,通过返回TreeNode的递归函数返回值删除节点,并将之作为新的root或者其左右子树
与之前的BST操作题目解法思路一致。

class Solution{
    public:
    TreeNode* trimBST (TreeNode* root, int low, int high){
        if (root == NULL) return NULL;

        if (root->val < low) return trimBST( root->right, low, high);
        if (root->val > high) return trimBST( root->left, low, high);
        
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);
        return root;
    }
};

108. 将有序数组转换为二叉搜索树

class Solution {
public:
    TreeNode* traversal(vector<int>& nums, int low, int high){(vector<int>& nums 带上
        if(low > high) return nullptr;
        int mid = low + (high - low)/2;
        TreeNode* node = new TreeNode(nums[mid]);
        node->left = traversal(nums, low, mid-1);前序遍历
        node->right = traversal(nums, mid+1, high);/能递归的部分就写在递归函数里!!
        return node;
    }
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        int high = nums.size()-1;///-1
        int low = 0;
        int mid = low + (high - low)/2;
		/自己写的低级错误。。↓
        // TreeNode* node = new TreeNode(nums[mid]);
        // if(low>0 && mid-1>0 && low < mid)        node->left = traversal(low, mid-1);
        // if(mid+1<nums.size() && high<nums.size() && high>mid)        node->right = traversal(mid+1, high);
        TreeNode* node = traversal(nums, low, high);
        return node;

    }
};

538. 把二叉搜索树转换为累加树

class Solution {
public:
    int pre ;///no need = 0,【代随】习惯于在主函数里对全局变量赋值;no need TreeNode* ,定义为int型就可以了。no need sum,用cur-val和pre滚雪球替代sum
    void traversal( TreeNode* cur){
        if(cur==NULL) return;

        traversal(cur->right);

        //if(pre != 0){
        cur->val += pre;
        //}
        pre = cur->val;

        traversal(cur->left);
        return;
    }
    TreeNode* convertBST(TreeNode* root) {
        pre = 0;
        traversal(root);
        return root;
    }
};

450. 删除二叉搜索树中的节点

(没有自己写)
二叉搜索树中删除节点遇到的情况有以下五种:
第一种情况:没找到删除的节点,遍历到空节点直接返回了
第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。

class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了
        if (root->val == key) {
            // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
            if (root->left == nullptr && root->right == nullptr) {
                ///! 内存释放
                delete root;
                return nullptr;
            }
            // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
            else if (root->left == nullptr) {
                auto retNode = root->right;
                ///! 内存释放
                delete root;
                return retNode;
            }
            // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
            else if (root->right == nullptr) {
                auto retNode = root->left;
                ///! 内存释放
                delete root;
                return retNode;
            }
            // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
            // 并返回删除节点右孩子为新的根节点。
            else {
                TreeNode* cur = root->right; // 找右子树最左面的节点
                while(cur->left != nullptr) {
                    cur = cur->left;
                }
                cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置
                TreeNode* tmp = root;   // 把root节点保存一下,下面来删除
                root = root->right;     // 返回旧root的右孩子作为新root
                delete tmp;             // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)
                return root;
            }
        }
        if (root->val > key) root->left = deleteNode(root->left, key);
        if (root->val < key) root->right = deleteNode(root->right, key);
        return root;
    }
};

二叉树总结

选择什么遍历顺序

在二叉树题目选择什么遍历顺序是不少同学头疼的事情,我们做了这么多二叉树的题目了,Carl给大家大体分分类。
涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点
普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算;偶尔用前序:单纯求深度、找所有路径 ,这是为了方便让父节点指向子节点。
求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。

层序遍历一刷的一些困惑
class Solution{
public:
vector<vector<int> > levelOrder(TreeNode* root){
    vector<vector<int> > ret;
    if(!root)return ret;
    //注意树为空的表述!根节点为空:(!root)
    //也可以用:(root == nullptr)【见题104】
    queue <TreeNode*>  q;
    q.push(root);//输入冰山,其顶部为root,是把二叉树的全部以根节点root为标志放进去
    while(!q.empty()){
        int currentLevelSize = q.size();
        ret.push_back(vector<int>());//注意新增空vector<int>()的表述
        for(int i = 1; i <= currentLevelSize; i++){
            TreeNode* node = q.front();//只取最顶部,无参数   auto!?
            q.pop();
            ret.back().push_back(node->val);//注意..中间的back()函数也是需要括号的

批注:这道题的理解花了很久时间,最后几乎句句cout测试才大致理解并默写出来,但是最后一个for循环之后理应已经达不到while的条件,却为何又循环了一遍 ?只能留给以后再遇到这道题的自己为现在的我解答了。。。
(0802:要看代码随想录的动图才能从整体上理解,不要纠结于que的临时size,看不懂上面的表述。。。没有又循环一遍啊?empty之后for结束,while也结束了啊,是不是把力扣官答和代码随想录的答案搞混了?代码随想录是for结束后push_back vec的,官答是在前面)
在这里插入图片描述
今天发现,这个题解的思路属于【广度优先算法】(BFS),和以下的BFS基本结构也很相似了

/** 
 * 广度优先搜索 
 * @param Vs 起点 
 * @param Vd 终点 
 */  
bool BFS(Node& Vs, Node& Vd){  
    queue<Node> Q;  
    Node Vn, Vw;  
    int i;  

    //初始状态将起点放进队列Q  
    Q.push(Vs);  
    hash(Vw) = true;//设置节点已经访问过了!  

    while (!Q.empty()){//队列不为空,继续搜索!  
        //取出队列的头Vn  
        Vn = Q.front();  

        //从队列中移除  
        Q.pop();  

        while(Vw = Vn通过某规则能够到达的节点){  
            if (Vw == Vd){//找到终点了!  
                //把路径记录,这里没给出解法  
                return true;//返回  
            }  

            if (isValid(Vw) && !visit[Vw]){  
                //Vw是一个合法的节点并且为白色节点  
                Q.push(Vw);//加入队列Q  
                hash(Vw) = true;//设置节点颜色  
            }  
        }  
    }  
    return false;//无解  
}  

一些不在代随里的题,只一刷时候做了,待再看

653.两数之和IV-输入BST

//哈希表
【http://c.biancheng.net/view/7192.html】:使用 set 容器存储的各个键值对,要求键 key 和值 value 必须相等。因此,当使用 set 容器存储键值对时,只需要为其提供各键值对中的 value 值(也就是 key 的值)即可。
map、multimap 容器都会自行根据键的大小对存储的键值对进行排序,set 容器也会根据 value 排序。
另外,使用 set 容器存储的各个元素的值必须各不相同。所以count只能得到0或1,因此可以用到bool中作为判断条件。
修改 set 容器中元素值的做法是:先删除该元素,然后再添加一个修改后的元素。

一道结合哈希表的题,注意是二叉搜索树,所以最后必须先left后right。

class Solution{
    public:
    set<int> s;
    bool findTarget(TreeNode* root, int k){
        if (!root) return false;
        bool contains = s.count(root->val);
        s.insert(k - root->val);
        return contains ||
        findTarget(root->left, k)  ||
        findTarget(root->right, k);

    }
};
113. 路径总和II
// /**
//  * 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 {
// private://?
//     vector<vector<int>> result;
//     vector<int> path;
//     // 递归函数不需要返回值,因为我们要遍历整个树
//     void traversal(TreeNode* cur, int count) {
//         if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点且找到了和为sum的路径
//             result.push_back(path);
//             return;
//         }

//         if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回

//         if (cur->left) { // 左 (空节点不遍历)
//             path.push_back(cur->left->val);
//             count -= cur->left->val;
//             traversal(cur->left, count);    // 递归
//             count += cur->left->val;        // 回溯
//             path.pop_back();                // 回溯
//         }
//         if (cur->right) { // 右 (空节点不遍历)
//             path.push_back(cur->right->val);
//             count -= cur->right->val;
//             traversal(cur->right, count);   // 递归
//             count += cur->right->val;       // 回溯
//             path.pop_back();                // 回溯
//         }
//         return ;
//     }
class Solution{
    private://全pubic也是可以的,试过了
    vector<int> path;
    vector<vector<int>> result;//可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)

    void traversal(TreeNode* cur, int count){
        if (!cur-> left && !cur->right && count == 0) {
            result.push_back(path);
            return;
        }
        if (!cur-> left && !cur->right) return;

        if (cur->left){
            count -= cur->left->val;
            path.push_back(cur->left->val);
            traversal(cur->left, count);
            count += cur->left->val;
            path.pop_back();

        }
        if (cur->right){
            count -= cur->right->val;
            path.push_back(cur->right->val);
            traversal(cur->right, count);
            count += cur->right->val;
            path.pop_back();//vector<int>的函数:进去 path.push_back(cur->right->val);出去path.pop_back()无参数

        }
        return;//试过了,没有也可以
    }
    public:

    vector<vector<int>> pathSum(TreeNode* root, int sum){
    result.clear();
    path.clear();    //必须在函数里面不然会报错  
        if (root == NULL)return result;//[]
        path.push_back(root->val);//忘记了
        traversal(root, sum - root->val);
        return result;
    }

};
// public:
//     vector<vector<int>> pathSum(TreeNode* root, int sum) {
//         result.clear();//第一次出现!!!注意
//         path.clear();
//         if (root == NULL) return result;// vector<vector<int>>默认是[]
//         path.push_back(root->val); // 把根节点放进路径
//         traversal(root, sum - root->val);//注意此处的数字参数已经是减去root->val 的值了
//         return result;
//     }
// };
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值