二叉树是一种非线性表数据结构,接下来会按照以下顺序来讲解:树、二叉树、二叉查找树、平衡二叉查找树、红黑树、递归树。
父节点
子节点
兄弟节点:父节点是同一个节点的节点
根节点:没有父节点的节点
叶子节点:没有子节点的节点
节点的高度:该节点到叶子节点的最长边数
节点的深度:该节点到根节点的边数
节点的层数:该节点深度 + 1
二叉树:每个节点最多有两个子节点,分别是左子节点和右子节点
满二叉树:除了叶子节点外,每个节点都有左子节点和右子节点
完全二叉树:叶子节点都在最底下两层、最后一层的叶子节点整体靠左排列、除了最后一层其他层的节点个数都要达到最大。优点是:在基于数组的顺序存储法存二叉树时,仅浪费下标0的位置。所以如果某棵树是完全二叉树时,用数组存储无疑是最节省内存的一种方式。堆就是一种完全二叉树,常用数组存储。
每个节点有3个子段:数据、left指针、right指针
i=0的位置不放数据,根节点存储在下标i=1的位置,则对于在i下标的节点,它的左子节点在2*i的位置、右子节点在2*i+1的位置。
特点:省内存,适合完全二叉树、堆(也是一种完全二叉树)。
前、中、后序,表示的是某节点相对于它的左右子树的先、中、后顺序。
从前中后序遍历图,可以看得出每个结点最多被访问2遍,所以二叉树前中后序遍历的时间复杂度为 O ( n ) O(n) O(n),其中n为节点数。
对于树中的任意节点来说,先打印这个节点,再打印它的左子树,最后打印它的右子树。
递推公式: t r a v e r s e ( r ) = { p r i n t ( r ) , t r a v e r s e ( r − > l e f t ) , t r a v e r s e ( r − > r i g h t ) } traverse(r) = \{print(r), traverse(r->left), traverse(r->right)\} traverse(r)={print(r),traverse(r−>left),traverse(r−>right)}
代码:
void preOrder(Node* root)
{
if(root == nullptr) return; //终止条件
print(root) // 伪代码
preOrder(root->left);
preOrder(root->right);
}
对于树中的任意节点来说,先打印它的左子树,再打印这个节点,最后打印它的右子树。
递推公式: t r a v e r s e ( r ) = { t r a v e r s e ( r − > l e f t ) , p r i n t ( r ) , t r a v e r s e ( r − > r i g h t ) } traverse(r) = \{traverse(r->left), print(r), traverse(r->right)\} traverse(r)={traverse(r−>left),print(r),traverse(r−>right)}
代码:
void inorder(Node* root)
{
if(root == nullptr) return;
inorder(root->left);
print(root);
inorder(root->right);
}
对于树中的任意节点来说,先打印它的左子树,再打印它的右子树,最后打印这个节点。
递推公式: t r a v e r s e ( r ) = { t r a v e r s e ( r − > l e f t ) , t r a v e r s e ( r − > r i g h t ) , p r i n t ( r ) } traverse(r) = \{traverse(r->left), traverse(r->right), print(r)\} traverse(r)={traverse(r−>left),traverse(r−>right),print(r)}
代码:
void postorder(Node* root)
{
if(root == nullptr) return;
postorder(root->left);
postorder(root->right);
print(root);
}
可以看作以根节点为起点,图的广度优先遍历。
代码:
// 待添加
1. 给定一组数据,比如1,3,5,6,9,10,可以构建出多少种不同的二叉树?
补充知识:排列组合的20种解法总结https://www.bilibili.com/read/cv6224946
(1)首先确定有n个节点能构建出多少种不同的二叉树形状
(2)然后确考虑n个数字有多少种排列方法
以上二者相乘是最后的答案。
对于(1):卡特兰数f(n)=f(n-1)f(0) + f(n-2)f(1) + f(n-3)f(2) + … + f(1)f(n-2) + f(n-1)f(0)=C(n, 2n)/(n+1)
分析过程是递归的思想,详细见:n个节点总共能创建几种不同的二叉树
对于(2):如果没有重复数字, A n n = n ! A_n^n=n! Ann=n!种;如果有m1个重复的a1、有m2个重复的a2,那么 A n n A m 1 m 1 A m 2 m 2 = n ! m 1 ! m 2 ! \frac{A_n^n} {A_{m1}^{m1} A_{m2}^{m2}}=\frac{n!}{m1!m2!} Am1m1Am2m2Ann=m1!m2!n!种
二叉查找树(也叫二叉搜索树):树中的任意一个节点,其左子树中的节点的值都小于这个节点的值,而右子树节点的值都大于这个节点的值。
二叉查找树的优点:
如果二叉查找树有重复元素,怎么表示呢?
可以查找、插入、删除节点,另外还可以查找最大节点、最小节点、前驱节点、后继节点。
// 递归写法,自己瞎写,不保证对
node* find(node* root, int val)
{
// 终止条件
if(root == nullptr) return nullptr;
if(root->data == val)
{
return root;
}
// 递归
if(root->data > val) return find(root->left, val);
else return find(root->right, val);
}
// 迭代写法,自己瞎写,不保证对
node* find(node* root, int val)
{
node* p = root;
while(p != nullptr)
{
if(p->data == val) return p;
if(p->data > val) p = p->left;
else p = p->right;
}
return nullptr;
}
// 递归写法,自己瞎写,不保证对
void insertBinarySearchTree(Node* root, int val)
{
// 终止条件
if(root == nullptr)
{
root = Node(val);
return ;
}
if(root->data == val) return ;
if(root->data > val && root->left == nullptr)
{
root->left = Node(val);
return ;
}
if(root->data < val && root->right == nullptr)
{
root->right = Node(val);
return ;
}
// 递归过程
if(root->data > val) insertBinarySearchTree(root->left, val);
else insertBinarySearchTree(root->right, val);
}
// 迭代写法
void insertBinarySearchTree(Node* root, int val)
{
if(root == nullptr)
{
root = Node(val);
return ;
}
Node* p = root;
while(p != nullptr)
{
if(p->data == val) return ;
if(p->data > val)
{
if(p->left == nullptr)
{
p->left = Node(val);
return;
}
else
{
p = p->left;
}
}
else
{
if(p->right == nullptr)
{
p->right = Node(val);
return;
}
else
{
p = p->right;
}
}
}
}
void deleteBinarySearchTree(Node* root, int val)
{
if(root == nullptr || root->data == val) return nullptr;
// 先找到要删除的节点
Node* p = root;
Node* pp = nullptr; // 记录p的父节点
while(p != nullptr)
{
pp = p;
if(p->data > val) p = p->left;
else if(p->data < val) p = p->right;
}
if(p == nullptr) return ; // 没有找到要删除的节点
// 删除要删除的节点
// 1. 如果有左子节点和右子节点:找右子树里的最小节点
if(p->left != nullptr && p->right != nullptr)
{
Node* min_p = p->right;
Node* min_pp = p; // min_p的父节点
while(min_p->left != nullptr)
{
min_pp = min_p;
min_p = min_p->left;
}
// 此时min_p就是右子树里的最小节点
p->data = min_p->data; // 更换值
// 删除最小节点???为什么是这样写
p = min_p;
pp = min_pp;
}
// 2. 如果是叶节点||只有左子节点||只有右子节点
Node* child; // 记录p的子节点
if(p->left == nullptr && p->right == nullptr) child = nullptr;
else if(p->left == nullptr && p->right != nullptr) child = p->right;
else if(p->left != nullptr && p->right == nullptr) child = p->left;
if(pp->left == p) pp->left = child;
else pp->right = child;
}
最坏时间复杂度:左右子树极度不平衡时(例如都只有左子树),会退化成链表,查找的时间复杂度就是 O ( n ) O(n) O(n),n是节点个数
最好时间复杂度:左右子树很平衡(完全二叉树、或满二叉树),查找、插入、删除都跟二叉查找树的高度成正比,所以问题转换成,如何求一棵包含n个节点的完全二叉树的高度?答案是 O ( l o g n ) O(logn) O(logn),n是节点个数。
求解过程,假设节点个数为n,最大层数为L(高度H=L-1),对于完全二叉树:
n > = 1 + 2 + 4 + . . . + 2 ( L − 2 ) + 1 n >= 1+2+4+...+2^{(L-2)} + 1 n>=1+2+4+...+2(L−2)+1
n < = 1 + 2 + 4 + . . . + 2 ( L − 2 ) + 2 ( L − 1 ) n <= 1+2+4+...+2^{(L-2)} + 2^{(L-1)} n<=1+2+4+...+2(L−2)+2(L−1)
所以高度 H < = l o g 2 n H<=log_2n H<=log2n,也即时间复杂度 < = l o g n <=logn <=logn。由此也可见,二叉查找树树的平衡性会极大影响其复杂度,所以提出了很多种平衡二叉查找树,其中红黑树就是一种平衡二叉查找树。
1. 求一棵二叉树的高度
2. 为什么散列表能够O(1)查找,还需要二叉查找树这种O(logn)查找的数据结构呢?
平衡二叉查找树:二叉树中任意一个节点的左右子树的高度相差不能大于1。
满二叉树、完全二叉树都是平衡二叉树,但非完全二叉树也有可能是平衡二叉树。
由来:解决普通二叉查找树在频繁的插入、删除等动态更新的情况下,出现时间复杂度退化的问题。
严格的平衡二叉查找树,任意节点的左右子树的高度相差不超过1。
红黑树是不严格的平衡二叉查找树,从根节点到各个叶子节点的最长路径有可能比最短路径大一倍。
近似平衡:
即证明红黑树的高度稳定趋近 l o g 2 n log_2n log2n(极其平衡的二叉树的高度约为 l o g 2 n log_2n log2n)
如何在插入、删除节点的过程中将不平衡的二叉树调整成平衡的:左旋、右旋、改变颜色。