我們在前面學習了排序相關的知識,從今天開始,我們來學習數據結構中樹的相關東西。那麼什麼是樹呢?樹是一種非線性的數據結構。
樹是由 n( n >= 0 ) 個結點組成的有限集合。如果 n= 0,稱爲空樹;如果 n > 0,則:a> 有一個特定的稱之爲根(root)的結點;b> 根結點只有直接後繼,但沒有直接前驅;c> 除根以外的其它結點劃分爲 m( m >= 0 ) 個互不相交的有限集合T0, T1, … Tm-1,每個集合又是一棵樹,並且稱之爲根的子樹(sub tree)。下來我們來看看樹的示意圖,如下所示
下來我們來看一個樹中度的概念。它是指樹中的結點包含一個數據及若干指向子樹的分支,及誒單擁有子樹的數目稱爲結點的度。度爲 0 的結點稱爲葉結點,度不爲 0 的結點稱爲分支節點。樹的度定義爲所有節點中度的最大值。下來來看一個數的度爲 3 的示例,如下圖所示
下來來介紹下樹中的前驅和後繼。結點的直接後繼稱爲該結點的孩子,相應的,該結點稱爲孩子的雙親;結點的孩子的孩子的 ... 稱爲該結點的子孫,相應的,該結點稱爲子孫的祖先;同一個雙親的孩子之間互稱兄弟。下來來看看樹的前驅和後繼的結構示意圖
我們來看看樹中結點的層次,如下圖所示
樹也有有序性,什麼叫樹的有序性呢?如果樹中結點的各子樹從左向右是有次序的,子樹間不能互換位置,則稱該樹爲有序樹,否則爲無序樹。示意圖如下圖所示
那麼既然有樹的概念,就肯定有森林的概念。森林是由 n( n >= 0 ) 棵互不相交的樹組成的集合。那麼在樹中肯定也有一些常用的操作,如下
1、將元素插入樹中;
2、將元素從樹中刪除;
3、獲取樹的結點數;
4、獲取樹的高度;
5、獲取樹的度;
6、清空樹中的元素;
7、 ......
樹與結點的類關係可以如下表示
那麼我們下來來看看那樹和結點抽象類的具體源碼是怎樣寫的
Tree.h 源碼
#ifndef TREE_H #define TREE_H #include "TreeNode.h" #include "SharedPointer.h" namespace DTLib { template < typename T > class Tree : public Object { protected: TreeNode<T>* m_root; public: Tree() { m_root = NULL; } virtual bool ×××ert(TreeNode<T>* node) = 0; virtual bool ×××ert(const T& value, TreeNode<T>* parent) = 0; virtual SharedPointer< Tree<T> > remove(const T& value) = 0; virtual SharedPointer< Tree<T> > remove(TreeNode<T>* node) = 0; virtual TreeNode<T>* find(const T& value) const = 0; virtual TreeNode<T>* find(TreeNode<T>* node) const = 0; virtual TreeNode<T>* root() const = 0; virtual int degree() const = 0; virtual int count() const = 0; virtual int height() const = 0; virtual void clear() = 0; }; } #endif // TREE_H
TreeNode.h 源碼
#ifndef TREENODE_H #define TREENODE_H #include "Object.h" namespace DTLib { template < typename T > class TreeNode : public Object { public: T value; TreeNode<T>* parent; TreeNode() { parent = NULL; } virtual ~TreeNode() = 0; }; template < typename T > TreeNode<T>::~TreeNode() { } } #endif // TREENODE_H
下來我們來看看樹和結點的存儲結構是怎麼設計的,結構圖如下
設計要點:1、GTree 爲通用的樹結構,每個結點可以存在多個後繼結點;2、GTreeNode 能夠包含任意多指向後繼結點的指針;3、實現樹結構的所有操作(增,刪,查,等)。
GTree(通用樹結構)的實現框架如下圖所示
我們來看看通用樹結構(框架)是怎樣創建的,源碼如下
GTree.h 源碼
#ifndef GTREE_H #define GTREE_H #include "Tree.h" #include "GTreeNode.h" namespace DTLib { template < typename T > class GTree : public Tree<T> { public: bool ×××ert(TreeNode<T>* node) { bool ret = true; return ret; } bool ×××ert(const T& value, TreeNode<T>* parent) { bool ret = true; return ret; } SharedPointer< Tree<T> > remove(const T& value) { return NULL; } SharedPointer< Tree<T> > remove(TreeNode<T>* node) { return NULL; } GTreeNode<T>* find(const T& value) const { return NULL; } GTreeNode<T>* find(TreeNode<T>* node) const { return NULL; } GTreeNode<T>* root() const { return dynamic_cast<GTreeNode<T>*>(this->m_root); } int degree() const { return 0; } int count() const { return 0; } int height() const { return 0; } void clear() { this->m_root = NULL; } ~GTree() { clear(); } }; } #endif // GTREE_H
GTreeNode.h 源碼
#ifndef GTREENODE_H #define GTREENODE_H #include "Tree.h" namespace DTLib { template < typename T > class GTreeNode : public TreeNode<T> { public: LinkList<GTreeNode<T>*> child; }; } #endif // GTREENODE_H
我們在樹的設計中,爲什麼要在每個樹節點中包含指向前驅結點的指針呢?從根結點到葉結點是非線性的數據結構,但是從葉結點到根結點是線性的數據結構(鏈表),結果如下
下來我們就來一一的實現上面的查找、插入等操作
1、查找操作
查找的方式應分爲兩種:a> 基於數據元素值的查找:GTreeNode<T>* find(const T& value) const;b> 基於結點的查找:GTreeNode<T>* find(TreeNode<T>* node) const。
a> 基於數據元素值的查找,我們先在 protected 屬性中定義 find(node, value) 功能,在 node 爲根結點的樹中查找 value 所在的結點,實現思路如下
b> 基於結點的查找,還是在 protected 屬性中定義 find(node, obj) 功能,在 node 爲根結點的樹中查找是否存在 obj 結點,實現思路如下
具體查找相關源碼實現如下
#ifndef GTREE_H #define GTREE_H #include "Tree.h" #include "GTreeNode.h" namespace DTLib { template < typename T > class GTree : public Tree<T> { protected: GTreeNode<T>* find(GTreeNode<T>* node, const T& value) const { GTreeNode<T>* ret = NULL; if( node != NULL ) { if( node->value == value ) { return node; } else { for(node->child.move(0); !node->child.end() && (ret == NULL); node->child.next()) { ret = find(node->child.current(), value); } } } return ret; } GTreeNode<T>* find(GTreeNode<T>* node, GTreeNode<T>* obj) const { GTreeNode<T>* ret = NULL; if( node == obj ) { return node; } else { if( node != NULL ) { for(node->child.move(0); !node->child.end() && (ret == NULL); node->child.next()) { ret = find(node->child.current(), obj); } } } return ret; } public: GTreeNode<T>* find(const T& value) const { return find(root(), value); } GTreeNode<T>* find(TreeNode<T>* node) const { return find(root(), dynamic_cast<GTreeNode<T>*>(node)); } GTreeNode<T>* root() const { return dynamic_cast<GTreeNode<T>*>(this->m_root); } }; } #endif // GTREE_H
2、插入操作
插入的方式應分爲兩種:a> 插入新結點:bool ×××ert(TreeNode<T>* node);b> 插入數據元素:bool ×××ert(const T& value, TreeNode<T>* parent)。
那麼如何在樹中指定新結點的位置呢?問題分析:a> 樹是非線性的,無法採用下標的形式定位數據元素;b> 每一個樹結點都有唯一的前驅結點(父結點);c> 因此,必須先找到前驅節點,才能完成新結點的插入。
a> 新結點的插入,如下圖所示
插入新結點的流程如下圖所示
b> 插入數據元素,流程如下圖所示
下來我們來看看具體源碼是怎麼實現的
#ifndef GTREE_H #define GTREE_H #include "Tree.h" #include "GTreeNode.h" #include "Exception.h" namespace DTLib { template < typename T > class GTree : public Tree<T> { public: bool ×××ert(TreeNode<T>* node) { bool ret = true; if( node != NULL ) { if( this->m_root == NULL ) { node->parent = NULL; this->m_root = node; } else { GTreeNode<T>* np = find(node->parent); if( np != NULL ) { GTreeNode<T>* n = dynamic_cast<GTreeNode<T>*>(node); if( np->child.find(n) < 0 ) { np->child.×××ert(n); } } else { THROW_EXCEPTION(INvalidOPerationException, "Invalid parent tree node ..."); } } } else { THROW_EXCEPTION(InvalidParameterException, "Parement node cannot be NULL ..."); } return ret; } bool ×××ert(const T& value, TreeNode<T>* parent) { bool ret = true; GTreeNode<T>* node = GTreeNode<T>::NewNode(); if( node != NULL ) { node->value = value; node->parent = parent; ×××ert(node); } else { THROW_EXCEPTION(NoEnoughMemoryException, "No memory to create new tree node ..."); } return ret; } }; } #endif // GTREE_H
我們來寫點測試代碼,看看前面實現的查找和插入代碼是否正確
#include <iostream> #include "GTree.h" using namespace std; using namespace DTLib; int main() { GTree<char> t; GTreeNode<char>* node = NULL; t.×××ert('A', NULL); node = t.find('A'); t.×××ert('B', node); t.×××ert('C', node); t.×××ert('D', node); node = t.find('B'); t.×××ert('E', node); t.×××ert('F', node); node = t.find('E'); t.×××ert('K', node); t.×××ert('L', node); node = t.find('C'); t.×××ert('G', node); node = t.find('G'); t.×××ert('N', node); node = t.find('D'); t.×××ert('H', node); t.×××ert('I', node); t.×××ert('J', node); node = t.find('H'); t.×××ert('M', node); const char* s = "KLFNMIJ"; for(int i=0; i<7; i++) { TreeNode<char>* node = t.find(s[i]); while( node != NULL ) { cout << node->value << " "; node = node->parent; } cout << endl; } return 0; }
運行結果如下
我們看到已經實現了之前定義的樹結構。
3、清除操作
a> 定義:void clear();將樹中的所有結點清除(釋放堆中的結點),樹中數據元素的清除如下所示
b> free(node);清除 node 爲根結點的樹,釋放樹中的每一個結點,實現思路如下
具體源碼實現如下
#ifndef GTREE_H #define GTREE_H #include "Tree.h" #include "GTreeNode.h" namespace DTLib { template < typename T > class GTree : public Tree<T> { protected: void free(GTreeNode<T>* node) const { if( node != NULL ) { for(node->child.move(0); !node->child.end(); node->child.next()) { free(node->child.current()); } delete node; } } public: void clear() { free(root()); this->m_root = NULL; m_queue.clear(); } ~GTree() { clear(); } }; } #endif // GTREE_H
測試代碼如下
#include <iostream> #include "GTree.h" using namespace std; using namespace DTLib; int main() { GTree<char> t; GTreeNode<char>* node = NULL; GTreeNode<char> root; root.value = 'A'; root.parent = NULL; t.×××ert(&root); node = t.find('A'); t.×××ert('B', node); t.×××ert('C', node); t.×××ert('D', node); node = t.find('B'); t.×××ert('E', node); t.×××ert('F', node); node = t.find('E'); t.×××ert('K', node); t.×××ert('L', node); node = t.find('C'); t.×××ert('G', node); node = t.find('G'); t.×××ert('N', node); node = t.find('D'); t.×××ert('H', node); t.×××ert('I', node); t.×××ert('J', node); node = t.find('H'); t.×××ert('M', node); t.clear(); const char* s = "KLFNMIJ"; for(int i=0; i<7; i++) { TreeNode<char>* node = t.find(s[i]); while( node != NULL ) { cout << node->value << " "; node = node->parent; } cout << endl; } return 0; }
我們來看看結果
我們看到已經清空了樹。但是此時存在一個問題,那便是我們在 main 函數中是在堆上值指定的數據元素,上面的清除操作也會將這個堆中的數據元素刪除掉。這必然會導致問題,那麼對於樹中的結點來源於不同的存儲空間的話,此時我們應如何判斷堆空間中的結點並釋放?問題分析:單憑內存地址很難準確判斷具體的存儲區域,只有堆空間的內存需要主動釋放(delete),清除操作時只需要對堆中的結點進行釋放。
此時的解決方案:工廠模式。
i. 在 GTreeNode 中增加保護成員變量 m_flag;
ii. 將 GTreeNode 中的 operator new 重載爲保護成員函數;
iii. 提供工廠方法 GTreeNode<T>* NewNode();
iv. 在工廠方法中 new 新結點並將 m_flag 設置爲 true。
樹結點的工廠模式示例如下
我們來看看具體的源碼實現
#ifndef GTREENODE_H #define GTREENODE_H #include "Tree.h" #include "LinkList.h" namespace DTLib { template < typename T > class GTreeNode : public TreeNode<T> { protected: bool m_flag; GTreeNode(const GTreeNode<T>&); GTreeNode<T>* operator = (const GTreeNode<T>&); void* operator new(unsigned int size) throw() { return Object::operator new(size); } public: LinkList<GTreeNode<T>*> child; GTreeNode() { m_flag = false; } bool flag() { return m_flag; } static GTreeNode<T>* NewNode() { GTreeNode<T>* ret = new GTreeNode<T>(); if( ret != NULL ) { ret->m_flag = true; } return ret; } }; } #endif // GTREENODE_H
在上面的 delete node 操作時外面進行 node->flag() 的判斷,如果爲 true,我們再來進行刪除。爲例方便的進行說明,我們在這塊加個調試語句,再來一個 else 語句,裏面打印出不同存儲區域的數據元素,我們來看看結果
我們看到此時除了我們自己在堆上指定的 A 之外,剩下的數據元素已經全部被清除掉。
4、刪除操作
刪除的方式也分爲兩種:a> 基於數據元素值的刪除:SharedPointer< Tree<T> > remove(const T& value);b> 基於結點的刪除:SharedPointer< Tree<T> > remove(TreeNode<T>* node);
刪除操作成員函數的設計要點:1、將被刪結點所代表的子樹進行刪除;2、刪除函數返回一顆堆空間的樹;3、具體返回值爲指向樹的只能指針對象。樹中結點的刪除示意如下圖所示
如果當我們需要從函數中返回堆中的對象時,使用智能指針(SharedPointer)作爲函數的返回值。刪除操作功能的定義:void remove(GTreeNode<T>* node, GTree<T>*& ret);將 node 爲根結點的子樹從原來的樹中刪除,ret 作爲子樹返回(ret 指向堆空間中的樹對象)。刪除功能函數的實現思路如下
具體源碼實現如下
#ifndef GTREE_H #define GTREE_H #include "Tree.h" #include "GTreeNode.h" #include "Exception.h" namespace DTLib { template < typename T > class GTree : public Tree<T> { protected: void remove(GTreeNode<T>* node, GTree<T>*& ret) { ret = new GTree(); if( ret != NULL ) { if( root() == node ) { this->m_root = NULL; } else { LinkList<GTreeNode<T>*>& child = dynamic_cast<GTreeNode<T>*>(node->parent)->child; child.remove(child.find(node)); node->parent = NULL; } ret->m_root = node; } else { THROW_EXCEPTION(NoEnoughMemoryException, "No memory to create new tree ..."); } } public: SharedPointer< Tree<T> > remove(const T& value) { GTree<T>* ret = NULL; GTreeNode<T>* node = find(value); if( node != NULL ) { remove(node, ret); m_queue.clear(); } else { THROW_EXCEPTION(InvalidParameterException, "Can not find the node via parament value ..."); } return ret; } SharedPointer< Tree<T> > remove(TreeNode<T>* node) { GTree<T>* ret = NULL; node = find(node); if( node != NULL ) { remove(dynamic_cast<GTreeNode<T>*>(node), ret); m_queue.clear(); } else { THROW_EXCEPTION(InvalidParameterException, "Parament node is invalid ..."); } return ret; } }; } #endif // GTREE_H
我們來寫點測試代碼,刪除子樹 D,測試代碼如下
#include <iostream> #include "GTree.h" using namespace std; using namespace DTLib; int main() { GTree<char> t; GTreeNode<char>* node = NULL; GTreeNode<char> root; root.value = 'A'; root.parent = NULL; t.×××ert(&root); node = t.find('A'); t.×××ert('B', node); t.×××ert('C', node); t.×××ert('D', node); node = t.find('B'); t.×××ert('E', node); t.×××ert('F', node); node = t.find('E'); t.×××ert('K', node); t.×××ert('L', node); node = t.find('C'); t.×××ert('G', node); node = t.find('G'); t.×××ert('N', node); node = t.find('D'); t.×××ert('H', node); t.×××ert('I', node); t.×××ert('J', node); node = t.find('H'); t.×××ert('M', node); //SharedPointer< Tree<char> > p = t.remove(t.find('D')); t.remove(t.find('D')); const char* s = "KLFNMIJ"; for(int i=0; i<7; i++) { TreeNode<char>* node = t.find(s[i]); while( node != NULL ) { cout << node->value << " "; node = node->parent; } cout << endl; } return 0; }
我們來看看運行結果
我們看到子樹 D 已經被刪除了,如果我們想用這個刪除的子樹 D,該如何做呢?將上面的測試代碼中的註釋的那行放開,將下面的 remove 註釋掉,再將下面 for 循環中的 t.find(s[i]) 改爲 p->find(s[i]),我們來看看運行結果
我們看到打印出的是我們刪除的子樹 D。
5、其他屬性操作
a> 樹中結點的數目,功能定義:count(node);在 node 爲根結點的樹中統計結點數目,實現思路如下
樹的結點數目的計算示例如下:
b> 樹的高度,功能定義:height(node);獲取 node 爲根結點的樹的高度,實現思路如下
樹的高度計算示例如下:
c> 樹的度數,功能定義:degree(node);獲取 node 爲根結點的樹的度數,實現思路如下
樹的度計算示例
下來看看具體的源碼實現
#ifndef GTREE_H #define GTREE_H #include "Tree.h" #include "GTreeNode.h" #include "Exception.h" namespace DTLib { template < typename T > class GTree : public Tree<T> { protected int count(GTreeNode<T>* node) const { int ret = 0; if( node != NULL ) { ret = 1; for(node->child.move(0); !node->child.end(); node->child.next()) { ret += count(node->child.current()); } } return ret; } int height(GTreeNode<T>* node) const { int ret = 0; if( node != NULL ) { for(node->child.move(0); !node->child.end(); node->child.next()) { int h = height(node->child.current()); if( ret < h ) { ret = h; } } ret = ret + 1; } return ret; } int degree(GTreeNode<T>* node) const { int ret = 0; if( node != NULL ) { ret = node->child.length(); for(node->child.move(0); !node->child.end(); node->child.next()) { int d = degree(node->child.current()); if( ret < d ) { ret = d; } } } return ret; } public: int degree() const { return degree(root()); } int count() const { return count(root()); } int height() const { return height(root()); } }; } #endif // GTREE_H
測試代碼如下
#include <iostream> #include "GTree.h" using namespace std; using namespace DTLib; int main() { GTree<char> t; GTreeNode<char>* node = NULL; GTreeNode<char> root; root.value = 'A'; root.parent = NULL; t.×××ert(&root); node = t.find('A'); t.×××ert('B', node); t.×××ert('C', node); t.×××ert('D', node); node = t.find('B'); t.×××ert('E', node); t.×××ert('F', node); node = t.find('E'); t.×××ert('K', node); t.×××ert('L', node); node = t.find('C'); t.×××ert('G', node); node = t.find('G'); t.×××ert('N', node); node = t.find('D'); t.×××ert('H', node); t.×××ert('I', node); t.×××ert('J', node); node = t.find('H'); t.×××ert('M', node); cout << "t.count() : " << t.count() << endl; cout << "t.height() : " << t.height() << endl; cout << "t.degree() : " << t.degree() << endl; return 0; }
我們來看看運行結果
6、層次遍歷
如何按層次遍歷通用樹結構中的每一個數據元素呢?樹是非線性的數據結構,樹的結點沒有固定的編號方式。那麼我們就得提供一個新的需求,爲通用樹結構提供新的方法,能快速遍歷每一個結點。
設計思路(遊標):a> 在樹中定義一個遊標(GTreeNode<T>*);b> 遍歷開始前將遊標指向根結點(root());c> 獲取遊標指向的數據元素;d> 通過結點中的 child 成員移動遊標。提供一組遍歷相關的函數,按層次訪問樹中的數據元素。如下
層次遍歷算法:a> 原料:class LinkQueue<T>;b> 遊標:LinkQueue<T>::front();c> 思想:i. begin() --> 將根節點壓入隊列中;ii. current() --> 訪問隊頭元素指向的數據元素;iii. next() --> 隊頭元素彈出,將對頭元素的孩子壓入隊列中(核心);iv. end() --> 判斷隊列是否爲空。層次遍歷算法示例如下
下來我們來看看具體的源碼實現
GTreeNode.h 源碼
#ifndef GTREENODE_H #define GTREENODE_H #include "Tree.h" #include "LinkList.h" namespace DTLib { template < typename T > class GTreeNode : public TreeNode<T> { protected: bool m_flag; GTreeNode(const GTreeNode<T>&); GTreeNode<T>* operator = (const GTreeNode<T>&); void* operator new(unsigned int size) throw() { return Object::operator new(size); } public: LinkList<GTreeNode<T>*> child; GTreeNode() { m_flag = false; } bool flag() { return m_flag; } static GTreeNode<T>* NewNode() { GTreeNode<T>* ret = new GTreeNode<T>(); if( ret != NULL ) { ret->m_flag = true; } return ret; } }; } #endif // GTREENODE_H
GTree.h 源碼
#ifndef GTREE_H #define GTREE_H #include "Tree.h" #include "GTreeNode.h" #include "Exception.h" #include "LinkQueue.h" namespace DTLib { template < typename T > class GTree : public Tree<T> { protected: LinkQueue<GTreeNode<T>*> m_queue; GTree(const GTree<T>&); GTree<T>* operator = (const GTree<T>&); public: GTree() { } bool begin() { bool ret = (root() != NULL); if( ret ) { m_queue.clear(); m_queue.add(root()); } return ret; } bool end() { return (m_queue.length() == 0); } bool next() { bool ret = (m_queue.length() > 0); if( ret ) { GTreeNode<T>* node = m_queue.front(); m_queue.remove(); for(node->child.move(0); !node->child.end(); node->child.next()) { m_queue.add(node->child.current()); } } return ret; } T current() { if( !end() ) { return m_queue.front()->value; } else { THROW_EXCEPTION(InvalidParameterException, "No value at current position ..."); } } }; } #endif // GTREE_H
那麼在 remove 的操作中也要加上相應的隊列的清除:m_queue.clear(); 測試代碼如下
#include <iostream> #include "GTree.h" using namespace std; using namespace DTLib; int main() { GTree<char> t; GTreeNode<char>* node = NULL; GTreeNode<char> root; root.value = 'A'; root.parent = NULL; t.×××ert(&root); node = t.find('A'); t.×××ert('B', node); t.×××ert('C', node); t.×××ert('D', node); node = t.find('B'); t.×××ert('E', node); t.×××ert('F', node); node = t.find('E'); t.×××ert('K', node); t.×××ert('L', node); node = t.find('C'); t.×××ert('G', node); node = t.find('G'); t.×××ert('N', node); node = t.find('D'); t.×××ert('H', node); t.×××ert('I', node); t.×××ert('J', node); node = t.find('H'); t.×××ert('M', node); for(t.begin(); !t.end(); t.next()) { cout << t.current() << endl; } return 0; }
運行結果如下
我們看到已經將之前的樹結構層次遍歷了一遍。通過對樹的學習,總結如下:1、樹是一種非線性的數據結構,結點擁有唯一前驅(父結點)和若干後繼(子結點);2、樹的結點包含一個數據及若干指向其他結點的指針,樹與結點在程序中表現爲特殊的數據類型;3、基於數據元素的查找可判斷值是否存在於樹中,基於結點的查找可判斷樹中是否存在指定結點;4、插入操作是構建樹的唯一操作,執行插入操作時必須指明結點間的父子關係;5、插入操作必須正確處理指向父結點的指針,插入數據元素時需要從堆空間中創建結點;6、銷燬結點時需要決定是否釋放對應的內存空間,工廠模式可用於“定製”堆空間中的結點,只有銷燬定製結點的時候需要進行釋放;7、刪除操作必須完善處理父結點和子結點的關係,它的返回值爲指向樹的智能指針對象,函數中返回堆中的對象時使用智能指針作爲返回值;8、插入操作和刪除操作都依賴於查找操作;9、樹的結點沒有固定的編號方式,可以按照層次關係對樹中的結點進行遍歷;10、通過遊標的思想設計遍歷成員函數,遍歷成員函數是相互依賴,相互配合的關係,遍歷算法的核心爲隊列的使用。