【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)

二叉树

在计算机科学中,二叉树是每个节点最多有两个子树的树结构。在图论中,二叉树是一个连通的无环图,并且每一个顶点的度不大于3。

一. 旋转(Rotation): 从果园转换成二叉树

(1) 重画orchard,使得每个节点的正下方都是其第一个子节点,而不是所有节点的中间。
(2) 垂直连接节点及其第一个子节点,水平连接每个节点与其相邻的兄弟节点,删除原有的边(不包含上述的垂直边及水平边)。
(3) 顺时针旋转45°,则垂直连接成为二叉树的左连接,水平连接成为二叉树的右连接。
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第1张图片

二. 实现方式

  1. 顺序存储(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

    【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第2张图片

    对于一个高度为k的树需要2^k的空间来存储,对于满二叉树比较合适,但是对于其他普通的二叉树,显然这个存储结构不够高效

  2. 链式实现(linked implementation)
    用链表实现树型是一种比较自然的实现方法。一个节点结构包含两个指针,分别指向其左右子树。
    注意:链式实现存在两种表示——是否带头节点指针。
    【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第3张图片
    【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第4张图片

三. 二叉树的性质

前提:根节点位于第0层
(1) 在二叉树的第i层上至多有 2i 个结点(i≥0)。
(2) 深度为k的二叉树至多有 2k+11 个结点(k≥0)。
(3) 对任何一棵二叉树,如果其终端结点数为 n0 ,度为2的结点数为 n2 ,则 n0=n2+1
(4) 一棵深度为k且有 2k+11 个结点的二叉树称为满二叉树。对于深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树,如图所示。
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第5张图片
(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。

四. 树的遍历

  • 中序遍历(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 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。
    ② 对左子树和右子树分别进行步骤①,直到遍历到叶节点。
    【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第6张图片
    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);
}
  • 二叉搜索树删除节点:
    如图所示共有三种情况:
    【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第7张图片
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)=左子树高度 - 右子树高度

【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第8张图片
1. 平衡旋转(Rotation)
当AVL树插入一个新节点,就有可能造成失衡,此时必须重新调整树的结构。(此处图片截自zzz老师PPT)
RR型:单向左旋平衡处理
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第9张图片
LL型:单向右旋平衡处理
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第10张图片
RL型:双向旋转,先右后左
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第11张图片
LR型:双向旋转,先左后右
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第12张图片
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第13张图片
一个简单例子如下:
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第14张图片
【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)_第15张图片
2. AVL树最坏情况
即求问带有N个节点的AVL树的最大高度是多少?
Fh :高度为h的AVL树
|Fh| :该AVL树的节点数
则为了使用最少的节点得到最高的树,则可以使每个节点的平衡因子都为-1或1,得到Fibonacci树:

|Fh|=|Fh1|+|Fh2|+1

其中 |F0|=1 |F1|=2
通过计算Fibonacci数得到高度为
h1.44lg|Fh|

即最稀疏的带有n个节点的AVL树的高度为 1.44lgn
3. 伸展树(Splay tree)

伸展树:使得最近被访问或者频繁被访问的记录放到离根节点更近的地方。

在每一次插入或者检索节点时,都会将检索到的节点/插入的节点作为被修改的树的根节点。splay操作不单是把访问的记录搬移到了树根,而且还把查找路径上的每个节点的深度都大致减掉了一半。伸展树的旋转方式与AVL树相似,它的优势在于不需要记录用于平衡树的冗余信息。具体实现及分析可以参考[2]。

参考及代码demo

[1] 二叉搜索树/AVL树/字典树/哈夫曼树/并查集demo代码
[2] 伸展树的原理及实现源代码

你可能感兴趣的:(课程,数据结构,数据结构)