若图片无法查看,请进入完整链接查看!
二叉树的节点定义
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) {}
};
递归前序遍历:
void inorder(TreeNode *root)
{
if(!root) return;
cout << root->val << ' ';
inorder(root->left);
inorder(root->right);
}
递归前序遍历的步骤:
-
如果根节点为 null,直接返回。
-
否则,先打印当前根节点的值。
-
递归调用前序遍历函数,遍历左子树。
-
递归调用前序遍历函数,遍历右子树。
这样实现的前序遍历顺序就是:根结点 -> 左子树 -> 右子树。
递归的思想是:每个子问题(子树)都跟原问题(整颗树)一样,都可以使用同样的前序遍历方式来解决。
所以通过递归来实现前序遍历可以很简单地访问每一个节点,同时还省去了使用外部数据结构如栈来模拟递归调用栈的步骤。它利用函数调用栈本身就实现了前序遍历的逻辑。
非递归的前序遍历:
void preorder2(TreeNode *root)
{
stack<TreeNode*> s;
s.push(root);
while (!s.empty())
{
TreeNode *t = s.top();
s.pop();
cout << t->val << ' ';
if (t->right) s.push(t->right);
if (t->left) s.push(t->left);
}
}
前序遍历的非递归实现步骤:
-
将根节点压入栈
-
从栈pop出一个节点node
-
输出node的值
-
将node的右子节点(如果有)Push到栈
-
将node的左子节点(如果有)Push到栈
-
重复2-5步,直到栈为空
非递归实现的关键是用栈按照前序遍历顺序保存节点,并按后进先出的顺序访问这些节点。
每次从栈中pop出的节点即为当前遍历的节点,这样可以模拟调用栈的行为来实现前序遍历,从而避免使用递归。
这与递归实现的区别是,非递归使用了外部的数据结构栈来保存节点的遍历顺序,而递归直接利用函数调用栈来实现。
递归中序遍历:
void inorder(TreeNode *root)
{
if(!root) return;
inorder(root->left);
cout << root->val << ' ';
inorder(root->right);
}
中序遍历的实现步骤:
1.如果根节点为 null,直接返回。
2.否则递归调用 inorder,遍历左子树。
3.输出当前根节点的值。
4.递归调用 inorder,遍历右子树。
这样的顺序就实现了:左子树 -> 根节点 -> 右子树,即中序遍历。
与前序遍历不同的是,中序遍历先递归处理左子树,然后输出根节点,再处理右子树。
同样使用递归可以省去外部数据结构,直接利用函数调用栈来保存遍历顺序。每个子问题都可以通过中序遍历的逻辑来解决。
这样一步步深入左子树,然后输出节点,再深入右子树,就完成了整个二叉树的中序遍历。
非递归中序遍历:
void inorder2(TreeNode* root)
{
stack<TreeNode*> stk;
TreeNode* curr = root;
while(curr || !stk.empty())
{
// 把当前节点的左子节点全部压入栈
while(curr)
{
stk.push(curr);
curr = curr->left;
}
// 从栈中弹出一个节点计算
curr = stk.top();
stk.pop();
// 输出节点
cout << curr->val << " ";
// 让当前节点指向右子节点
curr = curr->right;
}
}
实现步骤:
-
初始化当前节点curr为根节点,栈stk为空。
-
如果curr节点不为空,或者栈不为空,进入循环。
-
如果curr不为空,遍历它的左子树,将节点全部压入栈。
-
当前节点为栈顶元素,出栈输出。
-
更新当前节点为该节点的右子节点。
-
重复2-5步,直到栈和curr都为空,遍历结束。
通过栈保存遍历顺序,先把左子树全部入栈,出栈时从左到右依次处理节点,实现了中序遍历的迭代逻辑。
利用栈模拟递归调用栈,可以非递归地完成二叉树的中序遍历。
递归后序遍历:
void postorder(TreeNode *root)
{
if(!root) return;
postorder(root->left);
postorder(root->right);
cout << root->val << ' ';
}
后序遍历的实现步骤:
-
如果根节点为null,直接返回
-
否则递归调用postorder函数,遍历左子树
-
递归调用postorder函数,遍历右子树
-
输出当前根节点的值
这样的顺序实现了:左子树 -> 右子树 -> 根节点,即后序遍历。
与前序和中序不同的是,后序遍历先递归处理左右子树,最后输出根节点。
和前两种遍历一样,递归方式直接利用函数调用栈,可以省去使用外部数据结构,以递归逻辑实现后序遍历。
一个个地深入左子树,然后深入右子树,最后输出根节点,就完成了整颗二叉树的后序遍历。
后序非递归遍历:
void postorder2(TreeNode* root)
{
stack<TreeNode*> stk;
TreeNode* curr = root;
TreeNode* prev = NULL;
while(curr || !stk.empty())
{
// 将当前节点及其左节点全部入栈
while(curr)
{
stk.push(curr);
curr = curr->left;
}
// 如果栈顶没有右子节点或右子节点已经访问完
curr = stk.top();
if(!curr->right || curr->right == prev)
{
stk.pop();
cout << curr->val << " ";
prev = curr;
curr = nullptr;
}
else // 有右子节点且还未访问
curr = curr->right;
}
}
实现思路:
-
将当前节点入栈,并遍历其左子树入栈
-
如果栈顶节点没有右子节点或右子节点访问完毕,则输出该节点
-
否则将当前节点指向右子节点
利用两个指针curr和prev,和一个标记是否访问过右子节点,可以完成后序遍历的迭代过程。
与递归中顺序相反,但依然可以输出后序序列。
层次遍历:
void levelOrder(TreeNode *root)
{
queue<TreeNode*> q;
q.push(root);
while (!q.empty())
{
TreeNode *t = q.front();
q.pop();
cout << t->val << ' ';
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
}
算法流程:
-
使用STL中的queue来实现队列的数据结构
-
首先将根节点入队
-
循环从队首取出节点
3.1 打印当前节点的值
3.2 如果左节点不为空,入队左节点
3.3 如果右节点不为空,入队右节点
-
重复3直到队列为空,这样一个层次一个层次地把整个二叉树遍历完即实现层次遍历。
关键点是利用队列的先入先出特性,可以按照各层的顺序依次处理节点,从而实现层次遍历。
王道课后题
#include <iostream>
#include <queue>
#include <stack>
using namespace std;
struct TreeNode
{
char val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(char x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(char x, TreeNode *left, TreeNode *right) : val(x),left(left), right(right) {}
};
void invertLevelOrder(TreeNode *root)
{
queue<TreeNode*> q;
stack<TreeNode*> s;
q.push(root);
while (!q.empty())
{
TreeNode *t = q.front();
q.pop();
s.push(t);
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
while (s.size())
{
cout << s.top()->val << ' ';
s.pop();
}
}
int main()
{
TreeNode *root = new TreeNode('A',
new TreeNode('B',
new TreeNode('D'),
new TreeNode('E')),
new TreeNode('C'));
invertLevelOrder(root);
return 0;
}
输出:E D C B A
构建的树的形状如下图所示:
// 递归求树的高度
int height(TreeNode *root)
{
if (!root) return 0;
return max(height(root->left), height(root->right)) + 1;
}
// 非递归求树的高度
int height2(TreeNode *root)
{
queue<TreeNode*> q;
int height = 0;
q.push(root);
while (q.size())
{
int n = q.size(); // 当前层的结点个数
while (n --)
{
TreeNode *t = q.front();
q.pop();
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
height ++;
}
return height;
}
算法步骤:
- 使用队列queue来层序遍历二叉树。
- 将根节点入队。
- 每遍历一层,计算当前层节点数量n,进行n次出队操作。
- 每出队一次,如果节点有左子节点,入队左子节点。如果节点有右子节点,入队右子节点。
- 每层遍历结束,height++。
- 循环结束时,height即为二叉树的高度。
关键点是使用队列实现层序遍历,每层节点入队出队,可以非递归实现二叉树的高度计算。时间复杂度O(n),空间复杂度O(n)。
该算法还可用来求树的最大宽度
,每层节点个数
。
二叉树按二叉链表形式存储,写一个判别给定二叉树是否是完全二叉树的算法。
完全二叉树的定义是:除了最后一层外,其他层的节点都必须是满的,并且最后一层的节点都靠左排列。
为了判断给定的二叉树是否是完全二叉树,我们可以使用层次遍历的方式来进行判断。
bool isCompleteTree(TreeNode *root)
{
queue<TreeNode*> q;
q.push(root);
bool leaf = false; // 是否遇到叶子节点
while (q.size())
{
TreeNode *t = q.front();
q.pop();
// 若之前已遇到叶节点,且当前节点还有子节点,返回false
if (leaf && (t->left || t->right)) return false;
// 若遇到叶节点
if (!t->left && !t->right) leaf = true;
// 若左子树为空且右子树不空
if (!t->left && t->right) return false;
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
return true;
}
算法思路:
- 使用队列层次遍历二叉树
- 用一个标记leaf记录是否遇到过叶子节点
- 如果已经遇到叶子节点,则后续所有节点都应该是叶子节点, 否则返回false
- 如果遇到左子树为空且右子树不空,返回false
- 遍历完仍然返回true,则表明满足完全二叉树性质
时间复杂度O(n),空间复杂度O(n)。
完整代码:
#include <iostream>
#include <queue>
#include <stack>
using namespace std;
struct TreeNode
{
char val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(char x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(char x, TreeNode *left, TreeNode *right) : val(x),left(left), right(right) {}
};
bool isCompleteTree(TreeNode *root)
{
queue<TreeNode*> q;
q.push(root);
bool leaf = false; // 是否遇到叶子节点
while (q.size())
{
TreeNode *t = q.front();
q.pop();
// 若之前已遇到叶节点,且当前节点还有子节点,返回false
if (leaf && (t->left || t->right)) return false;
// 若遇到叶节点
if (!t->left && !t->right) leaf = true;
// 若左子树不空且右子树空
if (!t->left && t->right) return false;
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
return true;
}
int main()
{
TreeNode *root = new TreeNode('A',
new TreeNode('B',
new TreeNode('D'),
new TreeNode('E')
),
new TreeNode('C'));
cout << isCompleteTree(root) << endl;
return 0;
}
#include <iostream>
#include <queue>
#include <stack>
using namespace std;
struct TreeNode
{
char val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(char x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(char x, TreeNode *left, TreeNode *right) : val(x),left(left), right(right) {}
};
int res = 0;
void dfs(TreeNode *root)
{
if (!root) return;
if (root->left && root->right) res ++;
dfs(root->left);
dfs(root->right);
}
// 第二种做法,类似王道参考答案
int get(TreeNode *root)
{
if (!root) return 0;
if (root->left && root->right)
return get(root->left) + get(root->right) + 1;
return get(root->left) + get(root->right);
}
int main()
{
TreeNode *root = new TreeNode('A',
new TreeNode('B',
new TreeNode('D'),
new TreeNode('E')
),
new TreeNode('C'));
dfs(root);
cout << res << endl;
cout << get(root) << endl;
return 0;
}
设树B是一棵采用链式结构存储的二叉树,编写一个把树B中所有结点的左、右子树进行交换的函数。
void swapLeftRight(TreeNode* root) {
if(root == nullptr) return;
swap(root->left, root->right);
swapLeftRight(root->left);
swapLeftRight(root->right);
}
函数做以下操作:
-
递归地遍历整棵树。
-
对每个结点,暂时保存left子节点指针,然后让right子节点作为新的left子节点,原left子节点作为新的right子节点。
-
这样就实现了这个结点的左右子树互换。
-
之后递归调用这个函数,继续操作左右子节点,从而完成整棵树所有结点的左右子树交换。
关键点是:
- 递归遍历所有结点
- 对每个结点使用第三个变量临时存储left,实现left和right的交换
- 交换后递归处理交换后的左右子树
这样就可以在O(N)时间内完成整棵树结点左右子树的交换,而不需要额外的空间。
假设二叉树采用二叉链存储结构存储,设计一个算法,求先序遍历序列中第 k(1≤k≤二叉树中结点个数)个结点的值。
#include <iostream>
#include <queue>
#include <stack>
using namespace std;
struct TreeNode
{
char val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(char x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(char x, TreeNode *left, TreeNode *right) : val(x),left(left), right(right) {}
};
// 求先序遍历序列中第k个结点的值
int res = 0, k = 4, i = 0;
void dfs(TreeNode* root)
{
i ++;
if (i == k) res = root->val;
if (root->left) dfs(root->left);
if (root->right) dfs(root->right);
}
int main()
{
TreeNode *root = new TreeNode('A',
new TreeNode('B',
new TreeNode('D'),
new TreeNode('E')
),
new TreeNode('C'));
dfs(root);
cout << (char)res << endl; // E
return 0;
}
已知二叉树以二叉链表存储,编写算法完成:对于树中每个元素值为x的结点,删去以它为根的子树
dfs和bfs都可以做,这里给出dfs的做法
void dfs(TreeNode *root, char x)
{
if (!root) return;
if (root->val == x) root = NULL;
else
{
cout << root->val << ' ';
dfs(root->left, x);
dfs(root->right, x);
}
}
// 返回根结点到p的路径
vector<TreeNode *> findPath(TreeNode *root, TreeNode *p)
{
vector<TreeNode*> s;
TreeNode *cur = root, *pre = NULL;
while (cur || s.size())
{
while (cur)
{
s.push_back(cur);
cur = cur->left;
}
cur = s.back();
if (cur->right && cur->right != pre)
cur = cur->right;
else
{
s.pop_back();
if (cur == p)
return s;
pre = cur;
cur = NULL;
}
}
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
vector<TreeNode *> path1 = findPath(root, p);
vector<TreeNode *> path2 = findPath(root, q);
for (int i = 0; ; i ++)
if (path1[i] != path2[i])
return path1[i - 1];
return NULL;
}