730 likes | 985 Views
第六章 树和二叉树. 本章内容. 树、二叉树的定义、性质和存储结构; 二叉树的遍历和线索化以及遍历算法的各种描述形式; 树遍历; 二叉树的多种应用。. 本章要点. 熟练掌握二叉树的结构特性; 熟悉二叉树的各种存储结构的特点及适用范围; 熟练掌握二叉树各种遍历策略的递归和非递归算法,了解遍历过程中 “ 栈 ” 的作用和状态,而且能灵活运用遍历算法实现二叉树的其它操作; 理解二叉树线索化的实质是建立结点与其在相应序列中的前驱或后继之间的直接联系; 熟悉树的各种存储结构及其特点; 学会编写实现树的各种操作的算法; 了解最优树的特性,掌握建立最优树和哈夫曼编码的方法。 .
E N D
本章内容 • 树、二叉树的定义、性质和存储结构; • 二叉树的遍历和线索化以及遍历算法的各种描述形式; • 树遍历; • 二叉树的多种应用。
本章要点 • 熟练掌握二叉树的结构特性; • 熟悉二叉树的各种存储结构的特点及适用范围; • 熟练掌握二叉树各种遍历策略的递归和非递归算法,了解遍历过程中“栈”的作用和状态,而且能灵活运用遍历算法实现二叉树的其它操作; • 理解二叉树线索化的实质是建立结点与其在相应序列中的前驱或后继之间的直接联系; • 熟悉树的各种存储结构及其特点; • 学会编写实现树的各种操作的算法; • 了解最优树的特性,掌握建立最优树和哈夫曼编码的方法。
树的定义 • 树是一类重要的非线性数据结构,是以分支关系定义的层次结构; • 树是n(n≥0)个结点的有限集,非空树满足: • 有且仅有一个特定的称之为根(root)的结点; • 除根以外的其余结点可分为m(0m<n)个互不相交的子集T1,T2,T3…Tm,其中每个子集本身又是一棵树,且称为根的子树。
1层 2层 3层 4层 A B C D E F G H I J 树的表示方法 • 特点:除根结点外,每个结点都仅有一个前趋(父)结点。 • 其它表示方法: • 嵌套集合表示法 • 凹入表表示法 参见教材120页
树的一些基本术语 • 结点的度(degree) • 结点所拥有的子树的数目。 • 叶子结点(leaf--又称终端结点 terminal node) • 度为零的结点。 • 分支结点(branch node) • 度不为零的结点 • 孩子( child) • 结点的子树的根称为结点的孩子。
树的一些基本术语(续) • 双亲( parent) • 结点是其孩子的双亲。 • 祖先( forefather) • 从树根到双亲的所有结点称为该结点的祖先。 • 子孙(progeny) • 以结点为根的子树的所有结点称为该结点的子孙。 • 兄弟(sibling) • 同一父母的所有孩子互称兄弟。 • 层次(level) • 树根为第一层,其孩子为第二层,孙子为第三层,以此类推。
树的一些基本术语(续) • 堂兄弟(cousin) • 双亲在同一层的结点互称堂兄弟。 • 深度(depth) • 树中结点的最大层次称为树的深度。 • 有序树(ordered tree) • 结点各子树从左至右是有秩序的树称为有序树。 • 无序树(unordered tree) • 结点各子树没有秩序的树称为无序树。 • 森林(forest ) • 森林是 m (m≥0) 棵互不相交的树的集合。
树的基本操作 • 初始化操作 InitTree(&T) • 求根函数 Root(T) • 求双亲函数 Parent(T,cur_e) • 求左孩子结点函数 LeftChild(T,cur_e) • 建树函数 CreateTree(&T,definiton) • 清空树函数 ClearTree(&T) • 插入子树函数 InsertChild(&T,&p,i,c) • 删除子树函数 DeleteChild(&T,&p,i) • 遍历函数 TraverseTree(T,visit()) • 求右兄弟函数 RightSibling(T,cur_e)
二叉树的定义 • 定义 • 二叉树是n(n0)个结点的有限集合,它或为空树(n=0),或由一个根结点和两棵互不相交的左子树和右子树的二叉树组成。 • 二叉树的特点: • 定义是递归的; • 0结点的度2; • 是有序树。
1 2 3 4 5 6 7 1 2 3 8 9 10 4 5 6 7 二叉树(续) • 二叉树的五种基本形态 • 两种特殊的二叉树 • 满二叉树:每一层上的结点数都是最大结点数。 • 完全二叉树:只有最下面两层结点的度可小于2,而最下一层的叶结点集中在左边若干位置上。
二叉树的性质 • 性质1 • 二叉树的第i层上至多有2i-1(i1)个结点。 • 性质2 • 深度为k的二叉树至多有2k-1个结点(k1)。 • 性质3 • 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1。 • 性质4 • 具有n个结点的完全二叉树的深度为 log2n+1。
0 1 2 3 4 5 6 0 1 2 7 8 9 3 4 5 6 二叉树的性质(续) • 性质5 • 对一棵具有n个结点的完全二叉树(其深度为log2n+1),对其结点按层从上至下(每层从左至右)进行0至n-1的编号,则对任一结点i(0i<n)有: • 若i=0,则i是根,无双亲;若i>0,则i的双亲是(i-1)/2。 • 若2i+1<n,则i有左孩子,左孩子是2i+1。 • 若2(i+1)<n,则有右孩子,右孩子是2(i+1)。
0 1 2 3 4 5 6 A B C D E F A B D E F C 二叉树的存储结构 • 顺序存储结构 • 按完全二叉树存储 #define MaxTreeSize 128 typedef TElemType SqBiTree[MaxTreeSize]; SqBiTree bt;
0 1 2 3 4 5 A B C D E F - 0 0 1 2 2 C F A B D E 二叉树的存储结构(续) • 顺序存储结构 • 用父母指针存储 • 存储结点数据和其父结点的序号 #define MaxTreeSize 100 typedef struct Node{ Telemtype data; int parent ; }Node; typedef Node bTree[MaxTreeSize];
链式存储结构 用二叉链表存储 链式存储结构 用三叉链表存储 bt bt A A ^ ^ B ^ C ^ ^ B ^ C ^ ^ D ^ D ^ E ^ ^ E ^ 二叉树的存储结构(续)
二叉链表的类型定义 typedef struct BiTNode { TElemType data; struct BiTNode*lchild; struct BiTNode*rchild; }BiTNode,*BiTree; 三叉链表的类型定义 typedef struct TriTNode { TElemType data; struct BiTNode*lchild; struct BiTNode*rchild; struct BiTNode*parent; }TriTNode,*TriTree; 二叉树的存储结构(续)
二叉树的基本操作 • 建树函数 CreateBiTree(&T) • 先序遍历函数 PreOrder(T,visit()) • 中序遍历函数 InOrder(T,visit()) • 后序遍历函数 PostOrder(T,visit()) • 按层次遍历函数 LevelOrder(T,visit())
√ √ √ 遍历二叉树 • 遍历二叉树 • 指按某条搜索路线走遍二叉树的每个结点,使得树中每个结点都被访问一次,且仅被访问一次。 根据搜索路径的不同,二叉树的遍历分为八种: 1、前序遍历(preorder traversal) DLR 2、中序遍历(inorder traversal) LDR 3、后序遍历(postorder traversal) LRD 4、逆前序遍历(inversepreorder traversal) DRL 5、逆中序遍历(inverse inorder traversal) RDL 6、逆后序遍历(inversepostorder traversal) RLD √ 7、按层次遍历(level-by-level traversal) 8、按层次逆遍历( inverse level-by-level traversal)
A B C D E F A B D E C 遍历二叉树示例 • 遍历方法 • 先序遍历:ABDEC • 中序遍历:DBEAC • 后序遍历:DEBCA 1-前序 2-中序 3-后序 4-逆前序 5-逆中序 6-逆后序 7-层次 8-层次逆 1. ABDCEF 2. DBAECF 3. DBEFCA 4. ACFEBD 5. FCEABD 6. FECDBA 7. ABCDEF 8. ACBFED
遍历二叉树的递归算法 void Preorder(BiTree t) { if (t) { visit(t); Preorder(t->lchild); Preorder(t->rchild); } } void Postorder(BiTree t) { if (t) { Postorder(t->lchild); Postorder(t ->rchild); visit( t ); } } void Inorder(BiTree t) { if (t) { Inorder(t->lchild); visit(t); Inorder(t->rchild); } }
利用遍历结果确定二叉树问题 A • 利用遍历结果确定二叉树问题 • 先序序列+中序序列 • 中序序列+后序序列 • 先序序列+后序序列 (x) 先序序列: ABCDEFGH 中序序列: BDCEAFHG F B G C D E H 思考:层序+先序/中序/后序, 能否确定?如何做? 例如:层序ABCDEFGHIJ,中序DBGEHJACIF
根据先序中序求后序的算法 void get_post(char *pre, char *in, char *post, int n) { if ( n == 0) return; for (k = 0; k < n; k++) if (in[k] == pre[0]) break; if (! k < n) { printf("错误的数据!\n"); exit(1); } post[n - 1] = pre[0]; get_post(pre + 1, in, post, k); get_post(pre + 1 + k, in + k + 1, post + k, n - k - 1); }
先序遍历的非递归算法(1) • 非递归算法1(朴素的非递归算法) void Preorder(Struct NODE *t) { init_stack(); push(t); while (!stack_empty()) { if ((p = pop()) != NULL) { visit(p); push(p->rchild); push(p->lchild); } } }
先序遍历的非递归算法(2) 非递归算法,考虑取消多余的空指针入栈,空指针个数可观,为n+1个 init_stack(); p = t; while (!stack_empty() || p) { visit(p); if (p->rchild) push(p->rchild); if (p->lchild) p = p->lchild; else p = pop(); } init_stack(); p = t; push(NULL); while (p) { visit(p); if (p->rchild) push(p->rchild); if (p->lchild) p = p->lchild; else p = pop(); }
后序遍历的非递归算法(1) • 非递归算法 init_stack(); push(root,0); while(!stack_empty()) { p = pop(&state); if (p == NULL) continue; if (state == 0) { push(p, 1); push(p->lchild, 0); } else if (state == 1) { push(p, 2); push(p->rchild, 0); } else { visit(p); } }
后序遍历的非递归算法(2) • 非递归算法 init_stack(); push(root, 0); while(!stack_empty()) { p = pop(&state); if (p == NULL) continue; if (state == 0) { push(p, 2); push(p->rchild, 0); push(p->lchild, 0); } else { visit(p); } }
中序遍历的非递归算法(1) • 朴素的算法 init_stack(); push(root, 0); while(!stack_empty()) { p = pop(&state); if (p == NULL) continue; if (state == 0) { push(p, 1); push(p->lchild, 0); } else if (state == 1) { visit(p); push(p->rchild, 0); } } • 效率问题 新压入堆栈的state0节点马上弹出然后再次压入作为state1节点
中序遍历的非递归算法(2) init_stack(); p = root; while (p) { push(p); p = p->lchild; } while(!stack_empty()) { p = pop(); visit(p); p = p->rchild; while (p) { push(p); p = p->lchild; } } init_stack(); p = root; while (p || !stack_empty()) { if (p) { push(p); p = p->lchild; } else { p = pop(); visit(p); p = p->rchild; } }
二叉树遍历的应用 • 在二叉树中查找结点值为x的结点 • 求二叉树中每个结点所处的层次 • 求二叉树的高度 • 复制一棵二叉树
G B C D E F A 0 1 2 34 5 6 A B C D E F G ^ 0 0 0 1 3 3 树的存储结构:双亲表示法 存储方法:使用顺序结构 #define MAX_TREE_SIZE 100 typedef struct PTNode { TElemType data; int parent; } PTNode; /* 节点的存储结构*/ typedef struct {/* 顺序存储结构 */ PTNode nodes[MAX_TREE_SIZE]; int r, n; /* 根位置和节点数*/ } PTree; 优点:简单,紧凑,易于寻找双亲节点 缺点:查询某节点的孩子效率低
树的存储结构:孩子表示法 方法一 struct PTNode { TElemType data; struct PTNode *child[NUM_CHILD]; }; 缺点:NUM_CHILD值不容易确定; 空域数目太多 方法二: 改进方法1,记录节点的度,分配合适大小的内存 方法三: 使用链表勾链,链表的每个节点记录一个孩子指针 优缺点比较和其他存储方案 选用的实际存储结构该考虑到可能需要操作(增加,删除,检索)
0 1 2 A 1 2 3 ∧ 3 B 4 ∧ 4 C ∧ 5 D 5 6 ∧ 6 A C E B G D F E ∧ F ∧ G ∧ 树的存储结构:孩子表示法(方法3)
#define MAX_TREE_SIZE 100 struct TNode { TElemType data; int degree; int child[1]; }; struct PTree { struct TNode *node[MAX_TREE_SIZE]; int n; } PTree; struct TNode *create_tnode(void) { struct TNode *p; int k, degree; scanf(“%d”, °ree); p = malloc(sizeof(struct TNode)+(degree-1)* sizeof(int)); P->degree = degree; for (k = 0; k < degree; k++) scanf(“%d”, &p->child[k]); input_node_data(p); return(p); } 树的存储结构:孩子表示法(方法2)
∧ ∧ ∧ ∧ G A B C D F E ∧ ∧ ∧ ∧ F E C D A G B 树的存储结构(孩子兄弟表示法) • 孩子兄弟表示法
森林与二叉树的转换 • 转换原则 • 按孩子兄弟表示法进行转换。 • 树与二叉树的转换 E D
A A B B C D E C F D E F G G 树和森林的遍历 • 树的遍历 • 先序遍历 • 先访问树的根结点,然后依次先根遍历根的每棵子树; ABEFCDG (二叉树先序) • 后序遍历 • 先依次后根遍历根的每棵子树,然后访问树的根结点 EFBCGDA (二叉树中序)
森林的遍历 • 先序遍历: • 访问森林中第一棵树的根结点; • 先序遍历第一棵树中根结点的子树森林; • 先序遍历除第一棵树外剩余的树构成的森林; 举例:A B C D E F G H I J • 中序遍历 • 中序遍历森林中第一棵树的根结点的子树森林; • 访问第一棵树的根结点; • 中序遍历除第一棵树外剩余的树构成的森林 举例:B C D A F E H J I G
集合的运算 find(x) 判断元素x属于哪个集合 Merge(Si, Sj) 将集合Si和Sj合并 集合的数据结构有多种实现方法 位图法 O(1) 有序表方法 O(n) 树型结构表示集合 采用的结构取决于集合大小和所进行的操作 集合的查找与合并
G B C D E F A 0 1 2 34 5 6 A B C D E F G ^ 0 0 0 1 3 3 树的存储结构:双亲表示法 存储方法:使用顺序结构 #define MAX_TREE_SIZE 100 typedef struct PTNode { TElemType data; int parent; } PTNode; /* 节点的存储结构*/ typedef struct {/* 顺序存储结构 */ PTNode nodes[MAX_TREE_SIZE]; int r, n; /* 根位置和节点数*/ } PTree; 优点:简单,紧凑,易于寻找双亲节点 缺点:查询某节点的孩子效率低