C/CPP与数据结构-第三节 树(一)
树
- 树的基本知识
- 树的定义:
- 由一个或多个(n>=0)节点组成的有限集合T,有且仅有一个结点称为根(root),当n>1时,其余的结点分为m(m>=0)个互不相交的有限集合T1,T2,…,Tm。每个集合本身又是一棵树,被称作这个根的子树。
- 注意:过去许多书籍中都定义n>=1,曾经有空树不是树的说法,但现在定义已改。
- 树的定义具有递归性,即树中还有数。
-
常见的术语:
根: 即根结点(没有前驱) 叶子: 即终端结点(没有后继) 森林: 指m棵不相交的树的集合(例如删除A后的子树个数) 有序树: 结点各子树从左至右有序,不能互换(左为第一) 无序树: 结点各子树可互换位置。 双亲: 即上层的那个结点(直接前驱) parent 孩子: 即下层结点的子树 (直接后继) child 兄弟: 同一双亲下的同层结点(孩子之间互称兄弟)sibling 堂兄弟:即双亲位于同一层的结点(但并非同一双亲)cousin 祖先: 即从根到该结点所经分支的所有结点 子孙: 即该结点下层子树中的任一结点 结点: 即树的数据元素 结点的度: 结点挂接的子树数(有几个直接后继就是几度,亦称“次数”) 结点的层次: 从根到该结点的层数(根结点算第一层) 终端结点: 即度为0的结点,即叶子 分支结点: 除树根以外的结点(也称为内部结点) 树的度: 所有结点度中的最大值(Max{各结点的度}) 树的深度(或高度): 指所有结点中最大的层数(Max{各结点的层次})
- 树的表示法有几种:
- 图形表示法
- 嵌套集合表示法
- 广义表表示法 :
( A ( B ( E ( K, L ), F ), C ( G ), D ( H ( M ), I, J ) ) )
- 目录表示法
- 左孩子-右兄弟表示法
- 就是将一棵树转化为二叉树
- 树的逻辑结构
- (特点): 一对多(1:n),有多个直接后继(如家谱树、目录树等等),但只有一个根结点,且子树之间互不相交。
- 树的存储结构
- 树是非线性结构,该怎样存储?
- 仍然有顺序存储、链式存储等方式。
- 树的顺序存储方案应该怎样制定?
- 可规定为:从上至下、从左至右将树的结点依次存入内存。
- 重大缺陷:复原困难(不能唯一复原就没有实用价值)。
- 树的链式存储方案应该怎样制定?
- 可用多重链表:一个前趋指针,n个后继指针。
- 细节问题:树中结点的结构类型样式该如何设计?
- 即应该设计成“等长”还是“不等长”?
- 缺点:等长结构太浪费(每个结点的度不一定相同);不等长结构太复杂(要定义好多种结构类型)。
- 可用多重链表:一个前趋指针,n个后继指针。
- 计算机如何实现各种不同进制的运算?
- 实现思路:先研究最简单、最有规律的二进制运算规律,然后设法把各种不同进制的运算转化二进制运算。
- 树的存储可否借鉴这种思路呢?
- 解决思路:先研究最简单、最有规律的树,然后设法把一般的树转化为这种简单的树
- 树是非线性结构,该怎样存储?
- 树的定义:
二叉树
- 为何要重点研究每结点最多只有两个 “叉” 的树?
- 二叉树的结构最简单,规律性最强;
- 可以证明,所有树都能转为唯一对应的二叉树,不失一般性
- 二叉树的定义:
- 定义:是n(n≥0)个结点的有限集合,由一个根结点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成 。
- 逻辑结构: 一对二(1:2)
- 基本特征:
- 每个结点最多只有两棵子树(不存在度大于2的结点);
- 左子树和右子树次序不能颠倒(有序树)。
-
问:具有3个结点的二叉树可能有几种不同形态?(5种) 普通树呢?(2种)
- 二叉树5种:
- 2、3、4、5有什么不同呢?
- 左子树/右子树的不同
- 普通树2种:
- 没有左子树、右子树的概念,因此2、3、4、5相同,只有2种
- 二叉树5种:
- 二叉树的性质
- 性质1:在二叉树的第i层上至多有2(i次方)-1个结点(i>0)。
- 性质2:深度为k的二叉树至多有2(k次方)-1个结点(k>0)。
- 性质3:对于任何一棵二叉树,若2度的结点数有n2个,则叶子数(n0)必定为n2+1 (即n0=n2+1)
- 即 叶子节点数= 度数为2的节点数 +1;
- 度数为2的节点:就是这个节点有2个子树
- 满二叉树:一棵深度为k 且有2(k次方) -1个结点的二叉树。(特点:每层都“充满”了结点)
- 完全二叉树:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满二叉树中编号从1至n的结点一一对应。特点如下:
- k-1层一定是满二叉树
- 最后一层:右边有叶子左边一定右叶子。就是靠左
- 性质4:具有n个结点的完全二叉树的深度必为[log(2底数)n]+1
- 解释:[]取下值的意思,
2的n次方 = 13
,那么n一定是3<n<4,那么此时n就去3,这就是向下取值。那么结果:3+1 = 4;
- 解释:[]取下值的意思,
-
性质5:对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)。
- 二叉树的存储
- 顺序存储接口
- 按二叉树的结点“自上而下、从左至右”编号,用一组连续的存储单元存储。
- 顺序存储后能否复原成唯一对应的二叉树形状?
- 若是完全/满二叉树则可以做到唯一复原。
- 而且有规律:下标值为i的双亲,其左孩子的下标值必为2i,其右孩子的下标值必为2i+1(即性质5)
- 例如,对应[2]的两个孩子必为[4]和[5],即B的左孩子必是D,右孩子必为E。
- 讨论:不是完全二叉树怎么办?
- 一律转为完全二叉树!
- 方法很简单,将各层空缺处统统补上“虚结点”,其内容为空。
- 缺点:浪费空间;插入、删除不便
- 二叉树的链式存储
- 那么二叉树的链表还如何表示呢?
- 二叉链表示法
- 一个结构体分为:左孩子指针、数据域、右孩子指针
- 左孩子指针指向左孩子结构体,右孩子指针指向右孩子结构体
-
二叉链结点数据类型定义:
typedef struct BiTNode { int data; struct BiTNode *lchild, *rchild; }BiTNode, *BiTree;
- 三叉链表示法:
- 如果需要倒查某结点的双亲,可以再增加一个双亲域(直接前趋)指针,将二叉链表变成三叉链表
- 一个结构体分为:左孩子指针、数据域、右孩子指针、双亲指针
-
三叉链结点数据类型定义:
typedef struct TriTNode { int data; //左右孩子指针 struct TriTNode *lchild, *rchild; //双亲指针 struct TriTNode *parent; }TriTNode, *TriTree;
- 二叉链表示法
-
树的另一种表示方法:双亲链表法
//双亲链表 #define MAX_TREE_SIZE 100 typedef struct BPTNode { int data; int parentPosition; //指向双亲的指针 //数组下标 char LRTag; //左右孩子标志域,表示当前结点为双亲的左或右孩子 }BPTNode; typedef struct BPTree { BPTNode nodes[100]; //因为节点之间是分散的,需要把节点存储到数组中 int num_node; //节点数目 int root; //根结点的位置 //注意此域存储的是父亲节点在数组的下标 }BPTree;
- 将结点全部放到一个数组中nodes
- 每个结点保存与父子结点的关系
//A BPTree myTree; // myTree.root = 0; // 数组的0号位置,是根节点 myTree.nodes[0].data = 'A'; //B myTree.nodes[1].data = 'B'; myTree.nodes[1].parentPosition = 0; //双亲数组下标 myTree.nodes[1].LRTag = 1; //A的左子 //C myTree.nodes[2].data = 'C'; myTree.nodes[2].parentPosition = 0; //双亲数组下标 myTree.nodes[2].LRTag = 2; //A的右子
- 那么二叉树的链表还如何表示呢?
- 顺序存储接口
- 遍历二叉树和线索二叉树
- 遍历二叉树(Traversing Binary Tree)
- 遍历规则
- 二叉树由根、左子树、右子树构成,定义为D、 L、R
- D、 L、R的组合定义了六种可能的遍历方案:
- LDR, LRD, DLR, DRL, RDL, RLD
- 若限定先左后右,则有三种实现方案:
- DLR : 先 (根)序遍历 ,即先根再左再右
- 先根,然后遍历根的左子树,左子树若还有子树接着遍历,知道根的左子树全部遍历完,再遍历右子树
- LDR : 中 (根)序遍历 ,即先左再根再右
- 从根节点A开始,找到根节点的左子树节点B,然后查看节点B是否有左子树,若有则县访问B节点的左子树,就这样一直下去。。。
- LRD : 后(根)序遍历 ,即先左再右再根
- 同理
- DLR : 先 (根)序遍历 ,即先根再左再右
- 举例:树:A(B(D,E),C)
- 先序遍历:A、B、D、E、C
- 先访问A,A的左子树B,B的左子树D,D没有左子树,所以访问B的右子树E,在访问A的右子树C
- 中序遍历:D、B、E、A、C
- 先从A节点开始,找A的左子树B,到B节点,先找B的左子树D,D节点没有左子树,因此就访问D,然后访问D的双亲B,然后访问B的右子树E,然后访问B的双亲A,然后再访问A的右子树。
- 后序遍历:D、E、B、C、A
- 先从A节点开始,找A节点的左子树B,到B节点,先找B的左子树D,D节点没有左子树,因此就访问D,然后访问B的右子树E,接着访问B,在访问A的右子树C,最后访问跟节点A
- 先序遍历:A、B、D、E、C
- 遍历规则
- 遍历二叉树(Traversing Binary Tree)
- 遍历算法实现:
-
先序遍历
DLR(NODE *root ){ if (root) {//非空二叉树 printf(“%d”,root->data); //访问D DLR(root->lchild); //递归遍历左子树 DLR(root->rchild); //递归遍历右子树 } }
-
中序遍历
LDR(NODE *root){ if(root !=NULL){ LDR(root->lchild); printf(“%d”,root->data); LDR(root->rchild); } }
-
后序遍历
LRD (NODE *root){ if(root !=NULL) { LRD(root->lchild); LRD(root->rchild); printf(“%d”,root->data); } }
-
-
代码示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdlib.h> #include<string.h> #include<stdio.h> typedef struct BitNode { int data; struct BitNode *lchild, *rchild; } BitNode; typedef struct BitNode* BitTree; //先序遍历 void preOrder(BitNode *T) { if (T == NULL) return; //先跟 printf("%d", T->data); //再左 if (T->lchild != NULL) { preOrder(T->lchild); } //再右 if (T->rchild != NULL) { preOrder(T->rchild); } } //中序遍历 void inOrder(BitNode *T) { if (T == NULL) return; //先左 if (T->lchild != NULL) { inOrder(T->lchild); } //再根 printf("%d", T->data); //再右 if (T->rchild != NULL) { inOrder(T->rchild); } } //后序遍历 void postOrder(BitNode *T) { if (T == NULL) return; //先左 if (T->lchild != NULL) { postOrder(T->lchild); } //再右 if (T->rchild != NULL) { postOrder(T->rchild); } //再根 printf("%d", T->data); } //计算一颗树的所有叶子数量 //思想:输出叶子结点比较简单,用任何一种遍历算法,凡是左右指针均空者,则为叶子,将其统计并打印出来。 void countLeftNum(BitNode *T,int *sum) { if (T == NULL) return; if (T->lchild ==NULL && T->rchild == NULL) { (*sum)++; } //求左子树的叶子节点个数 countLeftNum(T->lchild,sum); //求右子树 countLeftNum(T->rchild,sum); } //求树的深度 //算法思路:只查各结点后继链表指针,若左(右)孩子的左(右)指针非空,则层次数加1;否则函数返回。 //技巧:递归时应当从叶子开始向上计数,层深者保留。否则不易确定层数。 int depth(BitNode *T) { if (T == NULL) return 0; int leftDep = 0; int rigDep = 0; int deph = 0; leftDep = depth(T->lchild); rigDep = depth(T->rchild); deph = 1 + (leftDep > rigDep ? leftDep : rigDep); return deph; } //拷贝二叉树 /* 1. 先拷贝左子树 2. 再拷贝右子树 3. new一个结点,左指针指向左子树,右指针指向右子树 4. 如果左子树是一棵树,递归1/2/3 */ BitNode *copyTree(BitNode *T) { if (T == NULL) return NULL; BitNode *newlChild = NULL; BitNode *newrChild = NULL; BitNode *newNode = NULL; //先拷贝左子树 newlChild = copyTree(T->lchild); //在拷贝右子树 newrChild = copyTree(T->rchild); //创建新节点 newNode =(BitNode *)malloc(sizeof(BitNode)); if (newNode == NULL) return NULL; //拷贝当前旧节点到新节点 newNode->data = T->data; newNode->lchild = newlChild; newNode->rchild = newrChild; return newNode; } /* 1 2 3 4 5 */ void main() { BitNode nodeA, nodeB, nodeC, nodeD, nodeE; memset(&nodeA, 0, sizeof(BitNode)); memset(&nodeB, 0, sizeof(BitNode)); memset(&nodeC, 0, sizeof(BitNode)); memset(&nodeD, 0, sizeof(BitNode)); memset(&nodeE, 0, sizeof(BitNode)); nodeA.data = 1; nodeB.data = 2; nodeC.data = 3; nodeD.data = 4; nodeE.data = 5; nodeA.lchild = &nodeB; nodeA.rchild = &nodeC; nodeB.lchild = &nodeD; nodeC.lchild = &nodeE; //拷贝二叉树 { BitNode *newTree = copyTree(&nodeA); printf("中序遍历新树 \n"); inOrder(newTree); } //树的高度 { printf("\n树的高度:%d \n", depth(&nodeA)); } //树的叶子总数 { int s = 0; countLeftNum(&nodeA,&s); printf("叶子总数:%d", s); } //树的遍历 printf("\n先序遍历: "); preOrder(&nodeA); printf("\n中序遍历:"); inOrder(&nodeA); printf("\n后序遍历:"); postOrder(&nodeA); system("pause"); return; }
树的非递归遍历(中序遍历) 经典算法案例
中序 遍历的几种情况
分析1:什么时候访问根、什么时候访问左子树、什么访问右子树:
当左子树为空或者左子树已经访问完毕以后,再访问根
访问完毕根以后,再访问右子树。
分析2:非递归遍历树,访问结点时,为什么是栈,而不是其他模型(比如说是队列)。
先走到的后访问、后走到的先访问,显然是栈结构
分析3:结点所有路径情况
步骤1:
如果结点有左子树,该结点入栈;
如果结点没有左子树,访问该结点;
步骤2:
如果结点有右子树,重复步骤1;
如果结点没有右子树(结点访问完毕),根据栈顶指示回退,访问栈顶元素,并访问右子树,重复步骤1
如果栈为空,表示遍历结束。
注意:入栈的结点表示,本身没有被访问过,同时右子树也没有被访问过。
分析4:有一个一直往左走入栈的操作,中序遍历的起点
-
树如下图:
A(B(,C(D,)),E(,F(G(H,I),)))
-
按照上面算法分析:
栈:stack 输出:P 1. 经过A,A有左子树,则A入栈:stack(A) 2. 经过B,B无左子树,则访问B:P(B) 3. B有右子树,经过C, C有左子树,则C入栈:stack(AC) 4. 经过D,D无左子树,则访问D:P(BD) 5. D节点没有右子树,栈回退,访问栈顶元素C,则P(BDC), stack(A) 6. C无右子树,栈回退,访问栈顶元素A,则P(DBCA),stack() 7. A有右子树,经过E,E无左子树,则访问E,则P(DBCAE) 8. E有右子树,经过F,F有左子树,则F入栈: stacc(F) 9. 经过G,G有左子树,则G入栈,stack(FG) 10. 经过H,H无左子树,则访问H,P(DBCAEH) 11. H无右子树,则栈回退,访问栈顶元素G, P(DBCAEHG) stack(F) 12. G有右子树,经过I,I无左子树,则访问I,P(DBCAEHGI) 13. I无右子树,则栈回退,访问栈顶元素F,P(DBCAEHGIF),stack() 14. F无右子树,而且栈为空,则遍历结束
-
代码举例:
#include<iostream> #include<stack> using namespace std; typedef struct BitNode { int data; struct BitNode *lchild, *rchild; } BitNode; typedef struct BitNode* BitTree; //一直向左走,找到改树的起点 BitNode * goFarLeft(BitNode *T, stack<BitNode *> &s) { if (T == NULL) return NULL; while (T->lchild != NULL) //一直检查是否有左子树 { //有左子树则入栈 s.push(T); //经过左子树在循环 T = T->lchild; } //返回没有左子树的节点T return T; } //非递归中序遍历 void myInorder(BitNode *T) { //定义栈 stack<BitNode *> s; //一直往左走,找到中序遍历的起点 BitNode *t = goFarLeft(T, s); while (t != NULL) { cout << t->data << endl; //查看节点有右子树 if (t->rchild != NULL) { t = goFarLeft(t->rchild, s); } //没有右子树,且栈不为nil else if (!s.empty()) { //获取栈顶元素 t = s.top(); //出栈 s.pop(); } else //没有右子树,且栈为nil { t = NULL; } } } void main() { BitNode nodeA, nodeB, nodeC, nodeD, nodeE; memset(&nodeA, 0, sizeof(BitNode)); memset(&nodeB, 0, sizeof(BitNode)); memset(&nodeC, 0, sizeof(BitNode)); memset(&nodeD, 0, sizeof(BitNode)); memset(&nodeE, 0, sizeof(BitNode)); nodeA.data = 1; nodeB.data = 2; nodeC.data = 3; nodeD.data = 4; nodeE.data = 5; nodeA.lchild = &nodeB; nodeA.rchild = &nodeC; nodeB.lchild = &nodeD; nodeC.lchild = &nodeE; { //非递归中序遍历 myInorder(&nodeA); } system("pause"); return; }