数据结构笔记-树、二叉树

106 阅读12分钟

一、树的基本术语

  1. 结点的度:分支的个数,子树的个数
  2. 树的度:树中所有结点的度的最大值
  3. 叶子结点:度为0的结点
  4. 分支结点:度大于0的结点
  5. 结点路径:由从根到该结点所经分支和结点构成
  6. 结点的层次:根结点的层次为1,每个结点的层次为其父结点的层次+1
  7. 树的深度:树中叶子结点所在的最大层次
  8. 森林:m棵不相交的树的和
  9. 二元组:树的表示方法,Tree=(root, F),根节点和子树森林
  10. 3个结点的树有2种

二、二叉树的性质

  1. 有5种基本形态
  2. 第i层上至多有2^(i-1)个结点(i>=1)
  3. 深度为k的二叉树上至多含2^k-1个结点(k>=1)
  4. 若二叉树含有n0个叶子结点、n2个度为2的结点,则n0=n2+1

195e126b421298bf23714602ffb0ebd.jpg

  1. 满二叉树:深度为k,含有2^k-1个结点,每一层都有最多结点数,从根节点开始从上到下、从左到右,从1开始编号
  2. 完全二叉树:所含的n个结点和满二叉树中编号为1~n的结点一一对应;结点没有左孩子则一定无右孩子;度为1的结点最多有一个
  3. 具有n个结点的完全二叉树的深度为Log(2,n)+1

fc65a1bf1e1eb2588c77bf12ab70997.jpg 8. 若对含n个结点的完全二叉树从上到下、从左到右编号1~n,则对完全二叉树中任意一个编号为i的结点:

  • 若i=1,则该结点是二叉树的根,无双亲。否则,编号为i/2的结点为其双亲结点
  • 若2i>n,则该结点无左孩子,否则,编号为2i的结点为其左孩子结点
  • 若2i+1>n,则该结点无右孩子结点,否则,编号为2i+1的结点为其右孩子结点

ee3315ef66d5e06ef55c723b2532e13.jpg

三、二叉树的存储结构

一、顺序存储

  • 用一维数组存放二叉树的结点,每个结点在数组中存放的下标为高度相同的满二叉树中对应结点的编号
  • 若元素i有父亲,则父亲在i/2
  • 元素i的左孩子在2i,右孩子在2i+1
  • 更适合存放二叉树;单枝树会浪费存储
#define MAX_TREE_SIZE 100//二叉树的最大结点数
typedef TElemType SqBiTree[MAX_TREE_SIZE];
SqBiTree bt; 

二、链式存储

首先,我们看三种链式存储二叉树的方法,包括二叉链表、三叉链表、双亲数组
特点:n个结点的二叉链表中含有n+1个空指针域

//二叉链表 
typedef struct BiTNode {
	int data;
	struct BiTNode *lchild, rchild;//左右子树根结点地址 
}BiTNode, *BiTree;

//三叉链表
 typedef struct TriTNode {
	int data;
	struct BiTNode *lchild, rchild;
	struct BiTNode *parent;//双亲结点地址 
}TriTNode, *TriTree;

//双亲数组
typedef struct BPTNode {
	int data;
	int parent;//该结点的双亲结点在数组中的位置下标,根节点则为-1 
	int LRTag;//它是双亲的左孩子还是右孩子 
}BPTNode;
typedef struct BPTree {
	BPTNode nodes[MAX_TREE_SIZE];
	int n, r;//n为树中结点数,r为根节点位置 
};

接下来,我们讲遍历二叉树的方法
1.递归

//前序遍历 
void xxbl(BiTree T) {
	if(T) {
		printf("%c", T->data);//访问结点 
		xxbl(T->lchild);//遍历左子树 
		xxbl(T->rchild);//遍历右子树 
	}
}

//中序遍历 
void zxbl(BiTree T) {
	if(T) {
		zxbl(T->lchild);//遍历左子树 
		printf("%c", T->data);//访问结点 
		zxbl(T->rchild);//遍历右子树 
	}
}

//后序遍历 
void hxbl(BiTree T) {
	if(T) {
		hxbl(T->lchild);//遍历左子树 
		hxbl(T->rchild);//遍历右子树 
		printf("%c", T->data);//访问结点 
	}
}

结论:树中结点均不相同的情况下,前序+中序可以唯一确定一棵二叉树,后序+中序也可以,但是前序+后序不可以

2.层次遍历

#define MAX 100
void ss(BiTree T) {
	BiTree Q[MAX], p;
	int f = 0, r = 0;//首尾指针 
	if(T == NULL) return;
	Q[r++] = T;
	while(f != r) {
		p = Q[f++];
		printf("%c", p->data);
		if(p->lchild) {
			if(r >= MAX) {
				printf("overflow");
				exit(0);
			}
			Q[r++] = p->lchild;
		}
		if(p->rchild) {
			if(r >= MAX) {
				printf("overflow");
				exit(0);
			}
			Q[r++] = p->rchild;
		}
	}
} 

//这种算法会导致假溢出,所以接下来用循环队列 

3.非递归算法 递归算法书写简单,但效率低。

#define MAX 1000

//二叉链表 
typedef struct BiTNode {
	int data;
	struct BiTNode *lchild, rchild;//左右子树根结点地址 
}BiTNode, *BiTree;

//顺序栈
 typedef struct {
 	BiTree data[MAX];
 	int top;
 }SeqStack;

//先序非递归遍历二叉树
//访问根结点后,在访问左子树前,应保存其非空右子树 
void PreorderTraverse(BiTree T) {
	SeqStack s;
	s.top = -1;
	BiTree p = (BiTNode*)malloc(sizeof(BiTNode));
        //也可以用BiTree p = (BiTree)malloc(sizeof(BiTNode));
	p = T;
	while(p) {
		printf("%c", p->data);//访问p结点
		if(p->rchild) {//若p有右孩子 
			if(s.top == MAX-1) exit(0);//栈满了,错误 
			else s.data[++s.top] = p->rchild;//栈没满,则右孩子入栈
			p = p->lchild;//访问p的左孩子 
		} 
	//下一轮,若p的左孩子为空,则不再进行while,访问栈顶元素,相当于回溯 
	if(s.top != -1) p = s.data[s.top--];//若栈不空,则栈顶元素出栈 
	}
} 

//中序非递归遍历二叉树
//访问根结点的左子树前,应保存其根结点,以便访问根和根的右子树 
void InorderTraverse(BiTree T) {
	SeqStack s;
	s.top = -1;
	BiTree p = (BiTNode*)malloc(sizeof(BiTNode));
	p = T;
	while(p || (s.top != MAX - 1)) {
		while(p) { 
			if(s.top == MAX-1) exit(0);//栈满了,错误 
			s.data[++s.top] = p;//栈没满,则p入栈
			p = p->lchild;//访问p的左孩子 
		} 
	if(s.top != -1) {
		p = s.data[s.top--];//若栈不空,则栈顶元素出栈 
		printf("%c", p->data);//访问p结点
		p = p->rchild;
	}
} 

//后序非递归遍历二叉树
//访问根结点的左子树前,应保存其根结点,以便访问根的右子树和根 
void InorderTraverse(BiTree T) {
	SeqStack s;
	s.top = -1;
	BiTree p = (BiTNode*)malloc(sizeof(BiTNode));
	p = T;
	while(p || (s.top != MAX - 1)) {
		while(p) { 
			if(s.top == MAX-1) exit(0);//栈满了,错误 
			s.data[++s.top] = p;//栈没满,则p入栈
			p = p->lchild;//访问p的左孩子 
		} 
	if(s.top != -1) {
		p = s.data[s.top--];//若栈不空,则栈顶元素出栈 
		printf("%c", p->data);//访问p结点
		p = p->rchild;
	}
} 
 

后序遍历很复杂,可调试代码如下

#include <stdio.h>
#include <stdlib.h>

 #define MAX 5

//二叉链表 
typedef struct BiTNode {
	int data;
	struct BiTNode *lchild, *rchild;//左右子树根结点地址 
}BiTNode, *BiTree;

 typedef struct {
 	BiTree q;//遍历操作访问到的每一颗子树的根结点 
 	int flag;//flag=0表示目前在访问q的左子树,1是右子树 
 }dataelem;
 
//顺序栈
 typedef struct {
 	dataelem data[MAX];
 	int top;
 }SeqStack2;
 
void postorder(BiTree T) {
	SeqStack2 s;
	s.top = -1;
	BiTree p = (BiTNode*)malloc(sizeof(BiTNode));
	p = T;
	do {
		if(s.top == MAX-1) exit(0);//栈满了,错误
		while(p != NULL) {
			s.data[++s.top].q = p;
			s.data[s.top].flag = 0;
			p = p->lchild;
		}
		while((s.top > -1) && (s.data[s.top].flag == 1)) {//栈非空,且栈顶元素是右子树
			p = s.data[s.top--].q;
			printf("%d", p->data);//访问其根结点 
		}
		if(s.top > -1) {//若栈非空,且栈顶元素是左子树 
			s.data[s.top].flag = 1;
			p = s.data[s.top].q;
			p = p->rchild;
		}
	}while(s.top > -1);
} 

int main() {
    // 构造二叉树
    BiTNode node1 = {1, NULL, NULL};
    BiTNode node2 = {2, NULL, NULL};
    BiTNode node3 = {3, NULL, NULL};
    BiTNode node4 = {4, NULL, NULL};
    BiTNode node5 = {5, NULL, NULL};

    node1.lchild = &node2;
    node1.rchild = &node3;
    node2.lchild = &node4;
    node2.rchild = &node5;

    BiTree root = &node1;

    // 后序遍历二叉树
    printf("后序遍历结果为:");
    postorder(root);
    printf("\n");

    return 0;
}

3.二叉树的建立

void CreateBiTree(BiTree &T) {
	scanf("%c", &ch);
	if(ch == "#")T = NULL;
	else {
		T = (BiTNode*)malloc(sizeof(BiTNode));
		T->data = ch;//生成根节点
		CreateBiTree(T->lchild);//构造左子树
		CreateBiTree(T->rchild);//构造右子树 
	}
}

四、线索二叉树

线索:二叉链表中的空左孩子指针存放指向该结点前序遍历次序的直接前驱,空右孩子指针存放指向该结点前序遍历次序的直接后继,为空的指向NULL
分类:先序线索二叉树、中序线索二叉树、后序线索二叉树
1.中序线索化的代码

typedef enum {
	Link, Thread;//Link==0:指针;Thread==1:线索 
}PointerTag;

typedef struct BiThrNod {
	int data;
	struct BiThrNode *lc, *rc;//左右指针
	PointerTag LTag, RTag;//左右标志
	//该结点的lc非空时,LcTag为Link,则Lc指向左子树,否则为Thread,是线索,指向其前驱 
	//该结点的rc非空时,LcTag为Link,则Lc指向右子树,否则为Thread,是线索,指向其后继 
}BiThrNode, *BiThrTree;

//中序线索化
void InorderThreading(BiThrTree &Thrt, BiThrTree T) {
	Thrt = (BiThrTree)malloc(sizeof(BiThrTree T));//指向线索树根节点的指针Thrt
	//为方便操作,增设此结点,将其左指针指向根节点,右指针指向中序遍历的最后一个结点
	// 中序遍历的第一个结点的左指针指向该结点,序遍历的最后一个结点的右指针指向该结点
	if(!Thrt) exit(overflow);
	Thrt->LTag = Link;
	Thrt->RTag = Thread;
	Thrt->rc = Thrt;
	if(!T) Thrt->lc = Thrt;
	else {
		Thrt->lc = T;
		pre = Thrt;
		InThreading(T);
		//将线索树的最后一个节点的右指针指向线索树的根节点,并将RTag设置为Thread
		pre->rc = Thrt;
		pre->RTag = Thread;
		Thrt->rc = pre;
	}
} 

void InThreading(BiThrTree p) {
	if(p) {
		InThreading(p->lc);
		if(!p->lc){
			p->LTag = Thread;
			p->lc = pre;
		}
		if(!pre->rc) {
			p->RTag = Thread;
			p->rc = pre;
		} 
		pre = p;
		InThreading(p->rc);
		}
}

2.中序线索化链表的遍历

//提取中序遍历的第一个结点(最左下,无左子树) 
BiThrTree firstNode(BiThrTree T) {
	p = T;
	while(p->LTag == Link)p = p->lc;
	return p;
} 

//提取p结点的直接后继结点
//若无右子树,则为后继线索所指结点,否则为对其右子树进行中序遍历时访问的第一个结点
BrThrTree Succ(BiThrTree p) {
	if(p->RTag == Thread) return p->rc;
	else {
		p= p->rc;
		while(p->LTag == Link) p = p->lc;
		return p;
	}
} 

//中序遍历线索二叉树 
void traverse(BiThrTree T) {
	p = (BiThrTree)malloc(sizeof(BiThrTree T));
	for(p = firstNode(T); p; p = Succ(p))
		Visit(p);
} 

3.先序遍历线索二叉树

BrThrTree Succ(BiThrTree p) {
	if(p->LTag == Link) return p->lc;//若左子树存在,则p(中)的直接后继是左 
	else return p->rc;//左子树不存在,则右子树节点rc是p的直接后继 
} 

这里留一句,ppt ch6-3-2old 最后一页没看懂(

五、森林

(一)树的存储结构

1.树的双亲表示法

1712677566258.png

#define MAX_TREE_SIZE 100
typedef struct PTNode {
	int data;
	int parent;//若是根节点,parent=-1 
}PTNode;

typedef struct {
	PTNode nodes[MAX_TREE_SIZE];//序号从0开始,结点顺序无要求,根节点不一定在0 
	int r;//根节点位置 
	int n;//树中结点个数 
}PTree; 

2.树的孩子表示法

(1)定长结点的多重链表

每个结点存放值和孩子结点位置,每个结点的孩子指针个数=树中孩子最多的结点的孩子个数=树的度

1712677714054.png

2.不定长结点的多重链表

每个结点存放值和孩子结点位置,每个结点的孩子指针个数=该结点的孩子个数=结点的度

1712677869805.png

3.孩子单链表表示法

将每个结点的孩子结点拉成一个单链表,每个数组元素的data存放结点的值,firstchild存放其孩子单链表的头指针

1712678091076.png

可以将双亲表示法和孩子单链表表示法结合,加一个pa域,存放双亲结点在数组中的位置(0,1,2……)

//结点结构
typedef struct CTNode {
	int child;
	struct CTNode *next;
}*ChildPtr;

//数组元素类型
typedef struct {
	int data;
	ChildPtr firstchild;//孩子单链表的头指针 
}CTBox;

//树
typedef struct {
	CTBox nodes[MAX_TREE_SIZE];
	int r;//根节点位置 
	int n;//树中结点个数 
}CTree; 

4.孩子-兄弟表示法(树和二叉树的转换)

1712678564974.png

 typedef struct CSNode {
 	int data;
 	struct CSNode *fc;//长子指针域 
	struct CSNode *nb;//右邻兄弟指针域 
 }CSNode, *CSTree;

(二)森林与二叉树的转换

  • 森林由多棵树组成,将每棵树转换成二叉树,采用孩子-兄弟表示法
  • 每棵二叉树的右子树为空,从BTn开始依次将其根节点链为前一棵二叉树的根的右孩子

1712679199710.png

(三)树的遍历

1.遍历方法

(1)先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树
(2)后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点
(3)按层次遍历:若树不空,则自上而下自左至右访问树中每个结点

1712679386890.png

2.树的先根遍历 非递归算法

与二叉树的先序遍历相同

#define MAX 1000
 
//孩子-兄弟表示法 
 typedef struct CSNode {
 	int data;
 	struct CSNode *fc;//长子指针域 
	struct CSNode *nb;//右邻兄弟指针域 
 }CSNode, *CSTree;
 
//二叉链表 
typedef struct BiTNode {
	int data;
	struct BiTNode *lchild, rchild;//左右子树根结点地址 
}BiTNode, *BiTree;

//顺序栈
 typedef struct {
 	BiTree data[MAX];
 	int top;
 }SeqStack;

 //二叉树的先序遍历非递归算法 
void PreorderTraverse(BiTree T) {
	SeqStack s;
	s.top = -1;
	BiTree p = (BiTNode*)malloc(sizeof(BiTNode));
	p = T;
	while(p) {
		printf("%c", p->data);//访问p结点
		if(p->rchild) {//若p有右孩子 
			if(s.top == MAX-1) exit(0);//栈满了,错误 
			else s.data[++s.top] = p->rchild;//栈没满,则右孩子入栈
			p = p->lchild;//访问p的左孩子 
		} 
	//下一轮,若p的左孩子为空,则不再进行while,访问栈顶元素,相当于回溯 
	if(s.top != -1) p = s.data[s.top--];//若栈不空,则栈顶元素出栈 
	}
} 

(四)森林的遍历

1.森林的组成

1712679788276.png

2.森林的遍历

(1)先序遍历:若森林不空,则访问森林中第一棵树的根结点;先序遍历森林中第一棵树的子树森林;先序遍历森林中(除第一棵树之外)其余树构成的森林——即:依次从左至右对森林中的每一棵树进行先根遍历
(2)中序遍历:若森林不空,则中序遍历森林中第一棵树的子树森林;访问森林中第一棵树的根结点;中序遍历森林中(除第一棵树之外)其余树构成的森林----即:依次从左至右对森林中的每一棵树进行后根遍历

1712679936360.png

3.树的遍历和二叉树遍历的对应关系

1712679959364.png

111 ppt ch6-4练习题没写(