【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)
来源:互联网 发布:手机看淘宝实名认证了 编辑:程序博客网 时间:2024/05/19 23:16
二叉树
在计算机科学中,二叉树是每个节点最多有两个子树的树结构。在图论中,二叉树是一个连通的无环图,并且每一个顶点的度不大于3。
一. 旋转(Rotation): 从果园转换成二叉树
(1) 重画orchard,使得每个节点的正下方都是其第一个子节点,而不是所有节点的中间。
(2) 垂直连接节点及其第一个子节点,水平连接每个节点与其相邻的兄弟节点,删除原有的边(不包含上述的垂直边及水平边)。
(3) 顺时针旋转45°,则垂直连接成为二叉树的左连接,水平连接成为二叉树的右连接。
二. 实现方式
顺序存储(sequential storage)
使用数组array存储一个二叉树,则array[0]为根,存储在array[k]的节点的左孩子和右孩子分别位于array[2k+1]和array[2k+2]。如图所示,^表示空。下标 0 1 2 3 4 5 6 7 8 9 数据 A B C ^ E ^ G ^ ^ J对于一个高度为k的树需要2^k的空间来存储,对于满二叉树比较合适,但是对于其他普通的二叉树,显然这个存储结构不够高效。
- 链式实现(linked implementation)
用链表实现树型是一种比较自然的实现方法。一个节点结构包含两个指针,分别指向其左右子树。
注意:链式实现存在两种表示——是否带头节点指针。
三. 二叉树的性质
前提:根节点位于第0层
(1) 在二叉树的第i层上至多有
(2) 深度为k的二叉树至多有
(3) 对任何一棵二叉树,如果其终端结点数为
(4) 一棵深度为k且有
(5) 具有n个结点的完全二叉树的深度为不大于
(6) 如果对一棵有n个结点的完全二叉树的结点按层序编号(从第0层到最后一层,每层从左到右),则对任一结点i(1≤i≤n),有
a. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点x(其中x是不大于i/2的最大整数)。
b. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
c. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
四. 树的遍历
- 中序遍历(in-order traversal)
访问顺序为:左节点->根节点->右节点
typedef struct Node{ int data; Node* left; Node* right;}Node;// 递归实现void in_order_traversal1(Node* node){ if(node->left != NULL) in_order_traversal1(node->left); cout << node->data << " "; if(node->right != NULL) in_order_traversal1(node->right);} // 非递归实现void in_order_traversal2(Node* node){ Node* curNode = node; stack<Node*> s; while(curNode!=NULL || !s.empty()){ while(curNode != NULL){ s.push(curNode); curNode = curNode->left; } curNode = s.top(); s.pop(); cout << curNode->data << " "; curNode = curNode->right; }}
- 前序遍历(pre-order traversal)
访问顺序为:根节点->左节点->右节点
typedef struct Node{ int data; Node* left; Node* right;}Node;// 递归实现void pre_order_traversal1(Node* node){ cout << node->data << " "; if(node->left != NULL) pre_order_traversal1(node->left); if(node->right != NULL) pre_order_traversal1(node->right);} // 非递归实现void pre_order_traversal2(Node* node){ Node* curNode = node; stack<Node*> s; while(curNode!=NULL || !s.empty()){ while(curNode!=NULL){ cout << curNode->data << " "; s.push(curNode); curNode = curNode->left; } curNode = s.top(); s.pop(); curNode = curNode->right; }}
- 后序遍历(post-order traversal)
访问顺序为:左节点->右节点->根节点
typedef struct Node{ int data; Node* left; Node* right;}Node;// 递归实现void post_order_traversal(Node* node){ if(node->left != NULL) post_order_traversal1(node->left); if(node->right != NULL) post_order_traversal1(node->right); cout << node->data << " ";} // 非递归实现void post_order_traversal2(Node* node){ if(node == NULL) return; Node* curNode = node; Node* preNode = NULL; stack<Node*> s; s.push(curNode); while(!s.empty()){ curNode = s.top(); // 遇到叶节点或者节点的左右子树都已访问 if(curNode->left==NULL && curNode->right==NULL || preNode!=NULL && (preNode==curNode->left || preNode==curNode->right)){ cout << curNode->data << " "; s.pop(); preNode = curNode; } else{ if(curNode->right!=NULL){ s.push(curNode->right); } if(curNode->left!=NULL){ s.push(curNode->left); } } }}
- 层次遍历(level traversal)
访问顺序为:level 0->level 1-> …
typedef struct Node{ int data; Node* left; Node* right;}Node;void level_traversal(Node* node){ Node* curNode = node; queue<Node*> q; if(curNode != NULL) q.push(curNode); while(!q.empty()){ curNode = q.front(); q.pop(); cout << curNode->data << " "; if(curNode->left != NULL) q.push(curNode->left); if(curNode->right != NULL) q.push(curNode->right); }}
- 根据不同遍历序列得到重构二叉树
a. 前序遍历+中序遍历
前序:ABCDEF
中序:CBAEDF
① 根据前序遍历序列,二叉树首先遍历根节点,再遍历左子树,最后遍历右子树。可以确定的是第一个数据A为根节点。再根据中序遍历序列,二叉树首先遍历左子树再遍历根节点,最后遍历右子树,可以确定左子树为CB和右子树EDF。
② 对左子树和右子树分别进行步骤①,直到遍历到叶节点。
b. 后序遍历+中序遍历
后序:CBEFDA
中序:CBAEDF
① 根据后序遍历序列,二叉树首先遍历遍历左子树,再遍历右子树,最后遍历根节点。可以确定的是最后一个数据A为根节点。再根据中序遍历序列,二叉树首先遍历左子树再遍历根节点,最后遍历右子树,可以确定左子树为CB和右子树EDF。
② 对左子树和右子树分别进行步骤①,直到遍历到叶节点。
c. 前序遍历+后序遍历
根据前序遍历和中序遍历得到的二叉树结构可能不唯一。
五. 二叉搜索树(Binary search tree)
二叉搜索树中的节点满足以下条件:
1. 假如节点存在左孩子,则左孩子小于其父节点
2. 假如节点存在右孩子,则右孩子大于其父节点
3. 根节点的左子树和右子树也是二叉搜索树。
注意:二叉搜索树要求不存在相同的键值。
- 目标值检索:为了搜索一个目标值,通常会借用一个辅助函数:首先比较目标值与树的根节点的大小,假如目标值相同,则搜索结束;假如目标值小于根节点,则进入左子树;否则进入右子树。在子树中重复上述操作,知道找到目标值或者到达一个空子树。
// 递归实现 Node* search_for_node1(Node* sub_root, const int target){ if(sub_root==NULL || sub_root->data == target) return sub_root; if(sub_root->data > target) search_for_node1(sub_root->left, target); if(sub_root->data < target) search_for_node1(sub_root->right, target);}// 非递归实现 Node* search_for_node2(Node* sub_root, const int target){ if(sub_root==NULL || sub_root->data == target) return sub_root; while(sub_root!=NULL && sub_root->data != target){ if(sub_root->data > target) sub_root = sub_root->left; else if(sub_root->data < target) sub_root = sub_root->right; } return sub_root;}
时间复杂度分析:
对于最好情况,则二叉搜索树是一个几乎完全平衡的结构,那么拥有n个节点的树的比较次数复杂度为O(log n)。对于最坏情况,则二叉树为一个链式结构,那么搜索的复杂度与顺序搜索相同,为O(n)。假如二叉搜索树的构建是随机的(则不一定平衡),那么二叉树搜索的效率近似于二分检索。二叉搜索树插入节点:
类似于目标值查找,找到第一个空子树的位置,就将节点插入二叉树中。
void search_and_insert(Node* &sub_root, const int value){ if(sub_root==NULL){ sub_root = new Node(); sub_root->left = NULL; sub_root->right = NULL; sub_root->data = value; return; } if(sub_root->data > value) search_and_insert(sub_root->left, value); if(sub_root->data < value) search_and_insert(sub_root->right, value);}
- 二叉搜索树删除节点:
如图所示共有三种情况:
void remove_node(Node* &sub_root){ if(sub_root == NULL) cout << "No target!" << endl; else if(sub_root->left == NULL) sub_root = sub_root->right; // 情况1&2 else if(sub_root->right == NULL) sub_root = sub_root->left; // 情况2 else{ // 情况3 Node* parent = sub_root; Node* preNode = sub_root->left; while(preNode->right != NULL){ parent = preNode; preNode = preNode->right; } sub_root->data = preNode->data; if(parent == sub_root) sub_root->left = preNode->left; else parent->right = preNode->left; delete preNode; }}void search_and_destroy(Node* sub_root, const int target){ if(sub_root==NULL || sub_root->data == target){ remove_node(sub_root); } else if(sub_root->data > target) search_and_destroy(sub_root->left, target); else if(sub_root->data < target) search_and_destroy(sub_root->right, target);}
- 树排序(treesort)
注意并不是堆排序,首先构造二叉搜索树,然后中序遍历树可以得到一个有序序列。treesort与quicksort十分相似,
首先第一个数据作为根节点;
第二个数据想要插入二叉搜索树时,首先与根节点比较,类似地,在quicksort中,首先与pivot比较。根据比较结果,将第二个数据插入,作为左/右子树的根;
接下来的数据假如与第二个数据位于同一个子树,则需要与第二个数据相比较,同样类似于quitsort。由此,我们可以知道,treesort所需要的key值比较次数与quitsort相同。
相比于quitsort,treesort的优点是①不要求所有数据在一开始就是可获取的,因为treesort是对数据逐个插入;②treesort支持后续的插入与删除。缺点是对于已经有序或者接近有序的数据,treesort效率极低,生成的二叉搜索树是一条链。
六. 平衡二叉树(AVL tree)
AVL树是一种自平衡二叉查找树,在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。
平衡因子(balance factor)=左子树高度 - 右子树高度
1. 平衡旋转(Rotation)
当AVL树插入一个新节点,就有可能造成失衡,此时必须重新调整树的结构。(此处图片截自zzz老师PPT)
RR型:单向左旋平衡处理
LL型:单向右旋平衡处理
RL型:双向旋转,先右后左
LR型:双向旋转,先左后右
一个简单例子如下:
2. AVL树最坏情况
即求问带有N个节点的AVL树的最大高度是多少?
则为了使用最少的节点得到最高的树,则可以使每个节点的平衡因子都为-1或1,得到Fibonacci树:
其中
通过计算Fibonacci数得到高度为
即最稀疏的带有n个节点的AVL树的高度为
3. 伸展树(Splay tree)
伸展树:使得最近被访问或者频繁被访问的记录放到离根节点更近的地方。
在每一次插入或者检索节点时,都会将检索到的节点/插入的节点作为被修改的树的根节点。splay操作不单是把访问的记录搬移到了树根,而且还把查找路径上的每个节点的深度都大致减掉了一半。伸展树的旋转方式与AVL树相似,它的优势在于不需要记录用于平衡树的冗余信息。具体实现及分析可以参考[2]。
参考及代码demo
[1] 二叉搜索树/AVL树/字典树/哈夫曼树/并查集demo代码
[2] 伸展树的原理及实现源代码
- 【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)
- 3.16(c程序实现)特殊的平衡二叉搜索树之完全二叉搜索树
- 平衡二叉搜索树
- 数据结构 - 平衡二叉树
- 数据结构: 平衡二叉树
- 数据结构 平衡二叉树
- 【数据结构】平衡二叉树
- 数据结构&&平衡二叉树
- 数据结构--平衡二叉树
- 数据结构--平衡二叉树
- 数据结构---平衡二叉树
- 数据结构--平衡二叉树
- 数据结构 平衡二叉树
- 数据结构-平衡二叉树
- 数据结构-平衡搜索二叉树(AVL树)
- 数据结构——二叉搜索平衡树
- 数据结构(c++)<二叉搜索树>
- 数据结构中用C++实现平衡二叉搜索树
- Leetcode515. Find Largest Value in Each Tree Row
- 决策树之ID3算法及其Python实现
- 全排列---非递归实现
- 初试Vivado2014.4的FFT IP核
- Openstack使用官方ubuntu和Centos镜像无法使用ssh用户名密码登录登录的问题
- 【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)
- MySQL数据库
- 数值计算——一维非线性方程求解
- unreal component
- IDEA 配置JUnit 并自动生成Test文件夹和类
- Eclipse安装Maven流程
- 常量关键词const
- Android app欢迎页面停留几秒的实现
- android Bitmap 存储为 bmp