数据结构-二叉树

二叉树

二叉树的概述

  • 概述:二叉树是一种树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。
    • 二叉树可以为空,或者包含一个根节点和多个子树
    • 每个子树也是一个二叉树。
  • 二叉树种类:二叉树有多种特殊类型,包括满二叉树、完全二叉树、平衡二叉树等。
    • 满二叉树是一种每个非叶节点都有两个子节点的二叉树
    • 完全二叉树是一种除了最后一层外所有层都被填满,并且最后一层的节点都靠左对齐的二叉树
    • 平衡二叉树是一种左右子树高度差不超过1的二叉树
  • 二叉树的应用
    • 二叉树常用于实现搜索算法、排序算法和表达式求值等。

二叉链式结构体

/*二叉链表存储*/
typedef struct BTNode{
    char data;
    struct BTNode *lChild;						// 左孩子指针
    struct BTNode *rChild;						// 右孩子指针
}BTNode;

遍历算法

问题:
	为什么非递归遍历效率高于递归遍历效率?
答:
	因为非递归遍历使用的是自定义栈,而递归遍历使用的是系统栈,系统栈是所有递归函数都通用的栈,所以效率会低。
先序遍历(根左右)
递归
void preOrder(BTNode *p){
    if(p != NULL){
        cout<<p->data<<" ";
        preOrder(p->lChild);					// 先序遍历左子树
        preOrder(p->rChild);					// 先序遍历右子树
    }
}
非递归
分析:
	1)非递归遍历用到了栈(自定义栈)
	2)首先访问根结点,并让其入栈,再出栈(并输出根节点值)
    3)出栈后,访问当前结点 ,让其右孩子先入栈,左孩子再入栈(因为栈先进后出,而我们先访问的是左孩子)
    4)依次重复执行 2)和 3) 步骤,直到栈空为止
void preOrder(BTNode *bt){
	/*根结点不空,执行遍历*/
    if(bt != NULL){
        // 1)
        BTNode *stack[maxSize];						// 定义一个栈(存的都是BTNode类型指针)
        int top = -1;
        BTNode *p;
        // 2) 
        stack[++top] = bt;							// 根节点入栈
        while(top != -1){							// 栈不空
            p = stack[top--];						// p 指向根结点,并执行出栈
            visit(p);								// visit 访问结点
            if(p->rChild != NULL){
                stack[++top] = p->rChild;
            }
            if(p->lChild != NULL){
                stack[++top] = p->lChild;
            }
        }
    }
}
中序遍历(左根右)
递归
void inOrder(BTNode *p){
    if(p != NULL){
        inOrder(p->lChild);						// 中序遍历左子树
        cout<<p->data<<" ";
        inOrder(p->rChild);						// 中序遍历右子树
    }
}
非递归
分析:
	1)自定义栈,栈存放结点指针
	2)左孩子结点一直入栈,直到无左孩子为止
	3)然后判断栈是否空,不空,执行左孩子出栈、并输出,再将右孩子执行入栈
	4)直到栈空且当前指针为空,结束
void inOrder(BTNode *bt){
    if(bt){
        /*创建栈,存储结点指针*/
        BTNode *stack[maxSize];								// maxSize 已定义常量
        int top = -1;

        BTNode *p = bt;										// p 指针指向根结点

        while(top != -1 || p != NULL){
            /*左孩子结点一直入栈*/
            while(p != NULL){
                stack[++top] = p;
                p = p->lChild;
            }
            /*此时栈顶元素就是最左端结点*/
            if(top != -1){
                p = stack[top--];
                visit(p);									// visit 是访问函数,假设已定义
                p = p->rChild;	
            }
        }
    }
}
后序遍历(左右根)
递归
void postOrder(BTNode *p){
    if(p != NULL){
        postOrder(p->lChild);					// 后序遍历左子树
        postOrder(p->rChild);					// 后序遍历右子树
        cout<<p->data<<" ";
    }
}
非递归
分析:
	1)先序遍历: 1 2 3 5 4 后序遍历:3 5 2 4 1 逆后序遍历:1 4 2 5 3
    2)我们可以看到 逆后序遍历其实就是先序遍历改变左右子树遍历顺序,先右再左
    3)所以我们可以定义两个栈,stack1存储逆后序遍历,stack2存储从stack1中的数据,然后再执行stack2出栈即可得到后序遍历
    4)循环结束还是栈空的时候为止
void posOrder(BTNode *bt){
    if(bt != NULL){
        BTNode *stack1[maxSize];
        BTNode *stack2[maxSize];
        int top1 = -1;
        int top2 = -1;
        BTNode *p;
        /*根结点入栈*/
        stack1[++top1] = bt;
        while(top1 != -1){
            /*结点出栈,并访问*/
            p = stack1[top1--];
            
            /*stack1结点出栈后,进入stakc2中*/
            stack2[++top2] = p;
            
            /*先存入左子树结点再存入右子树结点*/
            if(p->lChild != NULL){
                stack1[++top1] = p->lChild;
            }
            if(p->rChild != NULL){
                stack1[++top1] = p->rChild;
            }
        }
        
        /*遍历输出stack2*/
        while(top2 != -1){
            p = stack2[top2--];
            visit(p);
        }
    }
}
层次遍历
分析:
	1)层次遍历要用到 队列,在这里我们使用循环队列,定义队列
	2)根结点入队列,然后根节点出队列(并输出访问),然后检查当前结点的左右子树,左非空,左子树先入队列,右非空,右子树再入队列
	3)然后当队列不空的时候一直循环,将队头元素弹出,重复 2
void level(BTNode *bt){
    /*定义队列,并初始化*/
    BTNode *que[maxSize];
    int front , rear;
    front = rear = 0;
    BTNode *p;
    if(bt != NULL){
		rear = (rear + 1)%maxSize;
        que[rear] = bt;
        
        while(front != rear){							// 队列不空
            front = (front + 1)%maxSize;
            p = que[front];
            visit(p);									// 访问 p
            if(p->lChild != NULL){						// 左子书进入队列
                que[(rear + 1)%maxSize] = p->lChild;
            }
            if(p->rChild != NULL){						// 右子树进入队列
                que[(rear + 1)%maxSize] = p->rChild;
            }
        }

    }
}

树的应用算法

  1. 表达式 (a-(b+c))*(d/e) 存储在一棵以二叉链表为存储结构的二叉树中(二叉树结点的data域为字符型),编写程序求出该表达式的值(表达式中的操作数都是一位的整数)。

    分析:
    	1(a-(b+c))*(d/e)  可以看作 A * B 
    	2)然后我们 可以先去计算 表达式 A ,再计算表达式 B,最后 A * B ,对应着先左 再右 再后,我们二叉树的后序遍历
    	3) 对于表达式,度要么为2 都要么为 0 ,没有度为 1
    int comp(BTNode *p){
        int A,B;
        if(p != NULL){											// 根结点不空时
            if(p->lchild != NULL && p->rchild !=NULL){			// 非叶子结点进行计算
                A = comp(p->lchild);
                B = comp(p->rchild);
                return op(A, B,p->data);						// 根据求得的 A,B 进行计算 
            }else{												// 叶子结点,是数值
                return p->data - '0';							
            }
        }else{
            return 0;											// 空树,表达式值为 0
        }
    }
    

    op 是返回 A p->data B 计算得来的函数值

  2. 写一个算法求一棵二叉树的深度,二叉树以二叉链表为存储方式。

    分析:
    	1)求二叉树的深度,我们可以去分别计算出  左子树 和 右子树的高度,取其中的最大值 加1 就是二叉树的深度
    	2)计算左子树深度 计算右子树深度 根,可以采用后序遍历
    
    int high(BTNode *p){
        int Lh,Rh;
        if(p == NULL){
            return 0
        }else{
            Lh = high(p->lchild);
            Rh = high(p->rchild);
            return (Lh>Rh?Lh:Rh) + 1;					// 返回左右子树深度最大值 加1
        }
    }
    
  3. 在一棵以二叉链表为存储结构的二叉树中,查找data 域值等于 key 的结点是否存在(找到任何一个满足要求的结点即可),如果存在,则将 q 指向该结点,否则 q 赋值为 NULL ,假设 data 为 int 型。

    分析:
    	1)随意一种遍历方式都可以
    	2)为了提高效率,假设我们在左子树中找到了,我们就不在右子树找了
    
    /*先序遍历*/
    void search(BTNode *p , BTNode *&q ,int key){				// q 改变用引用型
        if(p != NULL){											// p 为空,则 q 保持NULL
            if(p->data == key){
                q = p;
            }else{
                search(p->lchild,q,key);
                search(p->rchild,q,key);
            } 
        }
    }
    
    /*先序遍历高效*/
    void search(BTNode *p , BTNode *&q , int key){
        if(p != NULL){
            if(p->data == key){
                q = p;
            }else{
                search(p->lchild,q,key);
                if(q == NULL){								// 左子树没找到,再去右子树找
                    search(p->rchild,q,key);
                }
            }
        }
    }
    
  4. 假设二叉树采用二叉链表存储结构存储,编写一个程序,输出先序遍历中 第 k 个结点的值,假设 k 不大于总的结点数(结点 data 域类型为 char 型)。

    分析:
    	1)要输出第 k 个结点的值,所以要采用计数器 num,全局变量 
    	2)题目要求先序遍历,先访问根结点  再左子树,再访问右子树
    
    int num = 0;								// 定义计数器并初始化,全局变量
    void trave(BTNode *p , int k){
        if(p != NULL){
            ++num;
            if(num == k){
                cout<<p->data<<endl;
                return;								// 找到了,就无需再遍历了
            }
            trave(p->lchild,k);
            trave(p->rchild,k);
        }
    }
    
  5. 假设二叉树采用二叉链表存储结构存储,设计一个算法,求出该二叉树的宽度(具有结点数最多的那一层上的结点个数)。

    分析:
    	1)求二叉树宽度,可以定义队列,进行层次遍历
    	2)可以根据双亲结点找到其左右孩子结点的层号,这样知道所有结点层号
    	3)通过遍历找到宽度最大的那个,即是二叉树的宽度
    	4)用到非循环队列,设置 maxSize足够大
    
    typedef struct St{
        BTNode *p;
        int lno;							// 记录每个结点的层号
    }St;
    
    int maxNode(BTNode *bt){
        /*创建一个队列,并初始化*/
        St qu[maxSize];						// 非循环队列
        int front,rear;
        front=rear=0;						// 队列置空
        int Lno = 0;						// 层号
        int i,j,n,max = 0;					// i,j循环变量,n max 找最大值
        BTNode* q;
        
        if(bt != NULL){
            qu[++rear].p = bt;				// 结点入队列
            qu[rear].lno = 1;				// 根结点在第一层
            while(front != rear){
                q = qu[++front].p;
                Lno = qu[front].lno;
                if(q->lchild != NULL){
                    qu[++rear].p = q->lchild;
                    qu[rear].lno = Lno+1;			// 根据当前层号推测 子树层号
                }
                if(q->rchild != NULL){
                    qu[++rear].p = q->rchild;
                    qu[rear].lno = Lno + 1;
                }
            }
            
            /*循环结束后,Lno 中保存着最大层号,因为我们是将其存入一个足够大的队列中,rear 是二叉树结点的个数*/
            for(i=1;i<=Lno;++i){						// 从第一层开始遍历到最后一层,i层号
                n=0;
                for(j=0;j<rear;++j){					// j 相当于是从第一个元素到最后一个
                    if(qu[j].lno == i){					// qu[j].lno 存当前结点的层号
                        ++n;
                    }
                    if(max < n){
                        max = n;		
                    }
                }
            }
            return max;
        }else{
            return 0;									// 空树 返回 0
        }  
    }
    
  6. 假设二叉树采用二叉链表存储结构,设计一个算法,计算一棵给定二叉树的所有结点数。

    分析:
    	1)方法一:创建计数器,通过遍历,没遇到一个结点,计数器加1,这样就可以得到所有结点数
    	2)方法二:分冶,先去计算左子树个数,再去计算右子树个数,最后,左子树个数 + 右子树个数 + 1
    
    int num = 0;						// 定义全局变量,用来计数
    void count(BTNode *p){
        if(p != NULL){
            ++num;
            count(p->lchild);
            count(p->rchild);
        }
    }
    
    int count(BTNode* p){
        int A,B;
        if(p == NULL){
            return 0;
        }else{
            A = count(p->lchild);					// 查左子树结点个数
            B = count(p->rchild);					// 查右子树结点个数
            return A + B + 1// 左子树 + 右子树 + 根
        }
    }
    
  7. 假设二叉树采用二叉链表存储结构,设计一个算法,计算一棵给定二叉树的所有叶子结点数。

    分析:
    	1)方法一:计数器方式,条件是左右子树都为NULL ,计数器加1
    	2)方法二:分冶,先计算左子树叶子结点个数 再计算右子树叶子结点个数,两个相加
    
    int num = 0;
    void treeCount(BTNode *p){
        if(p != NULL){
            if(!p->lchild && !p->rchild){				// 左右子树均空
                ++num;
            }
            treeCount(p->lchild);
            treeCount(p->rchild);
        }
    }
    
    void treeCount(BTNode *p){
        int A,B;
        if(p == NULL){
            return 0;
        }else if(p->lchild == NULL && p->rchild == NULL){			// 是叶子结点返回 1
            return 1;
        }else{
            A = treeCount(p->lchild);
            B = treeCount(p->rchild);
            return A + B;											// 计算叶子结点个数
        }
    }
    
  8. 假设二叉树采用二叉链表存储结构,设计一个算法,利用结点的右孩子指针 rchild 将一棵二叉树的叶子结点按照从左往右的顺序串成一个单链表(在题目中定义两个指针 head 和 tail ,其中 head 指向第一个叶子结点,head 的初值为 NULL ,tail 指向最后一个叶子结点)。

    分析:
    	1)想要链接起所有叶子结点,我们第一步要去遍历二叉树,遇见叶子结点链入链表中
    	2)让head 指向第一个叶子结点,tail 一直指向最后一个叶子结点,遇见叶子结点修改 右孩子指针
    	3)尾插法
    
    void link(BTNode *p,BTNode *&head , BTNode *&tail){
        if(p != NULL){
            if(!p->lchild && !p->rchild){				// 遇见叶子结点
                if(head == NULL){						// 判断是否是第一个叶子结点
                    head = p;
                    tail = p;
                }else{
                    tail->rchild = p;					// 上一个叶子结点链接当前叶子结点
                    tail = p;							// 让 tail 一直指向最后一个叶子结点
                }
            }
            link(p->lchild , head , tail);
            link(p->rchild , head , tail);
        }
    }
    
  9. 在二叉树的二叉链式存储结构中,增加一个指向双亲结点的 parent 指针,设计一个算法,给这个指针赋值,并输出所有结点到根节点的路径。

    分析:
    	1)首先定义新的二叉链式存储结构,增加一个 parent 双亲指针
    	2)去遍历二叉树,走到某个结点,即可通过 parent 指针向上找到根结点
    	3)直到 parent 双亲指针为空,为止,过程中,输出 路径
    
    /*结构体*/
    typedef struct BTNode{
        char data;
        struct BTNode *lchild;
        struct BTNode *rchild;
        struct BTNode *parent;						// 双亲指针
    }BTNode;
    
    /*(1)给各个结点的 parent 双亲指针赋值*/
    void pTree(BTNode *p , BTNode *q){				// q 指向当前结点的双亲结点
        if(p != NULL){
            p->parent = q;							// q 刚开始为 NULL
            
            q = p;									// q 指向根结点
            
            pTree(p->lchild,q);
            pTree(p->rchild,q);
        }
    }
    
    /*(2)打印单个结点到根节点的路径*/
    void print(BTNode *p){
        while(p != NULL){							// 最终当指向根节点上的 NULL 时结束
            cout<<p->data<<" "<<endl;
            p = p->parent;			
        }
    }
    
    /*(3)打印所有结点*/
    void printAll(BTNode *p){
        if(p != NULL){
            print(p);								// 每到一个结点,都打印一下路径
            printAll(p->lchild);
            printAll(p->rchild);
        }
    }
    
  10. 假设满二叉树 b 的先序遍历序列已经存在于数组 A 中,长度为 n ,设计一个算法,将其转换为后序遍历序列。

    分析:
    	1)满二叉树,所以先序遍历第一个结点是根结点,后边等分,是左子树和右子树
    	2)先序遍历中第一结点是后序遍历中最后一结点
    	3)用分冶方法,一层一层递归的处理,重复 1)和 2
    void change(char pre[] , int L1 , int R1 , char post[] , int L2 , int R2){
        if(L1 <= R1){
            /*将pre中第一个结点(先序遍历根结点)放入post最后位置,即后序遍历根结点*/
            post[R2] = pre[L1];
            
            /*递归处理 pre 前一半序列,将其存在 post 对应的前一半位置*/
            change(pre,L1+1,(L1+1+R1)/2,post,L2,(L2+R2-1)/2);
            
           	/*递归处理 pre 后一半序列,将其存在 post 对应的后一半位置*/
            change(pre,(L1+1+R1)/2+1,R1,post,(L2+R2-1)/2+1,R2-1);
        }
    }
    
  11. 假设二叉树采用二叉链式存储结构,设计一个算法,求二叉树 b 中值为 x 的结点的层号。

    分析:
    	1)根据双亲结点可以找到子结点的层号
    	2)技巧图,我们定义一个层号变量,让其随着遍历 增或者减
    
    int L = 1;									// 代表着第一层
    void leno(BTNode *p , char x){
        if(p != NULL){
            if(p->data == x){
                cout<<L<<endl;					// 输出 x 所在层数
            }
            ++L;
            leno(p->lchild,x);
            leno(p->rchild,x);
            --L;
        }
    }
    
  12. 假设二叉树采用二叉链式存储结构,设计一个算法,输出根节点到每个叶子结点的路径。

    int i;
    int top = 0;
    char path[maxSize];
    
    void allPath(BTNode *p){
        if(p != NULL){
            path[top] = p->data;				// 入栈,也是 p 自上向下走的过程
            ++top;
            
            if(p->lchild == NULL && p->rchild == NULL){
                for(i=0;i<top;++i){
                    cout<<path[i];
                }
            }
            
            allPath(p->lchild);
            allPath(p->rchild);
            
            --top;								// 出栈,也是 p 自下向上走的过程
        }
    }
    
  • 32
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值