文章目录
一、树的术语
1、根节点
根节点是非空树中没有前驱的节点
2、节点度
每个节点的度等于其子树的个数(比如满二叉树,每个节点(除了叶节点)度为2)
3、叶节点
叶节点是非空树中没有后驱的节点
4、树的度
树的度是一个树中各个节点的度的最大值
二、二叉树性质
性质一:
第i层最多有2^(i-1) 个结点(i >= 1)
比如根节点是第1层,这层有2^(1-1) = 1,即成立
第i层最多有1个结点
性质二:
深度为k的二叉树总共最多有2^(k)-1个结点
这个是2^(i-1)和求得出来的
深度为k的二叉树最少有k个结点
性质三:
如果二叉树的叶节点的个数是n0,度为2的节点个数是n2,那么就有n0 = n2 + 1
了解就好了
证明:
边数 = 节点数-1 = n2 + n1 + n0 -1;
边数= 2 * n2 + n1
两个等式联合,就获得了性质三
三、满二叉树和完全二叉树的定义
1、满二叉树:每层没有空结点,即树的结点的数组是2^(i)-1个
2、完全二叉树的定义:当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时, 称之为完全二叉树
比如
上面两个是完全二叉树
这个不是完全二叉树
四、完全二叉树性质
性质一
完全二叉树的结点个数是n,那么完全二叉树的深度为log2(n) +1
这是由于在满二叉树中,第k层最后一个结点的编号为2^k - 1。通过这个性质推出
性质二
五、二叉树的存储结构
二叉树的存储结构可以分为顺序存储结构和链式存储结构
1.顺序存储
按照二叉树各个结点的编号对二叉树中的元素进行存储
这种方式的缺点
1)定存储kongjian
2)极端情况:单右子树(只有右节点),会造成空间的极大浪费(k / (2^k - 1)这种占比)
2.链式存储
每个结点包含三个变量:数值,左孩子指针,右孩子指针,即
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
};
六、二叉树遍历方式
二叉树有三种遍历方式:
先序遍历:root->left->right
中序遍历:left->root->right
后序遍历: left->right->root
使用迭代方式进行表达
1)先序遍历
void Preorder(TreeNode* root)
{
if(root == nullptr)
return;
cout << root->val << endl;
Preorder(root->left);
Preorder(root->right);
}
2)中序遍历
void Preorder(TreeNode* root)
{
if(root == nullptr)
return;
Preorder(root->left);
cout << root->val << endl;
Preorder(root->right);
}
3)后序遍历
void Preorder(TreeNode* root)
{
if(root == nullptr)
return;
Preorder(root->left);
Preorder(root->right);
cout << root->val << endl;
}
七、二叉树复制
TreeNode* Test(TreeNode* root)
{
if(root == nullptr)
return nullptr;
TreeNode* res = copy(root);
return res;
}
TreeNode* copy(TreeNode* root)
{
if(root == nullptr)
return nullptr;
TreeNode* temp = new TreeNode;
temp->val = root->val;
temp->left = copy(root->left);
temp->right = copy(root->right);
return temp;
}
上面是我写的代码,下面是《数据结构与算法》老师写的代码
八、二叉树节点数目计算
1.节点个数计算
int Test(TreeNode* root)
{
if(root == nullptr)
return 0;
return Test(root->left) + Test(root->right) + 1;
}
2.叶子节点个数计算
代码如下(示例):
int Test(TreeNode* root)
{
if(root == nullptr)
return 0;
if(root->left == nullptr && root->right == nullptr)
return 1;
return Test(root->left) + Test(root->right);
}
九、二叉排序树删除
二叉树删除结点,根据节点包含孩子情况,可以分为叶结点,只含有一个孩子节点和还有两个孩子的结点
1.叶子节删除
如果是叶结点直接删
2.只含有一个孩子结点删除
1)将孩子结点值赋值给需要删除的结点
2)删除孩子结点(类似于删除叶子结点)
3.含有两个孩子结点删除
此时有两种方式进行二叉树的删除:使用前驱结点值换之,再删除前驱结点;以后驱结点值换之,再删除后驱结点
这么操作的原因是:(以前驱结点换之为例)
删除结点可以看为以删除结点为根节点的子树的根节点。所以其前驱结点是左子树的最大叶节点。当使用最大叶节点再次作为该子树的根节点时,仍满足左子树<根节点<右子树的约束,所以这样操作是正确的
4.二叉排序树的删除操作的例子:
5.代码表示
/**
* 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:
TreeNode* deleteNode(TreeNode* root, int key) {
if (!root) return nullptr; // key not found in bst
if (key < root->val) {
root->left = deleteNode(root->left, key);
}
else if (key > root->val) {
root->right = deleteNode(root->right, key);
}
else {
// case 1: if the root itself is a leaf node
if (!root->left && !root->right) {
return nullptr;
}
// case 2: if the root only has right child
if (!root->left && root->right) {
return root->right;
}
// case 3: if the root only has left child
if (root->left && !root->right) {
return root->left;
}
// case 4: if the root has both left and right child
if (root->left && root->right) {
// find the successor from right subtree:
// 1. the successor must be the samllest element in subtree
// 2. the successor could be either the right or left child of its ancestor
auto ancestor = root;
auto successor = root->right;
while (successor->left) {
ancestor = successor;
successor = successor->left;
}
root->val = successor->val;
if (successor == ancestor->right) {
ancestor->right = deleteNode(successor, successor->val);
}
else {
ancestor->left = deleteNode(successor, successor->val);
}
}
}
return root;
}
};
十、线索二叉树
代码表示为;
struct TreeNode
{
int val;
bool ltag;
bool rtag;
TreeNode* right;
TreeNode* left;
}
十一、平衡二叉树
1、任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1
2、二叉树的高度计算是叶子结点为1,根节点等于max(height(left), height(right)) + 1
3、由于插入或者删除结点,导致二叉树失衡,此时需要进行二叉树调整。常见的二叉树失衡调整形式有以下四种:
1.LL型调整:
当左子树的左节点增加一个元素导致二叉树不平衡时,为LL型二叉树失衡
以下图例子为例,调整流程如下:
1)B结点和其左子树上升,
2)A结点和其右子树变为B结点的右孩子,
3)B结点原先右孩子变为A结点左孩子
代码实现
AVLTree* SingleLeftRotation(AVLTree *A)
{
AVLTree *B = A->Left;
A->Left = B->Right;
B->Right = A;
A->Height = max(GetHeight(A->Left), GetHeight(A->Right)) + 1;
B->Height = max(GetHeight(B->Left), A->Height) + 1;
return B;
}
2.RR型调整:
当右子树的右节点增加一个元素导致二叉树不平衡时,为RR型二叉树失衡
以下图例子为例,调整流程如下:
1)B结点和其右子树上升,
2)A结点和其左子树变为B结点的左孩子,
3)B结点原先左孩子变为A结点右孩子
代码实现
AVLTree* SingriRightRotation(AVLTree *A)
{
AVLTree *B = A->Right;
A->Right = B->Left;
B->Left = A;
A->Height = max(GetHeight(A->Left), GetHeight(A->Right)) + 1;
B->Height = max(GetHeight(B->Left),A->Height) + 1;
return B;
}
3.LR型调整:
当左子树的右节点增加一个元素导致二叉树不平衡时,为LR型二叉树失衡
以下图例子为例,调整流程如下:
1)C结点上升,
2)A结点和其右子树变为C结点的右孩子,
3)B结点原先左孩子变为C结点左孩子
4)C结点的左孩子变为B结点的右孩子
5)C结点的右孩子变为A结点的左孩子
代码实现
AVLTree* DoubleLeftRightRotation(AVLTree *A)
{
AVLTree* B = A->Left;
A->Left = SingriRightRotation(B);
return SingleLeftRotation(A);
}
3.RL型调整:
当右子树的左节点增加一个元素导致二叉树不平衡时,为RL型二叉树失衡
以下图例子为例,调整流程如下:
1)C结点上升,
2)A结点和其左子树变为C结点的左孩子,
3)B结点原先右孩子变为C结点右孩子
4)C结点的左孩子变为A结点的右孩子
5)C结点的右孩子变为B结点的左孩子
代码实现
AVLTree* DoubleRightLeftRotation(AVLTree *A)
{
AVLTree* B = A->Right;
A->right = SingriLeftRotation(B);
return SingleRightRotation(A);
}
十二、树和森林
1、树是n个结点的集合。如果n为0,则为空树。
树的特征:
1)只有一个根节点
2)其余节点可分为m(m>=0)个互不相交的有限集T1,T2,…,Tm
2、森林是有m(m>=0)棵互不相交的树的集合
注意:可以理解为树没有了根节点就是森林;森林有了根节点就是树
十三、树的存储方式
1.孩子链表
这种存储方式即:一个父节点,存储所有孩子节点,孩子节点从左到右呈链表存储
代码表示如下:
struct TreeNode
{
int val;
ListNode* child;
};
这种存储方式比较容易找孩子节点,但是不易找父节点
可以在原有节点处加一变量,便于父节点查找
struct TreeNode
{
int val;
ListNode* parent;
ListNode* child;
};
2.孩子兄弟表示法
1)实现:使用二叉链表作为树的存储结构,链表中每个结点的两个指针域分别指向第一个孩子结点和下一个兄弟结点
2)因此这种方法又叫做二叉树表示法,二叉链表表示法
代码表示如下:
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* next;
}
我们看上面的struct代码可以看出,孩子兄弟形的存储方式和二叉树形式一样,不一样的是二叉树的右指针指向右孩子,但是孩子兄弟形的右指针指向下一个兄弟
注意:
因此这种表示方法可以将树变成二叉树,同理可将二叉树变为树
1)、树的非左孩子全部变为左孩子的右孩子,即从树变为二叉树
2)、二叉树的右孩子变为上一个具有左孩子的孩子,即二叉树变为了树
十四、森林和二叉树的转换
1.转换过程
1)将各个树变为二叉树
2)将各个树的根节点用线连接
3)以第一棵树的根结点为二叉树的根,在以根节点为轴心,顺时针旋转,构成二叉树型结构
注意:
1)各个树变为二叉树,是通过孩子兄弟法存储结构实现的
2)将各个树的根节点用线连接,这个是重新将其余的树变为第一棵树的兄弟,再使用孩子兄弟法进行存储
十五、哈夫曼树
哈夫曼树又叫做最优二叉树,即树的带权路径长度最短的树
1.基本概念
1、路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
2、结点的路径长度:两个结点间路径上的分支数
3、树的路径长度
树的根节点到每个结点的路径之和
还以上图的例子为例,树的路径长度为:
注意:在结点数目相同时,完全二叉树是路径最短的树;但是反过来说不成立
4、权:将树中结点赋值给一个带有某种含义的数值,这个数值叫做结点的权。
5、结点的带权路径长度:从根结点到该结点间的路径长度和该结点的权的乘积
6、树的带权路径长度:树中所有叶子结点的带权路径长度之和
注意:
需要和树的路径长度相区分:
1)树的路径长度强调所有结点;但是树的带权路径长度强调是叶子结点
2)树的带权路径长度强调的是带权,但是树的路径长度不是
3)完全二叉树是路径最短的树,但不是带权路径长度最短的树
4)哈夫曼树中权重越大的叶子结点离根节点越近。
5)哈夫曼树不唯一
2.哈夫曼算法
构建哈夫曼树的方法流程如下:
3.哈夫曼树的存储方式
根据上面的哈夫曼树算法流程,使用顺序数组进行存储:
struct HashTreeNode
{
HashTreeNode()
{
parent = nullptr;
left = nullptr;
right = nullptr;
}
int val;
int weight;
HashTreeNode * parent;
HashTreeNode* left;
HashTreeNode* right;
//这个函数是为了在排序过程中能从小到大排列
bool operator < (const HashTreeNode &a, const HashTreeNode &b) {
if (a. weight > b. weight) return 1;
return 0;
}
};
void createHashTreeNode(HashTreeNode* root, int n)
{
if(n <= 1)
return;
int m = 2 * n;
vector< HashTreeNode*> visited;
priority_queue< HashTreeNode* > qp;
for(int i = 0; i < n; i++)
{
visited.push_back(new HashTreeNode());
cin >> visited[i]->weight;
qp.push(visited[i]);
}
for(int i = n; i <= m; i++)
{
HashTreeNode* m = qp.top();
qp.pop();
HashTreeNode* m1 = qp.top();
qp.pop();
auto it1 = find(visited.begin(), visited.end(), m);
auto it2 = find(visited.begin(), visited.end(), m1);
HashTreeNode* parentnode = new HashTreeNode();
(*it1)->parent = parentnode;
(*it2)->parent = parentnode;
parentnode->left = (*it1);
parentnode->right = (*it2);
parentnode->weight = (*it1)->weight + (*it2)->weight;
qp.push_back(parentnode);
visited.push_back(parentnode);
}
}