zoukankan      html  css  js  c++  java
  • 算法导论7.二叉查找树

    树几乎是使用最广泛地数据结构之一了,我想其原因是,在期望高度仅为 $lg n$ 的结构中存储了 $n$ 个元素。树中的每个操作,在每一层都使用了极具有价值的判断并降低了问题的复杂度,这几乎完美地体现了分治的思想。还有,从这篇博文开始,我开始用svg作为示意图而不是以前使用的png,这东西很符合我的……,呃,“理念”,唯一的问题就是创建起来有点麻烦……嗯。

    二叉查找树

    二叉查找树是一种既可以做优先级队列,又可以作散列表的数据结构,一般用链表实现。由于二叉查找树的实现代码较多,所以将其拆分为几个部分罗列。

    二叉树的每个节点,除了数据域,还有三个指针,分别指向父节点、左节点和右节点。二叉查找树最重要的性质是:某一个节点左子树所有节点值小于节点自身的值,右子树反之。这个性质保证了树中的每个元素能被容易查找到。

    template <typename T> class xBinaryTreeNode{
    public:
        xBinaryTreeNode(T input);
        T data;
        xBinaryTreeNode<T>* leftChild;
        xBinaryTreeNode<T>* rightChild;
        xBinaryTreeNode<T>* father;
    };
    template <typename T> xBinaryTreeNode<T>::xBinaryTreeNode(T input){
        data = input;
        leftChild = NULL;
        rightChild = NULL;
        father = NULL;
    }

    二叉树通过自己的属性获得树的头结点。我的实现里,head属性就是指向真实头结点(而不是哨兵节点)的指针。二叉树提供这样七个操作:构造二叉树,插入一个元素,删除一个节点,查询一个元素,获取二叉树中的最大/最小值,中序遍历输出二叉树。私有函数是提供给公有函数调用的,一般的情况是这样:私有函数需要传递一个节点作为参数,并对参数节点的左/右节点递归调用自身,而公有函数调用私有函数并将head作为参数传入。值得一提的是私有函数successor和predecessor,他们分别返回节点的后趋和前趋节点。

    template <typename T> class xBinarySearchTree{
    public:
        xBinarySearchTree(T dv);    // dv -> default Null Value
        bool insertValue(T value);
        bool deleteNode(xBinaryTreeNode<T>* node);
        void inorderTreeWalk();
        xBinaryTreeNode<T>* treeSearch(T value);
        T treeMin();
        T treeMax();
    private:
        xBinaryTreeNode<T>* head;
        T dValue;
        void inorderTreeWalk(xBinaryTreeNode<T>* headNode);
        xBinaryTreeNode<T>* treeSearch(T value, xBinaryTreeNode<T>* headNode);
        xBinaryTreeNode<T>* treeMinNode(xBinaryTreeNode<T>* headNode);
        xBinaryTreeNode<T>* treeMaxNode(xBinaryTreeNode<T>* headNode);
        xBinaryTreeNode<T>* successor(xBinaryTreeNode<T>* node);
        xBinaryTreeNode<T>* predecessor(xBinaryTreeNode<T>* node);
    };
    template <typename T> xBinarySearchTree<T>::xBinarySearchTree(T dv){
        head = NULL;
        dValue = dv;
    }

    二叉树的中序遍历是最简单的递归调用:先输出左子树,再输出自身值,最后输出右子树值。中序遍历得到的数值是排好序的,因为对每一个节点,比它小的节点都在前面(左子树中),比它小的节点反之。二叉树的插入和查询操作是很简单的递归操作,但是在每个节点处需要判断待查询/插入的元素比该节点大或小。这里我的实现,插入函数没有使用递归,而是用的迭代。同样,迭代可以用来获得最大值和最小值(从根节点开始,一直走左子树/右子树)。下面这部分代码虽然长,但都很简单,很容易理解。

    template <typename T> void xBinarySearchTree<T>::inorderTreeWalk(){
        inorderTreeWalk(head);
    }
    template <typename T> void xBinarySearchTree<T>::inorderTreeWalk(xBinaryTreeNode<T>* headNode){
        if (headNode == NULL){return;}
        inorderTreeWalk(headNode->leftChild);
        cout<<headNode->data<<",";
        inorderTreeWalk(headNode->rightChild);
    }
    template <typename T> bool xBinarySearchTree<T>::insertValue(T value){
        xBinaryTreeNode<T>* tmp = head;
        xBinaryTreeNode<T>* father = NULL;
        xBinaryTreeNode<T>* child = new xBinaryTreeNode<T>(value);
        while(tmp!=NULL){
            father = tmp;
            tmp = (tmp->data>=value) ? tmp->leftChild : tmp->rightChild;
        }
        if (father == NULL){head = child;}
        else{
            if (father->data>=value){father->leftChild = child;}
            else{father->rightChild = child;}
            child->father = father;
        }
        return true;
    }
    template <typename T> xBinaryTreeNode<T>* xBinarySearchTree<T>::treeSearch(T value){
        return treeSearch(value, head);
    }
    template <typename T> xBinaryTreeNode<T>* xBinarySearchTree<T>::treeSearch(T value, xBinaryTreeNode<T>* headNode){
        xBinaryTreeNode<T>* tmp = headNode;
        if (tmp == NULL){return NULL;}
        if (tmp->data == value){return tmp;}
        if (tmp->data < value){return treeSearch(value, tmp->rightChild);}
        if (tmp->data > value){return treeSearch(value, tmp->leftChild);}
        return NULL;
    }
    template <typename T> T xBinarySearchTree<T>::treeMin(){
        xBinaryTreeNode<T>* tmp = treeMinNode(head);
        if (tmp != NULL){return tmp->data;}
        else{return dValue;}
    }
    template <typename T> T xBinarySearchTree<T>::treeMax(){
        xBinaryTreeNode<T>* tmp = treeMaxNode(head);
        if (tmp != NULL){return tmp->data;}
        else{return dValue;}
    }
    template <typename T> xBinaryTreeNode<T>* xBinarySearchTree<T>::treeMinNode(xBinaryTreeNode<T>* headNode){
        if (headNode == NULL){return NULL;}
        xBinaryTreeNode<T>* tmp = headNode;
        while (tmp->leftChild != NULL){tmp = tmp->leftChild;}
        return tmp;
    }
    template <typename T> xBinaryTreeNode<T>* xBinarySearchTree<T>::treeMaxNode(xBinaryTreeNode<T>* headNode){
        if (headNode == NULL){return NULL;}
        xBinaryTreeNode<T>* tmp = headNode;
        while (tmp->rightChild != NULL){
            tmp = tmp->rightChild;
        }
        return tmp;
    }

    删除一个节点的操作比较复杂。 大致是这样一个思路:如果没有子树,直接删掉;只有一个子树,就把子树直接接在父节点上;如果有两个子树(麻烦来了),就把待删除节点的直接后趋节点(递归地)删掉,然后把删掉的节点里面值恢复到待删除的节点里,覆盖原来的值。

    template <typename T> bool xBinarySearchTree<T>::deleteNode(xBinaryTreeNode<T>* node){
        if (node->leftChild == NULL && node->rightChild==NULL){
            if (node->father == NULL){head = NULL;}
            else if (node->father->leftChild == node){node->father->leftChild = NULL;}
            else{node->father->rightChild = NULL;}
        }
        else if (node->leftChild == NULL && node->rightChild != NULL){
            if (node->father == NULL){head = node->rightChild;}
            else if (node->father->leftChild == node){node->father->leftChild = node->rightChild;}
            else{node->father->rightChild = node->rightChild;}
        }
        else if (node->leftChild != NULL && node->rightChild == NULL){
            if (node->father == NULL){head = node->leftChild;}
            else if (node->father->leftChild == node){node->father->leftChild = node->leftChild;}
            else{node->father->rightChild = node->leftChild;}
        }
        else{
            xBinaryTreeNode<T>* tmpNode = successor(node);
            T tmpValue = tmpNode->data;
            deleteNode(tmpNode);
            node->data = tmpValue;
        }
        return true;
    }

    这就涉及到如何求一个节点的直接后趋节点的问题。大致思路是:如果有右子树,直接后趋节点就是右子树里的最小值节点(子树里的最小节点上面已经说过怎么求了);如果没有右子树(事实上删除节点时不需要用到这个逻辑,因为既然要求直接后趋了,待删除的必然是两个子树都有的节点),麻烦来了,就要去找一个最近的祖先节点作为直接后趋,怎样的祖先节点呢?离该节点最近的,而且该节点属于这个祖先节点的左子树的祖先节点(不知道你懂了没)。我相信下面这个svg图形能给你有直观的印象,看,13的直接后趋节点是15。

    15 6 18 3 7 17 13

    template <typename T> xBinaryTreeNode<T>* xBinarySearchTree<T>::successor(xBinaryTreeNode<T>* node){
        if (node->rightChild != NULL){return treeMinNode(node->rightChild);}
        else if (node->father != NULL){
            xBinaryTreeNode<T>* tmp = node;
            while(tmp->father!=NULL){
                if (tmp->father->leftChild == tmp){
                    return tmp->father;
                }
                tmp = tmp->father;
            }
            return node;
        }
        else{return NULL;}
    }
    template <typename T> xBinaryTreeNode<T>* xBinarySearchTree<T>::predecessor(xBinaryTreeNode<T>* node){
        if (node->leftChild != NULL){return treeMaxNode(node->leftChild);}
        else if (node->father != NULL){
            xBinaryTreeNode<T>* tmp = node;
            while(tmp->father!=NULL){
                if (tmp->father->rightChild == tmp){
                    return tmp->father;
                }
                tmp = tmp->father;
            }
            return node;
        }
        else{return NULL;}
    }

    练习12.1-3 给出两种非递归的中序树遍历方法,较简单的一种使用栈作为辅助,另一种只能用固定大小的空间。见10.4-3。

    练习12.2-4 某教授认为他发现了一个二叉查找树的性质,即对某个关键字 $k$ 的查找在一个叶节点处结束,查找产生了一条路径。路径左侧的节点,路径上的节点,路径右侧的节点分别为集合 $A$,$B$,$C$ ,则对于任意的 $a\in A$,$b\in B$ 和 $c\in C$,有 $a\leq b \leq c$ 。请给出一个反例。

    反例就是:如上面解释直接后趋节点求法的图中,如果查找的关键字 $k=4$ ,取 $c=7$ 和 $b=15$ 。

    练习12.3-4 假设另一个数据结构中包含指向二叉查找树中某节点y的指针,并假设用过程TREE-DELETE删除y的前趋z,这样做会出什么问题,应当如何修改该过程以避免该问题。

    会使“另一个数据结构”中的指针无效,可以修改TREE_DELETE过程中,y节点的指针、z节点的父节点、左节点和右节点的相应指针来使z节点本身替代y节点而不是将z节点中的值拷贝到y节点中。

    思考题12-2 基数树。给定两个串 $a=a_{1}a_{2}...a_{p}$ 和 $b=b_{1}b_{2}...b_{q}$ ,其中每一个 $a_{i}$ 和 $b_{j}$ 都是一个位,比如 $a=10100$ 而 $b=101001$。利用基数树对两个串进行字典排序。

    思路:基数树的根节点表示空串,左子结点表示比父节点(设为串 $f$)多一位的且多出的一位为0的节点 $f0$,右子树 $f1$ ,比如串 $100$ 位于根节点的右子结点的左子结点的左子结点。因此对其字典排序就很简单:将其插入基数树,然后按照前序输出就可以了。

    思考题12-3 随机构造的二叉查找树中的平均节点深度。证明在一棵随机构造(通过随机的顺序插入 $n$ 个不同元素而构造)的二叉查找树中,$n$ 个节点的平均深度为 $O(\lg n)$ 。定义二叉树 $T$ 中所有节点的深度之和为 $P(T)$ ,某个节点 $x$ 的深度为 $d(x,T)$

    1. 很显然的,二叉树中节点的平均深度:
      $$\frac{1}{n}\sum_{x\in T}d(x,T)=\frac{1}{n}P(T)$$
    2. 假设 $T_{L}$ 和 $T_{R}$ 为树 $T$ 的左右子树。$T$ 的 $n$ 个节点,除去根节点,左右子树中共有 $n-1$ 各节点,每个节点在子树 $T_{L}$ 或 $T_{R}$ 中的深度比在树 $T$ 中的深度小1,所以:
      $$P(T)=P(T_{L})+P(T_{R})+n-1$$
    3. 二叉查找树是随机构造的,也就是说,含有 $n$ 个节点的树 $T$ 的根节点在这 $n$ 个节点中的顺序为 $1,2,3...n$ 都是等可能的,概率为 $1/n$ 。所以平均路径总长度为:
      $$P(n)=\frac{1}{n}\sum_{i=0}^{n-1}(P(i)+P(n-i-1)+n-1)$$
      考虑对称性,上式可写为:
      $$P(n)=\frac{2}{n}\sum_{k=1}^{n-1}P(k)+\Theta(n)$$
    4. 这里希望证明$P(n)\leq \Theta(\lg n)$。先假设结论 $P(n)\leq \Theta(\lg n)$ 对某个 $P(n)$ 成立,则有 $P(n)\leq ak\lg k+b$,代入 $P(n)$ 的表达式得到:
      $$P(n)\leq \frac{2}{n}\sum_{k=2}^{n-1}(ak\lg k+b)+\Theta(n)=\frac{2a}{n}\sum_{k=2}^{n-1}k\lg k+\frac{2b}{n}(n-2)+\Theta(n)$$
      先考虑这样一个式子(一会再去证明):
      $$\sum_{k=2}^{n-1}k\lg k\leq \frac{1}{2}n^{2}\lg n+\frac{1}{8}n^{2}$$
      有:
      $$P(n)\leq \frac{2a}{n}(\frac{1}{2}n^{2}\lg n-\frac{1}{8}n^{2})+\frac{2b}{n}(n-2)+\Theta(n)\leq an\lg n+b$$
      得证。
    5. 再补充证明一下这个式子:
      $$\sum_{k=2}^{n-1}k\lg k\leq \frac{1}{2}n^{2}\lg n+\frac{1}{8}n^{2}$$
      将 $\sum_{k=2}^{n-1}k\lg k$ 分成两个部分:
      $$\sum_{k=2}^{n-1}k\lg k<\lg(\frac{n}{2})\sum_{k=2}^{n/2-1}k+\lg n\sum_{k=n/2}^{n-1}k=\lg n\sum_{k=2}^{n-1}k-\sum_{k=2}^{n/2-1}k$$
      代入求和公式,并考虑 $k=1$ 的情况,有
      $$\sum_{k=2}^{n-1}k\lg k\leq \frac{1}{2}n(n-2)\lg n-\frac{1}{2}(\frac{n}{2}-1)\frac{n}{2}\leq \frac{1}{2}n^{2}\lg n+\frac{1}{8}n^{2}$$

    这样整道题目就证明了随机构造二叉树的高度的期望至多为 $\Theta(\lg n)$ 。

    作者:一叶斋主人
    出处:www.cnblogs.com/yiyezhai
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    编写程序,验证string是ipV4地址
    TCP三次握手和四次挥手
    链表和数组的区别
    cookie和session的区别
    GET和POST的区别
    TCP和UDP的区别
    java HashMap和Hashtable的区别
    java 堆和栈的区别
    最小栈的实现
    关于几个位运算的算法分析
  • 原文地址:https://www.cnblogs.com/yiyezhai/p/2849168.html
Copyright © 2011-2022 走看看