在计算机科学中,二叉树是每个节点最多有两个子树的树结构。在图论中,二叉树是一个连通的无环图,并且每一个顶点的度不大于3。
(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的空间来存储,对于满二叉树比较合适,但是对于其他普通的二叉树,显然这个存储结构不够高效。
前提:根节点位于第0层
(1) 在二叉树的第i层上至多有 2i 个结点(i≥0)。
(2) 深度为k的二叉树至多有 2k+1−1 个结点(k≥0)。
(3) 对任何一棵二叉树,如果其终端结点数为 n0 ,度为2的结点数为 n2 ,则 n0=n2+1 。
(4) 一棵深度为k且有 2k+1−1 个结点的二叉树称为满二叉树。对于深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树,如图所示。
(5) 具有n个结点的完全二叉树的深度为不大于 log2n 的最大整数。
(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。
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;
}
}
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;
}
}
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);
}
}
}
}
typedef struct Node{
int data;
Node* left;
Node* right;
}Node;
void level_traversal(Node* node){
Node* curNode = node;
queue 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);
}
}
二叉搜索树中的节点满足以下条件:
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);
}
AVL树是一种自平衡二叉查找树,在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。
平衡因子(balance factor)=左子树高度 - 右子树高度
1. 平衡旋转(Rotation)
当AVL树插入一个新节点,就有可能造成失衡,此时必须重新调整树的结构。(此处图片截自zzz老师PPT)
RR型:单向左旋平衡处理
LL型:单向右旋平衡处理
RL型:双向旋转,先右后左
LR型:双向旋转,先左后右
一个简单例子如下:
2. AVL树最坏情况
即求问带有N个节点的AVL树的最大高度是多少?
Fh :高度为h的AVL树
|Fh| :该AVL树的节点数
则为了使用最少的节点得到最高的树,则可以使每个节点的平衡因子都为-1或1,得到Fibonacci树:
伸展树:使得最近被访问或者频繁被访问的记录放到离根节点更近的地方。
在每一次插入或者检索节点时,都会将检索到的节点/插入的节点作为被修改的树的根节点。splay操作不单是把访问的记录搬移到了树根,而且还把查找路径上的每个节点的深度都大致减掉了一半。伸展树的旋转方式与AVL树相似,它的优势在于不需要记录用于平衡树的冗余信息。具体实现及分析可以参考[2]。
[1] 二叉搜索树/AVL树/字典树/哈夫曼树/并查集demo代码
[2] 伸展树的原理及实现源代码