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