树(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.它们的父节点相同,即不能一个为空,一个不为空。如果都不为空就把它们的子节点放入队列,然后继续判断,最后所有节点都满足不能一个为空,一个不为空。则两个树相同
//注意每一次遍历都要删除之前的遍历了的节点,因此先进先出,用队列