文章目录
二叉树的基本概念
二叉树(binary tree)是 n ( n > = 0 ) n(n>=0) n(n>=0) 个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点以及两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。每个根节点最多只有两个子节点的就叫做二叉树。
二叉树的特点
- 每个结点最多有两棵子树,所以二叉树中不存在度大于 2 的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
完全二叉树
对于一棵具有 n 个节点的二叉树按照层序编号,如果编号为 i ( 1 < = i < = n ) i(1<=i<=n) i(1<=i<=n) 的节点与同样深度的满二叉树中编号为 i 的节点在二叉树中位置完全相同,则这棵二叉树称为”完全二叉树“。如下图:
把完全二叉树的节点与同样深度的满二叉树各个节点进行编号比较,所有节点出现的位置相同,则是完全二叉树。所以,满二叉树一定是一棵完全二叉树,而完全二叉树不一定是满二叉树。
完全二叉树的深度: h = [ l o g 2 ( n + 1 ) ] + 1 h=[log_2(n+1)]+1 h=[log2(n+1)]+1 ([ ]表示向下取整)
完全二叉树的特点:
- 叶子节点只能出现在最下两层。
- 最下层的叶子一定集中左部连续位置。
- 倒数第二层,若有叶子节点,一定都在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即不存在右子树的情况。
- 同样节点的二叉树,完全二叉树的深度最小。
二叉树的存储结构
1、二叉树顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,井且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系。顺序存储一般只用于完全二叉树。
2、二叉链表
二叉树每个结点最多有两个孩子,所以为官设计一个数据域和两个指针域,我们称这样的链表叫做二叉链表。
typedef struct BiTNode
{
TElemType data; //结点数据
struct BiTNode *lchild, *rchild; //左右孩子结点
} BiTNode, *BiTree;
二叉树的遍历
二叉树的遍历(traversing binary tree)是指从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
递归实现:
void PreOrderTraverse(BiTree T)
{
if (T == NULL)
return;
cout << T->val;
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchile);
}
非递归实现:
使用栈来实现前序遍历,首先将根节点压入栈中,然后打印栈顶元素。入栈和出栈的顺序是相反的,所以我们先将根节点的右子节点压入栈中,然后将左子节点也压入栈中,打印栈顶元素。操作完元素后让栈顶元素出栈,在压入该元素节点的右、左子节点,以此操作即可得出二叉树的前序遍历元素。
void PreOrderTraverse(BiTree T)
{
if (T == NULL)
return;
stack<BiTree*> bi_tree;
bi_tree.push(T->data); // 先将根节点压入栈中
while (!bi_tree.empty()) {
BiTree* top = bi_tree.top();
cout << top->val; // 对栈顶元素操作
bi_tree.pop(); // 栈顶元素出栈
if (top->rchild != NULL) // 压入该元素节点的右子节点
bi_tree.push(top->rchild);
if (top->lchild != NULL) // 压入该元素节点的左子节点
bi_tree.push(top->lchild);
}
}
中序遍历
若树为空,则空操作返回。否则从根节点开始(并不是先访问根节点),中序遍历根节点的左子树,然后访问根节点,最后中序遍历右子树。
递归实现:
void InOrderTraverse(BiTree T)
{
if (T == NULL)
return;
InOrderTraverse(T->lchild);
cout << T->val;
InOrderTraverse(T->rchild);
}
非递归实现:
使用栈来实现二叉树的中序遍历,先将根节点压入栈中,然后一直遍历左子节点并压入栈中。完成之后将栈顶元素节点出栈并操作,再将出栈的这个节点的右子节点压入栈中,重复上述操作。
void InOrderTraverse(TreeNode* root)
{
if (root == nullptr)
return;
stack<TreeNode*> bi_tree;
while (!bi_tree.empty() || root != nullptr) {
while (root != nullptr) { // 遍历左子节点并压入栈中
bi_tree.push(root);
root = root->left;
}
root = bi_tree.top(); // 栈顶元素出栈
bi_tree.pop();
cout << root->val; // 操作元素
root = root->right; // 将右子节点压入栈中
}
}
后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
递归实现:
void PostOrderTraverse(BiTree T)
{
if (T == NULL)
return;
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
cout << T->val;
}
非递归实现:
使用两个栈来实现二叉树的后序遍历,我们很容易得到一种遍历方式:“根节点 -> 右子节点 -> 左子节点”,这种遍历方式是后序遍历的反序。根据栈 LIFO 的特性就可以实现后序遍历。
因此我们使用一个辅助栈来保存父节点,使用另一个栈保存我们需要的遍历结果的反序。
void postOrderTraverse(TreeNode* root)
{
if (root == nullptr)
return;
stack<TreeNode*> res;
stack<TreeNode*> tmp;
tmp.push(root);
while (!tmp.empty()) {
TreeNode* top = tmp.top();
tmp.pop();
if (top->left != nullptr)
tmp.push(top->left);
if (top->right != nullptr)
tmp.push(top->right);
res.push(top);
}
while (!res.empty()) {
top = res.top();
cout << top->val;
res.pop();
}
}
层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。即对二叉树的广度优先搜索(BFS)。
对于层序遍历,我们可以借助队列 queue 容器来实现,首先压入根节点,每次遍历我们用队列首元素遍历其左子节点并入队,然后遍历其右子节点并入队,最后将队首元素打印并出队,一次迭代直到队列为空。
void levelOrderTraverse(TreeNode* root)
{
queue<TreeNode*> level_node;
if (root == nullptr)
return;
level_node.push(root);
while (!level_node.empty()) {
TreeNode* front = level_node.front();
if (front->left != nullptr)
level_node.push(front->left);
if (front->right != nullptr)
level_node.push(front->right);
cout << front->val;
level_node.pop();
}
cout << endl;
}
二叉树的相关题目
1. 重建二叉树
题目描述:输入二叉树的前序遍历和中序遍历结果,重建二叉树。
算法分析:如前序遍历序列 {1, 2, 4, 7, 3, 5, 6, 8} 和中序遍历序列 {4, 7, 2, 1, 5, 3, 8, 6},根据前序遍历特点,二叉树根节点为 1,在中序遍历序列中找到根节点 1,则根节点的前三个节点为左子树节点,后面的节点为右子树节点。根据中序遍历得到的结果,在前序遍历序列中根节点 1 后面的三个节点即为左子树节点。
BinaryTreeNode* Construct(int* preorder, int* inorder, int length)
{
if (preorder == nullptr || inorder == nullptr || length <= 0)
return nullptr;
return ConstructCore(preorder, preorder + length - 1,
inorder, inorder + length - 1);
}
BinaryTreeNode* ConstructCore(int* stratPreorder, int* endPreorder
int* startInorder, int* endInorder)
{
// 前序遍历第一个数字就是根节点的值,初始化一个二叉树
int rootValue = startPreorder;
BinaryTreeNode* root = new BinaryTreeNode();
root->m_value = rootValue;
root->m_pLeft = root->m_pRight = nullptr;
if (startPreorder == endPreorder) {
if (startInorder == endInorder &&
*startPreorder == *startInorder)
return root;
else
throw std::exception("Invalid input.");
}
// 在中序遍历序列中找到根节点的值
int* rootInorder = startInorder;
while (rootInorder <= endInorder && *rootInorder != rootValue)
++rootInorder;
if (rootInorder == endInorder && *rootInorder != rootValue)
throw std::exception("Invalid input.");
int leftLength = rootInorder - startInorder;
int *leftPreorderEnd = startPreorder + leftLength;
if (leftLength > 0) {
// 构建左子树
root->m_pLeft = ConstructCore(startPreorder + 1,
leftPreorderEnd, startInorder, rootInorder - 1);
}
if (leftLength < endPreorder - startPreorder) {
// 构建右子树
root->m_pRight = ConstructCore(leftPreorderEnd + 1,
endPreorder, rootInorder + 1, endInorder);
}
return root;
}
2. 二叉树的下一个节点
题目描述:给定一个二叉树和其中的一个节点,找到中序遍历序列的下一个节点。树中节点有两个指向左右子节点的指针,和一个指向父节点的指针。
算法分析:分为三种情况:1)一个节点如果有右子树,则下一个节点就是它的右子树中最左子节点;2)节点没有右子树,且它是父节点的左子节点,则下一个节点就是它的父节点;3)节点没有右子树,且是父节点的右子节点,这种情况比较复杂,我们需要沿指向它父节点的指针一直向上遍历,直到找到一个节点是其父节点的左子节点,则这个节点的父节点就是下一个节点。
BinaryTreeNode* GetNext(BinaryTreeNext* pNode)
{
if (pNode == nullptr)
return nullptr;
BinaryTreeNode* pNext = nullptr;
// 情况1
if (pNode->m_right != nullptr) {
BinaryTreeNode* pRight = pNode->m_right;
while (pRight->m_left != nullptr)
pRight = pRight->m_left;
pNext = pRight;
}
// 情况2和3
else if(pNode->m_parent != nullptr) {
BinartTreeNode* pCurrent = pNode;
BinaryTreeNode* pParent = pNode->m_parent;
while (pParent != nullptr && pCurrent == pParent->m_right) {
pCurrent = pParent;
pParent = pParent->m_parent;
}
pNext = pParent;
}
return pNext;
}
3. 二叉树的子结构
题目描述:输入两颗二叉树 A 和 B,判断 B 是不是 A 的子结构。
算法分析:查找树 A 中是否有和树 B 一样的子结构,可以分为两步:1)在树 A 中找到和树 B 根节点值一样的节点 R;2)判断 A 中以 R 为根节点的子树是不是包含和树 B 一样的结构。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
bool result = false;
if (A != nullptr && B != nullptr) {
if (Equal(A->val, B->val))
result = DoesAHaveB(A, B);
if (!result)
result = isSubStructure(A->left, B);
if (!result)
result = isSubStructure(A->right, B);
}
return result;
}
bool DoesAHaveB(TreeNode* A, TreeNode* B) {
if (B == nullptr)
return true;
if (A == nullptr)
return false;
if (!Equal(A->val, B->val))
return false;
return DoesAHaveB(A->left, B->left)
&& DoesAHaveB(A->right, B->right);
}
bool Equal(int num1, int num2) {
return num1 == num2 ? true : false;
}
};
4. 二叉树的镜像
题目描述:完成一个函数,输出一棵二叉树的镜像。
算法分析:前序遍历二叉树,如果遍历的节点有子节点,就交换两个子节点的位置。
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if (root == nullptr)
return nullptr;
if (root->left == nullptr && root->right)
return nullptr;
TreeNode* pNode = root->left;
root->left = root->right;
root->right = pNode;
if (root->left != nullptr)
root->left = mirrorTree(root->left);
if (root->right != nullptr)
root->right = mirrorTree(root->right);
return root;
}
};