数据结构-树与二叉树
文章目錄
- 一:樹
- (1)樹的概念
- (2)樹的一些基本術(shù)語
- (3)樹的表示
- A:孩子兄弟表示法
- B:雙親表示法
- C:孩子表示法
- 二:二叉樹
- (1)二叉樹的概念
- (2)特殊的二叉樹
- (3)二叉樹的性質(zhì)
- (4)二叉樹的順序存儲結(jié)構(gòu)
- A:二叉樹順序存儲結(jié)構(gòu)
- B:堆
- (5)鏈式存儲結(jié)構(gòu)
- A:二叉樹的鏈式存儲結(jié)構(gòu)
- B:二叉樹的遍歷
- ①:層次遍歷
- ②:深度優(yōu)先遍歷
- 三:二叉樹相關(guān)問題
- (1)基礎(chǔ)問題
- (2)相關(guān)oj題
- 四:相關(guān)代碼
一:樹
(1)樹的概念
樹是一種非線性的數(shù)據(jù)結(jié)構(gòu),是有n個(n?\geqslant? 0,當n為0時叫做空樹)有限結(jié)點組成的一個具有層次關(guān)系的集合。把它稱為樹,是因為它和現(xiàn)實中的數(shù)十分相像,只不過是倒掛的樹
如下,樹中有一個非常特殊的結(jié)點,稱其為根節(jié)點,根節(jié)點沒有前驅(qū),除根結(jié)點外,剩余每個結(jié)點又可以看作是以它為根節(jié)點所組成的子樹,因此每一個結(jié)點都可以說是一個子樹的根節(jié)點,所以樹的定義時具有遞歸性質(zhì)的
(2)樹的一些基本術(shù)語
下面的這些概念,了解即可,不用刻意記憶
| 結(jié)點 | A,B,C等都是結(jié)點,結(jié)點不僅包含數(shù)據(jù)元素,而且包含指向子樹的分支。 | A結(jié)點不僅包含元素A,而且包含三個指向子樹的指針 |
| 結(jié)點的度 | 結(jié)點擁有的子樹或分支個數(shù) | A結(jié)點有三顆子樹,A的度為3 |
| 樹的度 | 樹中各結(jié)點度的最大值 | A,D結(jié)點的度最大為3,故樹的度為3 |
| 葉子結(jié)點 | 度為0的結(jié)點 | F,G,I,J,K,L,M均為葉子結(jié)點 |
| 非葉結(jié)點 | 度不為0的結(jié)點 | A,B,C,D,E,H,均為非葉結(jié)點 |
| 孩子 | 某結(jié)點子樹的根 | A結(jié)點的孩子為B,C,D |
| 雙親 | 與孩子的定義對應 | B,C,D的雙親都是A |
| 兄弟 | 同一個雙親的孩子互為兄弟 | B,C,D互為兄弟 |
| 祖先 | 從根到某結(jié)點的路徑上所有的結(jié)點,都是該結(jié)點的祖先 | K的祖先是A,B,E |
| 子孫 | 以某結(jié)點為根的子樹中的所有結(jié)點 | D的子孫是H,I,J,M |
| 層次 | 根節(jié)點為第一層,根的孩子是第二層次,以此類推 | 結(jié)點F處在第三層 |
| 結(jié)點的深度 | 是指從根節(jié)點到該結(jié)點路徑上的結(jié)點個數(shù) | \ |
| 結(jié)點的高度 | 從某結(jié)點往下走可能到達多個葉子結(jié)點,對應了通往這些葉子結(jié)點的路徑,其中最長的那條路徑上的結(jié)點的個數(shù)稱其為結(jié)點的高度 | D的高度為3 |
| 樹的高度(深度) | 樹中結(jié)點的最大層次 | 根節(jié)點的高度就是樹的高度 |
| 堂兄弟 | 雙親在同一層的結(jié)點互為堂兄弟 | G和H互為堂兄弟 |
| 有序樹 | 樹中結(jié)點的子樹從左至右是有次序的,不能交換 | \ |
| 無序樹 | 樹中結(jié)點的子樹沒有順序,可以任意交換 | \ |
| 豐滿樹 | 除了最底層外,其他層都是滿的 | \ |
| 森林 | 若干互不相交的樹的集合 | 上面的樹,將根結(jié)點A去除,剩余的就一個森林 |
(3)樹的表示
很多時候,樹的度是不能夠確定的,也就是說一個結(jié)點有多少個孩子是不可知的。Windows系統(tǒng)的目錄采用的就是樹形結(jié)構(gòu),我們從沒有聽過在一個文件夾里最多可以創(chuàng)建多少個文件等這樣的限制(前提是不受存儲限制)。所以使用單純的線性對應的結(jié)構(gòu)去存儲樹,是不可行的。
樹的存儲結(jié)構(gòu)主要有:孩子兄弟表示法,雙親表示法和孩子表示法
A:孩子兄弟表示法
樹的孩子兄弟表示法:一個結(jié)點有三個域,一個數(shù)據(jù)域和兩個指針域,第一個指針指向該結(jié)點的第一個孩子結(jié)點,第二個指針域指向該結(jié)點的兄弟結(jié)點。對于其孩子結(jié)點也是如此定義。
B:雙親表示法
樹的雙親表示法:使用一組連續(xù)的存儲空間來存放結(jié)點,結(jié)點按一定順序(一般是從上到下,從左到右)依次存放在數(shù)組中,數(shù)組的下標表示了該結(jié)點的位置,每個結(jié)點有一個數(shù)據(jù)域和一個指針域,指針域保存的是該結(jié)點的雙親結(jié)點在數(shù)組中的下標。
C:孩子表示法
樹的孩子表示法其實就是圖的鄰接表存儲結(jié)構(gòu),這里不再詳細敘述了
二:二叉樹
(1)二叉樹的概念
二叉樹應該是這一棵樹:每個結(jié)點最多有兩顆子樹,也即二叉樹的度最大為2,同時二叉樹的子樹有次序之分,不能顛倒
(2)特殊的二叉樹
- 滿二叉樹:滿二叉樹它的每一層的結(jié)點數(shù)都達到了最大值。如果一個二叉樹有k層,且其結(jié)點總數(shù)為2k-1個,那么它就是滿二叉樹
- 完全二叉樹:完全二叉樹概念有點抽象。簡單點來說:一個完全二叉樹是由對應的滿二叉樹進行刪除而來的,刪除的時候必須從上到下,從右向左,不能跳著刪除。
(3)二叉樹的性質(zhì)
- 規(guī)定根節(jié)點層數(shù)為1,則一顆非空二叉樹第i層最多有2i-1個結(jié)點
- 規(guī)定根節(jié)點的層數(shù)為1,則深度為h的二叉樹最大結(jié)點數(shù)為2h-1個。
題目:在具有 2n 個結(jié)點的完全二叉樹中,葉子結(jié)點個數(shù)為?
-
對任意一顆二叉樹,葉子結(jié)點(度為0的結(jié)點)總比度為2的結(jié)點多1個
推導過程如下:
/
①首先需要明白度為0的結(jié)點,引出0個分支,度為1的結(jié)點引出1個分支,度為2的結(jié)點引出3個分支,依次類推。 ② 假設一個二叉樹中葉子結(jié)點有n0個,那么它引出0×n0條分支,單分支結(jié)點數(shù)為n1個,它引出1×n1條分支,雙分支結(jié)點數(shù)為n2,它引出2×n2條分支。于是總的結(jié)點數(shù)為n0+n1+n2,總分支數(shù)為n1+2×n2條。 ③任何一個樹,總分數(shù)=總結(jié)點數(shù)-1,于是由②可知n1+2×n2=n0+n1+n2-1,化簡得到n0=n2+1.
/
這條性質(zhì)要注意靈活應用,很多時候不會這樣直接考
/
比如某道題問道“二叉樹的總的結(jié)點數(shù)為n,問空指針多少?”我們可以把所有的空指針都看作葉子結(jié)點,也就是說這個二叉樹所有節(jié)點都是雙分支結(jié)點,根據(jù)葉子結(jié)點數(shù)=雙分支結(jié)點數(shù)+1,所以在本樹中,葉子結(jié)點數(shù)(空指針數(shù))=雙分支結(jié)點數(shù)(總的結(jié)點數(shù))+1,也就是n+1. -
具有N個結(jié)點的完全二叉樹高度為 ?log2N?\left \lfloor log_{2}N\right \rfloor?log2?N?或者是?log2N+1?\left \lceil log_{2}N+1\right \rceil?log2?N+1?
(4)二叉樹的順序存儲結(jié)構(gòu)
A:二叉樹順序存儲結(jié)構(gòu)
對于一顆完全二叉樹,從根節(jié)點開始,從上到下,從左至右依次編號,按照編號存放在一個數(shù)組里,這就是完全二叉樹的順序存儲結(jié)構(gòu)。比如一個完全二叉樹,根節(jié)點編號為0,那么它的左孩子編號就是2×0+1,右孩子編號就是2×0+2,該二叉樹中任意一個結(jié)點(假設編號為i),其左孩子編號為2×i+1,右孩子為2×i+2.
之所以這里要限定為完全二叉樹,并不是說普通的二叉樹不能用數(shù)組存儲。既然一顆二叉樹要用數(shù)組存,那么它就一定要發(fā)揮數(shù)組的優(yōu)勢,比如說通過索引確定孩子的編號,而對于一個普通的二叉樹說來說,其結(jié)點分布可能是不均勻的,所以要使用數(shù)組存儲普通的二叉樹,必須要把這個樹轉(zhuǎn)化為其對應的完全二叉樹的形態(tài),勢必會造成大量空間的浪費
其實,數(shù)組在這里的真正用處是存儲堆,因為堆就是完全二叉樹。
B:堆
堆和堆排序的內(nèi)容,篇幅較長,見下面這篇文章
數(shù)據(jù)結(jié)構(gòu)-堆與堆排序
(5)鏈式存儲結(jié)構(gòu)
A:二叉樹的鏈式存儲結(jié)構(gòu)
使用二叉鏈表可以有效的存儲二叉樹。二叉鏈表中的每一個結(jié)點,有一個數(shù)據(jù)域和兩個指針域,這兩個指針域分別指向該結(jié)點的左孩子和右孩子。
二叉鏈表結(jié)構(gòu)定義如下
B:二叉樹的遍歷
如果要用一個詞來概括二叉樹的話,那么我選擇“遍歷”。遍歷可謂是二叉樹中最重要的運算,是其他一切運算的前提。
所謂遍歷,通俗的理解就是全部訪問一遍,在單鏈表那一章為什么不過度強調(diào)遍歷這個詞呢,因為單鏈表的遍歷就只有一種情況。對于二叉樹來講,其邏輯結(jié)構(gòu)的特點,導致了它有不同的遍歷方法,每個遍歷又有其特定的應用場景。
遍歷從大的方面來講可以分為兩種:廣度優(yōu)先遍歷(BFS),深度優(yōu)先遍歷(DFS)。對于二叉樹來說,它有四種遍歷方法,層次遍歷屬于BFS,先序,中序,后序遍歷屬于DFS
①:層次遍歷
一句話概括:自上而下,先左后右
②:深度優(yōu)先遍歷
以下動圖來自天勤計算機考研
天勤計算機考研
大部分人其實都知道一些二叉樹的遍歷的口訣,例如先序遍歷是“根左右”,中序遍歷“左根右”,或許遍歷左右根。通過這樣的口訣,也能夠很快的寫出樹的各種遍歷方式,但如果問到為什么是這樣,很多人卻無法說清楚。
呈現(xiàn)出不同的遍歷的方式的原因在于二叉樹的遞歸結(jié)構(gòu)和訪問時機的不同。
如下圖,這三種遍歷方式本質(zhì)是一樣的,每個結(jié)點都會經(jīng)歷三次訪問,二叉樹默認遞歸時其實就是先序遍歷,也就是先根節(jié)點,再左,后右。在其遞歸的過程,在不同時機訪問,就會造成不同的遍歷結(jié)果。
先序遍歷是指:第一次遇到這個結(jié)點訪問,第二次遇到或第三次遇到跳過該結(jié)點直接訪問下一個結(jié)點
中序遍歷是指:第一次遇到結(jié)點跳過,第二次再遇到結(jié)點訪問,第三次遇到跳過
后序遍歷是指:前兩次遇見結(jié)點不訪問,最后一次遇見時訪問
三:二叉樹相關(guān)問題
二叉樹不像鏈表,棧隊列那些結(jié)構(gòu)一樣,對于二叉樹,考察的主要對于它的應用。二叉樹的應用一定與遞歸離不開,遞歸代碼簡單,但是十分抽象。遞歸包括兩點:子問題和返回條件,要寫出遞歸代碼,首先不能把問題看的太復雜,要把問題細化,細化成最小的問題。對于二叉樹來說,其子問題就是劃分左子樹和右子樹,返回條件為空樹
為了方便說明,建立這樣一顆樹
(1)基礎(chǔ)問題
1:三種遍歷(遞歸)
下面的三種遍歷采用遞歸的方式,遞歸的方法代碼短小精悍,但是理解起來較為抽象
- 先序遍歷
leetcode中的這道先序遍歷的題,也可以用遞歸去做
二叉樹前序遍歷
- 中序遍歷
- 后序遍歷
2:求解樹的結(jié)點個數(shù)
更優(yōu)的寫法(分治法思想)
3:求解樹的葉子結(jié)點個數(shù)
假如沒有結(jié)點(root=NULL),那么就是0,假如只有一個結(jié)點A(葉子結(jié)點),那么它就是葉子結(jié)點,然后A的葉子結(jié)點就是其左子樹+右子樹
4:求解樹的高度
如果沒有結(jié)點(root=NULL),那么高度為0,如果只有一個結(jié)點A(葉子結(jié)點),那么高度為1,所以A的高度就是A的左子樹高度和右子樹高度之比,取大者+1
5:求二叉樹第K層的結(jié)點個數(shù)
也是一個遞歸過程。比如要求某個樹的第三層的結(jié)點個數(shù),那么就等于求其子樹的第二層的結(jié)點個數(shù),再等價于求其子樹的子樹的第一層的結(jié)點個數(shù)。傳入下一層時,讓k-1,同時傳入的是其孩子結(jié)點,這樣一旦root不等于空,k不等于1,就能一直遞歸下去,直到k=1時,不斷向上返回即可
6:找尋某個結(jié)點
如果root為空,返回NULL,如果root的值是X,則返回root;否則,該節(jié)點找不到,該去其孩子結(jié)點找,先定義一個左尋找結(jié)點,遞歸進入左孩子,如果左孩子找不到則什么也不返回,如果左孩子返回則不執(zhí)行右孩子,否則再去右孩子找。如果左右孩子都不返回,那么這棵樹沒有該節(jié)點
7:銷毀二叉樹
后序銷毀
8:二叉樹的層次遍歷
二叉樹的層次遍歷需要借助到隊列。先將根節(jié)點入隊列,然后如果隊列不空,就出隊,出隊時看其孩子是否存在,如果存在就將其孩子入隊列,然后迭代即可。
所以下面的操作中,會借助到隊列的相關(guān)操作,具體見
數(shù)據(jù)結(jié)構(gòu)-線性表之棧和隊列
同時需要注意,隊列中結(jié)點的數(shù)據(jù)域,也即val,可以設置為BTNode*,也就是數(shù)據(jù)存儲的是樹節(jié)點指針。
9:判斷是否為完全二叉樹
判斷是否為完全二叉樹可以從層序遍歷入手
所以,首先讓其按照層序遍歷次序入隊,同時出隊,如果遇到NULL,跳出循環(huán),接著再對后半部分進行出隊,如果出隊的過程中出現(xiàn)非空元素,那么它就不是完全二叉樹,反之則是
如下
(2)相關(guān)oj題
二叉樹oj題
四:相關(guān)代碼
BinaryTree.h
#pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h>typedef struct BTNode {char val;struct BTNode* _lchild;struct BTNode* _rchild; }BTNode;typedef struct ListNode {struct ListNode* _next;BTNode* val;//這里不用直接把樹的結(jié)點入隊列,直接入指針最方便 }ListNode; typedef struct Queue {ListNode* _front;ListNode* _rear; }queue; typedef BTNode* DataType;BTNode* CreatNode(char x);//申請結(jié)點void PreOrder(BTNode* root);//前序遍歷 void InOrder(BTNode* root);//中序遍歷 void PostOrder(BTNode* root);//后序遍歷 int TreeSize(BTNode* root);//求樹的結(jié)點 int LeafSize(BTNode* root);//求葉子結(jié)點數(shù) int TreeDeep(BTNode* root);//求樹的深度 int TreeLevelSize(BTNode* root,int k);//求樹的第K層的結(jié)點個數(shù) BTNode* TreeFind(BTNode* root, char x);//查找某個結(jié)點 void DestoryTree(BTNode* root);//銷毀void level(BTNode* root);//層次遍歷 int isCompleteTree(BTNode* root);//是否為完全二叉樹BinaryTree.c
#include "BinaryTree.h"void QueuePush(queue* pq, DataType x) {assert(pq);ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));assert(NewNode);NewNode->val = x;NewNode->_next = NULL;if (pq->_rear == NULL)//插入第一個節(jié)點,rear和front要同時指向newnode{pq->_rear = NewNode;pq->_front = NewNode;}else//正常情況{pq->_rear->_next = NewNode;pq->_rear = NewNode;} } BTNode* QueuePop(queue* pq) {assert(pq);assert(pq->_front);if (pq->_front == pq->_rear)//注意特殊情況,隊列中只剩一個元素,需要特殊處理,不然發(fā)生錯誤{BTNode* receive = pq->_front->val;//用于返回樹節(jié)點ListNode* release = pq->_front;//釋放結(jié)點pq->_front = NULL;pq->_rear = NULL;free(release);return receive;}else//正常情況{BTNode* receive = pq->_front->val; ListNode* release = pq->_front;pq->_front = pq->_front->_next;free(release);return receive;}}void PreOrder(BTNode* root) {if (root == NULL){printf("NULL ");return;}printf("%c ", root->val);PreOrder(root->_lchild); PreOrder(root->_rchild); } void InOrder(BTNode* root) {if (root == NULL){printf("NULL ");return;}InOrder(root->_lchild);printf("%c ", root->val);InOrder(root->_rchild); } void PostOrder(BTNode* root) {if (root == NULL){printf("NULL ");return;}PostOrder(root->_lchild);PostOrder(root->_rchild);printf("%c ", root->val); }BTNode* CreatNode(char x) {BTNode* NewNode = (BTNode*)malloc(sizeof(BTNode));NewNode->_lchild = NULL;NewNode->_rchild = NULL;NewNode->val = x;}int TreeSize(BTNode* root) {if (root == NULL)return 0;elsereturn 1+ TreeSize(root->_lchild) + TreeSize(root->_rchild);} int LeafSize(BTNode* root) {if (root == NULL)return 0;if (root->_lchild == NULL && root->_rchild == NULL)return 1;return LeafSize(root->_lchild) + LeafSize(root->_rchild);} int TreeDeep(BTNode* root) {if (root == NULL)return 0;if (root->_lchild == NULL && root->_rchild == NULL)return 1;return TreeDeep(root->_lchild) > TreeDeep(root->_rchild) ? TreeDeep(root->_lchild) + 1 : TreeDeep(root->_rchild) + 1;} int TreeLevelSize(BTNode* root, int k) {if (root == NULL)return 0;if (k == 1)return 1;return TreeLevelSize(root->_lchild, k - 1) + TreeLevelSize(root->_rchild, k - 1); } BTNode* TreeFind(BTNode* root, char x) {if (root == NULL)return NULL;if (root->val == x)return root;BTNode* findnode_left = TreeFind(root->_lchild, x);if (findnode_left)return findnode_left;BTNode* findnode_right = TreeFind(root->_rchild, x);if(findnode_right)return findnode_right;return NULL; } void DestoryTree(BTNode* root) {if (root == NULL)return;DestoryTree(root->_lchild);DestoryTree(root->_rchild);free(root);}void level(BTNode* root) {queue q;q._front = NULL;q._rear = NULL;//隊列的初始化BTNode* p;//中轉(zhuǎn)結(jié)點QueuePush(&q, root);//首先入根節(jié)點while (q._front != NULL)//隊列不空,進行出隊列{p = QueuePop(&q);//出來一個訪問一個,然后看其依次看其左右孩子是否存在,如果存在則入隊列printf("%c ", p->val);if (p->_lchild != NULL){QueuePush(&q, p->_lchild);}if (p->_rchild != NULL){QueuePush(&q, p->_rchild);} } }//返回1是完全,返回不是完全 int isCompleteTree(BTNode* root) {queue q;q._front = NULL;q._rear = NULL;if (root == NULL)return 1;//空樹是完全二叉樹QueuePush(&q, root);while (q._front != NULL){BTNode* midlle_node = QueuePop(&q);if (midlle_node == NULL)break;//如果為空,說明層次遍歷結(jié)束QueuePush(&q, midlle_node->_lchild);QueuePush(&q, midlle_node->_rchild);}while (q._front != NULL){BTNode* middle_front = QueuePop(&q);if (middle_front!=NULL)//如果后半部分出現(xiàn)了非空,那么就肯定不是完全二叉樹{return 0;}}return 1;}test.c
#include "BinaryTree.h"void test() {BTNode* A = CreatNode('A');BTNode* B = CreatNode('B');BTNode* C = CreatNode('C');BTNode* D = CreatNode('D');BTNode* E = CreatNode('E');A->_lchild = B;A->_rchild = C;B->_lchild = D;C->_lchild = E;//printf("A高度為%d\n", TreeDeep(A));//printf("B高度為%d\n", TreeDeep(B));//printf("A的第二層的高度為%d\n", TreeLevelSize(A, 1));//printf("結(jié)點的D的地址是%p\n", D);//printf("調(diào)用函數(shù)找到的D的地址是%p\n", TreeFind(A, 'D'));if(isCompleteTree(A)==1)printf("是完全二叉樹\n");elseprintf("不是完全二叉樹");}int main() {test(); }總結(jié)
以上是生活随笔為你收集整理的数据结构-树与二叉树的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (计算机组成原理)第二章数据的表示和运算
- 下一篇: 从零学React Native之07Vi