初識樹與二叉樹(基本知識講解與常見實現)

二叉樹鋪墊——樹

前面幾篇文章咱們主要介紹的線性表,棧,隊列,串,等等,都是一對一的線性結構,而今天咱們所講解的 「樹」 則是一種典型的非線性結構,非線性結構的特色就是,任意一個結點的直接前驅,若是存在,則必定是惟一的,直接後繼若是存在,則能夠有多個,也能夠理解爲一對多的關係,下面咱們就先來認識一下樹ios

樹的概念

下圖咱們平常生活中所見到的樹,能夠看到,從主樹幹出發,向上衍生出不少枝幹,而每一根枝幹,又衍生出一些枝丫,就這樣組成了咱們在地面上能夠看到的樹的結構,但對於每個小枝丫來說,歸根結底,仍是來自於主樹幹的層層衍生造成的。c++

咱們每每須要在計算機中解決這樣一些實際問題 例如:算法

  • 用於保存和處理樹狀的數據,例如家譜,組織機構圖
  • 進行查找,以及一些大規模的數據索引方面
  • 高效的對數據排序

先不提一些複雜的功能,就例如對於一些有樹狀層級結構的數據進行建模,解決實際問題,咱們就能夠利用 「樹」 這種結構來進行表示,爲了更符合咱們的習慣,咱們通常把 「樹」 倒過來看,咱們就能夠將其概括爲下面這樣的結構,這也就是咱們數據結構中的 「 樹」數組

樹中的常見術語

  • 結點:包含數據項以及指向其餘結點的分支,例如上圖中圓 A 中,既包含數據項 A 又指向 B 和 C 兩個分支微信

  • 特別的,由於 A 沒有前驅,且有且只有一個,因此稱其爲根結點數據結構

  • 子樹:由根結點以及根結點的全部後代導出的子圖稱爲樹的子樹函數

    • 例以下面兩個圖均爲上面樹中的子樹

  • 結點的度:結點擁有子樹的數目,簡單的就是直接看有多少個分支,例如上圖 A 的度爲2,B的度爲1post

  • 葉結點:也叫做終端結點,即沒有後繼的結點,例如 E F G H I學習

  • 分支結點:也叫做非終端結點,除葉結點以外的均可以這麼叫spa

  • 孩子結點:也叫做兒子結點,即一個結點的直接後繼結點,例如 B 和 C 都是 A 的孩子結點

  • 雙親結點:也叫做父結點,一個結點的直接前驅,例如 A 是 B 和 C 的雙親結點

  • 兄弟結點:同一雙親的孩子結點互稱爲兄弟結點 例如 B 和 C 互爲兄弟

  • 堂兄弟:雙親互爲兄弟結點的結點,例如 D 和 E 互爲堂兄弟

  • 祖先結點:從根結點到達一個結點的路徑上的全部結點,A B D 結點均爲 H 結點的祖先結點

  • 子孫結點:以某個結點爲根的子樹中的任意一個結點都稱爲該結點的子孫結點,例如 C 的子孫結點有 E F I

  • 結點的層次:設根結點層次爲1,其他結點爲其雙親結點層次加1,例如,A 層次爲1,B C 層次爲 2

  • 樹的高度:也叫做樹的深度,即樹中結點的最大層次

  • 有序/無序樹:樹中結點子樹是否從左到右爲有序,有序則爲有序樹,無序則爲無序樹

可能你們也看到了,上面我舉的例子,分支所有都在兩個之內,這就是咱們今天所重點介紹的一種樹—— 「二叉樹」

二叉樹

在計算機科學中,二叉樹(英語:Binary tree)是每一個結點最多隻有兩個分支(即不存在分支度大於2的結點)的樹結構。一般分支被稱做「左子樹」或「右子樹」。二叉樹的分支具備左右次序,不能隨意顛倒。 ——維基百科

根據定義須要特別強調的:

  • 是每一個結點最多隻有兩個分支,不是表明只能有兩個分支,而是最多,沒有或者只要一個都是能夠的
  • 左子樹和右子樹必須有明確的次序,即便只有一顆也要說明,具體是左子樹仍是右子樹

幾種特殊的二叉樹

(一) 滿二叉樹

一般狀況下,咱們見到的樹都是有高有低的,層次不齊的,若是一顆二叉樹中,任意一層的結點個數都達到了最大值,這樣的樹稱爲滿二叉樹,一顆高度爲 k 的二叉樹具備 2<sup>k</sup> - 1 次個結點

(二) 徹底二叉樹

徹底二叉樹是效率很高的數據結構,徹底二叉樹是由滿二叉樹而引出來的。對於深度爲K的,有n個結點的二叉樹,當且僅當其每個結點都與深度爲K的滿二叉樹中編號從1至n的結點一一對應時稱之爲徹底二叉樹

如何快速判斷是否是徹底二叉樹:

  • 若是一棵二叉樹只有最下面兩層結點的度能夠小於2,而且最下面一層的結點都集中在該層最左邊的連續位置上,此樹能夠成爲徹底二叉樹
  • 看着樹的示意圖,心中默默按照滿二叉樹的結構逐層順序編號,若是編號出現了空擋,就說明不是徹底二叉樹

(三) 正則二叉樹

正則二叉樹也稱做嚴格二叉樹,若是一顆二叉樹的任意結點,要麼是葉結點,要麼就恰有兩顆非空子樹,即除了度數爲0的葉結點外,全部分支結點的度都爲2

二叉樹的性質

性質1:一個非空的二叉樹的第 i 層上最多有 2<sup>i-1</sup> (i $\geq$ 0) 個結點

性質5:若是對一顆有 n 個結點的徹底二叉樹按層次自上而下(每層從左到右)對結點從1 到 n 進行編號,則對任意一個結點 i (i $\leq$ i $\leq$ n)

  • 若 i = 1 ,則結點 i 爲根,無雙親,若 i > 1,則雙親的編號是 [i/2]
  • 若 2i $\leq$ n ,則 i 的左孩子的編號爲 2i,不然無左孩子
  • 若 2i + 1 $\leq$ n, 則 i 的右孩子的編號爲 2i + 1,不然無右孩子

二叉樹的順序存儲結構

(一) 徹底二叉樹中

對於樹這種一對多的的關係,使用順序存儲結構確實不是很合理,可是也是能夠實現的

對於一個徹底二叉樹來講,將樹上編號爲 i 的結點存儲在一維數組中下標爲 i 的份量中,以下圖所示

(二) 普通二叉樹中

若是對於普通二叉樹,則須要現將一些空結點補充,使其成爲徹底二叉樹,新增的空結點設置爲 ^ 以下圖所示

(三) 較爲極端的狀況中

如在深度爲 k 右斜樹中,這種狀況下,它只有 k 個結點,可是根據前面的性質,咱們能夠知道,卻須要分配 2<sup>k-1</sup> 個存儲單元,這顯然對存儲空間是極大的浪費,因此看起來只有徹底二叉樹的狀況下,順序存儲方式比較實用

二叉樹的鏈式存儲結構(重點)

順序結構顯然不是很適合使用,因此在實際中,咱們會選擇鏈式存儲結構,鏈式存儲結構中,除了須要存儲自己的元素,還須要設置指針,用來反映結點間的邏輯關係,二叉樹中,每一個結點最多有兩個孩子,因此咱們設置兩個指針域,分別指向該結點的左孩子和右孩子,這種結構稱爲二叉鏈表結點(重點講解這一種)

二叉樹中經常進行 的一個操做是尋找結點的雙親,每一個結點還能夠增長一個指向雙親的指針域,這種結構稱爲三叉鏈表節點

利用二叉鏈表結點就能夠構成二叉鏈表,以下圖所示:

樹和二叉鏈表的代碼表示

(一) 樹的抽象數據類型

#ifndef _BINARYTREE_H_
#define _BINARYTREE_H_ 
#include<iostream>
using namespace std;

template<class T>
class binaryTree {
public:
	// 清空 
	virtual void clear()=0;
	// 判空,表空返回true,非空返回false					
	virtual bool empty()const=0;
	//二叉樹的高度 
	virtual int height() const=0;
	//二叉樹的結點總數 
	virtual int size()const=0;
	//前序遍歷 
	virtual void preOrderTraverse() const=0;
	//中序遍歷 
	virtual void inOrderTraverse() const=0;
	//後序遍歷 
	virtual void postOrderTraverse() const=0;
	//層次遍歷 
	virtual void levelOrderTraverse() const=0;
	virtual ~binaryTree(){};
};

#endif

(二) 二叉鏈表的表示

注意:咱們之因此將Node類型及其指針設置爲私有成員是由於,利於數據的封裝和隱藏,關於此概念,這一篇不過多說明了,咱們仍是重點關注算法的實現

#ifndef _BINARYLINKLIST_H_
#define _BINARYLINKLIST_H_
#include "binaryTree.h"
#include<iostream>
using namespace std;

//elemType爲順序表存儲的元素類型
template <class elemType>					
class BinaryLinkList: public binaryTree<elemType>
{ 
private:
	//二叉鏈表節點 
	struct Node {
		// 指向左右孩子的指針 
		Node *left, *right;
		// 結點的數據域 
		elemType data;
		//無參構造函數 
		Node() : left(NULL), right(NULL) {}
		Node(elemType value, Node *l = NULL, Node *r = NULL) {
			data = value;
			left = 1;
			right = r;
		}
		~Node() {}
	};
	
	//指向二叉樹的根節點 
	Node *root;
	//清空
	void clear(Node *t) const;
	//二叉樹的結點總數
	int size(Node *t) const;
	//二叉樹的高度
	int height(Node *t) const;
	//二叉樹的葉結點個數
	int leafNum(Node *t) const;
	//遞歸前序遍歷
	void preOrder(Node *t) const;
	//遞歸中序遍歷
	void inOrder(Node *t) const;
	//遞歸後序遍歷
	void postOrder(Node *t) const;
	
public:
	//構造空二叉樹 
	BinaryLinkList() : root(NULL) {}
    ~BinaryLinkList() {clear();}
    //判空 
    bool empty() const{ return root == NULL; }
    //清空 
    void clear() {
    	if (root)
    		clear(root);
    	root = NULL;
	}
	//求結點總數
	int size() const { return size(root); }
	//求二叉樹的高度
	int height() const { return heigth(root); }
	//二叉樹葉節點的個數
	int leafNum() const { return leafNum(root); }
	//前序遍歷
	void preOrderTraverse() const { if(root) preOrder(root); }
	//中序遍歷
	void inOrderTraverse() const { if(root) inOrder(root); }
	//後序遍歷
	void postOrderTraverse() const {if(root) postOrder(root); }
	//層次遍歷
	void levelOrderTeaverse() const;
	//非遞歸前序遍歷
	void preOrderWithStack() const;
	//非遞歸中序遍歷
	void inOrderWithStack() const;
	//非遞歸後序遍歷
	void postOrderWithStack() const;
};

#endif

二叉樹的遍歷

(一) 深度優先遍歷

概念:沿着二叉樹的深度遍歷二叉樹的節點,儘量深的訪問二叉樹的分支,主要分爲:前序遍歷,中序遍歷,後序遍歷,三種

  • 先序遍歷

    • 先訪問根節點,而後前序遍歷左子樹,最後前序遍歷左子樹 (根 - 左 - 右)
  • 中序遍歷

    • 先中序遍歷左子樹,再訪問根節點,最後中序遍歷右子樹 (左 - 根 - 右)
  • 後序遍歷

    • 前後序遍歷左子樹,後序遍歷右子樹,最後訪問根節點 (左 - 右 - 根)

舉個例子就清楚了:

以上圖爲例,三種遍歷方式的執行順序爲:

  • 前序遍歷:A - B - C - E - F
  • 中序遍歷:B - A - E - C - F
  • 後序遍歷:B - E - F - C - A

咱們以中序爲例:先中序遍歷左子樹,再訪問根節點,最後中序遍歷右子樹 (左 - 根 - 右)這是什麼意思呢?

中序遍歷,就是把每一個點都當作頭結點,而後每次都執行中序遍歷,也就是(左 - 根 - 右),等左邊空了,就返回訪問當前結點的父節點,也就是中,記錄後,再訪問右

例如:從根結點 A 出發,先訪問左孩子 B ,左邊沒有了,返回到 A ,訪問 A 右邊 C ,對其再進行中序遍歷, 即先訪問 E 而後返回 C 再 訪問 F 即:B - A - E - C - F

首先咱們先使用遞歸的方法來實現這三種遍歷方式,採用遞歸,給個人感受就是極其容易理解,並且寫代碼很簡潔,想要快速實現這種算法,簡直不要太快

(1) 前序遍歷-遞歸

template <class elemType>
void BinaryLinkList<elemType>:: preOrder(Node *t) const {
	if (t) {
		cout << t -> data << ' ';
		preOrder(t -> left);
		preOrder(t -> right);
	}
}

(2) 中序遍歷-遞歸

template <class elemType>
void BinaryLinkList<elemType>:: inOrder(Node *t) const {
	if (t) {
		preOrder(t -> left);
		cout << t -> data << ' ';
		preOrder(t -> right);
	}
}

(3) 後序遍歷-遞歸

template <class elemType>
void BinaryLinkList<elemType>:: postOrder(Node *t) const {
	if (t) {
		preOrder(t -> left);
		preOrder(t -> right);
		cout << t -> data << ' ';		
	}
}

提示:你們可能會注意到,在前面的定義中咱們定義了這樣三個方法,而且其都是公有的

//前序遍歷
void preOrderTraverse() const { if(root) preOrder(root); }
//中序遍歷
void inOrderTraverse() const { if(root) inOrder(root); }
//後序遍歷
void postOrderTraverse() const {if(root) postOrder(root); }

這是由於,前面遞歸的這三種方法,都須要一個Node類型的指針做爲參數,而指向二叉樹的根節點root又是私有的,這就致使咱們沒有辦法使用 BinaryLinklist類的對象來調用它,因此咱們須要寫一個公共的接口函數,也就是咱們上面這三個

雖然遞歸的方式簡單易懂,可是遞歸消耗的空間和時間都比較多,因此咱們能夠設計出另外一些算法來實現上面的三種遍歷,那就是利用棧的思想

(1) 前序遍歷-棧

template <class elemType>
void BinaryLinkList<elemType>::preOrderWithStack() const {
	//STL中的棧 
	stack<Node* > s;
	//工做指針,初始化指向根結點 
	Node *p = root;
	//棧非空或者p非空 
	while (!s.empty() || p) {
		if (p) {
			//訪問當前節點 
			cout << p -> data << ' ';
			//指針壓入棧 
			s.push();
			//工做指針指向左子樹 
			p = p -> left;
		} else {
			//獲取棧頂元素 
			p = s.top();
			//退棧 
			s.pop();
			//工做指針指向右子樹 
			p = p -> right; 
		}
	} 
}

(2) 中序遍歷-棧

template <class elemType>
void BinaryLinkList<elemType>::inOrderWithStack() const {
	//STL中的棧 
	stack<Node* > s;
	//工做指針,初始化指向根結點 
	Node *p = root;
	//棧非空或者p非空 
	while (!s.empty() || p) {
		if (p) {
			//指針壓入棧 
			s.push();
			//工做指針指向左子樹 
			p = p -> left;
		} else {
			//獲取棧頂元素 
			p = s.top();
			//退棧 
			s.pop();
			//訪問當前節點 
			cout << p -> data << ' ';
			//工做指針指向右子樹 
			p = p -> right; 
		}
	} 
}

(3) 後序遍歷-棧

後序遍歷略微特殊,在其中設置了Left 和 Right 兩個標記,用來區分棧頂彈出的結點是從棧頂結點的左子樹返回的仍是右子樹返回的

template <class elemType>
void BinaryLinkList<elemType>::postOrderWithStack() const {
	//定義標記 
	enum ChildType {Left,Right};
	//棧中元素類型
	struct StackElem {
		Node *pointer;
		ChildType flag;
	}; 
 	StackElem elem;
	//STL中的棧 
	stack<StackElem> S;
	//工做指針,初始化指向根結點 
	Node *p = root;
	while (!S.empty() || p) {
		while (p != NULL) {
			elem.pointer = p;
			elem.flag = Left;
			S.push(elem);
			p = p -> left;
		}
		elem = S.top();
		S.pop();
		p = elem.pointer;	
		//已經遍歷完左子樹 
		if (elem.flag == Left) {
			elem.flag = Right;
			S.push(elem);
			p = p -> right;
			//已經遍歷完右子樹 
		} else { 
			cout << p -> data << ' ';
			p = NULL;
		}
	}
}

用棧的方式來實現這三種遍歷,確實沒有遞歸方式容易理解,學習這部分時能夠對照一個簡單的圖,來思考,能夠幫助你更好認識代碼,能夠參考上面個人舉例圖

(二) 廣度優先遍歷

廣度優先遍歷,又叫作寬度優先遍歷,或者層序遍歷,思想就是,從根節點開始訪問,從上而下逐層遍歷,在同一層中,按照從左到右的順序對結點逐個訪問

咱們可使用隊列的思想來完成這樣一種遍歷方式

  • 初始化隊列,根節點入隊
  • 隊列非空,循環執行下面三步,不然結束
  • 出隊一個節點,同時訪問該節點
  • 若該節點左子樹非空,則將其左子樹入隊
  • 若該節點右子樹非空,則將其右子樹入隊
template <class elemType>
void BinaryLinkList<elemType>::levelOrderTeaverse() const {
	queue<Node* > que;
	Node *p = root;
	if (p) que.push(p);
	while (!que.empty()) {
		//取隊首元素 
		p = que.front();
		//出隊 
		que.pop();
		//訪問當前節點 
		cout << p -> data << ' ';
		//左子樹入隊
		if (p -> left != NULL)
			que.push(p -> left);
		//右子樹入隊 
		if (p -> right != NULL)
			que.push(p -> rigth);
	}
}

二叉樹的其餘常見運算

(一) 求節點總數

template <class elemType>
int BinaryLinkList<elemType>::size(Node *t) const {
	if (t == NULL)
		return 0;
	return 1 + size(t -> left) + size(t -> right);
}

(二) 求二叉樹高度

template <class elemType>
int BinaryLinkList<elemType>::height(Node *t) const {
	if (t == NULL) 
		return 0;
	else {
		int lh = height(t -> left);
		int rh = height(t -> right);
		return 1 + ((lh > rh) ? lh : rh);
	}
}

(三) 求葉結點個數

template <class elemType>
int BinaryLinkList<elemType>::leafNum(Node *t) const {
	if (t == NULL)
		return 0;
	else if ((t -> left == NULL) && (t -> right == NULL))
		return 1;
	else
		return leafNum(t -> left) + leafNum(t -> right);
}

(四) 清空

template <class elemType>
void BinaryLinkList<elemType>::clear(Node *t) {
	if (t -> left)
		clear(t -> left);
	if (t -> right)
		clear(t -> right); 
	delete t;
}

結尾:

若是文章中有什麼不足,或者錯誤的地方,歡迎你們留言分享想法,感謝朋友們的支持!

若是能幫到你的話,那就來關注我吧!若是您更喜歡微信文章的閱讀方式,能夠關注個人公衆號

在這裏的咱們素不相識,卻都在爲了本身的夢而努力 ❤

一個堅持推送原創開發技術文章的公衆號:理想二旬不止

相關文章
相關標籤/搜索