【算法和数据结构】二叉树的定义和封装(C++实现)

       首先给出个人对于二叉树的理解:一个有限的节点集合,集合可以为空,或者仅含有根节点,又或者由一个根节点r和称为左右子树的两个不想交的二叉树构成。在这里,对于二叉树的理解用了递归的方法。

       在上面的基础上,就很容易理解什么叫做完全二叉树了:如果二叉树中每个内部节点均有两个左右子节点,且所有叶子具有同样的深度,那么这棵二叉树可以称之为完全二叉树。

       和前面所分享的大顶堆一样,首先给出二叉树的数组实现方式,其实现基于这样一个基本事实:对于节点i,若左右子节点均存在,其左子节点为2i+1,右子节点为2i+2。在下面给出的C++实现中,我们用简单的int类型举例,并且用-1标记不存在的节点。

二叉树的数组实现方式:

/*二叉树的数组实现*/

/*
1:对于节点i,若左右子节点均存在,其左子节点为2i+1,右子节点为2i+2
2:我们用-1来标记不存在的子节点
*/

class BinaryTree
{
public:
    BinaryTree(int size, int *pRoot);                       //创建树
    ~BinaryTree();                                          //销毁树
    int* searchNode(int nodeIndex);                         //由索引查找值
    bool addNode(int nodeIndex, char direction, int *pNode);//添加nodeIndex的孩子节点
    bool deleteNode(int nodeIndex, int *pNode);             //删除指定节点
    void treeTraverse();                                    //遍历
    int getTreeSize();                                      //返回树的大小

private:
    int *m_pTree;       //指向树的指针
    int arraySize;      //数组大小
    int treeSize;       //树的大小
};

BinaryTree::BinaryTree(int size, int *pRoot)      //size表示树的最大节点数
{
    arraySize = size;
    treeSize = 1;                     //初始化时指定了树根
    m_pTree = new int[size];

    /*初始化树,表示当前树无任何节点*/
    if(m_pTree!=NULL)     //不为空表示分配内存成功
        for (int i = 0; i < size; i++)
        {
            m_pTree[i] = -1;
        }

    m_pTree[0] = *pRoot;       //指定树根
}

BinaryTree::~BinaryTree()
{
    delete[]m_pTree;
    m_pTree = NULL;
}

int* BinaryTree::searchNode(int nodeIndex)
{
    if (nodeIndex < 0 || nodeIndex >= arraySize)//索引不合法
        return NULL;
    if (m_pTree[nodeIndex] == -1)//索引节点不存在
        return NULL;

    return &m_pTree[nodeIndex];
}

bool BinaryTree::addNode(int nodeIndex, char direction, int *pNode)
{
    if (nodeIndex < 0 || nodeIndex >= arraySize)//索引不合法
        return false;
    if (m_pTree[nodeIndex] == -1)//索引节点不存在
        return false;

    /*插入到左孩子节点*/
    if (direction == 'l')
    {
        if ((2 * nodeIndex + 1) < arraySize && m_pTree[2 * nodeIndex + 1] == -1)
        {
            m_pTree[2 * nodeIndex + 1] = *pNode;
            treeSize++;
            return true;
        }
        else
            return false;
    }

    /*插入到右孩子节点*/
    else if (direction == 'r')
    {
        if ((2 * nodeIndex + 2) < arraySize && m_pTree[2 * nodeIndex + 2] == -1)
        {
            m_pTree[2 * nodeIndex + 2] = *pNode;
            treeSize++;
            return true;
        }
        else
            return false;
    }
    else
        return false;
}

bool BinaryTree::deleteNode(int nodeIndex, int *pNode)
{
    if (nodeIndex < 0 || nodeIndex >= arraySize)//索引不合法
        return false;
    if (m_pTree[nodeIndex] == -1)//索引节点不存在
        return false;

    *pNode = m_pTree[nodeIndex];
    m_pTree[nodeIndex] = -1;
    treeSize--;
    return true;
}

void BinaryTree::treeTraverse()   //遍历数组即可
{
    for (int i = 0;i < arraySize;i++)
    {
        if (m_pTree[i] != -1)
            cout << m_pTree[i] << " ";
    }
}

int BinaryTree::getTreeSize()
{
    return treeSize;
}

      基于数组的实现方式比较简单(时间代价较优),但是我们设想这样的两种情况:

  1. 由于存储二叉树的数组大小从初始化后便固定不变,假设在后期动态维护此二叉树过程中,删除了大量的节点,如此势必造成数组中大量位置的空缺,使得很多存储空间被白白浪费,空间利用率不高;
  2. 当因为某种情况需要增加大量节点时,若节点总数超出数组大小,则需要复制整棵树到一个更大的数组空间中,当数据量巨大时,这种操作是耗时耗力的。

       为了避免上述在空间利用方面的问题,下面给出基于链表实现的二叉树方式:

       同样,对于节点的数据以int这种简单的数据类型为例,在应用中也完全可以很方便的扩展到更加复杂的数据类型。

二叉树的链表实现:

/*节点类*/
class Node
{
public:
    int index;    //节点索引
    int data;     //节点数据,这里以简单类型int为例
    Node *p_lChild, *p_rChild, *p_Parent;   //左孩子指针,右孩子指针,父指针

    Node();
    ~Node();
    Node* searchNode(int nodeIndex); //搜索指定节点,被BinaryTree类中同名方法调用
    void deleteNode();     //删除当前结点,递归删除其子树
    void preorderTraversal();   //前序遍历在当前结点的操作
    void inorderTraversal();    //中序遍历在当前结点的操作
    void postorderTraversal();  //后序遍历在当前结点的操作
};

Node::Node()
{
    index = 0;
    data = 0;
    p_lChild = NULL;
    p_rChild = NULL;
    p_Parent = NULL;
}
Node::~Node()
{
    p_lChild = NULL;
    p_rChild = NULL;
    p_Parent = NULL;
}
Node* Node::searchNode(int nodeIndex)
{
    if (this->index == nodeIndex)    //当前结点为搜索节点
        return this;

    Node *temp = NULL;

    if (this->p_lChild != NULL)   //比较当前结点左孩子和待搜索节点
    {
        if (this->p_lChild->index == nodeIndex)
            return this->p_lChild;
        else
        {
            temp = this->p_lChild->searchNode(nodeIndex);
            if (temp != NULL)
                return temp;
        }
    }
    if (this->p_rChild != NULL)   //比较当前结点右孩子和待搜索节点
    {
        if (this->p_rChild->index == nodeIndex)
            return this->p_rChild;
        else
        {
            temp = this->p_rChild->searchNode(nodeIndex);
            return temp;
        }
    }
    return NULL;
}
void Node::deleteNode()
{
    if (this->p_lChild != NULL)    //当前结点存在左孩子,则递归删除左子树
        this->p_lChild->deleteNode();
    if (this->p_rChild != NULL)    //当前结点存在右孩子,则递归删除右子树
        this->p_rChild->deleteNode();

    if (this->p_Parent != NULL)    //当前结点不为根
    {
        if (this->p_Parent->p_lChild == this)   //当前结点为其父节点的左孩子
            this->p_Parent->p_lChild = NULL;
        if (this->p_Parent->p_rChild == this)   //当前结点为其父节点的右孩子
            this->p_Parent->p_rChild = NULL;
    }

    delete this;   //释放当前结点
}
void Node::preorderTraversal()
{
    cout << this->data << " ";   //当前结点
    if (this->p_lChild != NULL)   //递归遍历左子树
        this->p_lChild->preorderTraversal();
    if (this->p_rChild != NULL)   //递归遍历右子树
        this->p_rChild->preorderTraversal();
}
void Node::inorderTraversal()
{
    if (this->p_lChild != NULL)   //递归遍历左子树
        this->p_lChild->inorderTraversal();
    cout << this->data << " ";   //当前结点
    if (this->p_rChild != NULL)   //递归遍历右子树
        this->p_rChild->inorderTraversal();
}
void Node::postorderTraversal()
{
    if (this->p_lChild != NULL)   //递归遍历左子树
        this->p_lChild->postorderTraversal();
    if (this->p_rChild != NULL)   //递归遍历右子树
        this->p_rChild->postorderTraversal();
    cout << this->data << " ";   //当前结点
}


/*二叉树类*/
class BinaryTree
{
public:
    BinaryTree();     //创建树
    ~BinaryTree();    //销毁树

    Node* searchNode(int nodeIndex);     //根据索引搜索节点
    bool addNode(int nodeIndex, char direction, Node *pNode);   //增加节点
    bool deleteNode(int nodeIndex, Node *pNode);   //删除指定索引节点,放入pNode

    void preorderTraversal();    //前序遍历
    void inorderTraversal();     //中序遍历
    void postorderTraversal();   //后序遍历

private:
    Node *m_pRoot;        //根指针,根节点不存放数据
};

BinaryTree::BinaryTree()
{
    m_pRoot = new Node();
}
BinaryTree::~BinaryTree()
{
    deleteNode(0, NULL);     //根节点索引为0
    //m_pRoot->deleteNode();
    delete m_pRoot;
}
Node* BinaryTree::searchNode(int nodeIndex)
{
    return m_pRoot->searchNode(nodeIndex);
}
bool BinaryTree::addNode(int nodeIndex, char direction, Node *pNode)
{
    Node *temp = searchNode(nodeIndex);    //此处调用BinaryTree类的方法
    if (temp == NULL)                //拟挂载节点不存在
        return false;

    Node *node = new Node();    //读取拟添加节点的数据
    if (node == NULL)           //申请内存失败
        return false;
    node->index = pNode->index;
    node->data = pNode->data;

    if (direction == 'l')    //挂载到nodeIndex节点的左边
    {
        temp->p_lChild = node;
        node->p_Parent = temp;
    }
    if (direction == 'r')      //挂载到nodeIndex节点的右边
    {
        temp->p_rChild = node;
        node->p_Parent = temp;
    }
    temp = NULL;
    return true;
}
bool BinaryTree::deleteNode(int nodeIndex, Node *pNode)
{
    Node *temp = searchNode(nodeIndex);    //此处调用BinaryTree类的方法
    if (temp == NULL)                //拟删除节点不存在
        return false;

    if(pNode!=NULL)    //将待删除节点数据复制到pNode中
        pNode->data = temp->data;

    temp->deleteNode();
    temp = NULL;
    return true;
}
void BinaryTree::preorderTraversal()
{
    m_pRoot->preorderTraversal();
}
void BinaryTree::inorderTraversal()
{
    m_pRoot->inorderTraversal();
}
void BinaryTree::postorderTraversal()
{
    m_pRoot->postorderTraversal();
}

       不同于数组实现方式,这里我们要定义节点类型(class Node):其主要包括节点索引(这样可以更快的查找到此节点)、节点数据项、左右孩子指针和父指针。三个指针分别用于指向当前结点的左右孩子节点和其父亲节点。

       另外,值得注意的三点是:

  • 这里在实现的时候,根节点将不用于存储数据项。
  • 三种遍历方式、搜索指定节点、删除指定节点等操作均以递归函数实现(和树的递归定义有一定关系)。
  • 实现过程中,,因为存在很多指针的情况,小伙伴们一定要特别注意防止内存泄漏问题,不要把头弄晕了哦。

       关于递归操作的封装,在实现时会发现,如果在类BinaryTree对象上进行操作,会变得无从下手,而在某一个节点上进行上述递归操作是很方便的,故而我们把上述操作下移,在Node类中封装其实现逻辑,而在BinaryTree类对象中只是简单的调用Node类封装好的相关方法即可。

       小结: 对比数组实现方式和链表实现方式,可以发现:数组实现方式在时间代价上是更加优化的,而链表实现方式在空间利用上更加灵活。实际应用中应根据实际情况进行选择。

        点击这里下载完整源码,包括最小生成树等方法的封装。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值