您的位置:首页 > 理论基础 > 数据结构算法

数据结构-----二叉树,树,森林之间的转换

2017-03-28 22:02 405 查看
图片和转换步骤来自这里

本文主要描述具体实现

用一种略微老土的话描述:

二叉树:每一节点最多有2个子节点,左边的叫左节点,右边的叫右节点,自己叫根节点。

树:每个节点的子节点数量不受限制。

森林:由若干个树构成的整体。

完了。

所以在你回忆完二叉树老生常谈的四种遍历后,又有那么一丁丁想要进军普通树的欲望的话,想想每个树节点应该怎么定义(毕竟想要转换成一个东西,好歹应该先弄清它里面是如何存数据的)。

树节点

每个树节点都有若干个类型相同的子节点,可以利用数组存储。所以我们的树节点有了它应该有的样子,像这样:

template<class T>
class TreeNode
{
public:
TreeNode(const T& element, size_t pSize = 0) :
m_pData(element),
m_pSize(pSize)
{
m_pNext = new TreeNode<T>*[m_pSize];
}

void setSize(size_t pSize)
{
m_pSize = pSize;
delete []m_pNext;
m_pNext = new TreeNode<T>*[m_pSize];

}

T m_pData;
size_t m_pSize;
TreeNode<T> **m_pNext;
};


因此,每次申请一个树节点指针时至少应该告诉它存储的数据,子节点大小可以在弄清楚有多少个子节点之后利用setSize()函数设置。

完成树节点的定义后,树的类也就没什么特别的,毕竟很少会遇见像是让你在树中插入删除一个节点这种变态的问题,那是(更特别的)二叉树做的事情。唯一需要的可能就一个输出函数(好歹检验一下转换的对不对噻?),其他个性化功能自己添加就好,重点放在二者转换上。

template<class T>
class Tree
{
public:
Tree(TreeNode<T> *pRoot = NULL); //构造函数
~Tree();//析构函数

void levelPrint() const; //层次输出
private:
TreeNode<T> *m_pRoot; //根节点
size_t m_pSize; //节点数量
};


简单提一句,二叉树节点包括两个节点指针(左右),其余的没什么区别。

二叉树->树

步骤1:

若某个节点X的左孩子存在,则将这个左孩子的右孩子节点,右孩子的右孩子节点,右孩子的右孩子的右孩子节点…,都作为节点X的孩子。将节点X与这些右孩子用线连接起来。

步骤2:

删除原二叉树中所有节点与其右孩子节点的连线(不包括根节点的右节点,右节点的右节点…….,因为树会劈叉啦~)。

步骤3:

层次调整。


树" title="">

首先应该明确的是,

1. 我们应该对二叉树的每一个节点都进行像步骤1那样的操作。

2. 对于步骤二,只需在构建树节点时不将原二叉树的某个根节点的右节点加到这个根节点的子节点数组(next数组)中即可。

3. 需要有个变量记录根节点,用于返回。

你可以试着带着这三步转换一棵二叉树(图片上的例子也不错),兴许在转换的过程中,你会发现比较好的方法来对每一个节点都做步骤1操作。

如果实在太懒,那就接着读吧(-__-)b

第一步,

从二叉树的根节点开始,目的是申请一个树节点,然后对它的next数组赋值。

所以无疑先计算它的子节点个数(查找个数在二叉树中进行),左节点算一个,然后不断查找右节点,直到为NULL。

(注:pNode是此时二叉树子树的根节点)

BinaryTreeNode<T> *pLeftNode = pNode->leftNode;
size_t pCnt = 0;
while(pLeftNode)
{
++pCnt;
pLeftNode = pLeftNode->rightNode;
}


第二步,

申请一个树节点指针作为这个位置子树的根节点,同时对next数组赋值(赋值的过程和计算子节点个数的过程类似,都是不断查找右节点,遇见一个二叉树的右节点就申请一个树节点):

(注:parentNode是此时树中子树的根节点)

TreeNode<T> *parentNode = new TreeNode<T>(pNode->m_pData, pCnt);
pLeftNode = pNode->leftNode;
pCnt = 0;
while(pLeftNode)
{
TreeNode<T> *pChildNode = new TreeNode<T>(pLeftNode->m_pData);
parentNode->m_PNext[pCnt++] = pChildNode;
pLeftNode = pLeftNode->rightNode;
}


一个树节点申请完毕,同时也另它有了pCnt个子节点。

第三步,

对parentNode节点的所有孩子做同样的事情(从根节点开始想,根节点任务完成了,该是它小弟们的show time了)。

再回忆一下“同样的事情”:

步骤1:

若某个节点X的左孩子存在,则将这个左孩子的右孩子节点,右孩子的右孩子节点,右孩子的右孩子的右孩子节点…,都作为节点X的孩子。将节点X与这些右孩子用线连接起来

哎呀!感觉遇到了些熟悉的字眼呢,或许递归可以解决这个问题(话说第一次接触递归还是在汉诺塔……)!

既然决定使用递归了,就需要考虑如何在两层递归之间衔接。还记得刚才的代码吗,根节点和它的子节点是在同一层申请的,这显然不符合我们的认知(子节点都申请好了,在下一层调用时作为根节点又申请了一次)。想想应该怎么办。

既然不能在同一层申请,那就将根节点放到上一层申请喽(似乎只有这两种可能性,不会有人会想先申请子节点然后才申请根节点吧……)。

既然如此,我们将树中子树的根节点(用于赋值它的next数组)和二叉树中子树的根节点(用于确定next数组大小)作为递归函数的参数。还记得树节点中那个setSize()函数吗,它好像派上用场了!

别忘了树子树的根节点(parentNode)在上一层递归中已经申请了内存,这层递归是用来申请它的孩子们的。

template<class T>
void binaryTreeToTree(TreeNode<T> *parentNode, BinaryTreeNode<T> *pNode)
{
//确定子节点数量
BinaryTreeNode<T> *pLeftNode = pNode->leftNode; size_t pCnt = 0; while(pLeftNode) { ++pCnt; pLeftNode = pLeftNode->rightNode; }

//第一次调用递归函数,parentNode是NULL(树种没有根节点)
//记录根节点,改变子节点数量
if(parentNode == NULL)
{
parentNode = new TreeNode<T>(pNode->m_pData, pCnt);
m_pRoot = parentNode; //m_pRoot,用于返回根节点
}
parentNode->setSize(pCnt);

//为next数组赋值
pLeftNode = pNode->leftNode;
pCnt = 0;
while(pLeftNode)
{
TreeNode<T> *pChildNode = new TreeNode<T>(pLeftNode->m_pData);
parentNode->m_pNext[pCnt++] = pChildNode;
pLeftNode = pLeftNode->rightNode;
}

//递归调用
pLeftNode = pNode->LeftNode;
for(int i = 0; i < pCnt; ++i)
{
binaryTreeToTree(parentNode->m_pNext[i], pLeftNode);
pLeftNode = pLeftNode->rightNode;
}
}


基本功能实现后总是需要对细节进行加工的,上述代码没有对二叉树根节点的右节点进行处理(看步骤2,下面,在下面呢)

步骤2:

删除原二叉树中所有节点与其右孩子节点的连线(不包括根节点的右节点,右节点的右节点…….,因为树会劈叉啦~)。

或许你应该思考一下应该怎么做,不过也没什么特别的(希望看完我说的之后你会这么觉得)。

每层递归函数处理子节点之前先判断参数中二叉树子树根节点是否满足步骤2括号中的情况,如果满足,同时又存在右节点,pCnt加一,然后申请参数中树的子树根节点子节点的时候多申请一个用于存放二叉树子树根节点的右节点

可能有点绕口(因为实在是担心会弄混树节点和二叉树节点)。不过更新一下上述代码还是有必要的。

(注:m_pBinaryRoot表示二叉树的根节点)

template<class T>
void binaryTreeToTree(TreeNode<T> *parentNode, BinaryTreeNode<T> *pNode)
{
//确定子节点数量
BinaryTreeNode<T> *pLeftNode = pNode->leftNode; size_t pCnt = 0; while(pLeftNode) { ++pCnt; pLeftNode = pLeftNode->rightNode; }

//处理步骤2括号中的情况
BinaryTreeNode<T> *targetNode = m_pBinaryRoot<T>;
bool isRightNode = false;
while(targetNode)
{
if(targetNode == pNode && pNode->rightNode != NULL)
{
isRightNode = true;
break;
}
targetNode = targetNode->rightNode;
}
if(isRightNode)
++pCnt;

//第一次调用递归函数,parentNode是NULL(树种没有根节点)
//记录根节点,改变子节点数量
if(parentNode == NULL)
{
parentNode = new TreeNode<T>(pNode->m_pData, pCnt);
m_pRoot<T> = parentNode; //m_pRoot,用于返回根节点
}
parentNode->setSize(pCnt);

//为next数组赋值
pLeftNode = pNode->leftNode;
pCnt = 0;
while(pLeftNode)
{
TreeNode<T> *pChildNode = new TreeNode<T>(pLeftNode->m_pData);
parentNode->m_pNext[pCnt++] = pChildNode;
pLeftNode = pLeftNode->rightNode;
}

//处理步骤2括号中情况
if(isRightNode)
{
TreeNode<T> *pChildNode = new TreeNode<T>(pNode->rightNode->m_pData);
parentNode->m_pNext[pCnt++] = pChildNode;
}

//递归调用
pLeftNode = pNode->LeftNode;
for(int i = 0; i < pCnt; ++i)
{
binaryTreeToTree(parentNode->m_pNext[i], pLeftNode);
if(i == pCnt - 1 && isRightNode) //处理步骤2括号中情况
pLeftNode = pNode->rightNode;
else
pLeftNode = pLeftNode->rightNode;
}
}


功能基本实现完成,为了结构清晰,另外一个函数用于调用这个递归函数

template<class T>
BinaryTreeNode<T> *m_pBinaryTree = NULL;

template<class T>
TreeNode<T> *m_pRoot = NULL;

template<class T>
TreeNode<T>* binaryTreeToTree(BinaryTreeNode<T> *pRoot)
{
m_pBinaryRoot<T> = pRoot;
binaryTreeToTree<T>(NULL, pRoot);
return m_pRoot<T>;
}


树->二叉树

步骤1:

在所有兄弟节点之间添加连线。

步骤2:

树中的每一个节点,只保留它与第一个孩子节点的连线,删除它与其他孩子节点之间的连线。

步骤3:

层次调整。


二叉树" title="">

受益于树节点结构,对于某个节点来说,它的所有兄弟节点和它在同一个数组中,这就省得花时间去弄明白它的兄弟在哪,过的怎么样,是谁,叫什么名字,长得好不好看。。。。

另外我们可以认为,兄弟之间添加的那些线都是指向右节点的线。所以根据上面的经验,处理完某个根节点X后,将X的每一个孩子都作为新的根节点,做和X同样的事情。

第一步:

从树的根节点开始,构建一个二叉树节点,使其左节点是树根的第一个孩子节点。

(注:pNode是树中此时子树的根节点)

BinaryTreeNode<T> *pLeftNode = new BinaryTreeNode(pNode->m_pNext[0]->m_pData);
parentNode->leftNode = pLeftNode; //保留它与第一个孩子的连线(步骤2)


第二步:

让根节点的左孩子节点的右指针指向它的下一个兄弟,一直指到最后一个兄弟。

size_t pCnt = pNode->m_pSize;
for(int i = 1; i < pCnt; ++i)
{
BinaryTreeNode<T> *pRightNode = new BinaryTreeNode(pNode->m_pNext[i]->m_pData);
pLeftNode->rightNode = pRightNode;
pLeftNode = pRightNode;
}


第三步:

将树中子树的根节点的每一个孩子作为新的根节点,对它的孩子们做同样的事情(兄弟之间连线的事情啦)。

根据前面的经验,递归函数的参数是二叉树子树的根节点和树子树的根节点。

需要注意的是,二叉树子树的根节点(parentNode)在上一层递归中已经申请了内存,这层递归是用来申请它的左孩子,以及左孩子的右孩子,左孩子的右孩子的右孩子…的。

template<class T>
void treeToBinaryTree(BinaryTreeNode<T> *parentNode, TreeNode<T> *pNode)
{
//记录根节点
if(parentNode == NULL)
{
parentNode = new BinaryTreeNode<T>(pNode->m_pData);
m_pRoot<T> = parentNode; //m_pRoot保存二叉树根节点,用于返回。
}

size_t pCnt = pNode->m_pSize;
if(pCnt == 0)
return;

//保留与第一个孩子节点的连线
BinaryTreeNode<T> *pLeftNode = new BinaryTreeNode(pNode->m_pNext[0]->m_pData);
parentNode->leftNode = pLeftNode;

//将所有兄弟连在一起(这里将兄弟当作右节点)
for(int i = 1; i < pCnt; ++i)
{
BinaryTreeNode<T> *pRightNode = new BinaryTreeNode(pNode->m_pNext[i]->m_pData);
pLeftNode->rightNode = pRightNode;
pLeftNode = pRightNode;
}

//递归调用
pLeftNode = parentNode->leftNode;
for(int i = 0; i < pCnt; ++i)
{
treeToBinaryTree(pLeftNode, pNode->m_pNext[i]);
pLeftNode = pLeftNode->rightNode;
}
}


和上面一样,增加一个函数调用这个递归函数。

template<class T>
BinaryTreeNode<T>* m_pRoot = NULL;

template<class T>
BinaryTreeNode<T>* treeToBinaryTree(const Tree<T> &pRoot)
{
treeToBinaryTree(NULL, pRoot.root());
return m_pRoot<T>;
}


二叉树->森林

步骤1:

从根节点开始,若右孩子存在,则把与右孩子节点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除…。直到所有根节点都没有右节点。

步骤2:

将每棵分离后的二叉树转化成树。


森林" title="">

将二叉树拆开,会出现很多个二叉树,可以考虑用vector来存储这些二叉树的根节点,然后对vector中的每一个二叉树进行转换,将树指针存在另一个vector中用于返回。

vector<BinaryTreeNode<T> *> pBinaryVector;
vector<Tree<T> *> pForestVector;


//拆分二叉树
BinaryTreeNode<T> *targetNode = pRoot.root();
BinaryTreeNode<T> *nextNode = NULL;
whlie(targetNode != NULL)
{
nextNode = targetNode->rightNode;
targetNode->rightNode = NULL;
pBinaryVector.push_back(targetNode);
targetNode = nextNode;
}

//将每个二叉树都转化成树
for(int i = 0; i < pBinaryVector.size(); ++i)
{
Tree<T>* pTree = new Tree<T>(binaryTreeToTree<T>(m_pBinaryVector.at(i)));
pForestVector.push_back(pTree);
}


森林->二叉树

步骤1:

将每棵树转换成二叉树

步骤2:

第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根节点作为前一棵二叉树根节点的右孩子。


二叉树" title="">

和二叉树转森林相似(实际上是步骤正好相反),将每棵树转换成二叉树,然后将这些二叉树连起来。

vector<Tree<T> *> pForestVector;


BinaryTreeNode<T>* m_pRoot = treeToBinaryTree(pForestVector.at(0));
BinaryTreeNode<T>* curRoot = m_pRoot;
size_t pSize = pForestVector.at(0)->size();

for(int i = 1; i < pForestVector.size(); ++i)
{
BinaryTreeNode<T> *pNextRoot = treeToBinaryTree(pForestVector.at(i));
pSize += pForestVector.at(i)->size();
curRoot ->rightNode = pNextRoot;
curRoot = pNext;
}

BinaryTree<T> *binaryTree = new BinaryTree<T>(m_pRoot, pSize);
return binaryTree;


问题和改进

大体的实现已经完成,剩下的就是细节加工,并且代码仍然存在安全隐患。

二叉树转换森林的过程中返回的是存储着树指针的vector,我们在自己设计的函数中申请大量节点,然后返回给用户,用户并不知道这些节点是怎么来的,所以更不会手动将这些节点内存释放。这就造成了内存泄漏。

解决方案:用vector存储智能指针,智能指针管理每一个树。这样当程序结束时,智能指针自动调用管理对象的析构函数,而树的析构函数刚好可以释放我们申请的大量节点的内存。

森林转换二叉树的过程中返回的二叉树指针也是利用动态内存申请的,用户不会自己释放,同样造成内存泄漏。

解决方案:返回智能指针,让智能指针管理二叉树对象,二叉树的析构函数释放申请的内存。

完整代码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  数据结构 二叉树