第一部分: 理论基础
①二叉树的种类:做题过程中,最常见的二叉树包括满二叉树、完全二叉树、二叉搜索树以及平衡二叉搜索树。
Ⅰ、满二叉树:如果⼀棵⼆叉树只有度为0的结点和度为2的结点,并且度为0的结点在同⼀层 上,则这棵⼆叉树为满⼆叉树。满二叉树的深度为k,则有个节点。满二叉树如下图所示:
Ⅱ、完全二叉树:除最底层节点可能没填满外,其余每层节点数都达到最大值,底层节点都连续集中在该层最左边的若⼲位置。若最底层为第 h 层,则该层包含 1~ 个节点。
Ⅲ、二叉搜索树:二叉搜索树满足以下条件,若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右⼦树上所有结点的值均大于它的根结点的值; 二叉搜索树的左、右子树也分别为⼆叉搜索树。下面两棵树都属于二叉搜索树。
Ⅳ、平衡二叉搜索树:又称为AVL(Adelson-Velsky and Landis)树,具有以下性质:它是 ⼀棵空树或它的左右两个子树的⾼度差的绝对值不超过1,并且左右两个子树都是⼀棵平衡⼆叉树。
②二叉树的存储方式:可以是链式存储也可以是线性存储的。链式存储就类似于链表结构,有一个val,两个指针分别指向左子节点和右子节点;线性存储可以放到数组中,对于数组中的第i个元素,他的左子树下表为2*i+1,右子树下标为2*i+2。
③二叉树的遍历方式:遍历方式可以分为深度优先遍历和广度优先遍历两大类。深度优先遍历中又可以分为先序遍历、中序遍历和后序遍历的方式。
④二叉树的定义:链式存储的⼆叉树节点的定义方式用代码表示如下:
class TreeNode{
public:
int val;
TreeNode* left;
TreeNode* right;
public:
TreeNode(int x): val(x), left(nullptr), right(nullptr) {}
};
第二部分: 递归遍历
递归方法是必须掌握的一种方法,递归法的步骤可以总结为以下三步(递归三部曲):
Ⅰ、确定函数的参数与返回值;
Ⅱ、确定递归终止条件;
Ⅲ、确定单层递归逻辑。
明确了这三个步骤,再写递归的时候,思路就清晰很多了,下面以二叉树的前序遍历、中序遍历以及后序遍历为例,演示递归写法,代码如下:
首先是二叉树的前序遍历(递归方法)代码:
class Solution {
public:
vector<int> result;
vector<int> preorderTraversal(TreeNode* root) {
if(!root) return result;
result.push_back(root->val);
preorderTraversal(root->left);
preorderTraversal(root->right);
return result;
}
};
接下来,二叉树的中序遍历(递归方法)代码:
class Solution {
public:
vector<int> result;
vector<int> inorderTraversal(TreeNode* root) {
if(!root) return result;
inorderTraversal(root->left);
result.push_back(root->val);
inorderTraversal(root->right);
return result;
}
};
最后,是二叉树的后续遍历(递归方法)代码:
class Solution {
public:
vector<int> result;
vector<int> postorderTraversal(TreeNode* root) {
if(!root) return result;
postorderTraversal(root->left);
postorderTraversal(root->right);
result.push_back(root->val);
return result;
}
};
观察上面三种不同的遍历方法,代码是十分相似的,只是单层递归逻辑存在差异,顺序不同而已。
第三部分:迭代遍历
迭代法需要通过stack这种数据结构来完成,前序遍历(中左右)和后序遍历(左右中)的写法比较接近,后序遍历可以在前序遍历的基础上稍作修改来完成,而中序遍历写法与上述两者有一定差异。
前序遍历的迭代算法代码如下:
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) {
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->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<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
};
前序遍历代码为,此时与中序遍历相比只是改变了两行代码的顺序:
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(NULL);
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
后序遍历代码为,此时与中序遍历相比只是改变了两行代码的顺序:
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
Day14打卡!!!