树算法 树算法 树的三种存储结构的分析 1. 树的存储结构: 注意噢, 这里说的是树, 没说是二叉树。 树的存储要求既要存储结点的 数据元素本身, 又要存储结点之间的逻辑关系。 下面我们就来介绍 3 种常用的存储结构: 双亲存储结构、 孩子链存储结构、 孩子兄弟链存储结构。 (1) 双亲存储结构: 这种存储结构是一种顺序存储结构, 用一组连续空间存储树的所有结点, 同时在每个结点中附设一个伪指针指示其双亲结点的位置。 typedef struct { int data; // 结点的值 int parent; // 指向双亲的位置 } PTree[MaxSize]; 不知道大家看到这个定义之后有没有觉得眼熟, 这种定义感觉上是不是和静态链表很像? 用一组连续的空间存放所有结点, 用伪指针(静态链表里面叫游标) 来当作指针用。 如果我们再翻翻书确认一下自己的想法的话, 你会发现树的双亲存储结构的定义和静态链表的定义的区别仅仅是单词拼写„„这说明了 什么? 树的双亲存储结构实际上就是静态链表。 (2) 孩子链存储结构: 既然名字中带“链” , 那我们就应该能够想到这种存储结构就不是用一组连续的空间来存放结点的了 。 事实也是如此。 在这种存储结构中, 每个结点不仅包含数据值, 还包括指向所有孩子结点的指针。 typedef struct node{ int data; // 结点的值 struct node *sons[MaxSons] ; // 指向孩子结点, 其中 MaxSons 为最多的孩子结点个数} TSonNode; 我想这个定义大家有疑惑的地方估计就在那个 MaxSons 上。 为什么就用孩子结点个数最多的呢? 我们都知道, 对于一颗一般的树来说, 每个结点的度是可以不相同的, 因此,如果按各个结点的度设计变长结构, 则后续算法的实现将会非常麻烦, 因此为了简单化,就选用最大的度了。 (3) 孩子兄弟链存储结构: 前面两个定义都是为结点设计两个域, 而这里的孩子兄弟链则是将结点分成了 3 个部分。 一个数据元素域, 一个指向该结点的第一个孩子结点的指针域, 一个指向该结点的下一个兄弟结点的指针域。 typedef struct tnode { int data; struct tnode *bp; // 指向兄弟 struct tnode *sp; // 指向孩子结点 } TSBNode; 由于树的孩子兄弟链的存储结构具有两个指针域, 且这两个指针域是有序的。 所以孩子兄弟链存储结构实际上是把树转换为二叉树的存储结构。 (还记得左孩子右兄弟是什么么? ) 后面会知道, 把树转换为二叉树所对应的结构恰好就是这种孩子兄弟链存储结构。所以, 孩子兄弟链存储结构的最大的有点就在于可以实现树和二叉树的相互转换。 但是孩子兄弟链也不可能是完美的, 比如, 从当前结点查找双亲结点时, 需要从树的根结点开始逐个结点比较查找。 二叉树的存储结构——顺序存储、 链式存储我们昨天搞定了 树的三种存储结构, 那今天就来看看树中最具有代表性也最为重要的一类树——二叉树。 二叉树主要有顺序存储结构和链式存储结构两种。 1. 二叉树的顺序存储结构: 这种结构就是用一组地址连续的存储单元来存放二叉树的数据元素。 因此, 确定好树中各数据元素存放的次序是很重要的, 因为存放次序的相互位置是要能够反映元素之间的逻辑关系的。 typedef struct { int data[MaxSize]; // 存放二叉树结点的值 int n; // 存放 data 中的元素个数, 含空结点 } SqBTree; 二叉树的顺序存储结构中结点的存放次序: 对该树中每个结点进行编号, 其编号从小到大的顺序就是结点存放在连续存储单元的先后次序。 编号的规则是这样的: 首先把树的根结点编为 1 号, 然后按照层次从上到下、 每层从左到右的顺序, 对每一个结点进行编号。编号为 i 的双亲结点, 那么它的左孩子结点的编号就是 2i, 右孩子结点的编号为(2i+1) 。 正是通过这样的一种关系, 我们可以通过存储位置来反映逻辑关系的。 这种存储结构对于完全二叉树来说是非常合适的, 因为完全二叉树, 用土话说就是结点不会有间隔的, 这样的话, 反应到数组中就会充分利用存储空间。 但是对于一般的二叉树就不是那么好了 , 因此, 下面介绍的儿二叉树链式存储结构是我们通常采用的存储方式。 2. 二叉树的链式存储结构: 这种结构是说用一个链表来存储一棵二叉树, 二叉树中的每一个结点用链表中的一个链结点来存储。 typedef struct node { int data; // 数据元素 struct node *lchild; // 指向左孩子结点 struct node *rchild; // 指向右孩子结点 } BTNode; 我想这个定义大家都一定很熟悉了 吧, 我就不再多言了 。 二叉树的基本运算和实现(1) ——按值查找结点刚刚我们看完了二叉树的存储结构, 我想大家已经迫不及待的想看看算法实现了吧, 我们就立刻进入这部分内容噢。 以下的二叉树均是采用链式存储结构的。 1. 查找二叉树中的结点: 采用递归算法在二叉树 b 中查找值为 x 的结点。 找到后返回其指针, 否则返回 NULL。 BTNode *FindNode (BTNode *b, int x) { BTNode *p; if (b==NULL) // 空树时返回 NULL return NULL; else if (b->data==x) // 找到值为 x 的点, 返回其指针 return b; else { p=FindNode (b->lchild, x) ; // 在左子树中查找 if (p != NULL) // 若找到, 则返回其指针 return p; else // 若未找到, 则在右子树中查找 return FindNode(b->rchild, x) ; } } 我想看到这么一段, 大家也许会觉得有点无从下手的感觉, 但是我们像对付单链表一样来看看这段代码, 其实, 这也是不用背的 一上来, 也许有的同学就被这个运算的返回值搞的云里雾里的, 为什么要用指针座返回值啊? 其实我们这个时候静下来想一想单链表的按元素值查找的算法的返回值类型, 大家一想应该就知道了, 单链表的是 int 类型。 这时候疑问更严重了 …人家好好的用了 一个挺好的 int, 到你这凭什么搞特殊化啊? 这时, 我们就不能停留在表面上了 , 我们需要深层次的挖掘内在的联系。 大家再想想单链表的那个 int 类型的返回值的含义是什么, 是结点在单链表中的位置。 好的, 那我们按照这个思路, 那这里在二叉树里面按值查找结点的话应该也是要返回元素的位置。 所以我们想, 最简单的找到一个结点, 不就是一个指针指向它么? 那这里不就是应该返回指针么? 呵呵, 看到这, 很多同学不是会心的一笑, 而是一个大大大的问号, 这里怎么不用int 来返回位置呢? 因为二叉树和链表的很大的不同就是二叉树里面的结点不一定是挨着的, 所以用 i++; 这样的方法是很麻烦的。 到这, 应该没有疑问了吧。 大家想, 要想在一棵二叉树里面找到一个结点, 一个正常人的思维应该是……首先判断 这棵树是不是一个空树, 如果是的, 那就 GG 吧…………如果不是空树, 那就先看这个树的 根结点是不是要找的结点; 如果根结点不是的, 那么就从根结点的左子树开始查找, 如果还 是没找到, 那么就从根结点的又子树开始查找, 如果还是没找到, 那就说明找不到了… 上面那个是人工的思维过程, 其实计算机和我们差不多, 它也是这么想的。 所以就会出 现了 上面的代码了 。 现在我想 if (b==NULL) return NULL; else if (b->data==x) return b; 这里应该是没疑问了吧, 只需要再提一点, BTNode *b 这里的 b 是指向二叉树的根结 点的, 这个是可以类比的。 在单链表中的 L 就是头指针, 所以这里的 b 也可以理解为头指 针; 在单链表中的头指针是指向单链表的第一个结点的(注意, 这里我没有说是头结点还是 开始结点) , 同理, 二叉树里的 b 也是指向第一个结点的, 那这个结点就是根结点, 所以, 这里的 b 就是指向根结点的。 好, 我们再往后走……真的好长啊……接下来, 我们就得接触二叉树树中不可能回避 的问题——递归算法。 我想说的是二叉树里的递归算法是非常重要的, 因为这体现了二叉树 的一个很重要的性质。 大家都知道二叉树的定义就是递归的, 一个事物的定义是递归的, 可 想而知, 递归在这个事物中的重要性了。 顺便说一句: 行列式也是递归定义的……递归的具 体内容参见《DS 高分笔记》 , 讲的超级好的, 绝对超越了 严蔚敏… 这下, 再看 p=FindNode (b->lchild, x) ; if (p != NULL) // 若找到, 则返回其指针 return p; else // 若未找到, 则在右子树中查找 return FindNode(b->rchild, x) ; 这里应该就不难了 吧, 首先, 把 FindNode 的返回值赋给 p, 因为返回值是一个指针, 所以如果 P 不为 NULL, 那么就说明找到了 这个结点, 那么就直接返回 p 就 OK。 如果在左 子树没有找到的话, 那么就在右子树里找所以直接 return FindNode(b->rchild, x) ; 就可以了 。 因为不管找到还是没找到, 一定会给我们一个返回值而我们通过这个返回值就可以确定结论 了。 二叉树的基本运算和实现(2) ——找孩子节点、 求树高 1. 找孩子节点: 该运算直接返回*p 结点的左孩子结点或右孩子结点的指针。 因为是找结点, 所以返回类型就是指针类型了 。 至于为什么我想前面一个已经讲的非常清楚了。 BTNode *LchildNode (BTNode *p) // 找左孩子结点 { return p->lchild; } BTNode *RchildNode (BTNode *p) // 找右孩子结点 { return p->rchild; } 2. 求树高: 这是一个递归算法的模型。 int BTNodeHeight (BTNode *b) { int lchildh, rchildh; if (b==NULL) // 空树的高度是 0 return 0; else { lchildh=BTNodeHeight(b->lchild) ; // 求左子树的高度 lchildh rchildh=BTNodeHeight(b->rchild) ; // 求右子树的高度 rchildhreturn (lchildh>rchildh) ? (lchild+1) : (rchildh+1) ; } } 我们还是和昨天的按值查找一样, 来先用人工的方法看看是怎么样求树高的, 这里我们都是使用递归算法的, 因为写起来简单。 我们也是要先看看这个树是不是一棵空树, 如果不是空树, 那么, 我们的思路一定是这样的, 把根结点左子树的树高算出来, 然后把根结点右子树的树高算出来。 最后比较这两个子树的高度, 哪个高就选哪个, 然后最后整个树的高度就是子树的高度加 1. 这就算完了。 二叉树的递归遍历算法 这里只是写出最基本的算法模型, 具体到每个题目, 代码是不一样的, 但是模型还是一样的, 所以, 各位要是看具体的题目的话, 我想没有比率辉的《DS 高分笔记》 更好的辅导书了。 首先, 我们要介绍一下什么叫遍历。 二叉树的遍历就是指按照一定的次序访问二叉树中的所有结点, 且每个结点仅被访问一次的过程。 我想大家一不小心可能就忽略了 这个“只能访问一次” 的条件。 1. 二叉树的遍历递归算法: void PreOrder (BTNode *b) { if (b!=NULL) { printf ("%c ", b->data) ; // 访问根结点 PreOrder (b->lchild) ; // 先序遍历左子树 PreOrder (b->rchild) ; // 先序遍历右子树 } } void InOrder (BTNode *b) { if (b!=NULL) { InOrder (b->lchild); printf ("%c ", b->data) ; InOrder (b->rchild)l } } void PostOrder (BTNode *b) { if (b!=NULL) { PostOrder (b->lchild) ; PostOrder (b->right) ; printf ("%c ", b->data) ; } } 我想这三个算法放到一起, 大家一定就看出了其中的端倪, 三种遍历其实就是 printf()函数的三个位置。 放在最前面就是先序, 放在中间就是中序, 放在后面就是后续。 OK, 我这里就说这么多啦… 二叉树的层次遍历算法 1. 二叉树的层次遍历算法: 首先, 我们必须要掌握什么叫层次遍历, 然后才谈得上算法。 层次遍历, 对某一层的结点访问完后, 再按照他们的访问次序对各个结点的左、 右孩子顺序访问, 这样一层一层进行, 先访问的结点其左右孩子也要先访问, 这样与队列的操作原则比较吻合。 因此, 层次遍历算法采用环形队列 qu 来实现的。 大家脑海中现在请浮现出一个 3 层的满二叉树, 然后大家对他进行层次遍历, 关注的是第二层和第三层。 第二层有 2 个结点, 第三层有 4 个结点。 下面大家想, 第二层左边的那个结点在第二层中是最先被访问的, 第三层最左边的那个结点是第三层中最先被访问的。到这, 我们应该有这么一种感觉, 那就是每一层最左边的这个结点好像是被访问完之后就被排到了队尾, 然后等队列前面的元素(这里就是指某一层中的其余结点) 出队(被访问) , 这样到了下一层的时候, 它又是第一个被访问了。 我想说到这, 大家都会想得到要用环形队列了吧。 2. 层次遍历的过程: 先将根结点进队, 在队不空时循环, 从队列中出列一个结点*p, 访问它; 若它有左孩子结点, 将左孩子结点进队; 若它有右孩子结点, 将右孩子结点进队。如此操作, 直到队空为止。 void LevelOrder (BTNode *b) { BTNode *p; BTNode *qu[MaxSize]; // 定义环形队列, 存放结点指针 int front, rear; // 定义队头和队尾指针 front=rear=-1; // 置队列为空队列 rear++; qu[rear]=b; // 根结点指针进入队列 while (front ! = rear) // 队列不为空时 { front=(front+1) % MaxSize; p=qu[front]; // 队头元素出队列 printf ("%c ", p->data) ; // 访问...