1、理论知识
(1)、满二叉树
如果一棵二叉树只有度为0的节点和度为2的节点,并且度为0的节点在同一层上,则这棵二叉树为满二叉树。
(2)、完全二叉树
除了底层节点可能没有填满,其余每层的节点数都达到了最大值,并且底层的节点都集中在该层最左边的若干位置。
(3)、二叉搜索树
前面介绍的二叉树都没有数值,而二叉搜索树是有数值的。二叉搜索树是一个有序树,满足如下规则:
一、若它的左子树不为空,则左子树上所有节点的值都小于它的根节点的值。
二、弱它的右子树不为空,则右子树上所有节点的值都大于它的根节点的值。
三、它的左、右子树也分别为二叉排序树。
(4)、平衡二叉搜索树
平衡二叉搜索树又称为AVL树,它是一棵空树,或者它的左右两个字树的高度差的绝对值不超过1,并且两个字树都是一棵平衡二叉树。
2、二叉树的遍历
深度优先:递归、迭代;
广度优先:迭代;
编程语言都是通过栈这种数据结构实现递归的,也就是说,前序、中序、后序遍历都可以通过栈使用非递归的方式实现。
而广度优先遍历一般使用队列实现,这也是由队列先进先出的特点决定的,因为通过先进先出的结构,才能一层一层地遍历二叉树。
递归法实现二叉树的遍历:
#include
using namespace std;
typedef struct _tag_BitNode {
int data;
struct _tag_BitNode* lchild;
struct _tag_BitNode* rchild;
_tag_BitNode(int val) {
data = val;
lchild = nullptr;
rchild = nullptr;
}
}BitNode, *BitNodePtr;
void PrevOrder(BitNode* root) {
if (root == nullptr) {
return;
}
cout << "the node data is :" << root->data << endl;
PrevOrder(root->lchild);
PrevOrder(root->rchild);
}
void InOrder(BitNode* root) {
if (root == nullptr) {
return;
}
InOrder(root->lchild);
cout << "the node data is :" << root->data << endl;
InOrder(root->rchild);
}
void PostOrder(BitNode* root) {
if (root == nullptr) {
return;
}
PostOrder(root->lchild);
PostOrder(root->rchild);
cout << "the node data is :" << root->data << endl;
}
void main() {
BitNode node1(1);
BitNode node2(2);
BitNode node3(3);
BitNode node4(4);
BitNode node5(5);
node1.lchild = &node2;
node1.rchild = &node3;
node3.lchild = &node4;
node3.rchild = &node5;
PrevOrder(&node1);
InOrder(&node1);
PostOrder(&node1);
cout << "hello world" << endl;
}
3、前、中、后序的迭代遍历
为什么可以用迭代法(非递归的方式)实现二叉树的前中后序遍历呢?
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回值等都压入调用栈,然后在结束本层递归操作的时候,从栈顶弹出上一次递归的各项参数,这是也是为什么递归可以返回上一次位置的原因。
3.1前序遍历
leetcode144:二叉树的前序遍历
前序遍历是中>左>右,每次先处理中间的节点,先将根节点入栈,然后将右孩子加入栈,最后再将左孩子入栈。
为什么先将右孩子入栈,再将左孩子入栈呢?因为这样的顺序才是中->左->右。
void PreOrderIter(BitNode* root) {
stack st;
if (root == nullptr) {
return;
}
st.push(root);
while (!st.empty()) {
BitNode* temp_node = st.top();
st.pop();
cout << "the node data is :" << temp_node->data << endl;
if (temp_node->rchild != nullptr) {
st.push(temp_node->rchild);
}
if (temp_node->lchild != nullptr) {
st.push(temp_node->lchild);
}
}
}
此时会发现使用迭代法写出前序遍历的代码并不难。是不是修改前序遍历代码的顺序就可以实现中序遍历了呢?
接下来使用迭代法写中序遍历的代码的时候,会发现套路又不一样了,目前的前序遍历逻辑无法直接应用到中序遍历上。
3.2中序遍历
leetcode: 94二叉树的中序遍历。
在使用迭代法处理元素的过程中,涉及以下两个操作。
处理:将元素放入result数组。
访问:遍历节点。
为什么3.1节中的前序遍历的代码不能喝中序遍历的代码通用呢?因为前序遍历的顺序是中->左->右,先访问和处理的元素是中间节点,所以才能写出相对简洁的代码:要访问的元素和处理的元素顺序是一致的,都是中间节点。
中序遍历的顺序是左->中->右,先访问的是二叉树顶部的节点,然后一层层向下访问,直到到达树左面的底部,再开始处理节点(也就是把节点的数值放入result数组),这就造成了处理顺序和访问顺序是不一致的。
在使用迭代法实现中序遍历时,就需要借用指针的遍历来访问节点,使用栈处理节点上的元素。
中序遍历的代码如下:
//借用指针的遍历来访问节点,使用栈来处理节点上的元素
void InOrderIter(BitNode* root) {
stack st;
BitNode* cur = root;
if (root == nullptr) {
return;
}
while (cur != nullptr || !st.empty()) {
if (cur != nullptr) {
st.push(cur);
cur = cur->lchild;
}
else {
cur = st.top();
cout << "the node data is :" << cur->data << endl;
st.pop();
cur = cur->rchild;
}
}
}
3.3 后序遍历
leetcode145:二叉树的后序遍历
后序遍历的顺序是左->右->中,只需要调整前序遍历的代码顺序,变成中->右->左的遍历顺序,然后反转输出数组。
vector PostOrderIter(BitNode* root) {
vector vec;
stackst;
if (root == nullptr) {
return vec;
}
st.push(root);
while (!st.empty()) {
BitNode* node = st.top();
vec.push_back(node->data);
st.pop();
if (node->lchild != nullptr) {
st.push(node->lchild);
}
if (node->rchild != nullptr) {
st.push(node->rchild);
}
}
reverse(vec.begin(), vec.end());
return vec;
}
我们使用跌代法写出了二叉树的前、中、后序遍历的代码,可以看出前序遍历和中序遍历完全是两种风格的代码,并不像递归写法那样代码稍作调整就可以实现前、中、后序遍历。
这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放入result数组)可以同步处理,但是中序遍历就无法做到同步。
难道二叉树前、中、后序遍历的迭代法不能统一代码风格吗(即前序遍历的代码改变顺序就可以实现中序遍历和后序遍历)?
当然可以,这种写法在下一节会重点讲解。
4、前、中、后序统一迭代法。
我们在3.2节中使用递归的方式实现了二叉树前、中、后序遍历。在3.3节当中使用栈实现了二叉树的前、中、后序遍历(非递归)。之后发现迭代法实现的前、中、后序遍历的代码风格不统一·,除了前序遍历和后序遍历有关联,中序遍历完全就是另一个风格了,一会儿用栈遍历,一会儿用指针遍历。
针对三种遍历方式,使用迭代法是可以写成统一风格的代码的。
以中序遍历为例,在3.3节中提到的无法使用栈无法同时解决访问节点(遍历节点)和处理节点(将元素放入结果集)不一致的问题。
解决方案是将要访问的节点放入栈,将要处理的节点也放入栈中但是要做好标记,即将要处理的节点放入栈之后,紧接着放入一个空指针作为标记,这种方法也可以叫作标记法。
(1)、使用迭代法实现中序遍历
vector InOrderTraversal(BitNode* root) {
vector result;
stack st;
if (root == nullptr) {
return vector();
}
st.push(root);
while (!st.empty()) {
BitNode* node = st.top();
if (node != nullptr) {
st.pop();
if (node->rchild != nullptr) {
st.push(node->rchild);
}
st.push(node);
st.push(nullptr);
if (node->lchild != nullptr) {
st.push(node->lchild);
}
}
else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->data);
}
}
return result;
}
(2)、使用迭代法实现前序遍历
vector PreOrderTraversal(BitNode* root) {
vector result;
stack st;
if (root == nullptr) {
return vector();
}
st.push(root);
while (!st.empty()) {
BitNode* node = st.top();
if (node != nullptr) {
st.pop();
if (node->rchild != nullptr) {
st.push(node->rchild);
}
if (node->lchild != nullptr) {
st.push(node->lchild);
}
st.push(node);
st.push(nullptr);
}
else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->data);
}
}
return result;
}
(3)、使用迭代法实现后序遍历
vector PostOrderTraversal(BitNode* root) {
if (root == nullptr) {
return vector();
}
vector result;
stack st;
st.push(root);
while (!st.empty()) {
BitNode* node = st.top();
if (node != nullptr) {
st.pop();
st.push(node);
st.push(nullptr);
if (node->rchild != nullptr) {
st.push(node->rchild);
}
if (node->lchild != nullptr) {
st.push(node->lchild);
}
}
else {
st.pop();
node = st.top();
result.push_back(node->data);
st.pop();
}
}
return result;
}
此时我们写出了统一风格的迭代法代码,但统一风格的迭代法的代码并不好理解,而且在面试中直接写出来还是有难度的。所以读者可以根据自己的个人喜好,对于二叉树的前、中、后序遍历,选择使用一种自己容易理解的递归和迭代法。
5、二叉树的层次遍历。广度优先遍历
leetcode102:二叉树的层序遍历。层序遍历就是从左到右一层一层地遍历二叉树。
层序遍历需要借助一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而使用栈先进后出适合模拟深度优先遍历,也就是递归的逻辑。
vector> LevelOrder(BitNode* root) {
if (root == nullptr) {
return vector>();
}
vector> result;
queue que;
que.push(root);
while (!que.empty()) {
int size = que.size();
vector ret;
for (int i = 0; i < size; i++) {
BitNode* node = que.front();
ret.push_back(node->data);
que.pop();
if (node->lchild != nullptr) {
que.push(node->lchild);
}
if (node->rchild != nullptr) {
que.push(node->rchild);
}
}
result.push_back(ret);
}
return result;
}
6、反转二叉树
leetcode226:反转二叉树 反转一颗二叉树。
如果想反转二叉树,那么把每个节点的左右孩子交换一下即可。
关键在于遍历顺序,应该选择哪一种遍历顺序呢?
遍历的过程中反转每个节点的左右孩子就可以达到整体反转的效果。注意,只要把每一个节点的左右孩子反转一下,就可以达到整体反转的效果。
这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子反转两次。
那么可不可以使用层序遍历呢?依然可以,只要把每个节点的左右孩子反转一下的遍历方式都是可以的。
(1)、递归法
BitNode* InvertTree(BitNode* root) {
if (root == nullptr) {
return root;
}
swap(root->lchild, root->rchild);
InvertTree(root->lchild);
InvertTree(root->rchild);
return root;
}
(2)、迭代法
BitNode* InverTreeIter(BitNode* root) {
if (root == nullptr) {
return root;
}
stackst;
st.push(root);
while (!st.empty()) {
BitNode* node = st.top();
st.pop();
swap(node->lchild, node->rchild);
if (node->rchild != nullptr) {
st.push(node->rchild);
}
if (node->lchild != nullptr) {
st.push(node->lchild);
}
}
return root;
}
BitNode* InverTreeIterLevel(BitNode* root) {
if (root == nullptr) {
return nullptr;
}
queue que;
que.push(root);
while (!que.empty()) {
int size = que.size();
for (int i = 0; i < size; i++) {
BitNode* node = que.front();
que.pop();
swap(node->lchild, node->rchild);
if (node->lchild) {
que.push(node->lchild);
}
if (node->rchild) {
que.push(node->rchild);
}
}
}
return root;
}
针对二叉树的问题,解题之前一定要想清楚究竟使用前、中、后序遍历,还是层序遍历。二叉树解题的大忌就是自己稀里糊涂就把代码写出来了(因为这道题相对简单),但不清楚是如何遍历二叉树的。
针对反转二叉树,本节给出了一种递归、两种迭代(一种是模拟深度优先遍历,另一种是层序遍历)的写法,读者也可以有自己的解法,但一定要形成方法论,这样才能举一反三。
7、对称二叉树
leetcode:101 给出一个二叉树,判断其是不是中心轴对称的。
首先要想清楚,判断二叉树是否对称要比较的是哪两个节点,要比较的可不是左右节点。
判断二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互反转的,理解这一点就知道了其实要比较的是两棵树(这两课树是根节点的左右子树),所以在递归遍历的过程中,也需要同时遍历这两棵树。
那么如何比较呢?
要比较的是两棵子树的里侧和外侧是否相等。
遍历的顺序应该是怎样的呢?
本题只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两棵字数的内侧节点和外侧节点是否相等。因为要遍历两课树,而且要比较内侧和外侧节点是否相等,所以准确地说,一棵树的遍历顺序是左->右->中,另一棵树的遍历顺序是右->左->中。
两个遍历顺序都可以理解为后序遍历,尽管已经不是严格意义上的在一棵树上进行的后序遍历。
(1)、递归法
要比较两个节点的数值是否相同,首先要确定两个节点是否为空,否则后面数值的时候就会操作空指针了。
节点为空的情况:
左节点为空,右节点不为空->不对称,则返回false。
左节点不为空,右节点为空->不对称,则返回false。
左、右节点都为空->对称,返回true。
此时已经排除了节点为空的情况,那么剩下的就是左右节点不为空的情况。
左右节点都不为空->比较节点数值,不相同就返回false。
确定单层递归逻辑。
单层递归的逻辑就是处理左右节点都不为空且数值相同的情况:
比较二叉树外侧是否对称;传入的是左节点的左孩子和右节点的右孩子。
比较二叉树内侧是否对称;传入的是左节点的右孩子和右节点的左孩子。
如果二叉树内侧对称、外侧也对称,就返回true,有一侧不对称就返回false。
ypedef struct _tag_BitNode {
int data;
struct _tag_BitNode* lchild;
struct _tag_BitNode* rchild;
_tag_BitNode(int val) {
data = val;
lchild = nullptr;
rchild = nullptr;
}
}BitNode, *BitNodePtr;
bool BitTreeCompare(BitNode* left, BitNode* right) {
if (left != nullptr && right == nullptr) {
return false;
}
else if (left == nullptr && right != nullptr) {
return false;
}
else if (left == nullptr && right == nullptr) {
return true;
}
else if(left->data != right->data){
return false;
}
bool outside = BitTreeCompare(left->lchild, right->rchild);
bool inside = BitTreeCompare(left->rchild, right->lchild);
return outside && inside;
}
2、迭代法。
这道题目也可以使用迭代法,但要注意,这里的迭代法可不是前、中、后序的迭代写法,因为本题的本质是判断两棵树是否相互反转,已经不是所谓的二叉树遍历的前、中、后序的关系了。
我们可以使用队列比较两棵树(根节点的左右字树)是否相互反转(注意,这里不是层序遍历)
使用对列:
bool BitTreeCompareIterQue(BitNode* root) {
if (root == nullptr) {
return false;
}
queue que;
que.push(root->lchild);
que.push(root->rchild);
while (!que.empty()) {
BitNode* left = que.front();
que.pop();
BitNode* right = que.front();
que.pop();
if (left == nullptr && right == nullptr) {
continue;
}
if (left == nullptr || right == nullptr || (left->data != right->data)) {
return false;
}
que.push(left->lchild);
que.push(right->rchild);
que.push(left->rchild);
que.push(right->lchild);
}
return true;
}
使用栈:
细心的读者可能发现,这种迭代法实现其实是把左右两棵字树要比较的元素按照一定的顺序放进一个容器,然后成对地取出来进行比较,那么使用栈也是可以的。
bool BitTreeCompareLevel(BitNode* root) {
if (root == nullptr) {
return false;
}
stack st;
st.push(root->lchild);
st.push(root->rchild);
while (st.empty()) {
BitNode* left = st.top();
st.pop();
BitNode* right = st.top();
st.pop();
if (left == nullptr && right == nullptr) {
continue;
}
if (left == nullptr || right == nullptr || (left->data != right->data)) {
return false;
}
st.push(left->rchild);
st.push(right->lchild);
st.push(left->rchild);
st.push(right->rchild);
}
return true;
}