680 likes | 828 Vues
段景山. 软件技术基础. 树结构. 制作 主讲. 段景山. 树的基本概念. 3.1 树的基本概念 非线性结构 对于结构中的一个结点,可能有多个前趋和多个后继 线性表中是有且仅有一个前趋和一个后继 3.1.1 树的定义 ( 教材 p31 ) 树是以分支关系定义的层次结构。 倒生树:树根在上,根上分茎,茎上分叶 是族谱、社会组织机构一类实体的逻辑抽象. 树的定义. 对定义的理解 ( 1 )有限集 ( 2 )递归定义:树,根,子树 ( 3 )有且仅有一个根结点不存在空树 ( 4 )子树是互不相交的有限集 ( 5 )树的层次性.
E N D
段景山 软件技术基础 树结构 制作 主讲 段景山
树的基本概念 • 3.1 树的基本概念 • 非线性结构 • 对于结构中的一个结点,可能有多个前趋和多个后继 • 线性表中是有且仅有一个前趋和一个后继 • 3.1.1树的定义 (教材p31) • 树是以分支关系定义的层次结构。 • 倒生树:树根在上,根上分茎,茎上分叶 • 是族谱、社会组织机构一类实体的逻辑抽象
树的定义 • 对定义的理解 • (1)有限集 • (2)递归定义:树,根,子树 • (3)有且仅有一个根结点不存在空树 • (4)子树是互不相交的有限集 • (5)树的层次性 除根结点以外的结点有且仅有一个父结点 有且仅有一个根结点
树的定义 • 树是一种数据结构 • Tree = ( D , R ) • D:元素的集合 • R:元素关系的集合 • (父、子关系 前驱、后继关系) • 各结点没有或仅有一个父结点 • 有且仅有一个根结点 • 各结点可以有任意个子树
树的术语 • 3.1.2 树的术语 • 1)结点 • 2)度与深度 根结点 叶结点 (茎)分支结点 没有后继,仅有前驱 有且仅有一个前驱,可以有多个后继 没有前驱,仅有后继 结点的度:该结点拥有的子树数目。 树的度:最大的结点度 深度:最大的层次数
树的术语 孩子 双亲 孩子 兄弟 祖先 子孙 3)A节点的 A
树的术语 • 4)路径(树枝,分支) • 从K1出发自上而下到K2所经历的所有结点序列 K1 K4 K7 K2 K1 K3 K4 K6 K7 K5 树上两节点之间的路径有?条 K2
树的术语 • 5)有序树与无序树 • 有序树:兄弟有长幼之分,从左至右。交换兄弟位置,变成不同的树。
树的术语 • 6)森林 • 不相交的树的集合
树的存储 • 3.2树的存储 • 3.2.1连续顺序存储 a [ 0 ] K1 a [ 1 ] a [ 2 ] K2 K5 a [ 3 ] K4 K6 a [ 4 ] 连续线性的下标不能很好的反映树的分支关系(非线性)
树的存储 • 3.2.2、链接存储--多重链表 • 树的节点 对应于 链表的链点 • 树节点间的分支关系用链点间的指针描述 • 链点可能有多个指针--多重链表,每个指针描述对应节点的一个分支关系 • 有且仅有一个根链点 • 不同的指针指向不同的子树根链点 • 一个子树有仅有一个根链点 • 问题,一个结点里究竟该有多少个指针呢? DATA 子树1 2 3
二叉树 • 3.3二叉树 • 3.3.1、定义 • 二叉树是结点的有限集,或为空,或由根的左、右子树组成,左右子树又分别是符合定义的二叉树。 • 对比树的定义: • 空二叉树树的定义中没有空树的概念 • 不多于2个孩子 树的节点可以有任意个子树 • 子树有左右之分 无序树可不区分左右 • 树的其它定义适用于二叉树:根茎叶、度、路径 仍然是递归定义。 二叉树是树吗?
二叉树 • (4)二叉树的形态 仅有左子树 无子树 空树 仅有右子树 有左右子树
二叉树的性质 • 3.3.2、二叉树的性质 • (1)在二叉树的第i层上最多有2i-1个结点 • 第i层的结点数最多是第i-1层的两倍 • (2)深度为k的二叉树最多有2k - 1个结点 • (3)叶结点数比具有两个孩子的结点数多 个 1 二叉树的数学特性 二叉树与二进制之间存在必然联系, 进而可以与整个数学发生关联了
二叉树的性质 • (3)叶结点数比具有两个孩子的结点数多1个 设n0为叶结点个数, n1为具有一个孩子的分支结点个数, n2为具有两个孩子的分支结点个数, n为结点总个数 条件1、 n = n0 + n1 + n2 条件2、 n = 分支的个数 + 1 设为分支的个数为B 条件3、 B = 2 n2 + n1 所以: 2n2 + n1 + 1 = n0 + n1 + n2 结论: n0 = n2 + 1;
二叉树的性质 • (4)深度为K的满二叉树,结点个数为2k-1 • 满二叉树:所有的结点要么有两个孩子,要么一个也没有。所有的叶结点都位于同一层。 • 满二叉树:“装满”节点的二叉树 • 半满二叉树:深度为K的二叉树,K-1层是满二叉树,K层节点个数不足2K-1个
1 2 3 4 5 6 二叉树的性质 • (5)具有n个节点的完全二叉树,深度为 [log2n]+1 • 完全二叉树:特殊的半满二叉树,最后一层节点从左至右依次排列,没有间断。 • 如果对节点数为n的完全二叉树自上而下,从左至右依次编号,则节点i的父结点为[ i / 2 ] • 若2i≤n,则i的左后继是2i;若2i>n,则i无左后继 • 若2i+1≤n,则i的右后继是2i+1;若2i>n,则i无右后继
完全二叉树 • 关于完全二叉树的其他描述形式 • 如果对满二叉树的节点从上至下,从左至右连续编号,具有n个节点的完全二叉树各节点与同样深度的满二叉树的前n个节点一一对应 • 叶节点仅位于下两层,对任一节点,若其右子树的深度为1,则其左子树的深度不小于1
二叉树 满二叉树 半满二叉树 半满二叉树 半满二叉树 完全二叉树 非完全二叉树 非完全二叉树
顺序存储二叉树 • 3.3.3 顺序存储二叉树 • 将完全二叉树从上到下,从左到右编号后,结点号码可作为数组的下标,从而将完全二叉树顺序存储下来。当给出任意结点i,我们可以知道它的父结点为[ i/2 ],两个孩子分别为2i和2i+1。 • 一般的二叉树相对于同样深度的完全二叉树,缺失了部分结点,在顺序存储时,这些位置要空出来。以维持结点编号之间的父子换算关系 • 如此存放,将浪费较多空间
1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 10 10 12 12 14 14 9 9 11 11 13 13 15 15 H A C B D E G F I K J M O U P a[1] a[15] H A C B E G F I M U P a[1] a[15] 顺序存储二叉树 • 例 H A C B D E G F I K J M O U P H A C 数组的下标可以体现逻辑关系 B E G F I M U P
data Lchile Rchild 用链表实现二叉树 • 3.4 用链表实现二叉树 • 二叉树链点的定义 • 二叉树的定义 typedef struct tnode_type{ elemtype data; struct tnode_type *Lchild; struct tnode_type *Rchild; }tnode_type; 左孩子,左子树 右孩子,右子树 typedef struct tree_type{ tnode_type *root; int num; }tree_type; 根结点指针
d d d d d d d d L L L L L L L L R R R R R R R R 用链表实现二叉树 • 二叉树的链表结构
B A C 二叉树的遍历 • 3.5、二叉树的操作 • 3.5.1遍历操作 • 分支及根的遍历顺序 B B B 中根遍历 中序遍历 C C C A A A 左 根 右 后根遍历 后序遍历 特点: 左 右 根 • 以根被访问顺序来划分不同的遍历方法 • 在子树的访问顺序中始终以左子树优先 先根遍历 先序遍历 根 左 右
二叉树的遍历 • 1)中根遍历(中序遍历) A 左 根 右 B K C D L F E I M G H J N O Q P F G C E H B D J I A L N P O Q M K
二叉树的遍历 • 2)先根遍历(先序遍历) A 根 左 右 B K C D L F E I M G H J N O Q P A B C F G E H D I J K L M N O P Q
二叉树的遍历 • 3)后根遍历(后序遍历) A B K 左 右 根 C D L F E I M G H J N O Q P G F H E C J I D B P Q O N M L K A
课堂练习 A • 写出这颗二叉树的三种遍历顺序 B K • 思考: • 第一个被遍历的节点, • 最后一个被遍历的节点, • 哪种算法最容易找到 C D L F E I M G H J N 1、中根遍历(左根右) O F G C E H B D J I A L N P O Q M K Q 2、先根遍历(根左右) P A B C F G E H D I J K L M N O P Q 3、后根遍历(左右根) G F H E C J I D B P Q O N M L K A
回到 回溯 二叉树的遍历 • 中根遍历算法 • 算法实现分析 • 遍历过程: • 从根开始 X B A C 1、找到B,但不访问B 2、根据B找到A,访问A 3、再回到B、访问B 4、根据B找到C,访问C 方法一、利用栈来实现回朔
A B K C D L F E I M G H J N O Q P 1、树(子树)根入栈,不访问 2、左子树入栈,左子树的各子树根依次入栈即反复进行步骤1 3、当左子树为空时,出栈,访问根结点 4、根节点右子树入栈 (新树入栈,到步骤1去遍历右子树) G F 5、当右子树为空时, 出栈,访问(祖先)爷结点, 将爷结点的右子树入栈 (新树入栈,回到步骤1) E C H D B A 总结:树入栈后一直朝左走(一路进栈),走不动时出栈并访问节点。同时将该节点右子树入栈。如果其右子树为空,就再出栈一个节点,访问,出栈节点右子树入栈
A B K L C D I F E M NULL H G J N NULL NULL O Q P p • 算法框架 while( ! end_of(tree)){ ...... } “遍历” 中根访问与堆栈 while( ! end_of(tree)){ if( p != NULL ){ push(stack , p) p = p->Lchild; } else{ p = pop(stack); process(p); p = p->Rchild; }} p指针指向即将访问的子树 F G C E B A
A B K C D L F E I M G H J N O Q P • 算法框架 P指针 NULL NULL 中根访问与堆栈 while( ! end_of(tree)){ if( p != NULL ){ push(stack , p) p = p->Lchild; } else{ p = pop(stack); process(p); p = p->Rchild;} } end_of(tree)的实现 empty(stack) “假入栈”:先放一个 假节点在栈里 当最后一个节点K出栈时 会连续两次出栈,获得栈空的满足 K ^_^ A
A B K C D L F E I M G H J N O Q P • 算法框架 P指针 NULL 中根访问与堆栈 end_of(tree)的实现 while( ! empty(tree)){ if( p != NULL ){ push(stack , p) p = p->Lchild; } else{ p = pop(stack); process(p); p = p->Rchild; }} 观察A出栈时和K出栈时 的P指针情况,可利用 P != NULL || !empty(stack) A出栈后 p = k; 且栈为空 K出栈后 p = NULL 且栈为空 A K
中根遍历算法(法一) void inorder( tree){ tnode_type * p; create_stack( stack ); p = tree->root; while( p != NULL || ! empty(stack) ){ if ( p != NULL){ push(stack, p); p = p -> Lchild; } else{ p = pop(stack); process(p); p = p->Rchild;} 访问结点 } }
先根遍历算法 void preorder( tree){ tnode_type * p; create_stack( stack ); p = tree->root; while( p != NULL || ! empty(stack) ){ if ( p != NULL){ process(p); push(stack, p); p = p -> Lchild; } else{ p = pop(stack); process(p); p = p->Rchild;}} }
后根遍历 void postorder( tree){ tnode_type * p; create_stack( stack ); p = tree->root; while( p != NULL || ! empty(stack) ){ if ( p != NULL){ push(stack, p); p = p -> Lchild; } else{ p = pop(stack); p = p->Rchild; }} 思考 后根遍历算法在基本框架上如何更动以完成 process( p ) 放到哪里? }
利用递归的遍历算法 • 方法二:利用递归调用来实现回溯 • 中根遍历递归算法 void inorder( root ){ if( root->Lchild != NULL) inorder (root->Lchild); process(root); if( root->Rchild != NULL) inorder(root->Rchild); } 左根右
void inorder( A ){ if( A->Lchild != NULL) inorder (A->Lchild); process( A ); if( A->Rchild != NULL) inorder(A->Rchild); } A B D void inorder( B ){ if( B->Lchild != NULL) inorder (B->Lchild); process( B ); if( B->Rchild != NULL) inorder( B->Rchild); } void inorder( D ){ if( D->Lchild != NULL) inorder ( D->Lchild); process( D ); if( D->Rchild != NULL) inorder( D->Rchild); } c void inorder( C ){ if( C->Lchild != NULL) inorder ( C->Lchild); process( C ); if( C->Rchild != NULL) inorder( C->Rchild); } BCAD 左根右
利用递归的遍历算法 • 后根遍历递归算法 void postorder( root ){ if( root->Lchild != NULL) postorder (root->Lchild); if( root->Rchild != NULL) postorder(root->Rchild); process(root); } 左右根
利用递归的遍历算法 • 先根遍历递归算法 void preorder( root ){ process(root); if( root->Lchild != NULL) preorder (root->Lchild); if( root->Rchild != NULL) preorder(root->Rchild); } 根左右
B A C 利用递归的遍历算法 void inorder( root ){ if( root->Lchild != NULL) inorder (root->Lchild); process(root); if( root->Rchild != NULL) inorder(root->Rchild); } 中根遍历 中序遍历 ABC ACB 后根遍历 后序遍历 void postorder( root ){ if( root->Lchild != NULL) postorder (root->Lchild); if( root->Rchild != NULL) postorder(root->Rchild); process(root); } 先根遍历 先序遍历 BAC void preorder( root ){ process(root); if( root->Lchild != NULL) preorder (root->Lchild); if( root->Rchild != NULL) preorder(root->Rchild); }
二叉树的遍历 • 对比递归与非递归算法 • 递归算法更简洁,更多依靠系统提供的“用户程序调用栈”,该栈的使用对用户是不可见的——两种算法的本质一样 • 非递归算法在算法中直接掌握栈结构的调用 • 二叉树的深度决定了递归调用的深度,决定了栈的长度。 • 当二叉树的深度较深时,系统提供的“用户程序调用栈”可能出现溢出,这时需要算法自行掌握栈的使用。
二叉树的建立(上机练习) • 3.5.2 建立二叉树 • 设输入次序:(以先根为序) ABC_ _ DEF _ _ G _ _ _ H _ I _ JK _ _ L _ _ A 根据输入的情况,将新节点放在指定的位置,然后从新节点开始下一个过程 B H 利用先根遍历算法框架,建立二叉树 _ D C I _ _ _ _ J E F G K L _ _ _ _ _ _ _ _ 每一个空格“_”表示一个空指针
二叉树的建立(上机练习) • 利用先根遍历算法框架,按照遍历顺序,逐个结点地建立二叉树。类似于过河时,先放一块石头在脚前,然后移动到石头上,再放一块石头,再移动…过河以后,石桥也搭成了。 • 根据输入的情况,将新结点放在指定的位置,然后从新节点开始下一个过程,如果输入是空格,则置结点的相应子树指针为空
二叉树的建立(上机练习) • 以先根遍历算法为基础,改变为二叉树的建立算法 void c_preorder( root ){ process(root); if( root->Lchild != NULL) c_preorder (root->Lchild); if( root->Rchild != NULL) c_preorder(root->Rchild); } 输入一个值,并生成新链点; 将新链点挂在root的左子树上; 输入一个值,并生成新链点; 将新链点挂在root的右子树上;
二叉树的建立(上机练习) void c_preorder( root ){ if( root->Lchild != NULL) c_preorder (root->Lchild); if( root->Rchild != NULL) c_preorder(root->Rchild); } read(ch),temp = NULL; if( ch != ‘ ‘){ temp = create_node( ch ); temp->Lchild = NULL; temp->Rchild = NULL;} 输入一个值,并生成新链点; node * create_node(ch) { node * p; p = (node *)malloc(sizeof(node)); p->data = ch; return p; } root->Lchild = temp; 将新链点挂在root的左子树上; 输入一个值,并生成新链点; 将新链点挂在root的右子树上;
二叉树的建立(上机练习) void create_tree( tree ){ read( ch ); 输入新元素 if ( ch = = ‘ ‘ ){ tree = NULL; return; } 根据输入,生成新链点,主要包括申请链点空间 p = create_node(ch); create a new node p; p->data = ch; p->Lchild = NULL; p->Rchild = NULL 生成整个树的根 c_preorder(p); 以先根遍历算法为基础递归生成二叉树 }
E H 二叉树的操作 • 3.5.3 二叉树的插入操作 • 新结点插入在某个结点的左或右孩子处 • 问题:原来的子树应该插在新结点的左孩子还是右孩子? B 插入前的遍历顺序:FGCEHBD C D 例:要求新节点new插入在 C 的右孩子处 new F 法1:若E子树插在新节点的左子树? 则遍历顺序为 F G C E H new B D … G 法2:若E子树插在新节点的右子树? 则遍历顺序为 F G C new E H B D … 思考 哪一种插入方式更合理? 法2维持遍历顺序一致
右 new new 右 二叉树的插入 • 插入时,选择遍历顺序也一致的方法 根 根 左 左 根 右 new 左 右 根 左 根 右 左 左 根 new 右
左 课堂练习 • 插入时,选择遍历顺序也一致的方法。根据同样的思想,思考如果要求新结点插入在根结点的左侧,则原来的左子树要放在新结点的左子树还是右子树? 根 new 右