二叉树简单汇总

一、二叉树常见算法

二、二叉搜索树(BST,也称二叉查找树):
1、定义:
是指一棵空树或者具有下列性质的二叉树:
任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
任意节点的左、右子树也分别为二叉查找树;
没有键值相等的节点。

2、性质:
构建一个有n个不同关键字的二查搜索树的期望高度为h = O(lgn);
下述所有查找、删除、插入等操作的时间复杂度为O(h)

3、定义二叉搜索树:

// 对于任一节点x,满足其左子树上的节点key都不大于x.key
// 右子树上的节点key都不小于x.key
private class SearchTree {
    int key;
    SearchTree parent;
    SearchTree left;
    SearchTree right;
}

4、查询:(时间复杂度O(lgn))
1)递归实现:

public SearchTree Search1(SearchTree TNode, int target) {
    if (TNode == null || target == TNode.key) {
        return TNode;
    }

    if (target > TNode.key) {
        return Search(TNode.right, target);// 递归查询右子树
    } else {
        return Search(TNode.left, target);
    }
}

2)迭代实现(效率相对要高的多):

public SearchTree Search(SearchTree TNode, int target) {
    while (TNode != null && target != TNode.key) {
        if (target > TNode.key) {
            TNode = TNode.right;
        } else {
            TNode = TNode.left;
        }
    }
    return TNode;
}

5、查询最大最小关键字:(时间复杂度O(lgn))
1)查找最小关键字:即寻找最左节点

public SearchTree MinTreeNode(SearchTree TNode) {
    while (TNode.left != null) {
        TNode = TNode.left;
    }
    return TNode;
}

2)查找最大关键字:即寻找最右节点

public SearchTree MaxTreeNode(SearchTree TNode) {
    while (TNode.right != null) {
        TNode = TNode.right;
    }
    return TNode;
}

6、查询后继和前驱:(时间复杂度O(lgn))
1)查找后继:
分为两种情况进行讨论:
(1)若该节点有右节点,则后继为其右节点的最左节点
(2)若该节点无右子树,则应该回溯到期祖宗节点进行考虑
(I)若节点为其父节点x.p的左子节点,即x.key小于x.p.key
(II)若节点为父节点的右子节点,查找大于其key的后继依然无法满足;故仍向上追溯直至某一节点z,直至使得x在其z左子树中。

public SearchTree TreeSuccessor(SearchTree XNode) {
    if (XNode.right != null) {
        return MinTreeNode(XNode.right);// 即查找右子树的最小关键字
    }

    SearchTree YNode = XNode.parent;
    while (YNode != null && XNode == YNode.right) {// 循环至根节点之前的NIL或者该节点为父节点的左子节点
        XNode = YNode;
        YNode = YNode.parent;
    }

    return YNode;// 注意返回值
}

2)查找前驱:
类比于查找前驱的问题,
(1)当该节点有左子树,则前驱即为其左子树的最大关键字
(2)若该节点只有右子树,则返回其祖宗节点考虑,直至找到该节点x为z的右子树中的节点,则z为x的前驱

public SearchTree TreePreDecessor(SearchTree XNode) {
    if (XNode.left != null) {
        return MaxTreeNode(XNode.left);
    }

    SearchTree YNode = XNode.parent;
    while (YNode != null && XNode == YNode.left) {
        XNode = YNode;
        YNode = YNode.parent;
    }
    return YNode;
}

7、插入节点:(时间复杂度O(lgn))
插入较为简单,类似于二分查找进行插入即可
注意搜索二叉树的特点,插入的节点最后一定成为了二叉树的叶子节点 通过不断比较x.key与节点y的key,确定节点x应该在y的左子树还是右子树
同时处理二叉树要时常注意根节点的特殊性,进行考虑

public void TreeInsert(SearchTree TRoot, SearchTree XNode) {
    SearchTree YNode = null;
    while (TRoot != null) {
        YNode = TRoot;// 避免退出循环时TRoot为null,而无法进行访问
        if (XNode.key > TRoot.key) {
            TRoot = TRoot.right;
        } else {
            TRoot = TRoot.left;
        }
    }
    XNode.parent = YNode;
    // 注意根节点情况
    if (YNode == null) {// 即TRoot=null,未进入循环
        TRoot = XNode;
    }
    // 要判断XNode是左节点还是右节点
    else if (XNode.key < YNode.key) {
        YNode.left = XNode;
    } else {
        YNode.right = XNode;
    }

}

8、删除节点:(时间复杂度O(lgn))
删除要考虑三种情况:(暂不考虑XNode不在树中的情况)
1)XNode为叶子节点,没有子孩子,则将其删除,并将其父节点对应孩子节点置为null即可
2)XNode有一个孩子节点YNode,无论是左右孩子,将YNode替代XNode即可
3)XNode有两个孩子节点时,查找XNode的后继节点MNode(因为XNode左右节点都存在,则其后继节点一定存在)
然后让MNode的值替换XNode的值,删除MNode即可;于是下面问题就转化成为删除节点MNode的问题,仍然也需要分成这些情况(但是后继节点一定为叶子节点,故不用开率第三种情况,只需要考虑前两种情况即可)

// 替代辅助函数,注意这里面的替代仅是将VNode交换到UNode的位置,UNode本身并未作改变,后继也未继承
private void Transplant(SearchTree TRoot, SearchTree UNode, SearchTree VNode) {
    // 考虑根节点的情况
    if (UNode.parent == null) {
        TRoot = VNode;
    } else if (UNode == UNode.parent.left) {
        UNode.parent.left = VNode;
    } else {
        UNode.parent.right = VNode;
    }
    if (VNode != null) // 注意此条件判断,null是无法设置各种状态的
    {
        VNode.parent = UNode.parent;
    }
}

/********************** 删除函数 ************************/
public void TreeDelete(SearchTree TRoot, SearchTree XNode) {
    // 情况一
    if (XNode.left == null && XNode.right == null) {
        XNode = null;// 省去判断XNode是否为根节点
    }
    // 情况二
    else if ((XNode.left != null)&&(XNode.right == null)) {
        Transplant(TRoot, XNode, XNode.left);
    } else if ((XNode.right != null)&&(XNode.left == null)) {
        Transplant(TRoot, XNode, XNode.right);
    }
    // 情况三
    else {
        SearchTree YNode = XNode.right;
        // 情况3.1
        if (YNode.left == null) {
            Transplant(TRoot, XNode, YNode);
            // 交换之后注意设置子孩子
            YNode.left = XNode.left;
            YNode.left.parent = YNode;
        }
        // 情况3.2
        else {
            SearchTree MNode = MinTreeNode(YNode);
            Transplant(TRoot, MNode, MNode.right);// 用NNode替换MNode
            // 注意MNode发生改变
            MNode.right = XNode.right;
            MNode.right.parent = MNode;// 要注意设置XNode孩子的节点的parent
            Transplant(TRoot, XNode, MNode);
            MNode.left = XNode.left;
            MNode.left.parent = MNode;
        }
    }
}

三、二叉树三种遍历
二叉树三种遍历方式,即先序,中序,后序遍历,是根据访问根节点的优先顺序来划分的;
先序遍历就是先访问根节点,再访问左右节点;中序遍历则是先访问左节点,再访问根节点,最后访问右节点;后序遍历类推;

1、定义二叉树:
二叉树的定义分为两种方式,一种是链式存储,即使用链表形式;一种是顺序存储,使用数组来实现,为二叉树每个节点规定标号;
使用顺序存储的缺点是不够灵活,适合表示完全二叉树,非完全二叉树使用顺序存储方式可能会存在多个空值情况,造成空间浪费;
1)链式存储:

//链式存储
public static class BinaryTreeNode {
    int mValue;
    BinaryTreeNode mLeft;
    BinaryTreeNode mRight;

    public BinaryTreeNode(int mValue) {
        this.mValue = mValue;
    }
}

2)顺序存储:

private final int MAX_SIZE = 10;
//顺序存储
class BinaryTreeNode2 {
    int[] data = new int[MAX_SIZE];
    int length;
}

2、递归实现三种遍历:
递归实现较为简单,只需要调整访问根节点的位置顺序即可;

/******************递归实现***************************/
//先序遍历
public int PreOrderTreeWalk(BinaryTreeNode pNode) {
    if(pNode == null)
        return 0;
    visitNode(pNode);
    PreOrderTreeWalk(pNode.mLeft);
    PreOrderTreeWalk(pNode.mRight);
    return 1;
}

//中序遍历
public int InOrderTreeWalk(BinaryTreeNode pNode) {
    if(pNode == null)
        return 0;
    InOrderTreeWalk(pNode.mLeft);
    visitNode(pNode);
    InOrderTreeWalk(pNode.mRight);
    return 1;
}

//后序遍历
public int PostOrderTreeWalk(BinaryTreeNode pNode) {
    if(pNode == null)
        return 0;
    PostOrderTreeWalk(pNode.mLeft);
    PostOrderTreeWalk(pNode.mRight);
    visitNode(pNode);
    return 1;
}

3、先序遍历(非递归)
使用栈为辅助来实现先序遍历
1)一般实现方法:
即先一直往左访问,知道访问到某一节点的左节点为空,再以该节点的右节点为始,继续往下访问;
使用Stack后入先出,注意先序的访问顺序为先进行访问:

public int PreOrderTraverse(BinaryTreeNode pNode) {
    Stack<BinaryTreeNode> stack = new Stack<>();
    if (pNode == null)
        return 0;

    while (!stack.isEmpty() || pNode != null) {
        while (pNode != null) {
            //先访问
            visitNode(pNode);
            stack.push(pNode);
            //遍历左节点
            pNode = pNode.mLeft;
        }
        //返回顶层元素
        pNode = stack.peek();
        stack.pop();
        //遍历右节点
        pNode = pNode.mRight;
    }
    return 1;
}

2)根据Stack的特性,后入先出,则访问一个节点值只有,先push右子节点,再push左子节点,则在之后的访问顺序中,一定是先访问左节点,再访问右节点(根节点已经访问过了);

public int PreOrderTraverse2(BinaryTreeNode pNode) {
    if (pNode == null)
        return 0;
    Stack<BinaryTreeNode> stack = new Stack<>();
    stack.push(pNode);

    while (!stack.isEmpty()) {
        pNode = stack.pop();
        visitNode(pNode);

        if (pNode.mRight != null)
            stack.push(pNode.mRight);
        if (pNode.mLeft != null)
            stack.push(pNode.mLeft);
    }
    return 1;
}

4、中序遍历(非递归):
类同于先序遍历,只不过注意根节点的访问顺序的不同

//中序遍历
public int InOrderTraverse(BinaryTreeNode pNode) {
    Stack<BinaryTreeNode> stack = new Stack<>();
    if (pNode == null)
        return 0;

    while (!stack.isEmpty() || pNode != null) {
        while (pNode != null) {
            stack.push(pNode);
            pNode = pNode.mLeft;
        }
        pNode = stack.pop();
        visitNode(pNode);
        pNode = pNode.mRight;
    }
    return 1;
}

5、后序遍历(非递归):
后序遍历比较复杂,需要确定左右节点都访问之后,才对根节点进行访问;
1)方法一:
用一个标记标记右子树是否访问过
对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

//用以实现后续遍历的辅助结构
private class HelpNode {
    BinaryTreeNode treeNode;
    boolean isFirst;
}

public int PostOrderTraverse(BinaryTreeNode pNode) {
    if (pNode == null)
        return 0;
    Stack<HelpNode> stack = new Stack<>();
    HelpNode helpNode;
    while (!stack.isEmpty() || pNode != null) {
        //一直循环至最左节点
        while (pNode != null) {
            HelpNode temp = new HelpNode();
            temp.treeNode = pNode;
            temp.isFirst = true;
            stack.push(temp);
            pNode = pNode.mLeft;
        }

        if (!stack.isEmpty()) {
            helpNode = stack.pop();

            if (helpNode.isFirst)//表示第一次,即每一个要被访问的根节点要被push两次
            {
                helpNode.isFirst = false;
                stack.push(helpNode);
                pNode = helpNode.treeNode.mRight;//右节点的是否有效则移至循环的开始出进行判断
            } else {
                visitNode(helpNode.treeNode);
                pNode = null;
            }
        }
    }
    return 1;
}

2)方法二:双栈法
即再使用一个辅助栈stack2,stack1的访问顺序有点像先序遍历中第二种方法,先序遍历的访问顺序为根,左,右;在这里调整stack1的push顺序,使得stack1的访问顺序为根,右,左,则依次push进stack2中,stack2弹出顺序可想而知,就是后序遍历的顺序;

public int PostOrderTraverse2(BinaryTreeNode pNode) {
    if (pNode == null)
        return 0;
    Stack<BinaryTreeNode> stack1 = new Stack<>();
    Stack<BinaryTreeNode> stack2 = new Stack<>();//辅助栈
    //存入根节点,初始化
    stack1.push(pNode);
    //stack1弹出的元素,压入stack2,在将该元素的左右节点压入stack1
    while (!stack1.isEmpty()) {
        pNode = stack1.pop();
        stack2.push(pNode);

        if (pNode.mLeft != null) {
            stack1.push(pNode.mLeft);
        }
        if (pNode.mRight != null) {
            stack1.push(pNode.mRight);
        }
    }

    //stack弹出的即是后序遍历的顺序
    while (!stack2.isEmpty()) {
        visitNode(stack2.pop());
    }
    return 1;
}

3)方法三:
要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点P,先将其入栈。
I)如果P不存在左孩子和右孩子,则可以直接访问它;
II)或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点。
若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在根结点前面被访问。

public int PostOrderTraverse3(BinaryTreeNode pNode) {
    if (pNode == null)
        return 0;

    BinaryTreeNode preVisitedNode = null;
    Stack<BinaryTreeNode> stack = new Stack<>();
    stack.push(pNode);

    while (!stack.isEmpty()) {
        pNode = stack.peek();
        if ((pNode.mLeft == null && pNode.mRight == null)//左右子树均为空的情况,即叶子节点
                || (preVisitedNode != null &&
                (preVisitedNode == pNode.mLeft || preVisitedNode == pNode.mRight)))//左右子树已经被访问的情况,如果有右子树,则下一次栈顶一                                                                                                   //定为右子树;若无右子树,则栈顶为根节点;故保证了左子/                                                                                                   //树-右子树-根节点的访问顺序
        {
            visitNode(pNode);
            preVisitedNode = stack.pop();
        } else {
            if (pNode.mRight != null)
                stack.push(pNode.mRight);//注意push的顺序,先访问右子树
            if (pNode.mLeft != null)
                stack.push(pNode.mLeft);
        }
    }

    return 1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值