树的定义
树的抽象数据类型
树的存储结构
二叉树的定义
二叉树的性质
二叉树的存储结构
遍历二叉树
二叉树的建立
线索二叉树
树、森林与二叉树的转换
赫夫曼树
总结
递归
二叉树的存储
// 定义一个二叉树节点的结构体TreeNode
struct TreeNode {
int val; // 节点值
TreeNode *left; // 指向左子树的指针
TreeNode *right; // 指向右子树的指针
TreeNode(int x) : val(x), left(NULL), right(NULL) {} // 构造函数初始化
};
二叉树的递归遍历
写递归算法时,需要注意的点(1)确定递归函数的参数和返回值;(2)确定终止条件;(3)确定单层递归的逻辑。
// 前序遍历
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) 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;
}
};
//TreeNode* root,这是一个指向 TreeNode 类型对象的指针。在函数调用时,传递的是指向二叉树根节点的指针,表示从根节点开始进行先序遍历。
一次二叉树的递归调用过程:
当调用 `traversal(root, result)` 时,实际上触发了一个递归的过程,该过程会遍历二叉树并将节点的值加入到 `result` 中。让我用一个简单的例子来说明递归遍历的过程。
假设有以下二叉树的结构:
```
1
/ \
2 3
/ \
4 5
```
对应的 `TreeNode` 结构可能如下:
```cpp
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
```
现在,我们调用 `traversal(root, result)`,其中 `root` 指向根节点。递归过程如下:
1. **第一次调用:** `traversal(root, result)`,其中 `root` 指向节点 1。
- 将节点 1 的值(1)加入 `result` 中。
- 调用 `traversal(root->left, result)`,即 `traversal` 函数的第二个递归调用,其中 `root` 指向节点 2。
2. **第二次调用:** `traversal(root, result)`,其中 `root` 指向节点 2。
- 将节点 2 的值(2)加入 `result` 中。
- 调用 `traversal(root->left, result)`,即 `traversal` 函数的第三个递归调用,其中 `root` 指向节点 4。
3. **第三次调用:** `traversal(root, result)`,其中 `root` 指向节点 4。
- 将节点 4 的值(4)加入 `result` 中。
- 节点 4 为叶子节点,没有左右子树,递归结束。
4. **返回到第二次调用:** 返回到节点 2 的递归调用,调用 `traversal(root->right, result)`,即 `traversal` 函数的第四个递归调用,其中 `root` 指向节点 5。
5. **第四次调用:** `traversal(root, result)`,其中 `root` 指向节点 5。
- 将节点 5 的值(5)加入 `result` 中。
- 节点 5 为叶子节点,没有左右子树,递归结束。
6. **返回到第二次调用:** 返回到节点 2 的递归调用,节点 2 的左右子树都已经遍历完毕,递归结束。
7. **返回到第一次调用:** 返回到节点 1 的递归调用,调用 `traversal(root->right, result)`,即 `traversal` 函数的第五个递归调用,其中 `root` 指向节点 3。
8. **第五次调用:** `traversal(root, result)`,其中 `root` 指向节点 3。
- 将节点 3 的值(3)加入 `result` 中。
- 调用 `traversal(root->left, result)`,即 `traversal` 函数的第六个递归调用,其中 `root` 指向 `NULL`(因为节点 3 的左子树为空)。
9. **第六次调用:** `traversal(root, result)`,其中 `root` 指向 `NULL`。
- 由于 `root` 为 `NULL`,递归直接结束。
10. **返回到第五次调用:** 返回到节点 3 的递归调用,调用 `traversal(root->right, result)`,即 `traversal` 函数的第七个递归调用,其中 `root` 指向 `NULL`(因为节点 3 的右子树为空)。
11. **第七次调用:** `traversal(root, result)`,其中 `root` 指向 `NULL`。
- 由于 `root` 为 `NULL`,递归直接结束。
12. **返回到第五次调用:** 返回到节点 3 的递归调用,节点 3 的左右子树都已经遍历完毕,递归结束。
13. **返回到第一次调用:** 返回到节点 1 的递归调用,节点 1 的左右子树都已经遍历完毕,递归结束。
最终,`result` 中的值为 `[1, 2, 4, 5, 3]`,即二叉树的先序遍历结果。
递归并不会结束,只会返回上一层,因为其是基于栈构建的
在递归调用中,当遇到叶子节点时执行 `return` 语句,表示当前递归分支的结束。这个 `return` 语句会将控制权返回到调用该递归的上一层。如果递归发生在函数的最后一行,那么函数调用会结束,控制权将返回到调用方。
在先序遍历的例子中,递归调用 `traversal` 函数是在处理每个节点的过程中,其中包括左子树的递归调用和右子树的递归调用。当遇到叶子节点时,递归分支会通过 `return` 结束,但这只是结束了当前分支的递归调用,而不是整个程序的执行。
在递归的过程中,程序会回溯到之前的调用点,继续执行后续的代码。这是因为递归是通过调用栈(call stack)来管理的,每次函数调用都会在栈上创建一个新的帧(frame)。当递归分支结束时,栈帧会被弹出,控制权返回到上一层的调用点。
具体来说,在先序遍历的例子中,当递归调用结束时,程序会返回到上一层的递归调用点,继续执行那个调用点之后的代码。这个过程会一直持续,直到栈上的所有帧都被弹出,整个程序执行完毕。
//后序遍历
class Solution{
public:
void traversal(TreeNode* node, vector<int>& vec)
{
if (node == NULL) return;
traversal(node->left, vec);
traversal(node->right, vec); // right
vec.push_back(node->val);
}
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> result;
traversal(root,result);
return result;
}
};
中序:
class Solution
{
public:
void Traversal(TreeNode* cur,vector<int>& vec)
{
if(cur == NULL) 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:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 取栈顶元素,中
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 先将右子树入栈
if (node->left) st.push(node->left); // 再将左子树入栈
}
return result;
}
};
后序(前序颠倒)
前序是中左右,后序遍历是左右中,所以实施过程,中右左,那反过来就是左右中了
class Solution{
public:
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> result;
stack<TreeNode*> st;
if(root == NULL) return result;
st.push(root);
while(!st.empty())
{
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if(node->left) st.push(node->left);
if(node->right) st.push(node->right);
}
reverse(result.begin(),result.end());
return result;
}
}
中序(先访问根节点,但先处理的是左子树的左叶子,访问的顺序和处理的顺序不一样)
用指针遍历元素(这里相比于前序遍历就多了指针,前序遍历没有),用栈来处理元素(需要多看一下视频)
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
代码一定要天天敲,多敲几遍,加油
递归遍历是维护一个隐式的栈
迭代遍历是维护一个显式的栈
二叉树的统一遍历
统一迭代法很难理解,在这里不做赘述,后续熟练了可以考虑进一步掌握
二叉树的层序遍历
利用队列实现,逐层遍历
class Solution
{
public:
vector<vector<int>> levelorder(TreeNode* root)
{
vector<vector<int>> result;
queue<TreeNode*> que;
if(root != NULL) que.push(root);
while(!que.empty())
{
int size = que.size();
vector<int> vec;
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;
}
}