二叉搜索树和堆

[size=x-small]这周学习了二叉搜索树和堆的原理。
一、二叉搜索树
    1、定义
    二叉搜索树又称二叉查找树,它是一棵空树,或者是一棵具有如下特征的非空二叉树:
    1)若它的左子树非空,则左子树上所有结点的关键字均小于根结点的关键字。
    2)若它的右子树非空,则右子树上所有结点的关键字均大于根结点的关键字。
    3)左右子树本身又各是一棵二叉查找树。
    特殊情况:如果树中允许存在关键字相同的结点,那么1和2两个特征应改为小于等于和大于等于。
    2、二叉搜索树的抽象数据类型和链接存储类
    二叉搜索树的抽象数据类型除了与一般二叉树的抽象数据类型完全相同外,还需要有自己的特殊方法,如查找、更新、插入和删除。
    下面给出二叉搜索树的接口类:
   
    package com.data.tree.po.search;

import com.data.tree.po.BinaryTree;

/**
 * 二叉搜索树接口定义
 * 说明:二叉搜索树的抽象数据类型除了与一般二叉树的抽象数据类型完全相同外,还需要有自己的特殊方法,
 *       如查找、更新、插入、删除元素等方法
 * Created by admin on 2016/7/24.
 */
public interface BinarySearchTree extends BinaryTree {

    /**
     * 从二叉搜索树中查找值为obj的结点,如果存在则返回结点值,否则返回null
     * @param obj
     * @return
     */
    Object find(Object obj);

    /**
     * 从二叉树中更新值为obj的结点,如果更新成功则返回原值否则返回空
     * @param obj
     * @return
     */
    Object update(Object obj);

    /**
     * 向二叉树中插入值为obj的结点,使得插入后仍是一课二叉搜索树,插入成功则返回true,如果遇到相同结点,则插入失败返回false
     * @param obj
     * @return
     */
    boolean insert(Object obj);

    /**
     * 从二叉搜索树中删除值为obj的结点,如果成功则返回true,否则返回false
     * @param obj
     * @return
     */
    boolean delete(Object obj);

    /**
     * 按照关键字从小到大的次序遍历输出一棵二叉搜索树中的所有结点值
     */
    void ascend();
}
    

    实现类,二叉搜索树应该继承一般二叉树的链接存储类和LinkBinaryTree(这个类参考我的上一篇关于二叉树的博文 http://xiaoyun34286136.iteye.com/blog/2313079)并实现二叉搜索树的接口,具体定义如下:
package com.data.tree.po.search;

import com.data.tree.po.BTreeNode;
import com.data.tree.po.LinkBinaryTree;

/**
 * 二叉搜索树的链接存储类需要继承一般二叉树的链接存储类,并实现二叉搜索树的接口类
 * Created by admin on 2016/7/24.
 */
public class LinkBinarySearchTree extends LinkBinaryTree implements BinarySearchTree {

    /**
     * 无参构造器
     */
    public LinkBinarySearchTree(){
        super(); // 初始化为一棵空树
    }

    /**
     * 含参构造器
     * @param root
     */
    public LinkBinarySearchTree(BTreeNode root) {
        super(root); // 把已知二叉搜索树的树根引用赋给root
    }

    /**
     * 从二叉搜索树中查找值为obj的结点,如果存在则返回结点值,否则返回null,
     * 时间复杂度:若该二叉搜索树是一课理想平衡二叉树或者接近理想二叉树,那么进行查找的时间复杂度为0(㏒₂N),N为树的总结点数;
     *             极端情况下,如果为一棵单支树(这是最极端和最坏的情况),则其时间复杂度为O(n)。所以,综合来看,二叉搜索树在查找
     *             上比在集合或者线性表上进行顺序查找的的时间复杂度O(n)要快很多,这也是二叉搜索树的优势所在,当然,建立搜索二叉树花费的时间也要更多
     * 空间复杂度:O(1)
     * @param obj
     * @return
     */
    public Object find(Object obj) {
        if(root == null) {
            return null;
        }
        // 定义局部变量存放迭代结点,初始化为根结点
        BTreeNode node = root;
        while (node != null) {
            if(((Comparable)obj).compareTo(node.element) == 0) { // 查找成功返回结点值
                return node.element;
            }else if(((Comparable)obj).compareTo(node.element) < 0) { // 如果该值小于根结点,那么像左子树查找
                node = node.left;
            }else { // 如果该值大于跟结点,那么查找右子树
                node = node.right;
            }
        }
        return null; // 没有对应的值则返回空
    }

    /**
     * 从二叉树中更新值为obj的结点,如果更新成功则返回原值否则返回空
     * 时间复杂度:0(㏒₂N)
     * 空间复杂度:O(1)
     * @param obj
     * @return
     */
    public Object update(Object obj) {
        // 如果二叉树为空直接返回空
        if (root == null) {
            return null;
        }
        /**
         * 1、将根结点值赋给临时结点
         * 2、根据要更新的结点值查找该结点的位置,如果等于则直接更新该结点值,如果小于该结点值,那么迭代左子树,否则,迭代右子树,如果还是未找到,那么返回空
         */
        BTreeNode node = root;
        while(root != null) {
            int result = ((Comparable)obj).compareTo(node.element);
            if(result == 0) {
                Object target = node.element;
                node.element = obj;
                return target;
            }else if(result < 0) {
                node = node.left;
            } else {
                node = node.right;
            }
        }
        return null;
    }

    /**
     * 向二叉树中插入值为obj的结点,使得插入后仍是一课二叉搜索树,插入成功则返回true,如果遇到相同结点,则插入失败返回false
     * 思路:查找插入位置从根结点开始,若树根指针为空,那么新结点就是根结点;否则,若obj等于根结点,则表明具有obj值的结点已经存在,
     *       不允许进行插入,返回false表示插入失败;若obj小于根结点,则根据根的左指针指向的左子树继续查找插入位置;若obj大于根结点,
     *       则沿着根的右指针在右子树上继续查找插入位置。依次逐层向下查找,当查找到一个结点的左指针或者右指针为空,那么这个空的指针
     *       位置就是obj新结点的插入位置。插入链接完成后返回true
     * 时间复杂度:0(㏒₂N)
     * 空间复杂度:O(1)
     * @param obj
     * @return
     */
    public boolean insert(Object obj) {
        BTreeNode node  = root;
        BTreeNode pt = null;
        int result = 0;
        // 第一步,查找插入位置
        while(node != null) {
            pt = node;
            result = ((Comparable)obj).compareTo(node.element);
            if(result == 0) {
                return false;
            }else if(result < 0) { // 查找左子树
                node = node.left;
            }else {
                node = node.right;
            }
        }
        // 第二步,执行插入操作
        BTreeNode s = new BTreeNode(obj);
        // 如果为空树,那么将该结点作为根结点插入
        if(pt == null) {
            root = s;
        }else if(result < 0) {
            pt.left = s;
        }else {
            pt.right = s;
        }
        return true;
    }

    /**
     * 从二叉搜索树中删除值为obj的结点,如果成功则返回true,否则返回false
     * 思路:分为三种情况
     *      第一种:删除叶子结点,此种操作很简单,只需将其双亲结点链接到它的指针去掉。
     *      第二种:删除单支结点,这种删除操作也比较简单,删除该结点时,只需要将唯一的一个后继指针链接到它所在的链接位置即可
     *      第三种:删除双支结点,这种删除比较复杂,因为待删除的结点有两个后继指针,需要妥善处理。删除这种结点的常用方法是:
     *              首先把它的中序前驱结点(即结点左子树中最右下的一个结点)的值赋给该结点的值域,然后再删除它的中序前驱结点,因为它的中序前驱结点的右指针必为空,
     *              所以只要把中序前驱结点的左指针链接到中序前驱结点所在的链接位置即可。
     * @param obj
     * @return
     */
    public boolean delete(Object obj) {
        if(root == null) {
            return false;
        }
        // 初始化根结点
        BTreeNode node = root;
        // 该结点指向node的双亲
        BTreeNode pt = null;
        // 比较值结果:0-相同 小于零查找左子树,大于零查找右子树
        int result = 0;
        // 从树根开始逐层向下查找待删除的值为obj的结点
        while(node != null) {
            result = ((Comparable)obj).compareTo(node.element);
            if(result == 0) {
                break;
            }else if(result < 0) { // 小于零,查找左子树
                pt = node;
                node = node.left;
            }else { // 大于零,查找右子树
                pt = node;
                node = node.right;
            }
        }
        // 分3中不同情况删除已查找到的结点
        // 1、node为叶子结点
        if(node.left == null && node.right == null) {
            if(node == root) {
                root = null;
            }else if(pt.left == node){
                pt.left = null;
            }else {
                pt.right = null;
            }
        }//2、删除单支结点的情况
        else if(node.left == null || node.right == null) {
            // node为根结点的处理情况
            if(node == root) {
                if (node.left == null) {
                    root = node.right;
                }else {
                    root = node.left;
                }
            }else if(pt.left == node && node.left == null) {
                pt.left = node.right;
            }else if(pt.left == node && node.right == null) {
                pt.left = node.left;
            }else if(pt.right == node && node.left == null) {
                pt.right = node.right;
            }else if(pt.right == node && node.right == null) {
                pt.right = node.left;
            }
        }
        // 3、该结点为双分支节点的处理情况
        else {
            BTreeNode s1 = node;
            BTreeNode s2 = node.left;
            // 验证该结点左孩子的右子树查找中序前驱结点
            while(s2.right != null) {
                s1 = s2;
                s2 = s2.right;
            }
            // 将该结点的中序前驱结点s2的值域赋给node结点
            node.element = s2.element;
            // 删除右子树为空的s2结点,使它的左子树链接到它所在的链接位置
            if(s1 == node) { // 对node的中序前驱结点时node的左孩子时的情况进行处理
                node.left = s2.left;
            }else{
                s1.right = s2.left;
            }
        }
        return true;
    }

    /**
     * 按照关键字从小到大的次序遍历输出一棵二叉搜索树中的所有结点值
     */
    public void ascend() {
        // 通过调用中序遍历方法实现
        traverseBTree("inOrder");
    }
}

    3、测试类
   
import com.data.tree.po.search.BinarySearchTree;
import com.data.tree.po.search.LinkBinarySearchTree;

/**
 * 二叉搜索树测试算法
 * Created by admin on 2016/7/25.
 */
public class Example7_1 {
    public static void main(String[] args) {
        // 1、定义并创建链接存储的空二叉搜索树
        BinarySearchTree bst = new LinkBinarySearchTree();
        // 2、用数组a保存带插入的二叉搜索树的一批整数
        Integer[] a = {23,45,89,40,73,12,49,72,20,44};
        // 3、根据数组a建立链接存储结构的二叉搜索树
        for (Integer i : a) {
            bst.insert(i);
        }
        // 4、以广义表形式输入链接存储结构的二叉搜索树
        System.out.println("二叉搜索树的广义表表示:");
        bst.printBTree();
        System.out.println();
        // 5、前序遍历二叉搜索树
        System.out.println("前序遍历:");
        bst.traverseBTree("preOrder");
        // 6、中序遍历二叉搜索树
        System.out.println("中序遍历:");
        bst.traverseBTree("inOrder");
        // 7、后序遍历二叉搜索树
        System.out.println("后序遍历:");
        bst.traverseBTree("postOrder");
        // 8、层次遍历二叉搜索树
        System.out.println("层次遍历:");
        bst.traverseBTree("levelOrder");
        // 9、按结点值升序输出二叉搜索树的所有结点
        System.out.println("升序输出:");
        bst.ascend();
        // 10、计算二叉搜索树的深度
        System.out.println("二叉树的深度:" + bst.depthBTree());
        // 11、计算二叉搜索树的结点数
        System.out.println("二叉树的结点数:" + bst.countBTree());
        // 12、从二叉搜索树中查找并输出值为38的结点
        System.out.println("查找结点值为38的结果:" + bst.find(38));
        // 13、从二叉搜索树中删除值为73的结点
        System.out.println("删除 73 的结果:" + bst.delete(73));
        // 14、从二叉搜索树中删除值为40的结点
        System.out.println("删除 40 的结点:" + bst.delete(40));
        // 15、从二叉搜索树中删除值为23的结点
        System.out.println("删除 23 的结点:" + bst.delete(23));
        // 16、以广义表的形式重新输出二叉搜索树
        System.out.println("删除73,40,23后,该二叉搜索树的广义表形式:");
        bst.printBTree();
    }
}
    


执行结果:

二叉搜索树的广义表表示:
23(12(,20),45(40(,44),89(73(49(,72)))))
前序遍历:
23 12 20 45 40 44 89 73 49 72
中序遍历:
12 20 23 40 44 45 49 72 73 89
后序遍历:
20 12 44 40 72 49 73 89 45 23
层次遍历:
23 12 45 20 40 89 44 73 49 72
升序输出:
12 20 23 40 44 45 49 72 73 89
二叉树的深度:6
二叉树的结点数:10
查找结点值为38的结果:null
删除 73 的结果:true
删除 40 的结点:true
删除 23 的结点:true
删除73,40,23后,该二叉搜索树的广义表形式:
20(12,45(44,89(49(,72))))

4、应用:
1、信息检索。
2、mysql索引(二叉搜索树的衍生树B树)。

二、堆
    1、堆的定义
       堆分为大根堆和小根堆两种。对于小根堆,是具有如下性质的完全二叉树:
       1)若根结点存在左孩子,则根结点的值小于等于左孩子结点的值。
       2)若根结点存在右孩子,则根结点的值小于等于右孩子结点的值。
       3)以左右孩子为根的子树又各是一个堆。
       对于大根堆,只需把上面的性质的小于等于改为大于等于即可。
    2、堆的接口类
    堆是满足一定条件要求的完全二叉树,需要有自己的插入和删除方法,所以堆的接口类要继承一般二叉树的接口类,同时定义自己的插入和删除。如下所示:
package com.data.tree.po.heap;

import com.data.tree.po.BinaryTree;

/**
 * 堆的定义:堆是具有以下特性的完全二叉树(完全二叉树是指除最后一层外,其余层都是满的,并且最后一层或者是满的,或者是在最右边缺少连续若干个结点),对于小根堆,具有以下特性:
 *           1)若树根结点存在左孩子,则根结点的值小于等于左孩子结点的值。
 *           2)若根结点存在右孩子,则根结点的值也小于等于右孩子结点的值。
 *           3)以左、右孩子为根的子树又各是一个堆。
 *           大根堆的定义与上述类似,只需把条件“小于等于”改为“大于等于”即可。
 * 定义堆的接口,需要有自己的插入和删除方法。
 * Created by admin on 2016/7/26.
 */
public interface Heap extends BinaryTree {
    /**
     * 向堆中插入一个元素,并保持插入后仍然是一个堆
     * @param obj
     */
    void insert(Object obj);

    /**
     * 删除堆顶元素并返回,并保证删除堆顶元素后仍然是一个堆
     * @return
     */
    Object delete();
}

    3、堆的存储结构和顺序存储类,插入、删除和广义表输出算法
      由于堆是一棵完全二叉树,所以适宜用顺序存储,这样既能充分利用存储空间,又便于访问孩子结点和双亲结点。
      具体插入和删除的实现原理和算法,我写在了代码相关方法注释中,堆的存储结构代码定义如下:
package com.data.tree.po.heap;

/**
 * 堆同一般的二叉树一样既可以采用顺序存储,也可以采用链接存储,但是由于堆是一棵完全二叉树,所以适宜采用顺序存储,
 * 这样既能充分利用存储空间,也便于访问孩子节点和双亲结点
 *
 * 顺序存储结构堆类的定义
 * Created by admin on 2016/7/26.
 */
public class SequenceHeap implements Heap {
    /** 数组初始长度 */
    final int minSize = 10;
    /** 数组声明,元素类型为系统根基类 */
    private Object[] heapArray;
    /** 当前堆的实际长度 */
    private int len;

    /**
     * 无参构造器,初始化堆长度为minSize,实际长度为0
     */
    public SequenceHeap(){
        this.len = 0;
        this.heapArray = new Object[minSize];
    }

    public SequenceHeap(int n) {
        if(n < minSize) {
            n = minSize;
        }
        this.len = 0;
        this.heapArray = new Object[n];
    }

    /**
     * 插入步骤:向堆中插入一个元素时,首先将该元素写入到堆尾,即堆中最后一个元素的后面,也就是下标为len的位置上,然后经调整成为一个新堆。由于在原有堆上插入一个新元素后,
     *           可能导致原来以该元素的双亲结点为根的子树不为堆,从而使整个树不为堆,所以必须进行调整使之成为一个堆。调整的方法如下:若新元素小于双亲结点的值,就让它们互换位置,
     *           新元素换到双亲位置后,似的该位置为根的子树成为堆,但新元素依然可能小于此位置的双亲结点的值,从而使以上一层的的双亲结点为根的子树不为堆,还需要按照上述方法继续调整,
     *           这样持续下去,直到以此新位置的双亲结点为根的子树为一个堆或者调整到堆顶为止,此时得到的整个完全二叉树又成为了一个新的堆。
     * @param obj
     * 时间复杂度:此算法的运行时间主要取决于while循环执行的次数,它等于新元素向双亲位置逐层上移的次数,该次数最多等于树的深度减1,所以该算法的时间复杂度为O(log₂N),其中N为堆的大小
     */
    public void insert(Object obj) {
        // 堆满时对堆的顺序存储进行扩容,分配大一倍的空间并进行复制操作
        if(len == heapArray.length) {
            Object[] p = new Object[len*2];
            for(int i = 0; i < len; i++) {
                p[i] = heapArray[i];
            }
            heapArray = p;
        }
        // 向堆尾添加新元素
        heapArray[len++] = obj;
        // 给i初始值,用i指向待调整元素的当前位置
        int i = len - 1;
        // 寻找新元素的最终位置
        while(i != 0) {
            int j = (i-1)/2; // j指向下标为i的元素的双亲结点
            int result = ((Comparable)obj).compareTo(heapArray[j]);
            // 如果该结点的值大于等于双亲结点的值,那么这个堆调整结束,退出循环
            if(result >= 0)
                break;
                // 将新插入的结点的值赋值给双亲结点,双亲结点下移
                heapArray[i] = heapArray[j];
                // 继续向上一层寻找插入位置
                i = j;
        }
        heapArray[i] = obj;
    }

    /**
     * 从堆顶删除元素并返回
     * 思路:从堆顶删除元素就是删除堆顶元素并使之返回。堆顶元素被删除后,留下的堆顶位置应由堆尾元素来填补,这样既保持了顺序存储结构的特点,有不需要移动其他任何元素。
     *       把堆尾元素移动到堆顶位置后,它可能不小于左右孩子结点的值,是整个二叉树不为堆,所以需要一个调整过程,使之变为一个含有n-1个元素的堆。调整过程如下:首先从
     *       根结点开始,若树根结点的值(即原堆尾元素)大于两个孩子结点中的最小值,就将它与最小值的孩子结点互换值,使得根结点的值小于两个孩子结点的值;根结点的值互换后,
     *       在检查新位置的左右孩子的值,如果大于等于其中的最小值,那么仍然需要互换位置,如此,检查对调,直到其左右孩子的最小值大于该点的值。
     * @return
     */
    public Object delete() {
        if(len == 0) {
            return null;
        }
        Object temp = heapArray[0]; // 将堆顶元素暂存temp中以便返回
        len--; // 堆长度减一
        if(len == 0) { // 如果删除后变为空堆,则直接返回
            return temp;
        }
        Object x = heapArray[len];
        int i = 0;
        int j = 1;
        while(j <= len - 1) {
            // 先找出左右孩子的最小值
            // 如果右孩子比较小,应该是j指向右孩子
            if(((Comparable)heapArray[j]).compareTo(heapArray[j+1]) > 0 && j < len - 1) {
                j++;
            }
            if(((Comparable)x).compareTo(heapArray[j]) <= 0){
                break;
            }
            // 把孩子元素移到双亲位置
            heapArray[i] = heapArray[j];
            // 使i和j分别指向下一层结点
            i = j;
            j = 2*i + 1;
        }
        heapArray[i] = x;
        return temp;
    }

    public boolean createBTree(String gt) {
        System.out.println("在堆中不需要使用一般二叉树的建立算法,直接返回假");
        return false;
    }

    public boolean isEmpty() {
        return len == 0;
    }

    public void traverseBTree(String s) {
        System.out.println("在堆中不需要使用一般二叉树的遍历算法!");
    }

    public Object findBTree(Object obj) {
        System.out.println("在堆中不需要使用一般二叉树的查找算法,返回空!");
        return null;
    }

    public int depthBTree() {
        return (int)(Math.log10(len)/ (Math.log10(2))+1);
    }

    public int countBTree() {
        return len;
    }

    public void printBTree() {
        // 以树根元素的下标0为参数调用相应的递归函数进行输出
        output(0);
        System.out.println();
    }

    private void output(int ht) {
        if(ht < len) {
            System.out.print(heapArray[ht]);
            if(2*ht+1 < len) {
                System.out.print('(');
                output(2*ht+1);
                if(2*ht+2 < len) {
                    System.out.print(',');
                    output(2*ht+2);
                }
                System.out.print(')');
            }
        }
    }

    public void clearBTree() {
        len = 0;
        heapArray = null;
    }
}

测试类:
import com.data.tree.po.heap.Heap;
import com.data.tree.po.heap.SequenceHeap;

import java.util.Arrays;

/**
 * 堆算法测试类
 * Created by admin on 2016/7/29.
 */
public class Example7_2 {
    public static void main(String[] args) {
        // 1、定义并创建一个空堆hp
        Heap hp = new SequenceHeap();
        // 2、用数组a保存带插入堆中的一批整数
        Integer[] a = {23,45,89,40,73,12,49,72,20,44,86,33};
        System.out.println("建立堆的原始数据:");
        System.out.println(Arrays.toString(a));
        // 3、根据数组a建立一个堆hp
        for (int i : a) {
            hp.insert(i);
        }
        // 4、以广义表形式输出堆hp
        System.out.println("堆的广义表表示形式:");
        hp.printBTree();
        // 5、得到hp的深度
        System.out.println("堆的深度:" + hp.depthBTree());
        // 6、得到hp中元素的个数
        System.out.println("堆中的元素个数:" + hp.countBTree());
        // 7、依次删除堆hp中的元素并输出
        while(!hp.isEmpty()) {
            System.out.print(hp.delete() + ",");
        }
        System.out.println();
    }
}

运行结果:
建立堆的原始数据:
[23, 45, 89, 40, 73, 12, 49, 72, 20, 44, 86, 33]
堆的广义表表示形式:
12(20(40(72,45),44(73,86)),23(33(89),49))
堆的深度:4
堆中的元素个数:12
12,20,23,33,40,44,45,49,72,73,86,89,

    4、堆的应用场景
       1)在海量数据分析中,找前n大,前n小数据等。
       2)用堆合并多个有序数组

总结:二叉搜索树和堆是学习哈夫曼树、B树、红黑树的基础,也是想深入研究数据结构的基础知识,必经之路,努力吧,小伙子,说给我自己听,也说给努力的人,对数据结构有兴趣同学可以加我好友,一起学习探讨,为了更好的明天。我的qq号:391720181。

下次博文将介绍哈夫曼树,平衡二叉树,B树和红黑树的学习情况和原理,代码可能不写了。[/size]

猜你喜欢

转载自xiaoyun34286136.iteye.com/blog/2314596