算法导论-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)$ 。

posted @ 2013-01-09 08:51 一叶斋主人 阅读(...) 评论(...) 编辑 收藏