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

来源:互联网 发布:手机看淘宝实名认证了 编辑:程序博客网 时间:2024/05/19 23:16

二叉树

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

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

(1) 重画orchard,使得每个节点的正下方都是其第一个子节点,而不是所有节点的中间。
(2) 垂直连接节点及其第一个子节点,水平连接每个节点与其相邻的兄弟节点,删除原有的边(不包含上述的垂直边及水平边)。
(3) 顺时针旋转45°,则垂直连接成为二叉树的左连接,水平连接成为二叉树的右连接。
orchard2binaryTree

二. 实现方式

  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

    这里写图片描述

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

  2. 链式实现(linked implementation)
    用链表实现树型是一种比较自然的实现方法。一个节点结构包含两个指针,分别指向其左右子树。
    注意:链式实现存在两种表示——是否带头节点指针。
    二叉树
    链式实现

三. 二叉树的性质

前提:根节点位于第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的结点一一对应时称之为完全二叉树,如图所示。
这里写图片描述
(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<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)=左子树高度 - 右子树高度

AVL tree&non AVL tree
1. 平衡旋转(Rotation)
当AVL树插入一个新节点,就有可能造成失衡,此时必须重新调整树的结构。(此处图片截自zzz老师PPT)
RR型:单向左旋平衡处理
截图自张子瑧老师PPT
LL型:单向右旋平衡处理
截图自张子瑧老师PPT
RL型:双向旋转,先右后左
截图自张子瑧老师PPT
LR型:双向旋转,先左后右
截图自张子瑧老师PPT
截图自张子瑧老师PPT
一个简单例子如下:
截图自张子瑧老师PPT
截图自张子瑧老师PPT
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] 伸展树的原理及实现源代码

0 0
原创粉丝点击