数据结构与算法之八(二叉树)

旁白:对于二叉树相信都已经非常熟悉了,这里不介绍基本概念。

》为什么使用二叉树?

旁白:这个话题很犀利,我以前没注意到。书中做了如下解释:
我们前面聊的都是排序算法,见过数组和链表。有序数组通过二分查找可以将效率提升到O(logN),但是插入的时候需要移动很多元素,效率过低。而链表则正好相反,插入的时候效率很高,改一下节点指向就好,但是查找的时候只能顺藤摸瓜,效率很低。

使用二叉树,可以结合两者的优势,插入时依照链式存储的特点,改下指向就行,查找时可以使用二分查找的特性。

》二叉搜索树
一个二叉树所有的左子节点的关键字值小于这个节点,右子节点的关键字值大于或等于这个节点。这种树就称为二叉搜索树。

》二叉搜索树的随机性

旁白:很明显,根据二叉搜索树的定义,我们可以将数据有序的保存下来,然后通过中根遍历得到有序的数列。然而这种树根据插入时的顺序不一样,得到的树并不唯一。
比如:2,1,3,我们依次插入会得到以下树状结构

        2
      /  \
     1     3

如果是:1,2,3,我们依次插入会得到以下树状结构

        1
         \
          2
           \
            3

然而根据中根遍历最后的结果都是:1,2,3。第二种树称为非平衡树。
平衡树:即平衡二叉树,又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡树,左子节点与右子节点对称。
当序列基本有序,更容易出现非平衡二叉树

》二叉搜索树的java实现

旁白:当前代码是我手打出来的,未经验证,如果发现有问题可以联系我,请勿直接使用。

1)首先要存储每个节点,实际中的节点可能是很复杂的,可以通过定义Data类

class Data{
      int sortKey;   //比如依据这个关键字的值来比较大小
      ...        //其他数据
}

当然也可以通过实现Comparable接口来实现类对象的比较,这个java基础不讲了。

同时,每个节点有左子节点和右子节点的引用,当没有时指向null,所以Node的结构如下

class Node{
    Data mData;   //数据部分
    Node leftChild;   //左子节点
    Node rightChild;  //右子节点
    Node parent;  //当然也可以保存父节点,视具体需要

    //提供get方法方便调用
    getData
    getLeftChild
    getRightChild
    getParent
}

2)其次,我们要实现二叉树的算法:
对于一颗树,我们要维护一个根节点,通过根节点顺藤摸瓜就能得到整个树。

class Tree{
    Node root;   //指向树的根节点

    //其他方法需要有
    //1.根据Node数组或者其他集合构建树
    createTree(Node[])
    //2.增
    addNode(Node)
    //3.删
    delNode(int key)
    //4.改
    updateNode(Node)
    //5.查
    findNode(int key)
    //6.打印整颗树
    printTree()
    //其他方法根据具体需要
}

2.1) 构建树

void createTree(Node[] nodeArr){
    //对数组遍历
    for(Node n:NodeArr){
        if(root==null){       //第一个节点作为根节点
            root = n;
        }else{      //后面的直接调用添加节点
            addNode(n);
        }
    }
}

2.2)添加节点

//依次比较,二叉搜索过程:从根开始,先比较当前节点,如果比当前小,则找到当前节点的左子节点,否则找到当前节点的右子节点。然后循环,直到找不到下一个节点,如果比当前小,则将其插入左子节点,否则插入右子节点。
void addNode(Node node){
    //保存当前节点
    Node current = root;
    int key = node.getData().getKey();
    while(true){
        //缓存父节点
        Node parent = current;
        int currentKey = current.getData().getKey();
        if(key<currentKey){
            current = current.getLeftChild();
            if(current==null){     //结束战斗
                parent.setLeftChild(node);
                return;  
            }
        }else{
            current = current.getRightChild();
            if(current==null){     //结束战斗
                parent.setRightChild(node);
                return;  
            }
        }
    }
}

旁白:从这个插入过程可以看出我这个排序过程是稳定的,比如3(1),3(2),3(3),由于大于等于的时候都会去找右子节点,所以排序后中根遍历结果仍是3(1),3(2),3(3)。

2.3)删除节点

旁白:删除节点过程比较复杂,需要多点讨论
2.3.1)删除节点无子节点,这个好办直接将其父节点的对应引用置空就行
2.3.2)删除节点仅有一个子节点,这个也好办,子承父业,让它顶了原来它爹的位置
2.3.3) 删除节点有两个子节点,涉及到两个问题,谁来顶上,顶上之后,另外一个子节点怎么接上。
考虑下面这颗树:

          a
        b     c
      d  e  f   g

加入删除a,谁来顶上?b?or c?其实都可以,想想我们说的树的结构是随机的,谁顶上都可以通过后期调整成二叉搜索树,但是最佳答案是e或者f。为啥呢?考虑b顶上的话,节点e的位置将重新调整这个过程有点复杂。但是如果我把a的左子树中的最大一个,也就是e顶上,那么,a的左子树和右子树完全可以按照原来的结构放在e下面,不用调整。结构变为

           e
        b     c
      d     f   g

同理,f也是可以的。结构变为:

           f
        b     c
      d  e      g

总结:有两个子树的情况下,选取左子树中的最大节点,或者右子树的最小节点顶替,如此结果最佳,选择一种就行。用代码来实现就是遍历左子树从根开始找到右子节点的右子节点的…直到最后一个。下面来实现一下:

void delNode(int key){
    //首先找到关键字的值为key的节点
    Node current = root;
    while(true){
        //缓存父节点
        Node parent = current;
        int currentKey = current.getData().getKey();
        int isLeftChild = 0; //0为根节点,1为左,2为右子节点
        if(key==currentKey){     //找到了
            //该节点即将被删除,缓存两个子节点
            Node leftChild = current.getLeftChild();
            Node rightChild = current.getRightChild();
            //1.两子节点都为空  2.两子节点有一个为空
            if(leftChild==null || rightChild==null){
        Node child = (leftChild==null)?rightChild:leftChild;
                switch(isLeftChild){
                    case 0:    //当前是根节点
                         root = child;
                         break;
                    case 1:    //当前是父节点的左子节点
                         parent.setLeftChild(child);
                         break;
                    case 2:   //当前是父节点的右子节点
                         parent.setRightChild(child);
                         break; 
                }
            }else{//3.两个子节点都不为空
                Node max = leftChild;
                Node maxParent = current;   //缓存其父节点
                boolean isRightChild = false;   
                //循环查找左子树中的最大节点
                while(max.getRightChild()!=null){
                    maxParent = max;
                    max = max.getRightChild();
                    isRightChild = true;
                }
                //断开原来的联系
                if(isRightChild){
                    maxParent.setRightChild(null);
                }else{
                    maxParent.setLeftChild(null);
                }
                //将节点放入删除节点位置
                max.setLeftChild(leftChild);
                max.setRightChild(rightChild);
                switch(isLeftChild){
                    case 0:    //当前是根节点
                         root = max;
                         break;
                    case 1:    //当前是父节点的左子节点
                         parent.setLeftChild(max);
                         break;
                    case 2:   //当前是父节点的右子节点
                         parent.setRightChild(max);
                         break; 
                }   
            }
        }else if(key<currentKey){   //未找到,继续左子树查找
            current = current.getLeftChild();
            isLeftChild=1if(current==null){     //不存在,结束战斗
                return;  
            }
        }else{   //未找到,继续左子树查找
            current = current.getRightChild();
            isLeftChild=2if(current==null){     //不存在,结束战斗
                return;  
            } 
        }
}

2.4)2.5)更新和查找的代码其实最终依赖查找。而查找我们在上述过程中其实已经实现了,这里不写了。

2.6) 打印树,这里实现一个中根遍历,简单点用递归,打印左子树,打印中根,打印右子树,完成。

void printTree(Node node){
    //打印左子树
    printTree(node.getLeftChild());
    //打印中根
    if(node!=null){
        syso(" "+node.getData().toString());
    }
    //打印右子树
    printTree(node.getRightChild());
}

至此,二叉搜索树的java实现基本完成。

》二叉搜索树的效率:很明显对于查找的效率,因为树的结构是随机的,导致查找的效率也是随机的。如果是满二叉树可能还好点o(logN),像上面提到的一边倒的树,其实就是链表,效率很低o(N)。但是对于插入来说,效率还是很高的,没有什么移动成本。

》当然树也可以用其他结构存储,比如数组。所以表面上是一个数组,实际上是一个树并不奇怪,还记得我们前面第三节讲优先级队列的时候里面有一段神秘代码吗?其实就是一个树。

猜你喜欢

转载自blog.csdn.net/kkae8643150/article/details/78606984