1、二叉树理论基础
二叉树有两种主要的形式:满二叉树和完全二叉树。
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。
二叉搜索树
二叉搜索树是有数值的,是一个有序树
1、若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2、若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3、它的左、右子树也分别为二叉排序树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn。
注意unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
二叉树可以链式存储,也可以顺序存储
链式存储方式就用指针, 顺序存储的方式就是用数组。
链式存储如图:
用数组来存储二叉树,顺序存储的方式如图:
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
二叉树主要有两种遍历方式:
1、深度优先遍历:先往深走,遇到叶子节点再往回走。
2、广度优先遍历:一层一层的去遍历。
2、递归遍历
三部分析法:
1、确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
2、确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
3、确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中
前序遍历
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec)
{
if(cur == nullptr) return ;
vec.push_back(cur -> val);
traversal(cur->left ,vec);
traversal(cur->right,vec);
}
vector<int> preorderTraversal(TreeNode* root)
{
vector<int> result;
traversal(root, result);
return result;
}
};
preorderTraversal 函数
这个函数是公开的接口,用于启动前序遍历过程。
TreeNode* root:指向二叉树根节点的指针。
步骤:
初始化结果向量:vector result; 这行代码创建了一个空的vector,用于存储遍历结果。
启动遍历:traversal(root, result); 这行代码调用traversal函数,以根节点为起点开始遍历。遍历过程中,所有节点的值都将被添加到result向量中。
返回结果:return result; 这行代码返回填充了遍历结果的向量。
中序遍历
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec)
{
if(cur == nullptr) return ;
traversal(cur->left ,vec);
vec.push_back(cur -> val);
traversal(cur->right,vec);
}
vector<int> inorderTraversal(TreeNode* root)
{
vector<int> result;
traversal(root, result);
return result;
}
};
后序遍历
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec)
{
if(cur == nullptr) return ;
traversal(cur->left ,vec);
traversal(cur->right,vec);
vec.push_back(cur -> val);
}
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> result;
traversal(root, result);
return result;
}
};
3、层序遍历
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
为什么size是固定的
在二叉树的层序遍历中,当处理某一层的节点时,该层节点的数量(即队列中的元素数量)在遍历这一层的过程中是固定的。这是因为层序遍历的特性决定了:在当前层的所有节点被处理(即访问并可能将其子节点加入队列)之前,不会处理下一层的任何节点。
具体来说,当循环开始时,我们通过int size = que.size();获取了当前队列的大小,这个大小实际上就是当前层(即将要被处理的一层)的节点数。在随后的for循环中,我们遍历这个固定数量的节点,处理它们(访问它们的值,并将它们的子节点加入队列),但在这个过程中,队列的大小会因为子节点的加入而增加。然而,这些新增的子节点代表的是下一层的节点,它们在当前层的处理过程中不会被访问到。
如果我们在for循环的每次迭代中都使用que.size()作为循环条件,那么循环可能会提前结束,因为随着子节点的加入,队列的大小会增加,导致循环在处理完当前层的所有节点之前就停止了。这样,下一层的节点就可能被错误地包含在当前层的结果中,或者当前层的某些节点根本就没有被处理。
因此,为了确保只处理当前层的节点,我们需要在循环开始前就确定当前层的节点数(即队列的当前大小),并在循环中使用这个固定的大小作为迭代条件。这样,无论队列在循环过程中如何变化,我们都能确保只处理当前层的节点。