一、二叉树的理论基础
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。深度为k,有2^k-1个节点
完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
前面介绍的树,都没有数值的,二叉搜索树是有数值的了,二叉搜索树是一个有序树。若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。
并且左右两个子树都是一棵平衡二叉树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树。
存储方式:可以链式存储,也可以顺序存储。
链式存储就是使用指针,顺序存储就是使用数组。
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
二叉树的遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
二、二叉树的递归遍历
题目链接:
思路:
递归的三要素:
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void。
确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return。
确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值。
前序遍历的代码如下:
void traversal1(TreeNode* cur, vector<int>& v)
{
//前序遍历
if (!cur)
{
return;
}
v.push_back(cur->val);
traversal1(cur->left,v);
traversal1(cur->right,v);
}
vector<int> preorderTraversal(TreeNode* root)
{
vector<int>result;
this->traversal1(root, result);
return result;
}
中序遍历代码如下:
void traversal2(vector<int>&v, TreeNode* cur)
{
//中序遍历
if (!cur)
{
return;
}
traversal2(v,cur->left);
v.push_back(cur->val);
traversal2(v, cur->right);
}
vector<int> inorderTraversal(TreeNode* root)
{
vector<int>result;
this->traversal2(result, root);
return result;
}
后序遍历的代码如下:
void traversal3(TreeNode* cur, vector<int>& v)
{
if (!cur)
{
return;
}
traversal3(cur->left,v);
traversal3(cur->right,v);
v.push_back(cur->val);
}
vector<int> postorderTraversal(TreeNode* root)
{
vector<int>result;
this->traversal3(root, result);
return result;
}
三、二叉树的非递归遍历(迭代)
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。因此可以使用栈来进行操作。
前序遍历:要处理的结点顺序(加入result容器)与访问的顺序一致。
注意:为什么要单独把根节点先push进stack容器里?方便进入while循环里的逻辑一致
代码如下:
vector<int> preorderTraversal(TreeNode* root)
{
vector<int>result;
stack<TreeNode*>st;
if (!root)
{
return result;
}
st.push(root);
while (!st.empty())
{
//栈非空
TreeNode* temp = st.top();
result.push_back(temp->val);
st.pop();
if (temp->right)
{
st.push(temp->right);
}
if (temp->left)
{
st.push(temp->left);
}
}
return result;
}
中序遍历:要处理的结点顺序(加入result容器)与访问的顺序不一致。需要借助一个指针cur来遍历树的结点,stack容器里放遍历的元素,cur指针指向待遍历的元素和需要处理的元素。
先不断将每棵字树的左结点压栈,直到左指针为空为止,之后需要从栈里pop出一个元素,这个元素就是需要处理的元素,按照左中右的顺序,由于左指针为空了,相当于这个元素就是中结点,我们需要将它加入结果容器里,再将cur指针指向右节点,如果不存在右节点,说明这个子树已经遍历完了,继续pop出上一层的中结点。如果存在右节点,则将这个右节点压栈,再判断左子树的情况,重复上述过程。当栈为空,或者cur为空的时候的退出while循环。
代码如下:
vector<int> inorderTraversal(TreeNode* root)
{
//中序遍历
//栈记录访问过的元素,指针指向需要处理的元素
//当cur为空的时候,需要弹出元素(相当于是中)并进行处理,
stack<TreeNode*>st;
vector<int>result;
TreeNode* cur = root;
while (cur != NULL || !st.empty())
{
//先遍历完所有左边的元素
if (cur != NULL)
{
st.push(cur);
cur = cur->left;
//cur为空,则需要去处理右结点
}
else
{
//此时需要从栈里pop出元素
TreeNode* temp = st.top();
st.pop();
result.push_back(temp->val);
cur = cur->right;//去处理右结点,右结点为空,
//则需要去处理上一级的中结点
}
}
return result;
}
后序遍历:先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了。
代码如下:
vector<int> postorderTraversal(TreeNode* root)
{
//后序遍历
vector<int>result;
stack<TreeNode*>st;
if (!root)
{
return result;
}
st.push(root);
while (!st.empty())
{
TreeNode* temp = st.top();
st.pop();
result.push_back(temp->val);
if (temp->left)
{
st.push(temp->left);
}
if (temp->right)
{
st.push(temp->right);
}
}
reverse(result.begin(), result.end());
return result;
}