通俗易懂的二叉树

191 阅读4分钟

二叉树的存储结构

顺序存储

用一组连续的存储单元依次自上而下,自左至右存储完全二叉树上的节点元素。但是在最坏情况下,一个高度为h的且仅有h个节点的单支树却需要2^h-1个存储单元,造成极大的空间浪费

链式存储
  • 数据域
  • 左指针
  • 右指针

二叉树遍历

先序遍历
  • 访问根节点
  • 先序遍历左子树
  • 先序遍历右子树

对应的递归算法

void preOrder(BiTree t) {
    if (T != null) {
        visit(t);
        preOrder(t.lChild);
        preOrder(t.rChild);
    } 
}
中序遍历
  • 中序遍历左子树
  • 访问根节点
  • 中序遍历右子树

对应的递归算法

void inOrder(BiTree t) {
    if (T != null) {
        inOrder(t.lChild);
        visit(t);
        inOrder(t.rChild);
    } 
}

后序遍历
  • 后序遍历左子树
  • 后序遍历右子树
  • 访问根节点

对应的递归算法

void postOrder(BiTree t) {
    if (T != null) {
        postOrder(t.lChild);
        postOrder(t.rChild);
        visit(t);
    } 
}

时间复杂度
  • 时间复杂度O(n)
  • 空间复杂度O(n)
  • 递归工作栈深度为树的高度

三种遍历方式的区别:

  • 仅仅是访问非叶子节点的顺序不同
  • 通过递归到最深处的出口条件,假设父节点A,左右节点B,C,则先序遍历为ABC,中序遍历BAC,后序遍历为BCA

探索非递归的访问

借助栈,分析中序遍历的的访问过程

  1. 沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的节点
  2. 栈顶元素出栈并访问,若右孩子为空,继续从2执行;若右孩子不为空,将右子树转执行1

分析以上二叉树如下:

  1. 先将ABD依次压入栈,此时栈顶元素为D
  2. 访问D并出栈,发现右子树根节点为G
  3. 将G压入栈,G的左孩子为空,访问G并出栈
  4. G的右孩子为空,访问B并出栈,B的右孩子为空,访问A出栈
  5. A的右孩子不为空,将CE依次压入栈
  6. 访问E出栈,E的右孩子为空,访问C出栈
  7. C的右孩子不为空,将F压入栈
  8. 访问F并出栈

由上可得访问顺序为DGBAECF

根据分析可得中序遍历得非递归算法

void inOrder(biTree t){
    //初始化栈
    initStack(s);
    //p是遍历指针
    biTree p = t;
    //栈不空或者p不空是循环
    while (p != null || !isEmpty(s)){
        //一路向左
        if (p != null) {
            //当前节点入栈
            push(s,p);
            //左孩子不空,一直向左走
            p = p.lChild();
        }
        //出栈,并转向出栈节点得右子树
        else{
            //栈顶元素出栈
            pop(s,p);
            //访问出栈的元素
            vivist(p);
            //向右子树走,p赋值为当前节点右孩子
            p = p.rChild();
        }
    }
    
}

先序遍历的非递归访问和中序遍历是一样的,只需把访问节点元素放在入栈前面

void preOrder(biTree t){
    //初始化栈
    initStack(s);
    //p是遍历指针
    biTree p = t;
    //栈不空或者p不空是循环
    while (p != null || !isEmpty(s)){
        //一路向左
        if (p != null) {
            //访问元素
            vivist(p);
            //当前节点入栈
            push(s,p);
            //左孩子不空,一直向左走
            p = p.lChild();
        }
        //出栈,并转向出栈节点得右子树
        else{
            //栈顶元素出栈
            pop(s,p);
            //向右子树走,p赋值为当前节点右孩子
            p = p.rChild();
        }
    }
    
}

后续遍历跟先序、中序遍历有所区别,分析如下

  1. 沿着跟的左孩子,依次入栈,直到左孩子为空
  2. 读取栈定元素:若其右孩子不空且未被访问过,将右孩子转执行1;否则栈顶元素出栈并访问
void postOrder(biTree t) {
    initStack(s);
    biTree p = t;
    biTree r = null;
    while(p != null || !isEmpty(s)) {
        if (p != null) {
            push(s ,p);
            //走到最左
            p = p.lChild();
        }else {
            //读取栈定节点(非出栈)
            getTop(s ,p);
            //右子树存在,且未被访问过
            if (p.rChild() != null && p.rChild != r) {
                //转向右
                p = p.rChild();
                //压入栈
                push(s,p);
                //再走到最左
                p = p.lChild();
            //否则,弹出节点并访问
            }else{
                //弹出节点
                pop(s ,p);
                //访问节点
                visit(p);
                //记录最近访问的节点 
                r = p;
                //节点访问完后,重置p指针
                p = null;
            }
        }
    }
    
}

层次遍历
顾名思义就是一层一层的访问,从最上层开始往下,从左往右

  • 借助一个队列
  • 将二叉树的根节点入队,然后出队
  • 若他有左子树,则将左子树根节点入队;若它有右子树,则将右子树根节点入队
  • 然后出队,访问出队节点
  • 如此反复,直到队列为空
void levelOrder(biTree t){
    initQueue(Q);
    biTree p;
    //根节点入队
    enQueue(Q ,t);
    while (!isEmpty(Q)) {
        //出队
        deQueue(Q ,p);
        visit(p);
        if (p.lChild() != null) {
            enQueue(Q ,p.lChild());
        }
        if (p.rChild() != null) {
            enQueue(Q ,p.rChild());
        }
    }
}