数据结构:树

树(Tree):

由 n≥0 个节点与节点之间的关系组成的有限集合。当n=0 时称为空树,当n>0 时称为非空树。

Tree数据结构的一些要点

  • 树数据结构被定义为称为节点的对象或实体的集合,这些节点链接在一起以表示或模拟层次结构。

  • 树数据结构是一种非线性数据结构,因为它不以顺序方式存储。它是一种分层结构,树中的元素排列在多个级别中。

  • 在树数据结构中,最顶层的节点称为根节点root。每个节点都包含一些数据,数据可以是任何类型。在上面的树结构中,节点包含员工的姓名,因此数据类型将是字符串。

  • 每个节点都包含一些数据以及其他节点的链接或引用,这些节点可以称为子节点

  • 除了根节点以外,每个节点有且只有一个直接前驱节点

  • 包括根节点在内,每个节点可以有多个后继节点

树数据结构中使用的一些基本术语

「树的节点」 由一个数据元素和若干个指向其子树的树的分支组成。而节点所含有的子树个数称为 「节点的度」。度为 0的节点称为 「叶子节点」 或者 「终端节点」,度不为 0的节点称为 「分支节点」 或者 「非终端节点」。树中各节点的最大度数称为 「树的度」

一个节点的子树的根节点称为该节点的 「孩子节点」,相应的,该节点称为孩子的 「父亲节点」。同一个父亲节点的孩子节点之间互称为 「兄弟节点」

  • 节点的层次:从根节点开始定义,根为第 11 层,根的子节点为第 22 层,以此类推。

  • 树的深度(高度):所有节点中最大的层数。例如图中树的深度为 44。

  • 堂兄弟节点:父节点在同一层的节点互为堂兄弟。例如图中 G、K 互为堂兄弟节点。

  • 路径:树中两个节点之间所经过的节点序列。例如图中 E 到 G 的路径为 E - B - A - D - G。

  • 路径长度:两个节点之间路径上经过的边数。例如图中 E 到 G 的路径长度为 44。

  • 节点的祖先:从该节点到根节点所经过的所有节点,被称为该节点的祖先。例如图中 H 的祖先为 E、B、A。

  • 节点的子孙:节点的子树中所有节点被称为该节点的子孙。例如图中 D 的子孙为 F、G、K

可以通过在指针的帮助下动态创建节点来创建树数据结构。

​struct node  

{  

  int data;  

  struct node *left;  

  struct node *right;   

}  


数分为通用树和二叉树,通用树可以有0或者n个节点,二叉树只有0.1或者2个节点。

  • 二叉搜索树二叉搜索树是一种非线性数据结构,其中一个节点连接到n个节点。它是一种基于节点的数据结构。一个节点可以用三个字段表示二叉搜索树,即数据部分、左子节点和右子节点。二叉搜索树中一个节点最多可以连接两个子节点,因此该节点包含两个指针(左子指针和右子指针)。
    左子树中每个节点的值必须小于根节点的值,右子树中每个节点的值必须大于根节点的值。

  • AVL树它是二叉树的一种类型,或者我们可以说它是二叉搜索树的一种变体。AVL树满足二叉树二叉搜索树的性质。它是一种自平衡二叉搜索树,由Adelson Velsky Lindas发明。这里,自平衡是指平衡左子树和右子树的高度。这种平衡是根据平衡系数来衡量的。如果一棵树遵循二叉搜索树以及平衡因子,我们可以将其视为 AVL 树。平衡因子可以定义为左子树的高度和右子树的高度之间的差。平衡因子的值必须是0、-1或1;因此,AVL树中的每个节点的平衡因子值应该为0、-1或1。

  • 红黑树是二叉搜索树。红黑树的前提是我们应该了解二叉搜索树。在二叉搜索树中,左子树的值应小于该节点的值,右子树的值应大于该节点的值。我们知道,二分查找的时间复杂度在平均情况下是log2n,最好情况是O(1),最坏情况是O(n)。

    当对树执行任何操作时,我们希望我们的树是平衡的,以便所有操作(如搜索、插入、删除等)花费更少的时间,并且所有这些操作的时间复杂度为 log2n

    红黑树是一种自平衡二叉搜索树。AVL树也是一种高度平衡二叉搜索树,那么为什么我们需要红黑树呢?在AVL树中,我们不知道需要多少次旋转才能平衡树,但在红黑树中,最多需要2次旋转才能平衡树。它包含一个额外的位,表示节点的红色或黑色,以确保树的平衡。

现在数的基本知识点我们说完了,现在主要来看看二叉树:

二叉树可以分为满二叉树、完全二叉树、完美二叉树、退化二叉树、平衡二叉树。

具体是什么样的请自己找。

二叉树可以进行三种遍历:

前序(中前后)、中序(前中后)、后序(前后中)。

二叉树的前序遍历在前序遍历中,首先访问根节点,然后访问左子树,最后访问右子树。前序遍历的过程可以表示为【NLR】根节点->左节点->右节点。前序遍历时总是先遍历根节点,后序遍历时根节点是最后一项。前序遍历用于获取树的前缀表达式。

执行步骤:

  • 首先,访问根节点。

  • 然后,访问左子树。

  • 最后,访问右子树。

前序遍历的时间复杂度为O(n),其中“n”是二叉树的大小。

然而,如果我们不考虑函数调用的堆栈大小,前序遍历的空间复杂度为O(1) 。否则,前序遍历的空间复杂度为O(h),其中 'h' 是树的高度。

线性数据结构如栈、数组、队列等,只有一种方式来遍历数据。但在树这样的分层数据结构中,有多种方式来遍历数据。这里我们讨论另一种遍历树数据结构的方式,即中序遍历。

中序遍历有两种方法:

  • 使用递归进行中序遍历

  • 使用迭代方法进行中序遍历

后序遍历是用于访问树中节点的遍历技术之一。它遵循LRN(左-右-节点)原则。后序遍历技术遵循左右根策略。这里的左右根是指先遍历根节点的左子树,然后是右子树,最后遍历根节点。这里,后序名称本身表明最后将遍历树的根节点。后序遍历用于获取树的后缀表达式。

以下步骤用于执行后序遍历:

  • 通过递归调用后序函数来遍历左子树。

  • 通过递归调用后序函数来遍历右子树。

  • 访问当前节点的数据部分。

下面是一些二叉树的做题记录:

题目1.
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

递归

class Solution {

public:

    void prin(TreeNode* pre,vector<int> &res){

        if(pre == nullptr) return;

        res.push_back(pre->val);

        prin(pre->left,res);

        prin(pre->right,res);

    }

    vector<int> preorderTraversal(TreeNode* root) {

        vector<int> res;

        prin(root,res);

        return res;

    }

};


//思路:定义一个函数来返回当前的节点值,由于题目要求前序遍历,因此我们先返回根,在去找左节点、有节点。我们可以用递归解决这个问题,并把结果放入我们设计好的数组里面,返回数组即可

 2.迭代
 

2.迭代

class Solution {

public:

    vector<int> preorderTraversal(TreeNode* root) {

        vector<int> res;//用于记录答案的数组

        if(root == nullptr){

            return res;

        }

        stack<TreeNode*> stk;//用于维护数的关系的栈

        TreeNode* node = root;//定义一个指针好在移动的时候,遍历整个数

        while(!stk.empty()||node !=nullptr){

            while(node != nullptr){

                res.emplace_back(node->val);//向数组的最后一位插入元素,比push_back更快

                stk.emplace(node);//把节点地址放入栈中

                node = node->left;//不断向左移动

            }

            node = stk.top();//指向栈的顶端的数地址,弹出,指向该节点的右分支

            stk.pop();

            node = node->right;

        }

        return res;

    }

};

 //思路:定义一个栈来维护数的关系,定义一个数组来存储我们记录的数组的值。我们通过while循环来遍历整个数,由于是前序遍历,因此我们首先要遍历完左分支,遍历到最后一层最后一个左分支时,我们的栈里面存储的是最后一层最后一个左分支,我们把它弹出,在开始下一次的while循环遍历。最后返回数组就可以了

题目2.力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

 1.递归

class Solution {



public:

    void prin(TreeNode* pre,vector<int> &res){

        if(pre == nullptr) return;

        prin(pre->left,res);

        res.push_back(pre->val);

        prin(pre->right,res);

    }

    vector<int> inorderTraversal(TreeNode* root) {

        vector<int> res;

        prin(root,res);

        return res;

    }

};

//与上面的前序排列一样,不过只是把prin(pre->left,res);和res.push_back(pre->val);调换了一个位置,表示先左,再中,最后右

2.迭代
 

class Solution {

public:

    vector<int> inorderTraversal(TreeNode* root) {

        vector<int> res;//用于记录答案的数组

        if(root == nullptr){

            return res;

        }

        stack<TreeNode*> stk;//用于维护数的关系的栈

        TreeNode* node = root;//定义一个指针好在移动的时候,遍历整个数

        while(!stk.empty()||node !=nullptr){

            while(node != nullptr){

                stk.emplace(node);//把节点地址放入栈中

                node = node->left;//不断向左移动

            }

            node = stk.top();//指向栈的顶端的数地址,弹出,指向该节点的右分支

            res.emplace_back(node->val);//向数组的最后一位插入元素,比push_back更快

            stk.pop();

            node = node->right;

        }

        return res;

    }

};

 //思路:与上题迭代一样,我们需要把 res.emplace_back(node->val);移动到node = stk.top();后面,表示向数组记录左分子的最后一位,因为中序遍历是先记录输出,最左边的一个节点,此时栈中正好是这个节点

 3.力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

1.递归:
 

class Solution {

public:

    void prin(TreeNode* pre,vector<int> &res){

        if(pre == nullptr) return;

        prin(pre->left,res);

        prin(pre->right,res);

        res.push_back(pre->val);;

    }

    vector<int> postorderTraversal(TreeNode* root) {

        vector<int> res;

        prin(root,res);

        return res;

    }

};

 //同理它是遍历先左节点后在尝试去遍历右节点,先记录左节点的值,在尝试去记录右节点,对于如何理解,你可以想想有一个没有任何分支的节点在最左端开始记录的

class Solution {

public:

    vector<int> postorderTraversal(TreeNode* root) {

        vector<int> res;//用于记录答案的数组

        if(root == nullptr){

            return res;

        }

        stack<TreeNode*> stk;//用于维护数的关系的栈

        TreeNode *prev = nullptr;

        TreeNode* node = root;//定义一个指针好在移动的时候,遍历整个数

        while(!stk.empty()||node !=nullptr){

            while(node != nullptr){//每个遍历都需要从最左端开始

                stk.emplace(node);//把节点地址放入栈中

                node = node->left;//不断向左移动

            }

            node = stk.top();//指向栈的顶端的数地址,弹出

            stk.pop();

            if (node->right == nullptr || node->right == prev) {//node->right == nullptr的条件满足时,此时节点为左节点,先记录左节点,node->right == prev,prev指向的就是之前的左节点,如果满node->right == prev则说明它左节点遍历完了,已经记录到数组里面

                res.emplace_back(node->val);

                prev = node;//表示前一个节点

                node = nullptr;

            } else {//如果else满足就说明当前节点为中节点,说明该节点下单所有下节点都记录到了数组里面

                stk.emplace(node);

                node = node->right;

            }

        }

        return res;

    }

};

 //思路:在两个while中,第一个while移动到能移动的最下层的左分支,因为我们是后序遍历,因此我们还需要if去向stack中、res中插入结果,根据if中的条件去执行,由于较为复杂,需要自己去推导才可以理解,同时我也做了记录,解释if中所有的条件,对应什么情况。这样最后我们就遍历完整个数了

4. 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {

public:

    int maxDepth(TreeNode* root) {

        if (root == nullptr) return 0;

        return max(maxDepth(root->left), maxDepth(root->right)) + 1;

    }

};

 //思路:当前节点的最大深度为它的子节点中的最大的最大深度+1,为什么要加1呢,因为这个1是父节点本身,你算的是父节点的最大深度。在最末尾的节点,它的最大深度应该是1,它没有子节点,因此子节点的最大深度为0,即if (root == nullptr) return 0;因此我们可以利用递归解决

2.

class Solution {

public:

    int maxDepth(TreeNode* root) {

        if (root == nullptr) return 0;

        queue<TreeNode*> Q;//定义一普通队列,只能从后面插入值

        Q.push(root);

        int ans = 0;

        while (!Q.empty()) {//遍历完整个队列

            int sz = Q.size();//得到队列元素的个数

            while (sz > 0) {//遍历完整个队列的所有元素,每个元素的左节点右节点都放入队列,并删除父节点

                TreeNode* node = Q.front();Q.pop();

                if (node->left) Q.push(node->left);

                if (node->right) Q.push(node->right);

                sz -= 1;

            }

            ans += 1;//遍历完这一层,ans+1

        }

        return ans;

    }

};

 //思路:我们得到一层中的所有元素,并把它遍历完,之前遍历的元素都不要了,后面的元素重新插入,每次插入完一层,ans+1,代表现在遍历完了多少层,最后当ans不能在增加,就说明已经遍历所有层,此时ans就是最大深度

5. 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {

public:

    TreeNode* invertTree(TreeNode* root) {

        if (root == nullptr) {

            return nullptr;

        }

        TreeNode* left = invertTree(root->left);

        TreeNode* right = invertTree(root->right);

        root->left = right;//第一次反转在最后面的子叶开始

        root->right = left;

        return root;

    }

};

//利用递归实现由子叶到父叶的反转,逐渐反转,最后整个数被反转了

6.力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {

public:

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

        if (p == nullptr && q == nullptr) {

            return true;

        } else if (p == nullptr || q == nullptr) {//由于前面的条件是p == nullptr && q == nullptr,因此到这里,那么至少有一个节点不为空,如果又有一个节点为空,那么节点就不相同

            return false;

        } else if (p->val != q->val) {

            return false;

        } else {

            return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);//用&&来判断得到true还是false

        }

    }

};

//思路:判断两个数是否相同我们判断它的子节点是否相同,如果它们子节点都为空,则相同,如果由一个为空,另外一个不是,那么就一定不相同,最后我们在判断父节点值是否相同。数和它的子节点都这么判断,因此我们需要用递归实现。

//注意:&&和||

2.

class Solution {

public:

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

        if (p == nullptr && q == nullptr) {

            return true;

        } else if (p == nullptr || q == nullptr) {

            return false;

        }//两个条件判断数开始时是否相同,满足条件进行后续判断

        queue <TreeNode*> queue1, queue2;

        queue1.push(p);

        queue2.push(q);

        while (!queue1.empty() && !queue2.empty()) {

            auto node1 = queue1.front();

            queue1.pop();

            auto node2 = queue2.front();

            queue2.pop();

            if (node1->val != node2->val) {

                return false;

            }

            auto left1 = node1->left, right1 = node1->right, left2 = node2->left, right2 = node2->right;

            if ((left1 == nullptr) ^ (left2 == nullptr)) {

                return false;//^为异或符号,只有它们结果不同,才返回true,意思是left1和left2结果不同就返回false

            }

            if ((right1 == nullptr) ^ (right2 == nullptr)) {

                return false;

            }

            if (left1 != nullptr) {//放入它们的子节点

                queue1.push(left1);

            }

            if (right1 != nullptr) {

                queue1.push(right1);

            }

            if (left2 != nullptr) {

                queue2.push(left2);

            }

            if (right2 != nullptr) {

                queue2.push(right2);

            }

        }

        return queue1.empty() && queue2.empty();

    }

};

//思路:定义两个迭代器来遍历放入两个队列的两个树的节点,注意先放入左节点,再放入右节点。判断当前两个节点是否相同,相同满足以下条件:1.它们的父节点相同,即不能一个为空,一个不为空。如果都不为空就把它们的子节点放入队列,然后继续判断,最后所有节点都满足不能一个为空,一个不为空。则两个树相同

//注意每一次遍历都要删除之前的遍历了的节点,因此先进先出,用队列

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

years_GG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值