《算法导论》12.3节习题

  • 12.3-1 二叉搜索树insert操作的递归版本
void insert1(Node* pRoot, Node* pAdd)
{
    bool bLeft = pAdd->key < pRoot->key;
    Node* pNextRoot = bLeft ? pRoot->left : pRoot->right;
    if(pNextRoot)
        insert1(pNextRoot, pAdd);
    else
    {
        pAdd->parent = pRoot;
        if(bLeft)
            pRoot->left = pAdd;
        else
            pRoot->right = pAdd;
    }
}
  • 12.3-2 insert过程途经n个节点后遇到了空节点,便将待插入的元素安放上去了,接下来的search过程先路过同样的n个节点,最后与第n+1个节点比较发现相同,于是search完毕。

  • 12.3-3 由12.3-2可得,insert过程和search过程一样,是O(h)-time的,h代表树的高度。

    设一共有n个节点,构建一棵树需要调用n次insert,这个过程耗时O(nh),后续的中序遍历耗时是O(n)。所以排序过程的时间复杂度是由构建过程决定的。

    最坏的情况集合按照正序或者倒序排列,则h = n,总时间是O(n^2)。

    最好情况是集合按照层次遍历顺序排列,最后构建成一棵完全二叉树,h = lg(n) ,总时间为O(n*lg(n))。

  • 12.3-4 从同一棵二叉搜索树上先删除x再删除y,和先删除y再删除x,最终得到的树一定是一样的吗?

    不一定,举个反例:

//现在有一棵树,上面有1,2,3,4四个节点:
        2
    /        \
1            4
            /
           3
//如果 先删除1 再删除2,结果是:
        2                            2                            4
    /        \                            \                        /
1            4        -->                4        -->       3
            /                              /
           3                            3
//如果 先删除2 再删除1,结果是:
        2                                3                    3
    /        \                          /    \                     \
1            4        -->         1        4        -->        4
            /
           3
//得到的结果是不一样的。

下面说说,我是怎样想到这个反例的。

要删除x,根据删除的规则,如果x没有孩子,直接删除;如果x只有一个孩子,就用唯一的孩子顶替x的位置;如果x有两个孩子,就将x的后继节点s顶替x的位置。

从这个规则中可以发现,x有几个孩子会影响到x的接班人人选。如果删除的顺序可以影响到删除x时候x的孩子个数,就会影响到最终树的形状。

那么,y在x的什么位置上,删除y会对x孩子个数造成影响呢?y本身就是x的一个孩子,而且y没有孩子。下面分左孩子和右孩子讨论。因为x只有一个孩子y的情况太简单,也不能成为反例,不做讨论,下面对x有两个孩子的情况进行分析。称以x为根的树为X树。

如果y是x的左孩子,先删y,删除之后,x的孩子个数变成1,此时删除x,X树被其右子树取代,X树的根变为x的右孩子。反过来,先删除x,此时x有两个孩子,X树的根变为x的后继,只要其后继不是它的右孩子本身,那么结果就是不一样的。这也就是上面给出的反例。

如果y是x的右子树,先删y,再删x,X树被x左子树取代,先删x,y顶替x的位置,再删y,X树仍然被x的左子树取代,结果是一样的,不能作为反例。

还有一种可能,删除x,x的接班人是自己的后继s,原以s为根的树S会发生改变,从S树上节点能否找出一个y作为反例,我暂时没有想清楚。

  • 12.3-5 二叉搜索树的每个节点保存“后继”,“左孩子”,“右孩子”三个属性,在O(h)时间内实现insert delete search。(这道题在《算法导论》第三版的中文版翻译有误)

    为什么要把“父亲”属性替换成“后继”属性呢?相比保存“父亲”,保存“后继”属性的优势在于查找后继节点时间O(1),排序虽然都是O(n)但是常数项较小。执行这两种操作时,性能相当于单向链表。而执行插入、删除、查找操作时,性能相当于二叉树。

    我们先来总结一下这些操作需要读写哪些属性。

    insert操作需要修改的有:父亲节点的孩子属性,前驱节点和新插入节点的后继属性

    delete操作需要修改的有:父亲节点的孩子属性,前驱节点的后继属性

    search操作需要读取的有:节点的孩子属性

    根据上面的总结可以发现,这道题的关键点是在O(h)时间内找到父亲节点和前驱节点。

    查找父亲节点的做法是从Root向下逐级查找;如果当前节点没有左孩子,那么查找前驱节点的做法也是从Root向下查找,可以和父亲节点的查找工作合并起来。如果当前节点有左孩子,那么前驱节点是左孩子的最大节点。

#include <iostream>
#include <cassert>
using namespace std;
struct Node
{
    int key;
    Node* succ;
    Node* left;
    Node* right;
    Node(int k):key(k),succ(nullptr),left(nullptr),right(nullptr){}
};

Node* minimum(Node* pRoot);
Node* parent_pred(Node* pRoot, int key, Node*& pPred);//为新插入节点,找父亲的同时从父亲中找前驱

void insert(Node* pRoot, int key)
{
    Node* pNew = new Node(key);
    Node* pPred;
    Node* pParent = parent_pred(pRoot, key, pPred);
    Node* pHead = minimum(pRoot);
    //upate parent's child
    if(key < pParent->key)
        pParent->left = pNew;
    else
        pParent->right = pNew;
    //update pPred's succ and pNew's succ
    if(pPred)
    {
        pNew->succ = pPred->succ;
        pPred->succ = pNew;
    }
    else
    {
        pNew->succ = pHead;//注意:这个头结点一定要在插入之前获取
    }
}

Node* pred(Node* pNode);                                                            //从左子树上找前驱
Node* parent(Node* pRoot, Node* pNode);                                    //找父亲
Node* parent_pred(Node* pRoot, Node* pNode, Node*& pPred);    //为已有节点,找父亲的同时从父亲中找前驱
void Delete(Node*& pRoot, Node* pDelete)
{

    Node* pDeleteReplace = nullptr;

    if(!pDelete->left)
    {
        pDeleteReplace = pDelete->right;//no child    //only right
    }
    else
    {
        pDeleteReplace = pDelete->left; //only left
        if(pDelete->right)              //both
        {
            Node* pSucc = pDelete->succ;
            if(pSucc != pDelete->right)
            {
                Node* pSuccParent = parent(pRoot, pSucc);
                if(pSucc->key < pSuccParent->key)
                    pSuccParent->left = pSucc->right;
                else
                    pSuccParent->right = pSucc->right;
                pSucc->right = pDelete->right;
            }
            pSucc->left = pDelete->left;
            pDeleteReplace = pSucc;
        }
    }
    //update parent's child, pred's succ
    Node* pPred;
    Node* pDeleteParent = parent_pred(pRoot, pDelete, pPred);
    bool bLeft;
    if(pDeleteParent)
        bLeft = pDeleteParent->left == pDelete;

    if(pDeleteParent)
    {
        if(bLeft)
            pDeleteParent->left = pDeleteReplace;
        else
            pDeleteParent->right = pDeleteReplace;
    }
    else
    {
        pRoot = pDeleteReplace;
    }

    if(pPred)
    {
        pPred->succ = pDelete->succ;
    }
}

Node* search(Node* pRoot, int key)
{
    Node* pCurrent = pRoot;
    int keyCurrent;
    while(pCurrent)
    {
        keyCurrent = pCurrent->key;
        if(key == keyCurrent)
            break;
        if(key < keyCurrent)
            pCurrent = pCurrent->left;
        else
            pCurrent = pCurrent->right;
    }
    return pCurrent;
}

Node* minimum(Node* pRoot)
{
    Node* pMin = pRoot;
    while(pMin->left)
    {
        pMin = pMin->left;
    }
    return pMin;
}

Node* parent_pred(Node* pRoot, int key, Node*& pPred)
{
    Node* pCurrent = pRoot;
    Node* pParent = nullptr;
    pPred = nullptr;
    while(pCurrent)
    {
        pParent = pCurrent;
        if(key < pCurrent->key)
        {
            pCurrent = pCurrent->left;
        }
        else
        {
            pCurrent = pCurrent->right;
            pPred = pParent;
        }
    }
    return pParent;
}

Node* parent(Node* pRoot, Node* pNode)
{
    Node* pCurrent = pRoot;
    Node* pParent = nullptr;
    while(pNode != pCurrent)
    {
        assert(pCurrent);
        pParent = pCurrent;
        if(pNode->key < pCurrent->key)
            pCurrent = pCurrent->left;
        else
            pCurrent = pCurrent->right;
    }
    return pParent;
}

Node* parent_pred(Node* pRoot, Node* pNode, Node*& pPred) 
{
    Node* pCurrent = pRoot;
    Node* pParent = nullptr;
    pPred = nullptr;
    while(pNode != pCurrent)
    {
        assert(pCurrent);
        pParent = pCurrent;
        if(pNode->key < pCurrent->key)
            pCurrent = pCurrent->left;
        else
        {
            pCurrent = pCurrent->right;
            pPred = pParent;
        }
    }
    return pParent;
}

Node* pred(Node* pNode) 
{
    Node* pPred = pNode->left;
    while(pPred->right)
        pPred = pPred->right;
    return pPred;
}


void walk(Node* pRoot)
{
    Node* pCurrent = minimum(pRoot);
    while(pCurrent)
    {
        cout << pCurrent->key << "\t";
        pCurrent = pCurrent->succ;
    }
    cout << endl;
}

void test()
{
    //build
    Node* pRoot = new Node(4);
    insert(pRoot, 2);
    insert(pRoot, 5);
    insert(pRoot, 1);
    insert(pRoot, 3);
    insert(pRoot, 7);
    insert(pRoot, 6);
    insert(pRoot, 8);
    walk(pRoot);
    //search
    cout << search(pRoot, 3)->key << endl;
    cout << search(pRoot, 6)->key << endl;
    cout << search(pRoot, 4)->key << endl;
    //delete
    Delete(pRoot, pRoot->right);//delete 5
    Delete(pRoot, pRoot->left); //delete 2
    Delete(pRoot, pRoot->left); //delete 3
    Delete(pRoot, pRoot);// delete 4
    Delete(pRoot, pRoot->left);//delete 1
    walk(pRoot);
    //destroy
    Node* pCurrent = minimum(pRoot);
    while(pCurrent)
    {
        delete pCurrent;
        pCurrent = pCurrent->succ;
    }
    pCurrent = nullptr;
}
/*output
1   2   3   4   5   6   7   8
3
6
4
6   7   8
*/
  • 12.3-6 当x有两个孩子的时候,x的接班人y取x的前驱和后继都是可以的,具体实现方法是:用一个bool量控制下一个y取两者中的哪一个,交替着选取。在y取后继节点时,需要将y现有的右孩子托付给自己的父亲(已经在《算法导论》12.3节习题 实现);若y取前驱节点,需要做的修改是把自己的左孩子托付给自己的父亲。

转载于:https://www.cnblogs.com/meixiaogua/p/9895377.html

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值