五、樹與二叉樹
樹的基本概念
樹是n個結點的有限集合,n=0時,稱為空樹。非空樹滿足:
除了根節(jié)點外,任何一個結點都有且僅有一個前驅
- 結點的層次(深度):從上往下數
- 結點的高度:從下往上數
- 樹的高度(深度):總共有多少層
- 結點的度:有幾個孩子(分支)
- 樹的度:各節(jié)點的度的最大值
- 森林:m課互不相交的樹的集合(m可為0,空森林)
樹的性質:
- 結點數=總度數+1
- 度為m的樹、m叉樹的區(qū)別
- 度為m的樹第i層至多有mi-1個結點(i>=1);m叉樹第i層至多有mi-1個結點(i>=1)
- 高度為h的m叉樹至多有mh-1/m-1個結點
- 高度為h的m叉樹至少有h個結點;高度為h、度為m的樹至少有h+m-1個結點
- 具有n個結點的m叉樹的最小高度為向上取整 logm(n(m-1)+1)
二叉樹的概念
基礎概念
二叉樹時n個結點的有限集合(n可以為0)
由一個根節(jié)點和兩個互不相交的被稱為根的左子樹和右子樹組成,左子樹和右子樹又分別是一棵二叉樹
- 每個結點至多只有兩棵子樹
- 左右子樹不能顛倒(二叉樹是有序樹)
五種形態(tài):
特殊二叉樹:
滿二叉樹、完全二叉樹:
二叉排序樹:
平衡二叉樹:
常考性質
二叉樹??夹再|:
-
結點性質:n0=n2+1
-
第i層結點數
-
高度為h時最多結點數(滿二叉樹)
完全二叉樹??夹再|:
- 具有n個(n>0)結點的完全二叉樹高度(兩種推導)
- 可以由結點數n退出度為0、1、2的結點個數n0、n1、n2
總結:
存儲方式
二叉樹的順序存儲:
#define MaxSize 100
struct TreeNode {
int value;//結點中的數據元素
bool isEmpty;//判斷結點是否為空
};
int main()
{
TreeNode t[MaxSize];//定義一個長度為MaxSize的數組t,按照從上至下,從左至右的順序依次存儲二叉樹中的各個結點
for (int i = 0; i < MaxSize; i++)
{
t[i].isEmpty = true;//初始化時所有結點標記為空
}
}
完全二叉樹中,有著如下基本操作:
非完全二叉樹使用順序存儲:
二叉樹的鏈式存儲:
//定義儲存數據類型
struct ElemType {
int value;
};
typedef struct BiTNode {
ElemType data;//數據域
struct BiTNode* lchild, * rchild;//左右孩子指針
} BiTNode, * BiTree;
//初始化
void InitTree(BiTree root) {
root = (BiTree)malloc(sizeof(BiTNode));
root->data = { 1 };
root->lchild = NULL;
root->rchild = NULL;
}
//插入新結點
bool InsertNode(BiTree T, ElemType val) {
BiTNode* p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = val;
p->lchild = NULL;
p->rchild = NULL;
T->lchild = p;//作為左孩子
}
n個結點的二叉鏈表共有n+1個空鏈域
二叉樹遍歷及線索二叉樹
前中后以及層次遍歷
遞歸法:
先序遍歷
//先序遍歷
void PreOder(BiTree T) {
if (T != NULL) {
visit(T);//訪問根節(jié)點
PreOder(T->lchild);//遍歷左子樹
PreOder(T->rchild);//遍歷右子樹
}
}
中序遍歷
//中序遍歷
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild);//遍歷左子樹
visit(T);//訪問根節(jié)點
InOrder(T->rchild);//遍歷右子樹
}
}
后序遍歷
//后序遍歷
void PostOder(BiTree T) {
if (T != NULL) {
PostOder(T->lchild);
PostOder(T->rchild);
visit(T);
}
}
非遞歸法:
先序遍歷(需要用到輔助棧)
//先序遍歷
void PreOrder2(BiTree T)
{
SqStack S;
InitStack(S); BiTree p = T;//初始化棧S;p是遍歷指針
while (p || !IsEmpty(S)) //棧不空或p不空時循環(huán)
{
if (p) //一路向左
{
visit(p); Push(S, p);//訪問當前結點,并入棧
p = p->lchild;//左孩子不空,一直向左走
}
else //出棧,并轉向出棧結點的右子樹
{
Pop(S, p);//棧頂元素出棧
p = p->rchild;//向右子樹走,p賦值為當前結點的右孩子
} //返回while循環(huán)繼續(xù)進入if-else語句
}
}
中序遍歷
//中序遍歷
void InOrder2(BiTree T)
{
SqStack S;
InitStack(S);//初始化棧
BiTree p = T;//p為遍歷指針
while (p || !IsEmpty(S))
{
if (p)
{
Push(S, p);//當前結點入棧
p = p->lchild;//左孩子不空,一直向左走
}
else //出棧,并轉向出棧結點的右子樹
{
Pop(S, p); visit(p);//棧頂元素出棧,訪問出棧結點
p = p->rchild;//向右子樹走,p賦值為當前結點的右孩子
}//返回while循環(huán)繼續(xù)進入if-else語句
}
}
層次遍歷(需要用到輔助隊列)
//用于層序遍歷的輔助隊列
typedef struct LinkNode {
BiTNode* data;//存的是指針而非結點
struct LinkNode* next;
} LinkNode;
typedef struct {
LinkNode* front, * rear;//隊頭隊尾
} LinkQueue;
void InitQueue(LinkQueue& Q) {
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
//初始化時,front 、rear 都指向頭節(jié)點
Q.front->next = NULL;
}
bool EnQueue(LinkQueue& Q, BiTNode* x) {
//判滿?鏈式存儲一般不需要判滿,除非內存不足
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
if (s == NULL)return false;
s->data = x;
s->next = NULL;
Q.rear->next = s;//新節(jié)點插入到rear之后
Q.rear = s;//修改表尾指針
return true;
}
bool DeQueue(LinkQueue& Q, BiTNode* x) {
if (Q.front == Q.rear)return false;//隊空
LinkNode* p = Q.front->next;//用指針p記錄隊頭元素
x = p->data;//用x變量返回隊頭元素
Q.front->next = p->next;//修改頭節(jié)點的next指針
if (Q.rear == p)//此次是最后一個節(jié)點出隊
Q.rear = Q.front;//修改rear指針,思考一下為什么?
free(p); //釋放節(jié)點空間
return true;
}
bool isEmpty(LinkQueue Q) {
return Q.front == Q.rear ? true : false;
}
//層序遍歷
void levelOrder(BiTree T) {
LinkQueue Q;//輔助隊列
InitQueue(Q);//
BiTree p;
EnQueue(Q, T);
while (!isEmpty(Q)) {
DeQueue(Q, p);//隊頭結點出隊
visit(p);
if (p->lchild != NULL)
EnQueue(Q, p->lchild);
if (p->rchild != NULL)
EnQueue(Q, p->rchild);
}
}
應用:求樹的深度
//求樹的深度
int treeDepth(BiTree T)
{
if (T == NULL)
{
return 0;
}
else
{
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
//樹的深度=Max(左子樹深度,右子樹深度)+1
return l > r ? l + 1 : r + 1;
}
}
由二叉樹的遍歷序列可以構造出二叉樹
前序+中序:
后序+中序:
層序+中序:
若沒有中序遍歷則不能夠構造出二叉樹
線索二叉樹
線索二叉樹的存在,方便我們能夠找到某個結點的前驅以及后繼
線索二叉樹利用葉子結點中的空鏈域對前驅線索以及后繼線索進行存儲,并且使用ltag、rtag進行標注,tag==0表示指針指向的是孩子,tag==1表示指針指向的是“線索”
新的結構體:
struct ElemType {
int value;
};
typedef struct ThreadNode {
ElemType data;//數據域
struct ThreadNode* lchild, * rchild;//左右孩子指針
int ltag, rtag;//左右線索標志
} ThreadNode, * ThreadTree;
ThreadNode* pre = NULL;//全局變量用于暫存當前訪問結點的前驅
利用visit函數來進行線索化的操作:
void visit(ThreadNode* q) {
if (q->lchild == NULL) {//左子樹為空,建立前驅線索
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {//建立前驅結點的后繼線索
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
中序線索化:
//中序遍歷二叉樹,一邊遍歷一邊線索化
void InThread(ThreadTree T) {
if (T != NULL) {
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
//創(chuàng)建中序線索化二叉樹T
void CreatInThread(ThreadTree T) {
pre = NULL;
if (T != NULL) {//非空二叉樹才能線索化
InThread(T);//中序線索二叉樹
if (pre->rchild == NULL) {
pre->rtag = 1;//處理遍歷的最后最后一個結點
}
}
}
//中序線索化(王道教材版)
void InThread_CSKaoYan(ThreadTree p, ThreadTree& pre) {
if (p != NULL) {
InThread_CSKaoYan(p->lchild, pre);//遞歸,線索化左子樹
if (p->lchild == NULL) {//左子樹為空,建立前驅線索
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild == p;//建立前驅結點的后及線索
pre->rtag = 1;
}
pre = p;
InThread_CSKaoYan(p->rchild, pre);
}
}
//中序線索化二叉樹(王道教材版本)
void CreatInThread_CSKaoYan(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
InThread_CSKaoYan(T, pre);
pre->rchild = NULL;//思考:為什么處理最后一個結點時沒有判斷rchild 是否為NULL?
pre->rtag = 1;//答:因為最后一個結點的右孩子必為空。
}
}
先序線索化:
//先序線索化,一邊遍歷一邊線索化
void PreThread(ThreadTree T) {
if (T != NULL) {
visit(T);
if (0 == T->ltag) {//lchild不是前驅線索
PreThread(T->lchild);
}
PreThread(T->rchild);
}
}
//創(chuàng)建先序線索化二叉樹T
void CreatPreThread(ThreadTree T) {
pre == NULL;
if (T != NULL) {
PreThread(T);
if (pre->rchild == NULL) {
pre->rtag = 1;//處理遍歷的最后一個結點
}
}
}
//先序線索化(王道教程版本)
void PreThread_CSKaoYan(ThreadTree p, ThreadTree& pre) {
if (p != NULL) {
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild == p;//建立前驅結點的后及線索
pre->rtag = 1;
}
pre = p;
if (0 == p->ltag) {
PreThread_CSKaoYan(p->lchild, pre);
}
PreThread_CSKaoYan(p->rchild, pre);
}
}
//先序線索化二叉樹(王道教材版本)
void CreatPreThread_CSKaoYan(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
PreThread_CSKaoYan(T, pre);
if (pre->rchild = NULL)//處理遍歷的最后一個結點
pre->rtag = 1;
}
}
后序線索化:
//后序線索二叉樹
void PostThread(ThreadTree T) {
if (T != NULL) {
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
//創(chuàng)建后序線索二叉樹T
void CreatPostThread(ThreadTree T) {
pre == NULL;
if (T != NULL) {
PostThread(T);
if (pre->rchild == NULL) {
pre->rtag = 1;//處理遍歷的最后一個結點
}
}
}
//后序線索化(王道教程版本)
void PostThread_CSKaoYan(ThreadTree p, ThreadTree& pre) {
if (p != NULL) {
PostThread_CSKaoYan(p->lchild, pre);
PostThread_CSKaoYan(p->rchild, pre);
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild == p;//建立前驅結點的后及線索
pre->rtag = 1;
}
pre = p;
}
}
//后序線索化二叉樹(王道教材版本)
void CreatPostThread_CSKaoYan(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
PostThread_CSKaoYan(T, pre);
if (pre->rchild = NULL)//處理遍歷的最后一個結點
pre->rtag = 1;
}
}
而通過線索二叉樹找到前驅后繼又是另一大問題
中序線索二叉樹找后繼:
//中序線索二叉樹找中序后繼
//找到以P為根的子樹中,第一個被中序遍歷的結點
ThreadNode* FirstNode(ThreadNode* p) {
//循環(huán)找到最左下結點(不一定是葉結點)
while (0 == p->ltag) {
p = p->lchild;
}
return p;
}
//在中序線索二叉樹中找到結點p的后繼結點
ThreadNode* NextNode(ThreadNode* p) {
//在右子樹中最左下結點
if (0 == p->rtag)return FirstNode(p->rchild);
else return p->rchild;
}
//對中序線索二叉樹進行中序遍歷(利用線索實現(xiàn)的非遞歸算法),空間復雜度為O(1);
void InOrder(ThreadNode* T) {
for (ThreadNode* p = FirstNode(T); p != NULL; p = NextNode(p)) {
visit(p);
}
}
中序線索二叉樹找前驅:
//中序線索二叉樹找中序前驅
//找到以p為根的子樹中,最后一個被中序遍歷的結點
ThreadNode* LastNode(ThreadNode* p) {
//循環(huán)找到最右下結點(不一定是葉結點)
while (0 == p->rtag)p = p->rchild;
return p;
}
//在中序線索二叉樹中找到結點p的前驅結點
ThreadNode* PreNode(ThreadNode* p) {
//左下子樹中最右結點
if (0 == p->ltag)return LastNode(p->lchild);
else return p->lchild;
}
//對中序線索二叉樹進行逆向中序遍歷
void RevInOrder(ThreadNode* T) {
for (ThreadNode* p = LastNode(T); p != NULL; p = PreNode(p)) {
visit(p);
}
}
先序以及后序找前驅后繼的思路:
對于先序線索二叉樹找前驅以及后序線索二叉樹找后繼都較為復雜,需要對不同情況進行分類討論。
樹、森林
樹的存儲結構
雙親表示法
//樹——雙親表示法(順序存儲法)
#define MAX_TREE_SIZE 100
typedef struct {
int data; //數據元素
int parent;//雙親位置域
}PTNode;
typedef struct {
PTNode nodes[MAX_TREE_SIZE];//雙親表示
int n;//結點數
}PTree;
優(yōu)點:找雙親(父節(jié)點)很方便
缺點:找孩子不方便,只能從頭到尾遍歷整個數組
適用于“找父親”多“找孩子”少的應用場景,如:并查集
孩子表示法(順序存儲+鏈式存儲結合)
//樹——孩子表示法(順序+鏈式存儲)
struct CTNode {
int child;//孩子節(jié)點在數組中的位置
struct CTNode* next;//下一個孩子
};
typedef struct {
int data; //數據元素,數據元素類型不定
struct CTNode* firstChild;//第一個孩子
}CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];//雙親表示
int n, r;//結點數和根的位置
}CTree;
優(yōu)點:找孩子很方便
缺點:找雙親(父節(jié)點)不方便,只能遍歷每個鏈表
適用于“找孩子”多,“找父親”少的應用場景,如:服務流程樹
孩子兄弟表示法
//樹——孩子兄弟表示法(鏈式存儲)
typedef struct CSNode {
int data; //數據域,數據類型不定
struct CSNode* firstChild, * nextsibiling;//第一個孩子和右兄弟指針
}CSNode, * CSTree;
與二叉樹類似,采用二叉鏈表實現(xiàn),每個結點內保存數據元素和兩個指針,但兩個指針的含義與二叉樹結點不同。
用這種表示法存儲樹或森林時,從存儲視角來看形態(tài)上與二叉樹類似
樹、森林與二叉樹的轉換
樹轉二叉樹
森林轉二叉樹
二叉樹轉樹
二叉樹轉森林
樹、森林的遍歷
樹的先根遍歷
與相應二叉樹的先序序列相同
樹的后根遍歷
與相應二叉樹的中序序列相同
層次遍歷(廣度優(yōu)先)用隊列實現(xiàn)
森林的先序遍歷
效果等同于依次對各個樹進行先根遍歷,也等同于對相應二叉樹先序遍歷
森林中序遍歷
效果等同于依次對各個樹進行后根遍歷,也等同于對相應二叉樹中序遍歷
樹與二叉樹應用
哈夫曼樹
帶權路徑長度:
哈夫曼樹定義:
哈夫曼樹構造:
哈夫曼編碼:
并查集
利用雙親表示法的存儲方式,每個子集合以一棵樹表示,所有表示子集合的樹,構成表示全集合的森林,根節(jié)點的雙親結點為負數
初始化:
#define SIZE 10
int UFSets[SIZE];//集合元素數組
//初始化
void Initial(int S[])
{
for (int i = 0; i < SIZE; i++)
S[i] = -1;
}
查:
//查,找到x所屬集合(返回x所屬根節(jié)點)
int Find(int S[], int x)
{
while (S[x] >= 0)
x = S[x];
return x;//根節(jié)點S[]小于0
}
優(yōu)化查:
//查優(yōu)化,壓縮路徑
int Find2(int S[], int x)
{
int root = x;
while (S[root] >= 0) root = S[root];//循環(huán)找到根
while (x != root) //壓縮路徑
{
int t = S[x];//t指向x的父節(jié)點
S[x] = root;//x直接掛到根節(jié)點下
x = t;
}
return root;
}
并:(并之前要先找到兩個元素根節(jié)點)
//并,將兩個集合根節(jié)點傳入,合并為一個
void Union(int S[], int Root1, int Root2)
{
//判斷為不同集合
if (Root1 == Root2) return;
//將根Root2連接到另一根Root1下面
S[Root2] = Root1;
}
優(yōu)化并:
//并優(yōu)化,讓小樹合并到大樹,用根節(jié)點的絕對值表示樹的結點總數
void Union2(int S[], int Root1, int Root2)
{
if (Root1 == Root2) return;
if (S[Root2] > S[Root2]) //Root2更大,絕對值更小,結點數更小
{
S[Root1] += S[Root2];//累加結點總數
S[Root2] = Root1;//小樹合并到大樹
}
else
{
S[Root2] += S[Root1];
S[Root1] = Root2;
}
}
優(yōu)化前后對比:
文章來源:http://www.zghlxwxcb.cn/news/detail-537157.html
主要參考:王道考研課程
后續(xù)會持續(xù)更新考研408部分的學習筆記,歡迎關注。
github倉庫(含所有相關源碼):408數據結構筆記文章來源地址http://www.zghlxwxcb.cn/news/detail-537157.html
到了這里,關于【數據結構】24王道考研筆記——樹與二叉樹的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!