1. 程式人生 > >【c++】構建一棵簡單的二叉樹

【c++】構建一棵簡單的二叉樹

本文主要講了如何使用c++來構建一個二叉樹類,以及一些功能演算法的實現。文中大部分函式的思想都是遞迴,其中賦值運算子過載有傳統寫法和現代寫法兩個版本,層序遍歷是非遞迴,前、中、後序遍歷有遞迴和非遞迴兩個版本。

1、建構函式(遞迴)

2、拷貝建構函式(遞迴)

3、解構函式(遞迴)

4、賦值運算子過載(傳統/現代)

5、前中後序遍歷(遞迴/非遞迴)

6、層序遍歷(非遞迴)

7、查詢第k層結點個數(遞迴)

8、精確查詢值為x的結點,並返回當前結點的指標(遞迴)

9、查詢葉子結點個數(遞迴)

10、查詢結點總個數(遞迴)

11、計算樹的深度(遞迴)

樹的結點型別,每個結點都需要有一個指向右孩子的指標,一個指向左孩子的指標,以及一個數據。(當然,如果你想構造三叉樹的話,也可以增加一個指向父節點的指標。)

這裡我寫出了結點的建構函式,方便我們在建立樹的時候使用。

注意:這棵樹我使用了模板

template<typename T>
struct BinaryTreeNode
{
	BinaryTreeNode<T>* _left;//左孩子
	BinaryTreeNode<T>* _right;//右孩子
	T _data;//資料
	BinaryTreeNode(T data = T())//結點自己的建構函式,T()為一個匿名物件。
		:_left(NULL)//初始化為空
		, _right(NULL)
		, _data(data)
	{}
};
下面程式碼中會經常用到BinaryTreeNode<T>   所以將其重新命名為Node
typedef BinaryTreeNode<T> Node;



當我們向寫二叉樹類的時候,直接給類設定一個根結點,以這個根結點為基礎,構建二叉樹。
class BinaryTree
{
 public:
 private:
    BinaryTreeNode<T>* _root;//根節點
};

1、建構函式      BinaryTree(const T* a, size_t size,int index, const T& invalid)
建構函式有4個引數,T型別的指標a,傳參時傳一個數組,負責傳入資料。size儲存陣列a 的大小,index記錄下標,invalid表示非法值。
因為我們需要用到遞迴,所以在這個函式內部我們需要再封裝一個遞迴函式_MakeTree(),並讓它的返回值為一個Node*型別的指標。
BinaryTree(const T* a, size_t size,int index, const T& invalid)
{
	_root = _MakeTree(a,size,index,invalid);
}
我們先來觀察一個樹:
你會看到上面有許多NULL,這些NULL我們就可以理解為非法值invalid。這棵樹的前序遍歷為:  1  2  3  NULL  NULL  4   NULL   NULL  5   6  最後一個結點不需要非法值,到時候直接建立即可。 與上對應,我們傳陣列時,應該傳的值即為 int a[10] = {1,2,3,'#','#',4,'#','#',5,6}。非法值的值可以隨意設,這裡我設為‘#’,注意,你向以什麼的樣的順序建樹,就以什麼樣的順序傳參,事先要約定好。(這裡我用的是前序) 遞迴:當我們從陣列讀取到一個數據時,我們先要判斷這個值是不是合法,如果合法則new出一個結點並初始化作為當前結點,此時,進入左孩子遞迴函式讀取下一個資料(++index),並把這個函式的返回值鏈到當前結點root的left,同理,將右孩子遞迴函式的返回值鏈到當前結點的right。如果不合法則return,返回上一層函式。最後我們會得到一個根節點,例圖中的1。 _MakeTree函式實現:
Node* _MakeTree(const T* a, size_t size, int& index, const T& invalid)
{
	Node *root = NULL;
	if (index < size && a[index] != invalid)
	{
		root = new Node(invalid);
		root->_data = a[index];
		root->_left = _MakeTree(a, size, ++index, invalid);
		root->_right = _MakeTree(a, size, ++index, invalid);
	}
	return root;
}

2、拷貝建構函式
同上,同樣實現一個遞迴函式,返回值仍為Node*
BinaryTree(const BinaryTree<T>& t)
{
	_root = CopyTree(t._root);
}
遞迴:同樣為前序,從根節點開始,先訪問當前結點,判斷,若當前結點為空,則返回一個空。若當前結點不為空,則拷貝當期結點。然後遞迴進入當前結點的左子樹,同樣進行之前的步驟。左子樹處理完之後遞迴處理右子樹。然後返回當前結點(每一層的根節點)。 注意:上文提到的當前結點,在每一層的遞迴中值都是不一樣的。每遞迴一層,當前結點就會變成傳入的引數root。包括下文 CopyTree實現程式碼:
Node* CopyTree(const BinaryTreeNode<T>* _root)3、
{
	if (_root == NULL)
	{
		return NULL;
	}
	Node* root = new Node(_root->_data);
	root->_left = CopyTree(_root->_left);
	root->_right = CopyTree(_root->_right);
	return root;
}

3、解構函式 同上,但是解構函式不需要返回值。
~BinaryTree()
{
	Destroy(_root);
}
遞迴的道理都與上面兩個相同,這裡直接給出程式碼:
void Destroy( Node* _root)
{
	Node* tmp = _root;
	if (tmp == NULL)//如果根結點為空,則不需要delete,直接return。
	     return;
	Destroy(tmp->_left);
	Destroy(tmp->_right);
	delete tmp;
	tmp = NULL;
}
4、賦值運算子過載(=) 當我們寫任何賦值運算子過載時,都會有兩種寫法 ①傳統寫法,一個元素一個元素的拷貝,傳參時傳的是const的引用。 這樣賦值有個麻煩的地方,我們知道,當給一個結點賦值的時候,這個結點原本的內容,空間就要被銷燬掉。就是說我們既要new又要delete。當這個樹有1個億結點時,我們豈不是要重複1億次?有沒有一種更簡單的方法呢? ②現代寫法(建議使用,很巧妙),呼叫swap函式,交換兩個樹的root。傳參時用的值傳遞。
BinaryTree<T>& operator=(BinaryTree<T> t)
{
	if (this != &t)//自賦值的優化
	{
		std::swap(_root, t._root);
	}
	return *this;
}
我們都知道,值傳遞傳的是一份臨時拷貝(t為一份臨時拷貝),臨時拷貝的特性就是,它的存活週期只限於這個函式,當函式呼叫完畢時,它會自動銷燬,而我們也恰恰利用了這個特性。當我們執行std::swap(_root,t._root)這個語句的時候,臨時拷貝t的_root 和 本樹的(this)_root發生了交換。_root原本的值被賦給了臨時拷貝t._root,t._root的值被賦給了_root。當我們執行完這個程式的時候t自動銷燬,幫我們完成了銷燬_root原本內容的工作。我們即完成了賦值,又省去了一大部分工作,一舉兩得。 5、前中後序遍歷(遞迴)
①前序
這個我就不再多說了,構造,拷貝構造,析構,都是利用前序遍歷的道理來做的。當前結點-->左子樹-->右子樹。 程式碼:
void PrevOrder()
{
	_PrevOrder(_root);
	cout << endl;
}

_PrevOrder()
void _PrevOrder(Node* _root)
{
	Node* tmp = _root;
	if (tmp == NULL)
	{
		return;
	}
	cout << tmp->_data << " ";
	_PrevOrder(tmp->_left);
	_PrevOrder(tmp->_right);
}
②中序

判斷當前結點是否為空,為空的話,不處理,直接返回。先遞迴訪問當前結點左子樹,當左子樹處理完畢,再依次返回處理當前結點,再遞迴訪問當前結點右子樹。

void InOrder()
{
	_InOrder(_root);
	cout << endl;
}
_InOrder()
void _InOrder(Node* _root)
{
	Node* tmp = _root;
	if (tmp == NULL)
	{
		return;
	}
	_InOrder(tmp->_left);
	cout << tmp->_data << " ";
	_InOrder(tmp->_right);
}

③後序
判斷當前結點是否為空,為空的話,不處理,直接返回。不為空的話,先遞迴訪問當前結點節點的左子樹,再遞迴訪問當前結點根節點的右子樹,最後訪問當前結點。
void PostOrder()
{
	_PostOrder(_root);
	cout << endl;
}
  _PostOrder()
void _PostOrder(Node* _root)
{
	Node* tmp = _root;
	if (tmp == NULL)
	{
		return;
	}
	_PostOrder(tmp->_left);
	_PostOrder(tmp->_right);
	cout << tmp->_data << " ";
}
  6、前中後序非遞迴。 這裡我告訴大家一個真理,任何的遞迴都可以用棧來替換實現。這裡我就用一個輔助棧來實現前中後序的非遞迴。 ①前序          程式碼:
void PrevOrder_NonR()
{
	Node* cur = _root;
	stack<Node*> s;
	if (cur == NULL)
	{
		return;
	}
	while (cur || !s.empty())
	{
		while (cur)
		{
			s.push(cur);
			cout << cur->_data << " ";
			cur = cur->_left;
		}
		Node* top = s.top();
		s.pop();
		cur = top->_right;
	}
	cout << endl;
}


中序和後序的道理與上相同,只是當前節點的輸出做了小小的改動。下面直接貼出程式碼 ②中序(非遞迴)
void InOrder_NonR()
{
	Node* cur = _root;
	stack<Node*> s;
	if (cur == NULL)
	{
		return;
	}
	while (cur || !s.empty())
	{
		while (cur)
		{
			s.push(cur);
		        cur = cur->_left;
	        }
		Node* top = s.top();
		cout << top->_data << " ";
		s.pop();
		cur = top->_right;
	}
	cout << endl;
}
③後序(非遞迴) 後序的非遞迴較上面兩個多了一個變數,prev,它記錄了上一次訪問的節點。因為後序是最後訪問當前節點的,當我們訪問一個節點,我們不知道這個節點的右樹是否被訪問過。所以我們需要記錄一下上一個訪問的節點。以便訪問右樹時做判斷。
void PostOrder_NonR()
{
	Node* cur = _root;
	Node* prev = NULL;
	stack<Node*> s;
	while (cur || s.empty())
	{
		while (cur)
		{
			s.push(cur);
			cur = cur->_left;
		}
		Node* top = s.top();

		//如果右樹為空或者右樹已經訪問過,則訪問當前結點,並出棧
		//如果右樹不為空並且沒有訪問過,則訪問右樹
		if (top->_right == NULL || prev == top->_right)
		{
			cout << top->_data << " ";
			prev = top;
			s.pop();//返回父節點
		}
		else
		{
			cur = top->_right;
		}
	}
	cout << endl;
}


7、層序遍歷
顧名思義。層序遍歷就是按層來訪問一顆樹,一次訪問一層。這裡我們用到了一個佇列。                      
程式碼:
void _LevelOrder(Node* _root)
{
	Node *tmp = _root;
        queue<Node*> q;
	q.push(tmp);
	while (!q.empty())
	{
		Node* top = q.front();
		q.pop();
		cout << top->_data << " ";
		if (top->_left)
		{
			q.push(top->_left);
		}
		if (top->_right)
		{
			q.push(top->_right);
		}
	}
}


8、查詢第k層節點個數  這個問題,我們只需要查詢第k-1層有多少個孩子就行了。 同樣為遞迴,前序。
size_t FindKlevel(size_t k)
{
	return _FindKlevel(_root,k);
}

_FindKlevel()
size_t _FindKlevel(Node* _root,size_t k)
{
	Node *cur = _root;
	if (cur == NULL || k < 0)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	size_t left = _FindKlevel(cur->_left, k-1);
	size_t right = _FindKlevel(cur->_right, k-1);

	return  left + right;
}
9、精確查詢值為x的結點 前序遞迴查詢,如果根節點為空,返回NULL,如果當前節點等於x,返回當前節點的指標。如果當前節點不等於x,則遞迴進入左子樹查詢,若左子樹沒有,則遞迴進入右子樹查詢,若這棵樹中沒有x,返回NULL。
Node* Find(const T& x)
{
	return _Find(_root,x);
}
_Find()
Node* _Find(Node* _root,const T& x)
{
	Node* ret = NULL;
	Node* cur = _root;
	if (cur == NULL)
	{
		return NULL;
	}
	if (cur->_data == x)
	{
		ret = cur;
	}
	else
	{
	    ret = _Find(cur->_left,x);
	    if (ret == NULL)
	    {
		ret = _Find(cur->_right,x);
	    }
	}
	return ret;
}

10、查詢結點總個數。
結點總個數 = 當前節點個數+左子樹節點個數+右子樹節點個數。 前序遞迴查詢,若當前節點為空,返回,不為空則加1,--->遞迴左子樹----->遞迴右子樹。
size_t Size()
{
	return _Size(_root);
}
_Size()
size_t _Size( BinaryTreeNode<T>* _root )
{
	size_t ret = 0;
	if (_root == NULL)
	{
		return ret;
	}
	ret++;
	ret += _Size(_root->_left);
	ret += _Size(_root->_right);
}
11、計算樹的深度 樹的深度取左子樹深度+1和右子樹深度+1的最大值。(+1為根節點的深度)
size_t Depth()
{
	return _Depth(_root);
}
_Depth()
size_t _Depth(Node* _root)
{
	if (_root == NULL)
	{
		return 0;
	}
	int left = _Depth(_root->_left) + 1;
	int right = _Depth(_root->_right) + 1;
	return left > right ? left : right;
}





。請大家採用批評的態度來看這篇博文,博主水平也一般。。。。