数据结构-----二叉树,树,森林之间的转换
圖片和轉換步驟來自這里
本文主要描述具體實現
用一種略微老土的話描述:
二叉樹:每一節(jié)點最多有2個子節(jié)點,左邊的叫左節(jié)點,右邊的叫右節(jié)點,自己叫根節(jié)點。
樹:每個節(jié)點的子節(jié)點數量不受限制。
森林:由若干個樹構成的整體。
完了。
所以在你回憶完二叉樹老生常談的四種遍歷后,又有那么一丁丁想要進軍普通樹的欲望的話,想想每個樹節(jié)點應該怎么定義(畢竟想要轉換成一個東西,好歹應該先弄清它里面是如何存數據的)。
樹節(jié)點
每個樹節(jié)點都有若干個類型相同的子節(jié)點,可以利用數組存儲。所以我們的樹節(jié)點有了它應該有的樣子,像這樣:
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; };因此,每次申請一個樹節(jié)點指針時至少應該告訴它存儲的數據,子節(jié)點大小可以在弄清楚有多少個子節(jié)點之后利用setSize()函數設置。
完成樹節(jié)點的定義后,樹的類也就沒什么特別的,畢竟很少會遇見像是讓你在樹中插入刪除一個節(jié)點這種變態(tài)的問題,那是(更特別的)二叉樹做的事情。唯一需要的可能就一個輸出函數(好歹檢驗一下轉換的對不對噻?),其他個性化功能自己添加就好,重點放在二者轉換上。
template<class T> class Tree { public:Tree(TreeNode<T> *pRoot = NULL); //構造函數~Tree();//析構函數void levelPrint() const; //層次輸出 private:TreeNode<T> *m_pRoot; //根節(jié)點size_t m_pSize; //節(jié)點數量 };簡單提一句,二叉樹節(jié)點包括兩個節(jié)點指針(左右),其余的沒什么區(qū)別。
二叉樹->樹
步驟1:
若某個節(jié)點X的左孩子存在,則將這個左孩子的右孩子節(jié)點,右孩子的右孩子節(jié)點,右孩子的右孩子的右孩子節(jié)點…,都作為節(jié)點X的孩子。將節(jié)點X與這些右孩子用線連接起來。
步驟2:
刪除原二叉樹中所有節(jié)點與其右孩子節(jié)點的連線(不包括根節(jié)點的右節(jié)點,右節(jié)點的右節(jié)點…….,因為樹會劈叉啦~)。
步驟3:
層次調整。
首先應該明確的是,
1. 我們應該對二叉樹的每一個節(jié)點都進行像步驟1那樣的操作。
2. 對于步驟二,只需在構建樹節(jié)點時不將原二叉樹的某個根節(jié)點的右節(jié)點加到這個根節(jié)點的子節(jié)點數組(next數組)中即可。
3. 需要有個變量記錄根節(jié)點,用于返回。
你可以試著帶著這三步轉換一棵二叉樹(圖片上的例子也不錯),興許在轉換的過程中,你會發(fā)現比較好的方法來對每一個節(jié)點都做步驟1操作。
如果實在太懶,那就接著讀吧(-__-)b
第一步,
從二叉樹的根節(jié)點開始,目的是申請一個樹節(jié)點,然后對它的next數組賦值。
所以無疑先計算它的子節(jié)點個數(查找個數在二叉樹中進行),左節(jié)點算一個,然后不斷查找右節(jié)點,直到為NULL。
(注:pNode是此時二叉樹子樹的根節(jié)點)
第二步,
申請一個樹節(jié)點指針作為這個位置子樹的根節(jié)點,同時對next數組賦值(賦值的過程和計算子節(jié)點個數的過程類似,都是不斷查找右節(jié)點,遇見一個二叉樹的右節(jié)點就申請一個樹節(jié)點):
(注:parentNode是此時樹中子樹的根節(jié)點)
一個樹節(jié)點申請完畢,同時也另它有了pCnt個子節(jié)點。
第三步,
對parentNode節(jié)點的所有孩子做同樣的事情(從根節(jié)點開始想,根節(jié)點任務完成了,該是它小弟們的show time了)。
再回憶一下“同樣的事情”:
步驟1:
若某個節(jié)點X的左孩子存在,則將這個左孩子的右孩子節(jié)點,右孩子的右孩子節(jié)點,右孩子的右孩子的右孩子節(jié)點…,都作為節(jié)點X的孩子。將節(jié)點X與這些右孩子用線連接起來
哎呀!感覺遇到了些熟悉的字眼呢,或許遞歸可以解決這個問題(話說第一次接觸遞歸還是在漢諾塔……)!
既然決定使用遞歸了,就需要考慮如何在兩層遞歸之間銜接。還記得剛才的代碼嗎,根節(jié)點和它的子節(jié)點是在同一層申請的,這顯然不符合我們的認知(子節(jié)點都申請好了,在下一層調用時作為根節(jié)點又申請了一次)。想想應該怎么辦。
既然不能在同一層申請,那就將根節(jié)點放到上一層申請嘍(似乎只有這兩種可能性,不會有人會想先申請子節(jié)點然后才申請根節(jié)點吧……)。
既然如此,我們將樹中子樹的根節(jié)點(用于賦值它的next數組)和二叉樹中子樹的根節(jié)點(用于確定next數組大小)作為遞歸函數的參數。還記得樹節(jié)點中那個setSize()函數嗎,它好像派上用場了!
別忘了樹子樹的根節(jié)點(parentNode)在上一層遞歸中已經申請了內存,這層遞歸是用來申請它的孩子們的。
template<class T> void binaryTreeToTree(TreeNode<T> *parentNode, BinaryTreeNode<T> *pNode) {//確定子節(jié)點數量BinaryTreeNode<T> *pLeftNode = pNode->leftNode;size_t pCnt = 0;while(pLeftNode){++pCnt;pLeftNode = pLeftNode->rightNode;}//第一次調用遞歸函數,parentNode是NULL(樹種沒有根節(jié)點)//記錄根節(jié)點,改變子節(jié)點數量if(parentNode == NULL){parentNode = new TreeNode<T>(pNode->m_pData, pCnt);m_pRoot = parentNode; //m_pRoot,用于返回根節(jié)點}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;} }基本功能實現后總是需要對細節(jié)進行加工的,上述代碼沒有對二叉樹根節(jié)點的右節(jié)點進行處理(看步驟2,下面,在下面呢)
步驟2:
刪除原二叉樹中所有節(jié)點與其右孩子節(jié)點的連線(不包括根節(jié)點的右節(jié)點,右節(jié)點的右節(jié)點…….,因為樹會劈叉啦~)。
或許你應該思考一下應該怎么做,不過也沒什么特別的(希望看完我說的之后你會這么覺得)。
每層遞歸函數處理子節(jié)點之前先判斷參數中二叉樹子樹根節(jié)點是否滿足步驟2括號中的情況,如果滿足,同時又存在右節(jié)點,pCnt加一,然后申請參數中樹的子樹根節(jié)點子節(jié)點的時候多申請一個用于存放二叉樹子樹根節(jié)點的右節(jié)點。
可能有點繞口(因為實在是擔心會弄混樹節(jié)點和二叉樹節(jié)點)。不過更新一下上述代碼還是有必要的。
(注:m_pBinaryRoot表示二叉樹的根節(jié)點)
template<class T> void binaryTreeToTree(TreeNode<T> *parentNode, BinaryTreeNode<T> *pNode) {//確定子節(jié)點數量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(樹種沒有根節(jié)點)//記錄根節(jié)點,改變子節(jié)點數量if(parentNode == NULL){parentNode = new TreeNode<T>(pNode->m_pData, pCnt);m_pRoot<T> = parentNode; //m_pRoot,用于返回根節(jié)點}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;elsepLeftNode = 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:
在所有兄弟節(jié)點之間添加連線。
步驟2:
樹中的每一個節(jié)點,只保留它與第一個孩子節(jié)點的連線,刪除它與其他孩子節(jié)點之間的連線。
步驟3:
層次調整。
受益于樹節(jié)點結構,對于某個節(jié)點來說,它的所有兄弟節(jié)點和它在同一個數組中,這就省得花時間去弄明白它的兄弟在哪,過的怎么樣,是誰,叫什么名字,長得好不好看。。。。
另外我們可以認為,兄弟之間添加的那些線都是指向右節(jié)點的線。所以根據上面的經驗,處理完某個根節(jié)點X后,將X的每一個孩子都作為新的根節(jié)點,做和X同樣的事情。
第一步:
從樹的根節(jié)點開始,構建一個二叉樹節(jié)點,使其左節(jié)點是樹根的第一個孩子節(jié)點。
(注:pNode是樹中此時子樹的根節(jié)點)
第二步:
讓根節(jié)點的左孩子節(jié)點的右指針指向它的下一個兄弟,一直指到最后一個兄弟。
第三步:
將樹中子樹的根節(jié)點的每一個孩子作為新的根節(jié)點,對它的孩子們做同樣的事情(兄弟之間連線的事情啦)。
根據前面的經驗,遞歸函數的參數是二叉樹子樹的根節(jié)點和樹子樹的根節(jié)點。
需要注意的是,二叉樹子樹的根節(jié)點(parentNode)在上一層遞歸中已經申請了內存,這層遞歸是用來申請它的左孩子,以及左孩子的右孩子,左孩子的右孩子的右孩子…的。
和上面一樣,增加一個函數調用這個遞歸函數。
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:
從根節(jié)點開始,若右孩子存在,則把與右孩子節(jié)點的連線刪除。再查看分離后的二叉樹,若其根節(jié)點的右孩子存在,則連線刪除…。直到所有根節(jié)點都沒有右節(jié)點。
步驟2:
將每棵分離后的二叉樹轉化成樹。
將二叉樹拆開,會出現很多個二叉樹,可以考慮用vector來存儲這些二叉樹的根節(jié)點,然后對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:
第一棵二叉樹不動,從第二棵二叉樹開始,依次把后一棵二叉樹的根節(jié)點作為前一棵二叉樹根節(jié)點的右孩子。
和二叉樹轉森林相似(實際上是步驟正好相反),將每棵樹轉換成二叉樹,然后將這些二叉樹連起來。
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;問題和改進
大體的實現已經完成,剩下的就是細節(jié)加工,并且代碼仍然存在安全隱患。
二叉樹轉換森林的過程中返回的是存儲著樹指針的vector,我們在自己設計的函數中申請大量節(jié)點,然后返回給用戶,用戶并不知道這些節(jié)點是怎么來的,所以更不會手動將這些節(jié)點內存釋放。這就造成了內存泄漏。
解決方案:用vector存儲智能指針,智能指針管理每一個樹。這樣當程序結束時,智能指針自動調用管理對象的析構函數,而樹的析構函數剛好可以釋放我們申請的大量節(jié)點的內存。
森林轉換二叉樹的過程中返回的二叉樹指針也是利用動態(tài)內存申請的,用戶不會自己釋放,同樣造成內存泄漏。
解決方案:返回智能指針,讓智能指針管理二叉樹對象,二叉樹的析構函數釋放申請的內存。
完整代碼
總結
以上是生活随笔為你收集整理的数据结构-----二叉树,树,森林之间的转换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构-----跳表
- 下一篇: 0/1背包问题-----动态规划求解