1 / 68

软件技术基础

段景山. 软件技术基础. 树结构. 制作 主讲. 段景山. 树的基本概念. 3.1 树的基本概念 非线性结构 对于结构中的一个结点,可能有多个前趋和多个后继 线性表中是有且仅有一个前趋和一个后继 3.1.1 树的定义 ( 教材 p31 ) 树是以分支关系定义的层次结构。 倒生树:树根在上,根上分茎,茎上分叶 是族谱、社会组织机构一类实体的逻辑抽象. 树的定义. 对定义的理解 ( 1 )有限集 ( 2 )递归定义:树,根,子树 ( 3 )有且仅有一个根结点不存在空树 ( 4 )子树是互不相交的有限集 ( 5 )树的层次性.

muncel
Download Presentation

软件技术基础

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 段景山 软件技术基础 树结构 制作 主讲 段景山

  2. 树的基本概念 • 3.1 树的基本概念 • 非线性结构 • 对于结构中的一个结点,可能有多个前趋和多个后继 • 线性表中是有且仅有一个前趋和一个后继 • 3.1.1树的定义 (教材p31) • 树是以分支关系定义的层次结构。 • 倒生树:树根在上,根上分茎,茎上分叶 • 是族谱、社会组织机构一类实体的逻辑抽象

  3. 树的定义 • 对定义的理解 • (1)有限集 • (2)递归定义:树,根,子树 • (3)有且仅有一个根结点不存在空树 • (4)子树是互不相交的有限集 • (5)树的层次性 除根结点以外的结点有且仅有一个父结点 有且仅有一个根结点

  4. 树的定义 • 树是一种数据结构 • Tree = ( D , R ) • D:元素的集合 • R:元素关系的集合 • (父、子关系 前驱、后继关系) • 各结点没有或仅有一个父结点 • 有且仅有一个根结点 • 各结点可以有任意个子树

  5. 树的术语 • 3.1.2 树的术语 • 1)结点 • 2)度与深度 根结点 叶结点 (茎)分支结点 没有后继,仅有前驱 有且仅有一个前驱,可以有多个后继 没有前驱,仅有后继 结点的度:该结点拥有的子树数目。 树的度:最大的结点度 深度:最大的层次数

  6. 树的术语 孩子 双亲 孩子 兄弟 祖先 子孙 3)A节点的 A

  7. 树的术语 • 4)路径(树枝,分支) • 从K1出发自上而下到K2所经历的所有结点序列 K1 K4 K7 K2 K1 K3 K4 K6 K7 K5 树上两节点之间的路径有?条 K2

  8. 树的术语 • 5)有序树与无序树 • 有序树:兄弟有长幼之分,从左至右。交换兄弟位置,变成不同的树。

  9. 树的术语 • 6)森林 • 不相交的树的集合

  10. 树的存储 • 3.2树的存储 • 3.2.1连续顺序存储 a [ 0 ] K1 a [ 1 ] a [ 2 ] K2 K5 a [ 3 ] K4 K6 a [ 4 ] 连续线性的下标不能很好的反映树的分支关系(非线性)

  11. 树的存储 • 3.2.2、链接存储--多重链表 • 树的节点 对应于 链表的链点 • 树节点间的分支关系用链点间的指针描述 • 链点可能有多个指针--多重链表,每个指针描述对应节点的一个分支关系 • 有且仅有一个根链点 • 不同的指针指向不同的子树根链点 • 一个子树有仅有一个根链点 • 问题,一个结点里究竟该有多少个指针呢? DATA 子树1 2 3

  12. 二叉树 • 3.3二叉树 • 3.3.1、定义 • 二叉树是结点的有限集,或为空,或由根的左、右子树组成,左右子树又分别是符合定义的二叉树。 • 对比树的定义: • 空二叉树树的定义中没有空树的概念 • 不多于2个孩子 树的节点可以有任意个子树 • 子树有左右之分 无序树可不区分左右 • 树的其它定义适用于二叉树:根茎叶、度、路径 仍然是递归定义。 二叉树是树吗?

  13. 二叉树 • (4)二叉树的形态 仅有左子树 无子树 空树 仅有右子树 有左右子树

  14. 二叉树的性质 • 3.3.2、二叉树的性质 • (1)在二叉树的第i层上最多有2i-1个结点 • 第i层的结点数最多是第i-1层的两倍 • (2)深度为k的二叉树最多有2k - 1个结点 • (3)叶结点数比具有两个孩子的结点数多 个 1 二叉树的数学特性 二叉树与二进制之间存在必然联系, 进而可以与整个数学发生关联了

  15. 二叉树的性质 • (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;

  16. 二叉树的性质 • (4)深度为K的满二叉树,结点个数为2k-1 • 满二叉树:所有的结点要么有两个孩子,要么一个也没有。所有的叶结点都位于同一层。 • 满二叉树:“装满”节点的二叉树 • 半满二叉树:深度为K的二叉树,K-1层是满二叉树,K层节点个数不足2K-1个

  17. 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无右后继

  18. 完全二叉树 • 关于完全二叉树的其他描述形式 • 如果对满二叉树的节点从上至下,从左至右连续编号,具有n个节点的完全二叉树各节点与同样深度的满二叉树的前n个节点一一对应 • 叶节点仅位于下两层,对任一节点,若其右子树的深度为1,则其左子树的深度不小于1

  19. 二叉树 满二叉树 半满二叉树 半满二叉树 半满二叉树 完全二叉树 非完全二叉树 非完全二叉树

  20. 顺序存储二叉树 • 3.3.3 顺序存储二叉树 • 将完全二叉树从上到下,从左到右编号后,结点号码可作为数组的下标,从而将完全二叉树顺序存储下来。当给出任意结点i,我们可以知道它的父结点为[ i/2 ],两个孩子分别为2i和2i+1。 • 一般的二叉树相对于同样深度的完全二叉树,缺失了部分结点,在顺序存储时,这些位置要空出来。以维持结点编号之间的父子换算关系 • 如此存放,将浪费较多空间

  21. 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

  22. 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; 根结点指针

  23. d d d d d d d d L L L L L L L L R R R R R R R R 用链表实现二叉树 • 二叉树的链表结构

  24. B A C 二叉树的遍历 • 3.5、二叉树的操作 • 3.5.1遍历操作 • 分支及根的遍历顺序 B B B 中根遍历 中序遍历 C C C A A A 左 根 右 后根遍历 后序遍历 特点: 左 右 根 • 以根被访问顺序来划分不同的遍历方法 • 在子树的访问顺序中始终以左子树优先 先根遍历 先序遍历 根 左 右

  25. 二叉树的遍历 • 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

  26. 二叉树的遍历 • 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

  27. 二叉树的遍历 • 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

  28. 课堂练习 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

  29. 回到 回溯 二叉树的遍历 • 中根遍历算法 • 算法实现分析 • 遍历过程: • 从根开始 X B A C 1、找到B,但不访问B 2、根据B找到A,访问A 3、再回到B、访问B 4、根据B找到C,访问C 方法一、利用栈来实现回朔

  30. 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 总结:树入栈后一直朝左走(一路进栈),走不动时出栈并访问节点。同时将该节点右子树入栈。如果其右子树为空,就再出栈一个节点,访问,出栈节点右子树入栈

  31. 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

  32. 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

  33. 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

  34. 中根遍历算法(法一) 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;} 访问结点 } }

  35. 先根遍历算法 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;}} }

  36. 后根遍历 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 ) 放到哪里? }

  37. 利用递归的遍历算法 • 方法二:利用递归调用来实现回溯 • 中根遍历递归算法 void inorder( root ){ if( root->Lchild != NULL) inorder (root->Lchild); process(root); if( root->Rchild != NULL) inorder(root->Rchild); } 左根右

  38. 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 左根右

  39. 利用递归的遍历算法 • 后根遍历递归算法 void postorder( root ){ if( root->Lchild != NULL) postorder (root->Lchild); if( root->Rchild != NULL) postorder(root->Rchild); process(root); } 左右根

  40. 利用递归的遍历算法 • 先根遍历递归算法 void preorder( root ){ process(root); if( root->Lchild != NULL) preorder (root->Lchild); if( root->Rchild != NULL) preorder(root->Rchild); } 根左右

  41. 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); }

  42. 二叉树的遍历 • 对比递归与非递归算法 • 递归算法更简洁,更多依靠系统提供的“用户程序调用栈”,该栈的使用对用户是不可见的——两种算法的本质一样 • 非递归算法在算法中直接掌握栈结构的调用 • 二叉树的深度决定了递归调用的深度,决定了栈的长度。 • 当二叉树的深度较深时,系统提供的“用户程序调用栈”可能出现溢出,这时需要算法自行掌握栈的使用。

  43. 二叉树的建立(上机练习) • 3.5.2 建立二叉树 • 设输入次序:(以先根为序) ABC_ _ DEF _ _ G _ _ _ H _ I _ JK _ _ L _ _ A 根据输入的情况,将新节点放在指定的位置,然后从新节点开始下一个过程 B H 利用先根遍历算法框架,建立二叉树 _ D C I _ _ _ _ J E F G K L _ _ _ _ _ _ _ _ 每一个空格“_”表示一个空指针

  44. 二叉树的建立(上机练习) • 利用先根遍历算法框架,按照遍历顺序,逐个结点地建立二叉树。类似于过河时,先放一块石头在脚前,然后移动到石头上,再放一块石头,再移动…过河以后,石桥也搭成了。 • 根据输入的情况,将新结点放在指定的位置,然后从新节点开始下一个过程,如果输入是空格,则置结点的相应子树指针为空

  45. 二叉树的建立(上机练习) • 以先根遍历算法为基础,改变为二叉树的建立算法 void c_preorder( root ){ process(root); if( root->Lchild != NULL) c_preorder (root->Lchild); if( root->Rchild != NULL) c_preorder(root->Rchild); } 输入一个值,并生成新链点; 将新链点挂在root的左子树上; 输入一个值,并生成新链点; 将新链点挂在root的右子树上;

  46. 二叉树的建立(上机练习) 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的右子树上;

  47. 二叉树的建立(上机练习) 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); 以先根遍历算法为基础递归生成二叉树 }

  48. 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维持遍历顺序一致

  49. new new 右 二叉树的插入 • 插入时,选择遍历顺序也一致的方法 根 根 左 左 根 右 new 左 右 根 左 根 右 左 左 根 new 右

  50. 课堂练习 • 插入时,选择遍历顺序也一致的方法。根据同样的思想,思考如果要求新结点插入在根结点的左侧,则原来的左子树要放在新结点的左子树还是右子树? 根 new 右

More Related