数据结构与算法之线索化二叉树

540 阅读4分钟

线索化二叉树产生的背景

  • 链式二叉树结构

    从上图中我们能够看到叶子节点H、I、J、F、G都没有左右孩子,节点E没有右孩子,这样无形中浪费了链式的存储的空间

  • 线索化二叉树的好处

  1. 节省链式存储的空间
  2. 增加二叉树遍历效率。遍历二叉树时我们难免使用递归遍历二叉树,当我们线索化二叉树后遍历二叉树将不用递归就能完成,本文下面会讲。

如何线索化二叉树

合理利用节点没有左右孩子的指针地址

  • 给没有左右孩子节点添加前驱后继
  • 左孩子为NULL则左孩子指向前驱,右孩子为NULL则右孩子指向后继
  • 需要注意的按照某一种遍历顺序的第一个节点左孩子为NULL时,左孩子应指向NULL;最后一个节点的右孩子为NULL时,右孩子应指向NULL

    如图所示,按照中序遍历,H节点为第一个节点,它的左孩子应指向NULL;G节点为最后一个节点,它的右孩子应指向NULL

所有的前驱和后继都需要按照某一种遍历的次序逻辑,本文采用中序遍历

思考:如何区分一个节点的左孩子指针指向左孩子还是前驱呢?

  • 此时我们需要给节点引入标识符用来标识是指向左孩子还是前驱
  • 同理也要有标识符来标识指向右孩子还是后继


如上图所示,ltag用来标识指向左孩子还是前驱,rtag用来标识指向右孩子还是后继

线索化二叉树的实现

先遍历二叉树在遍历时设置前驱后继

  • 只有在遍历的时候才能知道该节点是否有左右孩子,才能设置标识符、设置前驱后继
  • 线索化二叉树的代码实现

typedef enum {
    Link,   //节点ltag == Link表示指向左孩子,节点rtag == Link表示指向右孩子
    Thread  //节点ltag == Thread表示指向前驱,节点rtag == Thread表示指向后继
}PointerTag;

typedef struct BiNode{
    CElemType data;
    PointerTag lTag;
    PointerTag rTag;
    struct BiNode *leftChild,*rightChild;
}BiNode,*BiTree;

#pragma mark - 构造二叉树
Status InitBiTree(BiTree *T){
    *T = NULL;
    return OK;
}
Status CreatBiTree(BiTree *T){
    CElemType data = str[indexs++];
    if (data == '#') {//空节点
        *T = NULL;
    }else{
        *T = (BiTree)malloc(sizeof(BiNode));
        if (!*T) {
            return ERROR;
        }
        (*T)->data = data;
        //生成左子树
        CreatBiTree(&(*T)->leftChild);
        if ((*T)->leftChild) {//如果左子树存在
            (*T)->lTag = Link;
        }
        //生成右子树
        CreatBiTree(&(*T)->rightChild);
        if ((*T)->rightChild) {//如果右子树存在
            (*T)->rTag = Link;
        }
    }
    return OK;
}

#pragma mark - 遍历二叉树
//全局变量,始终指向刚刚访问过得节点
BiTree pre;
//采用中序遍历二叉树
void MideTraversalBiTree(BiTree T){
    if (T) {
        MideTraversalBiTree(T->leftChild);
        printf("%c  ",T->data);
        if (T->leftChild) {//左子树存在
            T->lTag = Link;
        }else{//左子树不存在,指向前驱
            T->lTag = Thread;
            T->leftChild = pre;
        }
        //设置后继或者右孩子
        if (pre->rightChild) {//前驱右子树存在
            pre->rTag = Link;
        }else{//前驱右子树不存在,指向后继
            pre->rTag = Thread;
            //前驱右孩子指针指向后继(当前结点T)
            pre->rightChild = T;
        }
        //保持pre指向当前节点T
        pre = T;
        //继续递归遍历
        MideTraversalBiTree(T->rightChild);
    }
}

线索化二叉树的双向链表结构

线索化的二叉树在遍历的时候其实相当于操作一个双向链表结构,因此我们可以给线索化的二叉树添加一个头结点,如下图所示

  • 构造带有头结点的线索化二叉树

    Status MidHeadNodeBiTree(BiTree *H,BiTree T){
        //创建头结点
        *H = (BiTree)malloc(sizeof(BiNode));
        if (!*H) {//创建头结点失败
            return ERROR;
        }
        //设置ltag为指向左孩子
        (*H)->lTag = Link;
        //设置rtag指向后继
        (*H)->rTag = Thread;
        //设置头结点的后继为自己
        (*H)->rightChild = *H;
        if (!T) {//如果二叉树不存在,则头结点的左孩子指向自己
            (*H)->leftChild = (*H);
        }else{//二叉树存在
            //头结点的左孩子指向二叉树T
            (*H)->leftChild = T;
            //刚刚访问的变量指向头结点
            pre = *H;
            //中序遍历线索化二叉树
            MideTraversalBiTree(T);
            //最后一个节点的右孩子指向头结点
            pre->rightChild = *H;
            //最后一个节点线索化
            pre->rTag = Thread;
            //
            (*H)->rightChild = pre;
        }
        return OK;
    }
    
    
  • 遍历带有头结点的线索化后的二叉树

    void MidTraversalNodeBiTree(BiTree T){
        if (T) {
            BiTree p = T->leftChild;
            while (p != T) {
                //找到第一个节点
                while (p->lTag == Link) {
                    p = p->leftChild;
                }
                printf("%c  ",p->data);
                while (p->rTag == Thread && p->rightChild != T) {
                    p = p->rightChild;
                    printf("%c  ",p->data);
                }
                p = p->rightChild;
            }
        }
    }