数据结构和算法:二叉树

二叉树

二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构每每是二叉树形式,即便是通常的树也能简单地转换为二叉树,并且二叉树的存储结构及其算法都较为简单,所以二叉树显得特别重要。二叉树特色是每一个节点最多只能有两棵子树,即树的度最大为2,且有左右之分node

二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。算法

特殊类型

满二叉树:二叉树内只有度为2和0的节点,且度为0的节点在同一层,即除了树的最后一层的节点没有子节点,其余节点都有2个子节点,则这个二叉树就是满二叉树。满二叉树的节点数量为2^k-1(k为树的深度)数组

彻底二叉树:彻底二叉树的节点顺序是由上到下,由左到右的。即叶子节点所在的层级差异不能大于1,且右节点不为空时,左节点也不能为空。满二叉树必定就是彻底二叉树,但彻底二叉树不必定是满二叉树数据结构

代码实例ide

使用二叉树来存储水浒英雄好汉,模拟水浒英雄排名。post

首先须要先建立二叉树里的节点,用于存储水浒英雄的编号、名字和节点的左、右子节点。ui

//水浒英雄节点
class HeroNode{
    private int id;
    private String name;
    private HeroNode left;//节点的左子节点
    private HeroNode right;//节点的右子节点
    
    //3种遍历树的方法,在《数据结构与算法:树》的一章有过讲解
    //前序遍历(中-左-右)
    public void preOrder(){
        //先输出该节点(中)
        System.out.println(this);
        //往左子节点递归遍历子节点(左)
        if (this.left != null){
            this.left.preOrder();
        }
        //往右子节点递归遍历子节点(右)
        if (this.right != null){
            this.right.preOrder();
        }
    }

    //中序遍历(左-中-右)
    public void midOrder(){
        if (this.left != null){
            this.left.midOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.midOrder();
        }
    }

    //后序遍历(左-右-中)
    public void postOrder(){
        if (this.left != null){
            this.left.postOrder();
        }
        if (this.right != null){
            this.right.postOrder();
        }
        System.out.println(this);
    }

    public HeroNode(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

}

有了节点,接下来就须要建立二叉树类了,而二叉树的起点是根节点,咱们须要靠根节点才能够实现增删改查的操做,因此二叉树最重要的属性就是根节点。this

//二叉树
class BinaryTree{
    private HeroNode root;//根节点

    //实例化根节点,即实例化二叉树
    public void setRoot(HeroNode root) {
        this.root = root;
    }
    
      //由于是经过二叉树去操做节点,因此要为节点的遍历方法进行封装调用
    public void preOrder(){
        if (this.root != null){
            root.preOrder();
        }else {
            System.out.println("二叉树为空,没法遍历");
        }
    }

    public void midOrder(){
        if (this.root != null){
            root.midOrder();
        }else {
            System.out.println("二叉树为空,没法遍历");
        }
    }

    public void postOrder(){
        if (this.root != null){
            root.postOrder();
        }else {
            System.out.println("二叉树为空,没法遍历");
        }
    }
}

接下来咱们就能够建立一个二叉树,由于咱们没有为二叉树设定任何节点顺序规则,因此这里咱们先手动为二叉树添加节点,等到后面章节再讲解根据规则来自动添加节点和删除节点操做。spa

public class BinaryTreeDemo {
    public static void main(String[] args) {
        BinaryTree binaryTree = new BinaryTree();

        HeroNode root = new HeroNode(1, "宋江");
        HeroNode node1 = new HeroNode(2, "卢俊义");
        HeroNode node2 = new HeroNode(3, "吴用");
        HeroNode node3 = new HeroNode(4, "公孙胜");
        HeroNode node4 = new HeroNode(5, "关胜");

        //为各个节点设置关系
        root.setLeft(node1);
        root.setRight(node2);
        node2.setLeft(node4);
        node2.setRight(node3);
        //实例化二叉树根节点
        binaryTree.setRoot(root);
        
        //使用3种遍历方式遍历二叉树
        System.out.println("前序遍历:");
        binaryTree.preOrder();
        System.out.println("中序遍历:");
        binaryTree.midOrder();
        System.out.println("后序遍历:");
        binaryTree.postOrder();
    }
}

若是咱们要根据id查找单个水浒英雄信息时,只需将遍历方法修改一下便可,下面之前序遍历查找为例:3d

//前序遍历查找(修改节点方法只需添加个修改参数,并把返回代码改为修改代码)
public HeroNode preOrderSearch(int id){
    //若是当前节点就是查找的节点时,返回该节点(中)
    if (id == this.getId()){
        return this;
    }
    HeroNode result = null;
    //往左子节点递归遍历查找
    if (this.left != null){
        result = this.left.preOrderSearch(id);
    }
    //若是result不为空则代表已经查找到该节点,直接返回result
    if (result != null){
        return result;
    }
    //往右子节点递归遍历查找
    if (this.right != null){
        result = this.right.preOrderSearch(id);
    }
    return result;
}

 

顺序存储二叉树

二叉树的存储方式有俩种,一种是链式存储(上面代码所演示的),另外一种是顺序存储。顺序存储指的是用数组来存储二叉树,即根据层级,从第一层到最后一层由左到右的顺序存储在数组中。

顺序存储二叉树的节点总数量:数组的长度

下标为n的节点的左节点下标:n*2+1

下标为n的节点的右节点下标:n*2+2

下标为n的节点的父节点下标:(n-1)/2

注意:顺序存储方式只能存储彻底二叉树,若是要存储非彻底二叉树,需将它转换成彻底二叉树,即补值为0的节点,直到该二叉树变成彻底二叉树

代码实例

//顺序存储二叉树
class ArrayBinaryTree{
    //顺序存储的数组
    private int[] array;

    public ArrayBinaryTree(int[] array) {
        this.array = array;
    }

    public void preOrder(){
        preOrder(0);
    }

    public void midOrder(){
        midOrder(0);
    }

    public void postOrder(){
        postOrder(0);
    }

    //前序遍历
    public void preOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉树数组为空,没法遍历");
            return;
        }
        //输出该节点(中)
        System.out.println(array[index]);
        //往左递归遍历(左)
        if (index*2+1 < array.length){//判断左节是否为空
            preOrder(index*2+1);
        }
        //往右递归遍历(右)
        if (index*2+2 < array.length){//判断右节点是否为空
            preOrder(index*2+2);
        }
    }

    //中序遍历
    public void midOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉树数组为空,没法遍历");
            return;
        }
        if (index*2+1 < array.length){
            midOrder(index*2+1);
        }
        System.out.println(array[index]);
        if (index*2+2 < array.length){
            midOrder(index*2+2);
        }
    }

    //后序遍历
    public void postOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉树数组为空,没法遍历");
            return;
        }
        if (index*2+1 < array.length){
            postOrder(index*2+1);
        }
        if (index*2+2 < array.length){
            postOrder(index*2+2);
        }
        System.out.println(array[index]);
    }
}

 

线索化二叉树

在上图的二叉树遍历中,当咱们遍历到叶子节点时,须要一层一层的返回到下一个输出的节点,效率并不高,那咱们能不能直接让叶子节点指向在对应遍历方法中它的下一个节点呢?这时咱们就须要线索化二叉树了。

对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。

这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不一样,线索二叉树可分为前序线索二叉树中序线索二叉树后序线索二叉树三种。

代码实例

//中序线索化二叉树
class ThreadedBinaryTree{
    private HeroNode2 root;
    //在中序线索化时,保存前一个节点,默认为空
    private HeroNode2 pre;

    public void setRoot(HeroNode2 root){
        this.root = root;
    }

    public void threadedNodes(){
        threadedNodes(root);
    }

    //中序线索化二叉树
    public void threadedNodes(HeroNode2 node){
        //若是传入的参数节点为空,即已经超出二叉树范围,则直接返回
        if (node == null){
            return;
        }

        /**
         * 按照中序遍历(左中右)的顺序,依次为各个节点线索化
         */
        //先向节点的左节点递归,依次线索化左节点
        threadedNodes(node.getLeft());

        //为节点线索化
        /*
        若是该节点的左节点为空,则将该节点的左节点设置为中序线索化的前一个节点pre,
        并将该节点的左节点类型leftType设置为前驱节点1
        注意: 在第一次线索化时,左节点会设置为空,但由于是中序线索化,因此不影响
         */
        if (node.getLeft() == null){
            node.setLeft(pre);
            node.setLeftType(1);
        }
        /*
        若是前一个节点pre的右节点为空,则将中序线索化的前一个节点pre的右节点设置为当前节点,
        并将pre节点的右节点类型设为后继节点1
        逻辑:pre只有当它的右节点为空时才会进入线索化,而这时递归已经返回到中序遍历时pre节点的
        下一个遍历节点node,因此node就是在中序线索化中pre的下一个节点,即后继节点
         */
        if (pre != null && pre.getRight() == null){
            pre.setRight(node);
            pre.setRightType(1);
        }
        //线索化后要将当前节点设置会上一个节点
        pre = node;

        //再向节点的右节点递归,依次线索化右节点
        threadedNodes(node.getRight());
    }

    //遍历中序线索化二叉树(即输出中序遍历的结果)
    public void threadedList(){
        HeroNode2 node = root;

        //当node为空时,即中序线索化二叉树已经遍历完成
        while (node != null){
            //若是左节点的类型是子树0的话,则继续往节点的左节点走
            while (node.getLeftType() == 0){
                node = node.getLeft();
            }
            //当退出循环时,即node没有左子节点了,则输出node节点
            System.out.println(node);

            //若是右节点的类型是后继节点1时,则直接跳到后继节点并输出
            while (node.getRightType() == 1){
                node = node.getRight();
                System.out.println(node);
            }
            //当退出循环时,即node还有右子节点,则继续往节点的右子节点走
            node = node.getRight();
        }
    }
}

//线索化二叉树节点
/**
 * 由于二叉树线索化后,
 * 左节点的指向有两种状况:指向左子树或指向前驱节点;右节点的指向也有两种状况:指向右子树或指向后继节点;
 * 因此要分别为左、右节点建立一个属性,该属性能够说明左、右节点的指向状况。
 */
class HeroNode2{
    private int id;
    private String name;
    private HeroNode2 left;
    private HeroNode2 right;
    //左指向的类型,当为0时,指向的是左子树,当为1时,指向的是前驱节点
    private int leftType;
    //右指向的类型,当为0时,指向的是右子树,当为1时,指向的是后继节点
    private int rightType;

    public HeroNode2(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode2 getLeft() {
        return left;
    }

    public void setLeft(HeroNode2 left) {
        this.left = left;
    }

    public HeroNode2 getRight() {
        return right;
    }

    public void setRight(HeroNode2 right) {
        this.right = right;
    }

    public int getLeftType() {
        return leftType;
    }

    public void setLeftType(int leftType) {
        this.leftType = leftType;
    }

    public int getRightType() {
        return rightType;
    }

    public void setRightType(int rightType) {
        this.rightType = rightType;
    }

    @Override
    public String toString() {
        return "HeroNode2{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
相关文章
相关标签/搜索