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

二叉树

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

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

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

二. 实现方式
  1. 顺序存储(sequential storage)
    使用数组array存储一个二叉树,则array[0]为根,存储在array[k]的节点的左孩子和右孩子分别位于array[2k+1]和array[2k+2]。如图所示,^表示空。

    下标0123456789
    数据ABC^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] 伸展树的原理及实现源代码

攀枝花学院本科学生课程设计任务书 题 目 叉排序平衡二叉树实现 1、课程设计的目的 使学生进一步理解和掌握课堂上所学各种基本抽象数据类型的逻辑结构、存储结构和操作实现算法,以及它们在程序中的使用方法。 使学生掌握软件设计的基本内容和设计方法,并培养学生进行规范化软件设计的能力。 3) 使学生掌握使用各种计算机资料和有关参考资料,提高学生进行程序设计的基本能力。 2、课程设计的内容和要求(包括原始数据、技术要求、工作要求等) (1) (1)以回车('\n')为输入结束标志,输入数列L,生成一棵叉排序T; (2)对叉排序T作中序遍历,输出结果; (3)计算叉排序T查找成功的平均查找长度,输出结果; (4)输入元素x,查找叉排序T,若存在含x的结点,则删该结点,并作中序遍历(执行操作2);否则输出信息“无x”; (5)用数列L,生成平衡的叉排序BT:当插入新元素之后,发现当前的叉排序BT不是平衡的叉排序,则立即将它转换成新的平衡的叉排序BT; (6)计算平衡的叉排序BT的平均查找长度,输出结果。 3、主要参考文献 [1]刘大有等,《数据结构》(C语言版),高等教育出版社 [2]严蔚敏等,《数据结构》(C语言版),清华大学出版社 [3]William Ford,William Topp,《Data Structure with C++》清华大学出版社 [4]苏仕华等,数据结构课程设计,机械工业出版社 4、课程设计工作进度计划 第1天 完成方案设计与程序框图 第2、3天 编写程序代码 第4天 程序调试分析和结果 第5天 课程设计报告和总结 指导教师(签字) 日期 年 月 日 教研室意见: 年 月 日 学生(签字): 接受任务时间: 年 月 日 注:任务书由指导教师填写。 课程设计(论文)指导教师成绩评定表 题目名称 叉排序平衡二叉树实现 评分项目 分值 得分 评价内涵 工作 表现 20% 01 学习态度 6 遵守各项纪律,工作刻苦努力,具有良好的科学工作态度。 02 科学实践、调研 7 通过实验、试验、查阅文献、深入生产实践等渠道获取与课程设计有关的材料。 03 课题工作量 7 按期圆满完成规定的任务,工作量饱满。 能力 水平 35% 04 综合运用知识的能力 10 能运用所学知识和技能去发现与解决实际问题,能正确处理实验数据,能对课题进行理论分析,得出有价值的结论。 05 应用文献的能力 5 能独立查阅相关文献和从事其他调研;能提出并较好地论述课题的实施方案;有收集、加工各种信息及获取新知识的能力。 06 设计(实验)能力,方案的设计能力 5 能正确设计实验方案,独立进行装置安装、调试、操作等实验工作,数据正确、可靠;研究思路清晰、完整。 07 计算及计算机应用能力 5 具有较强的数据运算与处理能力;能运用计算机进行资料搜集、加工、处理和辅助设计等。 08 对计算或实验结果的分析能力(综合分析能力、技术经济分析能力) 10 具有较强的数据收集、分析、处理、综合的能力。 成果 质量 45% 09 插图(或图纸)质量、篇幅、设计(论文)规范化程度 5 符合本专业相关规范或规定要求;规范化符合本文件第五条要求。 10 设计说明书(论文)质量 30 综述简练完整,有见解;立论正确,论述充分,结论严谨合理;实验正确,分析处理科学。 11 创新 10 对前人工作有改进或突破,或有独特见解。 成绩 指导教师评语 指导教师签名: 年 月 日 摘要及关键字 本程序中的数据采用“形结构”作为其数据结构。具体采用的是“叉排序”。 叉排序(又称叉查找):(1)若不空,则上所有节点的值均小于它的根结点的值;(2)若不空,则上所有节点均大于它的根结点的值;(3)它的分别为叉排序叉平衡:若不是空,则(1)都是平衡二叉树;(2)的深度之差的绝对值不超过1。 本次实验是利用叉排序平衡二叉树达到以下目的:(1)以回车('\n')为输入结束标志,输入数列L,生成一棵叉排序T;(2)对叉排序T作中序遍历,输出结果;(3)计算叉排序T查找成功的平均查找长度,输出结果; (4)输入元素x,查找叉排序T,若存在含x的结点,则删该结点,并作中序遍历(执行操作2);否则输出信息“无x”;(5)用数列L,生成平衡的叉排序BT:当插入新元素之后,发现当前的叉排序BT不是平衡的叉排序,则立即将它转换成新的平衡的叉排序BT; (6)计算平衡的叉排序BT的平均查找长度,输出结果。 关键字:数列L,结点,叉排序,平衡二叉树        目录 摘要…………………………………………………………………………… 3 1 绪论………………………………………………………………………… 5 1.1 课程设计的目的…………………………………………………………… 5 1.2 相关知识的阐述…………………………………………………………… 5 1.2.1一位数组的存储结构…………………………………………………… 5 1.2.2建立叉排序……………………………………………………… 5 1.2.3中序遍历二叉树………………………………………………………… 5 1.2.4平均查找长度…………………………………………………………… 6 1.2.5平均二叉树(AVL)…………………………………………………… 6 1.2.6平衡因子………………………………………………………………… 7 1.2.7平衡二叉树的调整方法…………………………………………………… 7 2 方案设计……………………………………………………………… 8 2.1 模块功能………………………………………………………………………8 3 算法设计…………………………………………………………………… 8 3.1 算法流程图…………………………………………………………………… 8 4 详细设计……………………………………………………………… 10 4.1 主程序………………………………………………………………… 10 4.2 定义二叉树结构……………………………………………………………… 11 4.3 建立二叉树…………………………………………………………………… 11 4.3.1叉排序的查找…………………………………………………………11 4.3.2叉排序的插入…………………………………………………………11 4.4 中序遍历…………………………………………………………………12 4.5 平均查找长度…………………………………………………………………12 4.6 删除节点…………………………………………………………………12 4.7 判断平衡二叉树……………………………………………………………… 13 5 调试分析………………………………………………………………………… 14 5.1 时间复杂度的分析………………………………………………………………14 5.2 运行结果………………………………………………………………… 14 5.3 结果分析………………………………………………………………… 15 6 课程设计总结…………………………………………………………………… 16 参考文献………………………………………………………………………… 17 1 绪论 1.1 课程设计的目的 (1)使学生进一步理解和掌握课堂上所学各种基本抽象数据类型的逻辑结构、存储结构和操作实现算法,以及它们在程序中的使用方法。 (2)使学生掌握软件设计的基本内容和设计方法,并培养学生进行规范化软件设计的能力。 (3)使学生掌握使用各种计算机资料和有关参考资料,提高学生进行程序设计的基本能力。 1.2 相关知识的阐述 1.2.1 一维数组的存储结构 建立插排序,首先用一个一维数组记录下读入的数据,然后再用边查找边插入的方式将数据一一对应放在完全二叉树相应的位置,为空的结点用“0” 补齐。 1.2.2 建立叉排序 叉排序是一种动态表。其特点是:的结构通常不是一次生成的,而是在查找过程中,当中不存在关键字等于给定值的节点时再进行插入。新插入的结点一定是一个新添加的叶子节点,并且是查找不成功时查找路径上访问的最后一个结点的孩子或孩子结点。 插入算法: 首先执行查找算法,找出被插结点的父亲结点; 判断被插结点是其父亲结点的儿子。将被插结点作为叶子结点插入; 若二叉树为空,则首先单独生成根结点。 注意:新插入的结点总是叶子结点。 1.2.3 中序遍历二叉树 中序遍历二叉树算法的框架是: 若二叉树为空,则空操作; 否则(1)中序遍历(L); (2)访问根结点(V); (3)中序遍历(R)。 中序遍历二叉树也采用递归函数的方式,先访问2i,然后访问根结点i,最后访问2i+1.先向走到底再层层返回,直至所有的结点都被访问完毕。 1.2.4 平均查找长度 计算叉排序的平均查找长度时,采用类似中序遍历的递归方式,用s记录总查找长度,j记录每个结点的查找长度,s置初值为0,采用累加的方式最终得到总查找长度s。平均查找长度就等于s/i(i为中结点的总个数)。  假设在含有n(n>=1)个关键字的序列中,i个关键字小于第一个关键字,n-i-1个关键字大于第一个关键字,则由此构造而得的叉排序在n个记录的查找概率相等的情况下,其平均查找长度为:          ASL(n,i)=[1+i*(P(i)+1)+(n-i-1)(P(n-i-1)+1)]/n 其中P(i)为含有i个结点的叉排序的平均查找长度,则P(i)+1为查找中每个关键字时所用比较次数的平均值,P(n-i-1)+1为查找中每个关键字时所用比较次数的平均值。又假设表中n个关键字的排列是“随机”的,即任一个关键字在序列中将是第1个,或第2个,…,或第n个的概率相同,则可对上式从i等于0至n-1取平均值。最终会推导出:          当n>=2时,ASL(n)<=2(1+1/n)ln(n) 由此可见,在随机的情况下,叉排序的平均查找长度和log(n)是等数量级的。 另外,含有n个结点的叉排序其判定不是惟一的。对于含有同样一组结点的表,由于结点插入的先后次序不同,所构成的叉排序的形态和深度也可能不同。 而在叉排序上进行查找时的平均查找长度和二叉树的形态有关:  ①在最坏情况下,叉排序是通过把一个有序表的n个结点依次插入而生成的,此时所得的叉排序蜕化为棵深度为n的单支,它的平均查找长度和单链表上的顺序查找相同,亦是(n+1)/2。  ②在最好情况下,叉排序在生成的过程中,的形态比较匀称,最终得到的是一棵形态与分查找的判定相似的叉排序,此时它的平均查找长度大约是lgn。  ③插入、删除和查找算法的时间复杂度均为O(lgn)。 1.2.5 平衡二叉树( AVL ) ①平衡二叉树(Balanced Binary Tree)是指中任一结点的的高度大致相同。     ②任一结点的的高度均相同(如满二叉树),则二叉树是完全平衡的。通常,只要二叉树的高度为O(1gn),就可看作是平衡的。     ③平衡的叉排序指满足BST性质的平衡二叉树。     ④AVL中任一结点的的高度之差的绝对值不超过1。在最坏情况下,n个结点的AVL的高度约为1.44lgn。而完全平衡的二叉树高度约为lgn,AVL是最接近最优的。 1.2.6 平衡因子 二叉树上任一结点的深度减去的深度称为该结点的平衡因子,易知平衡二叉树中所有结点的因子只可能为0,-1和1。 平衡叉排序的在平衡因子绝对值等于2时开始调整到绝对值为1或0,在平衡因子绝对值为2时,叉排序会出现四种不同的情况的形,因此这时需要分别单独讨论来降低平衡因子。 1.2.7 平衡二叉树的调整方法   平衡二叉树是在构造叉排序的过程中,每当插入一个新结点时,首先检查是否因插入新结点而破坏了叉排序的平衡性,若是,则找出其中的最小不平衡子,在保持叉排序特性的前提下,调整最小不平衡子中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子。具体步骤如下: (1)每当插入一个新结点,从该结点开始向上计算各结点的平衡因子,即计算该结点的祖先结点的平衡因子,若该结点的祖先结点的平衡因子的绝对值均不超过1,则平衡二叉树没有失去平衡,继续插入结点; (2)若插入结点的某祖先结点的平衡因子的绝对值大于1,则找出其中最小不平衡子的根结点; (3)判断新插入的结点与最小不平衡子的根结点的关系,确定是哪种类型的调整; (4)如果是LL型或RR型,只需应用扁担原理旋转一次,在旋转过程中,如果出现冲突,应用旋转优先原则调整冲突;如果是LR型或LR型,则需应用扁担原理旋转两次,第一次最小不平衡子的根结点先不动,调整插入结点所在子,第次再调整最小不平衡子,在旋转过程中,如果出现冲突,应用旋转优先原则调整冲突; (5)计算调整后的平衡二叉树中各结点的平衡因子,检验是否因为旋转而破坏其他结点的平衡因子,以及调整后的平衡二叉树中是否存在平衡因子大于1的结点。 2 方案设计 2.1 模块功能 1.建立二叉树:要求以回车('\n')为输入结束标志,输入数列L,生成一棵叉排序T。 2.中序遍历并输出结果:要求将第一步建立的二叉树进行中序遍历,并将结果输出。 3.平均查找长度并输出:要求计算叉排序T查找成功的平均查找长度,输出结果。 4.删除节点:要求输入元素x,查找叉排序T,若存在含x的结点,则删该结点,并作中序遍历(执行操作2);否则输出信息“无x”。 5.生成平衡二叉树:要求用数列L,生成平衡的叉排序BT:当插入新元素之后,发现当前的叉排序BT不是平衡的叉排序,则立即将它转换成新的平衡的叉排序BT; 6.平均查找长度:计算平衡的叉排序BT的平均查找长度,输出结果。 3 算法设计 3.1 算法流程图 建立二叉树流程图: YES NO 主程序流程图: 中序遍历流程图: 删除节点流程图: 4 详细设计 4.1 主程序 void main() { node T=NULL; int num; int s=0,j=0,i=0; int ch=0; node p=NULL; printf("请输入一组数字并输入0为结束符:"); do{ scanf("%d",&num); if(!num) printf("你成功完成了输入!\n"); else insertBST(&T,num); }while(num); printf("\n\n---操作菜单---\n"); printf("\n 0: 退出" ); printf("\n 1: 中序遍历"); printf("\n 2: 平均查找长度"); printf("\n 3: 删除"); printf("\n 4: 判断是否是平衡二叉树"); while(ch==ch) { printf("\n 选择操作并继续:"); scanf("%d",&ch); switch(ch){ case 0: exit(0); /*0--退出*/ case 1: printf(" 中序遍历结果是:\n "); inorderTraverse(&T); break; case 2: s=0;j=0;i=0; calculateASL(&T,&s,&j,i); printf(" ASL=%d/%d",s,j); break; case 3: printf(" 请输入你想删除的数字:"); scanf("%d",&num); if(searchBST(T,num,NULL,&p)) { T=Delete(T,num); printf(" 你已成功删除该数字!\n "); inorderTraverse(&T); else printf(" 没有你想要删除的节点 %d!",num); break; case 4: i=0; balanceBST(T,&i); if(i==0) printf(" OK!这是平衡二叉树!"); else printf(" NO!"); break; default: printf("你的输入有误!请重新输入!\n"); break; } } } 4.2 定义二叉树结构 #include typedef struct Tnode { int data; struct Tnode *lchild,*rchild; }*node,BSTnode; 4.3 建立二叉树 4.3.1 叉排序的查找 searchBST(node t,int key,node f,node *p){ /*在根指针t所指叉排序中递归地查找其关键字等于key的数据元素,若查找成功,则指针p指向该数据元素节点,并返回(1),否则指针p指向查找路径上访问的最后一个节点并返回(0),指针f指向t的双亲,其初始调用值为NULL*/ if(!t) {*p=f;return (0);} /*查找不成功*/ else if(key==t->data) {*p=t;return (1);} /*查找成功*/ else if(keydata) searchBST(t->lchild,key,t,p); /*在中继续查找*/ else searchBST(t->rchild,key,t,p); /*在中继续查找*/ } 4.3.2 叉排序的插入 insertBST(node *t,int key){ /*当叉排序t中不存在关键字等于key的数据元素时,插入key并返回(1),否则返回(0)*/ node p=NULL,s=NULL; if(!searchBST(*t,key,NULL,&p)) /*查找不成功 */ { s=(node)malloc(sizeof(BSTnode)); s->data=key; s->lchild=s->rchild=NULL; if(!p) *t=s; /*被插入节点*s为新的根节点*/ else if(keydata) p->lchild=s; /*被插节点*s为孩子*/ else p->rchild=s; /*被插节点*s为孩子*/ return (1); } else return (0); /*中已有关键字相同的节点,不再插入*/ } 4.4 中序遍历 inorderTraverse(node *t) /*中序遍历*/ { if(*t){ if(inorderTraverse(&(*t)->lchild)) { printf("%d ",(*t)->data); if(inorderTraverse(&(*t)->rchild)); } } else return(1); } 4.5 平均查找长度 calculateASL(node *t,int *s,int *j,int i) /*计算平均查找长度*/ {if(*t){ i++; *s=*s+i; if(calculateASL(&(*t)->lchild,s,j,i)) { (*j)++; if(calculateASL(&(*t)->rchild,s,j,i)) {i--; return(1);} } } else return(1); } 4.6 删除节点 node Delete(node t,int key) { /*若叉排序t中存在关键字等于key的数据元素时,则删除该数据元素节点 */ node p=t,q=NULL,s,f; while(p!=NULL) { if(p->data==key) break; q=p; if(p->data>key) p=p->lchild; else p=p->rchild; } if(p==NULL) return t; if(p->lchild==NULL) { if(q==NULL) t=p->rchild; else if(q->lchild==p) q->lchild=p->rchild; else q->rchild=p->rchild; free(p); } else{ f=p; s=p->lchild; while(s->rchild) { f=s; s=s->rchild; } if(f==p) f->lchild=s->lchild; else f->rchild=s->lchild; p->data=s->data; free (s); } return t; } 4.7 判断平衡二叉树 int balanceBST(node t,int *i) /*判断平衡二叉树*/ { int dep1,dep2; if(!t) return(0); else { dep1=balanceBST(t->lchild,i); dep2=balanceBST(t->rchild,i); } if((dep1-dep2)>1||(dep1-dep2)dep2) return(dep1+1); else return(dep2+1); } 5 调试分析 5.1 时间复杂度的分析 为了保证叉排序的高度为lgn,从而保证然叉排序实现的插入、删除和查找等基本操作的时间复杂度为O(lgn)。 5.2 运行结果 图5.1.1 调试界面 在程序调试过程当中,编译时并没有报错,但是运行时总是出错,在查阅资料和同学的帮助下,发现程序未对数组初始化。添加数组初始化代码: s=(node)malloc(sizeof(BSTnode)) 输入一组数列,以结0结束: 图5.2.2运行界面一 中序遍历: 图5.2.3运行界面 计算平均查找长度 图5.2.4运行界面三 删除已有结点: 图5.2.5运行界面四 删除失败: 图5.2.6运行界面五 判断是否是平衡二叉树: 图5.2.7运行界面六 5.3 结果分析 通过运行程序和严密的求证,运行结果无误,不过对于转换平衡二叉树平衡二叉树平均查找长度未能实现,同时也无法实现图像显示。 6 课程设计总结 在这一周的课程设计中,其实对我来说还是收获颇多。这不光提高了我的程序设计能力,更为我的就业增加了筹码。对我们来说,独立完成这样课程设计是比较困难,其中包括模块的组成分析和模块功能的实现。最后我不得不从网上下载源程序,借助课本,困难地将几个模块串起来。最后终于完成了自己的课程设计。 这次实验中我也出现过一些比较严重的错误。在用一维数组顺序表结构编写程序时我错误的运用静态链表来实现函数功能。这是我对基本概念理解的模糊不清造成的。我原以为只要采用一维数组作为存储结构它就一定也是顺序表结构,而实质上这根本是两个不相干的概念。后来在同学的指点下我意识到自己的错误。不过收获也很不少。至少我又练习了运用静态链表来实现同样的功能,同时我也发现两者在很多函数上是互通的,只需稍作修改即可移植。 另外程序的不足之处是不能实现对0这个数字的存储,可以通过改变数字的存储结构方式来实现,如使用叉链表来作为数据的存储结构,即可实现该功能。还有就是可能自己学的还不够,对于最后两个要求未能完成,不得不说这是自己学艺不精。 现在觉得以前我对数据结构的认识是那么的肤浅,因此我下定决心寒假一定好好的把数据结构复习一遍。而且本次课程设计不光增强了我程序调试的能力,还有在面对一个较大的程序要冷静,不要浮躁,先分析模块要实现的功能,再把模块划分,最后到一个一个得模块实现,并且要不断地练习,这样,一个大的程序对我来说将不成问题。 参考文献 [1]刘大有等,《数据结构》(C语言版),高等教育出版社 [2]严蔚敏等,《数据结构》(C语言版),清华大学出版社 [3]William Ford,William Topp,《Data Structure with C++》清华大学出版社 [4]苏仕华等,数据结构课程设计,机械工业出版社
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值