1. 理论基础
最好熟悉自己使用语言的常见容器底层如何实现,最基本的map set,这样性能分析才方便
1.1 二叉树种类
一般做题过程中常见:满二叉树和完全二叉树
-
满二叉树:一棵树只有度为0的结点和度为2的结点,且度为0的结点在同一层
- 结点个数,2^k - 1,k为深度
-
完全二叉树:除最底层结点可能没填满外,其余每层结点数都达到最大,且最下面一层的节点都集中在该层最左边的若干位置
- 若最底层为第h层,则该层包含1~2^(h-1)个节点
- 之前的优先队列其实就是一个堆,堆就是一颗完全二叉树,同时保证父子节点的顺序关系
1.2 带数值的二叉树
-
二叉搜索树
- 是一个有序数,左子树不为空,则左子树上的所有节点值均小于他的根节点的值
- 若右子树不为空,则右子树上的所有节点值均大于他的根节点的值
- 他的左右子树也分别为二叉搜索树
-
平衡二叉搜索树(AVL,Adelson-Velsky and Landis)
-
他是一颗空树或它的左右两颗子树的高度差的绝对值不超过1,且左右子树都是一颗平衡二叉树
-
C++中map、set、multimap multiset的底层实现都是平衡二叉树,所以map set的增删操作时间复杂度都是logn,但是unordered_map unordered_set的底层实现是哈希表
-
1.3 二叉树的存储方式
- 一般链式存储(用指针),更易理解,也可顺序存储(用数组,奇偶索引查找)
2. 遍历方式
深度优先遍历,先往深走,遇到叶子结点再往回走,以下的前中后指的是中间结点的顺序
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 经常使用递归来写,前面提到栈来实现递归,也就是说栈可以来实现非递归方式
广度优先遍历,一层一层去遍历
- 层次遍历(迭代法)
- 一般采用队列来实现,由于其具有先进先出特点
// 链式存储二叉树结点
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
3. 递归遍历
三步走写好递归
-
- 确定函数参数和返回值
-
- 确定终止条件
- 3.确定单层递归的逻辑
// 前序遍历为例
// 1.要保存遍历的结果必然传入vector保存,当然结点需要传入,没有返回值,直接在vector中保存
void traversal(TreeNode *cur, vector<int>& vec)
// 2.确定终止条件,当前结点空结点,当然返回空
if (cur == NULL) return;
// 3.单层遍历逻辑,前序遍历,中左右,分别访问即可
vec.push_back(cur->val);
traversal(cur->left, vec);
traversal(cur->right, vec);
// 不难写出如下前序遍历代码
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;
}
};
// 中序
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> preorderTraversal(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); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
4. 迭代遍历
递归的实现就是:每一次递归调用都会把函数的局部变量、参数变量值和返回地址等压入调用栈中,然后递归返回时,从栈顶弹出上一次递归的各项参数,这就是递归为什么能返回上一层的原因
- 前序和后序可以一种风格,主要在于其访问顺序和修改顺序存在一致性或者强关联性,如结果逆序特点等
- 中序比较特殊,另一种风格,需要借助指针,一路往左
- 上述两种差异原因在于,访问结点(遍历结点,始终第一个是顶)和处理结点(把元素放进结果里)和处理结点不一致
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
// 前序遍历——迭代法
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root == nullptr) 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> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
if (root == nullptr) return result;
while (cur != nullptr || !st.empty()) {
if (cur != nullptr) {
st.push(cur);
cur = cur->left; // 左,借助指针一直走到最左侧叶子结点以下的NULL
} else {
cur = st.top();
st.pop();
result.push_back(cur->val); // 中,弹出的即为上次压进去的NULL结点的父节点
cur = cur->right; // 右,NULL父节点可能有右子结点
}
}
return result;
}
};
// 后序遍历——迭代
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root == nullptr) 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;
}
};
5. 统一迭代
该写法能够统一前中后的写法,但是较难理解
- 难点在于访问顺序都是从上往下,往下的过程中遇到的都是“根”结点,所以需要特殊处理
- 自己理解的是访问时压栈出栈,是为了能保留根节点的值,能找到路径,与传统遇到根结点就根据根节点的左右逻辑关系对其进行处理(保存操作)不同,统一法在于最后统一按照递归逻辑处理
// 中序
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != nullptr) 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 != nullptr) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != nullptr) {
st.pop();
if (node->right) st.push(node->right); // 与中序相比仅仅改变下面的中序位置,改成有左中
if (node->left) st.push(node->left);
st.push(node);
st.push(nullptr);
} 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 != nullptr) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != nullptr) {
st.pop();
st.push(node);
st.push(nullptr);
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;
}
};