树(Tree)是n(n>=0
)个结点的有限集。树形结构是一类(1:n)非线性数据结构。
如上图所示,每个结点都可以从根节点经过一个唯一的弧序列到达,此弧序列被称为路径,路径中的弧的个数称为路径长度。(结点也可写作节点)
二叉树(Binary Tree)是至多有两个子结点的树,每一个子节点都区分左右且不能颠倒顺序,左边子节点称为左子结点,右边子结点称为右子结点。
满二叉树:一颗深度为k且有 2 k − 1 2^k-1 2k−1个结点的二叉树称为满二叉树(每一层的结点都达到最大值)。
完全二叉树:叶子结点只可能在最下面的两侧上;对于任一结点,其右分支下的子孙最大层数为 l l l, 其左分支子孙最大层数必为 l + 1 l+1 l+1。
按照自上而下,从左到右给整个二叉树编号,(不是完全二叉树一律转为完全二叉树,即如果结点为空也要给编号),然后以编号为下标存入数组中。这种存储对于满二叉树和完全二叉树可以做到唯一复原,而且不会浪费空间,但如果不是满二叉树和完全二叉树将会浪费很大存储空间。
typedef struct node *tree_pointer;
typedef struct node {
int data;
tree_pointer left_child;
tree_pointer right_child;
} node;
/**
* LeetCode: 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) {}
* };
*/
二叉树由根、左子树、右子树构成,定义为D、L、R的组合共有六种遍历方案:LDR、LRD、DLR、DRL、RDL、RLD,在限定先左后右后,有三种方案:DLR(先序遍历)、LDR(中序遍历)、LRD(后序遍历) 。——可以看的出,“先、中、后”是按照访问结点D先于子树还是后于子树出现。
层次遍历:逐层从左到右遍历整个二叉树。(可以利用队列存放子树的指针)
二叉树遍历的时间复杂度和空间复杂度都是 O ( n ) O(n) O(n)。
一般遍历算法采用递归来做格外简单
void PreOrderTraverve(node *root)
{ // 先序遍历
if(root != NULL){
std::cout << root->data;
PreOrderTraverve(root->left_child);
PreOrderTraverve(root->right_child);
}
}
void InOrderTraverve(node *root)
{ // 中序遍历
if(root != NULL){
PreOrderTraverve(root->left_child);
std::cout << root->data;
PreOrderTraverve(root->right_child);
}
}
void PostOrderTraverve(node *root)
{
if(root != NULL){
PreOrderTraverve(root->left_child);
PreOrderTraverve(root->right_child);
std::cout << root->data;
}
}
给定
先序+中序
or后序+中序
可以唯一确定一颗二叉树而 先序+后序 不能。
先序遍历的根节点在前,中序遍历根节点左边是左子树的结点,右边是右子树的结点,后续遍历的根节点在最后。
所以给定前序+中序,根据前序遍历特点拿出先序序列第一个结点(先序第一个结点是根节点),将中序序列分为两段(左子树)根(左子树)
,继续重复直到还原。给定中序+后序,根据后序遍历特点,拿出后序序列最后结点(后序最后一个结点是根节点),将中序分为两段(左子树)根(左子树)`,继续重复直到还原。
二叉搜索树(Binary Search Tree),又名二叉排序树、有序二叉树、二叉查找树,是一颗空树或者具有下列特点的树:
重要性质:二叉搜索树的中序遍历是一个有序数组。如不同形态的二叉查找树(a)中序遍历:
6、8、10、11、13、14、15、17
。
二叉查找树比普通的树查找更快,插入、删除、查找的时间复杂度为 O ( l o g 2 n ) O(log_2^n) O(log2n),但当用有序数列创建一颗二叉搜索树时有可能生成一个单分支的二叉树,退化为一种线性链表似的结构了,它的查找时间复杂度还是 O ( n ) O(n) O(n),所以出现了平衡二叉树。
BST的操作有构建、插入、查找、删除等,这里不做实现,但是会实现平衡二叉树。
二叉搜索树的实现可以参考: 二叉搜索树详解(C++实现)
平衡二叉树(Balanced Binary Tree)又称为AVL树,它是二叉树搜索树的改进,使得二叉树尽量降低高度,保持高度平衡,减少树的平均搜索长度。它是具有以下特点的二叉搜索树:
——关于AVL,AVL是大学教授G.M. Adelson-Velsky 和 E.M. Landis 名称的缩写,他们提出的平衡二叉树的概念,为了纪念他们,将 平衡二叉树 称为 AVL树。
在AVL树中,任何结点的左右子树高度最大差别是1,为了度量高度差引入了下边的一个概念,平衡因子。
平衡因子 balance factor
平 衡 因 子 = 结 点 左 子 树 高 度 − 结 点 右 子 树 高 度 平衡因子=结点左子树高度-结点右子树高度 平衡因子=结点左子树高度−结点右子树高度
当然,平衡因子(bf)也可以是右子树高度减去左子树高低,为了统一我这里用左子树高度-右子树高低。
在AVL树中,平衡因子必须满足: 1 > = b f > = − 1 1>=bf >=-1 1>=bf>=−1, 否则就不是AVL树了。
在介绍AVL操作之前先来看看破坏平衡的四种情况。大致可以分为两种,内测和外侧,其中外侧包括左左型和右右型,内测包括左右型和右左型。
在平衡状态下,给一个结点(T)的左左孩子(X)插入子结点(M),导致结点(T)平衡因子不符合AVL树要求,这种情况是左左型,需要降低左子树的高度,进行右旋转。
右旋步骤:
注意:旋转以平衡因子被破坏的节点作为T。
其实Y可以是虚结点,当Y是一个虚结点的时候,插入M之前整个二叉树也是平衡的。
右旋动图展示:
在平衡状态下,给一个结点(T)的右右孩子(X)插入子结点(M),导致结点(T)平衡因子不符合AVL树要求,这种情况是右右型,需要降低右子树的高度,进行左旋转。
很容易看出,右右型和左左型是对称的,它们同是在AVL的外侧结点(X)插入子结点导致失衡。
左旋步骤:
左旋和右旋对称,相同的,Y也可以是虚结点。
如果Y是虚结点,那么在插入新的结点M后,R的平衡因子bf=-2, T的平衡因子也是-2,但是我们旋转选择距离插入节点更远的根节点T来做旋转。
左右型如果仅仅使用右旋转是不平衡的。
需要经过一个左旋将内测高度降低、外侧高度增加,然后在进行右旋降低左子树高度即可。
步骤:
插入的结点时M或者N,这里进行左旋的时候要注意是以T的左孩子结点L为根进行的;
左旋和右旋比较重要,先以L为根进行左旋,在以T为根进行右旋,如果看不清楚可以将左旋、右旋的图和上图进行比较,建议自己在纸上多画几次。
右左型和左右型对称,需要先以T的右孩子进行右旋,将右内测的高度降低、升高右外侧的高度,然后在以T为根进行左旋,完成平衡。
AVL树的的操作和BST的操作基本相同,只是在插入删除的时候AVL需要通过旋转保持树的平衡。
插入的时候从根结点开始,小就往左走,大就往右走,(注意不能相等)直到插入到合适的位置,如果是BST到这里就结束了,但是AVL需要根据新节点的位置进行四种不同的旋转以保证可以达到平衡。
查找是从根节点开始,小就往左走,大就往右走,如果相等就返回,如果到达叶子节点还没找到就查找失败。
注意:AVL树由于加了平衡,查找的复杂度 O ( l o g 2 n ) O(log_2^n) O(log2n),这是它同BST的优势所在。
平衡二叉树结点的删除比较复杂,主要取决于删除了结点之后是否平衡,如果失衡还要做出相应的调整。
主要把删除的结点分为三类:
(1) 叶子节点 (度为0的结点)
(2) 只有左子树或只有右子树(即删除的结点是度为1的结点)
(3) 既有左子树又有右子树(度为2的节点)
(1)当删除的节点是叶子节点,则将节点删除,然后从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,此时到根节点还发现没有失衡,则说此时树是平衡的;如果中间过程发现失衡,则判断属于哪种类型的失衡(左左,左右,右左,右右),然后进行调整。
(2)删除的节点只有左子树或只有右子树,这种情况其实就比删除叶子节点的步骤多一步,就是将节点删除,然后把仅有一支的左子树或右子树替代原有结点的位置,后面的步骤就一样了,从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,如果中间过程发现失衡,则根据失衡的类型进行调整。
(3)删除的节点既有左子树又有右子树,这种情况又比上面这种多一步,就是中序遍历,找到待删除节点的前驱或者后驱都行,然后与待删除节点互换位置,然后把待删除的节点删掉,后面的步骤也是一样,判断是否失衡,然后根据失衡类型进行调整。
同二叉树的中序遍历,先访问左子树,然后访问根最后在访问右子树,一般调用递归比较简单。
二叉搜索树的中序遍历是升序排列的,所以格外重要。
AVL的实现可以参考:STL源码笔记(18)—平衡二叉树AVL(C++封装+模板)
参考:
- 数据结构 —— 图解AVL树(平衡二叉树)
- 二叉查找树与平衡二叉树
- 种树:二叉树、二叉搜索树、AVL树、红黑树、哈夫曼树、B树、树与森林
说明:图片来自网络和以上博客,如有侵权请联系删除