定义
之前四篇博客分别介绍了线性结构中的[顺序表](/222)、[链表](/223)、[栈](/225)、[队列](/226)。从难度来讲,顺序表到链表是递增的。从实现来讲,栈和队列基于顺序表和链表(之前栈采用了顺序表的存储结构,队列采用了链表的存储结构)。此次介绍的二叉树虽是非线性结构的树形结构分支,但在其各个结点遍历的实现上,使用到了栈和队列的特性。
二叉树是一种特殊的线性结构,每个结点最多只有两个分支,称左孩子结点和右孩子结点。更多关于二叉树的特性,自行查阅资料。接下来只详细的介绍创建二叉树以及二叉树的遍历。
实现
定义结构
1 2 3 4 5 6 7
| typedef char TreeType;
typedef struct BitNode { TreeType key; struct BitNode *left; struct BitNode *right; } BitTree;
|
二叉树的结构和双向链表的结构一致,只是双向链表的两个指针构成线性结构,二叉树的两个指针构成非线性结构。
定义操作
构造空二叉树
1 2 3 4 5 6 7
|
void initBitTree(BitTree& root) { root.left = NULL; root.right = NULL; }
|
将根结点的左右两个指针置空。
创建二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
void createBitTree(BitTree** parent) { char key = getchar(); getchar(); if (key == '#') { *parent = NULL; return; } else { *parent = (BitTree*) malloc(sizeof(BitTree)); if (*parent == NULL) exit(9); (*parent)->key = key; createBitTree(&((*parent)->left)); createBitTree(&((*parent)->right)); } }
|
先序遍历的方式构建二叉树,输入#号表示当前结点的左孩子结点或右孩子结点为空。
递归先序遍历
1 2 3 4 5 6 7 8 9 10
|
void preOrderTraverse(BitTree* parent) { if (parent != NULL) { visit(*parent); preOrderTraverse(parent->left); preOrderTraverse(parent->right); } }
|
先访问根结点,再依次递归访问左子树和右子树
递归中序遍历
1 2 3 4 5 6 7 8 9 10
|
void inOrderTraverse(BitTree* parent) { if (parent != NULL) { inOrderTraverse(parent->left); visit(*parent); inOrderTraverse(parent->right); } }
|
先递归遍历左子树,再访问根节点,最后递归访问右子树。
递归后序遍历
1 2 3 4 5 6 7 8 9 10
|
void postOrderTraverse(BitTree* parent) { if (parent != NULL) { postOrderTraverse(parent->left); postOrderTraverse(parent->right); visit(*parent); } }
|
先依次递归访问左子树和右子树,最后访问根结点。
*三序遍历的递归方式简单的介绍到这里,三序遍历的非递归方式一个比一个难,这是本人自行思考写出的算法,若说阅读了参考资料那也是很久之前的事情。因此觉得,这三段代码还是很有阅读价值。
非递归先序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
void preOrderTraverseNormal(BitTree* parent) { Stack stack; if (initStack(stack) == 0) return; if (parent == NULL) return; push(stack, *parent); BitTree topNode; while (pop(stack, topNode)) { visit (node); if (topNode.right != NULL) { push(stack, *(topNode.right)); } if (topNode.left != NULL) { push(stack, *(topNode.left)); } } }
|
非递归遍历的重点是手动构造递归栈。首先将根结点入栈,然后在while循环中,先将栈顶结点出栈,并依次将该结点的右孩子结点和左孩子结点入栈(如果存在),知道栈为空pop函数返回0 结束循环。
非递归中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
void inOrderTraverseNormal(BitTree* parent) { Stack stack; if (initStack(stack) == 0) return; if (parent == NULL) return; push(stack, *parent); BitTree topNode; while (top(stack, topNode)) {
while (topNode.left != NULL) { push(stack, *(topNode.left)); topNode = *(topNode.left); }
int flag = 1; while (flag && topNode.right == NULL) { pop(stack, topNode); visit(topNode); flag = top(stack, topNode); }
if (pop(stack, topNode)) { visit(topNode); push(stack, *(topNode.right)); } } }
|
先将根结点入栈,然后根据根结点一直寻找到该左子树的最左边结点,访问该结点。如果该结点不存在右子树,直接将该结点出栈,并一直出栈到栈顶的结点存在右子树。此时将栈顶结点出栈,并将该结点的右孩子结点入栈,并寻找到该结点右子树的最左边结点。
非递归后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
void postOrderTraverseNormal(BitTree* parent) {
Stack stack, backup; if (initStack(stack) == 0) return; if (initStack(backup) == 0) return; if (parent == NULL) return; push(stack, *parent); BitTree topNode; BitTree backupNode; BitTree lastNode; while (top(stack, topNode)) { int flag = top(backup, backupNode); if (flag == 0 || compareTreeNode(topNode, backupNode) == 0) { if (topNode.left != NULL && compareTreeNode(*(topNode.left), lastNode) == 0) { push(stack, *(topNode.left)); continue; } push(backup, topNode); if (topNode.right != NULL && compareTreeNode(*(topNode.right), lastNode) == 0) { push(stack, *(topNode.right)); continue; } } else { pop(backup, backupNode); pop(stack, topNode); visit(topNode); lastNode = topNode; } } }
|
考虑到后续遍历的特殊性质,根结点会在左孩子结点和右孩子结点出栈时访问两次。多数资料上的实现方式都是通过在每个结点中添加一个标志记录根节点的访问次数。为了维护之前定义好的结构体的完整性。用一个备用栈,完美的解决问题。
取递归栈中的栈顶元素和备用栈中的栈顶元素(如果存在)对比,如果相同,就是第二次遍历到该结点。分别将递归栈和备用栈中的栈顶元素出栈,访问并记录当前出栈的结点。如果不相同,就是第一次访问该结点,此时需要考虑当前递归栈中栈顶结点是否存在左子树或右子树以及上次出栈的结点是否是该结点的左孩子结点或右孩子结点。
当该结点不存在左子树或上次出栈的结点是该结点的左孩子结点,则标记当前递归栈中的栈顶结点已经访问过一次,将该结点添加到备用栈中。
说了这么多,有点绕。经过长时间的思考以及两次优化之后的成果。越是难懂的算法不一定就是最高级的算法,此处 ,不对我的代码做任何评价。
层次遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
void levelOrderTraverse(BitTree* parent) { Queue queue = createQueue(); enterQueue(queue, *parent); BitTree node; while (exitQueue(queue, node)) { visit(child); if (node.left != 0) enterQueue(queue, *node.left); if (node.right != 0) enterQueue(queue, *node.right); } }
|
看完前面三个非递归的遍历算法,也许都晕了。层次遍历,没有递归或非递归而言,自顶向下、从左到右访问二叉树中所有的结点。先访问的结点子树上的全部结点一定比后访问结点子树上的全部结点先访问,所以层次遍历用到了队的特性。
访问结点
1 2 3 4 5 6
|
void visit(BitTree node) { printf("%c", node.key); }
|
这只是模拟访问结点的操作,可根据需要自定定义该函数的功能。
上述代码中用到的栈和队列中的函数,都是复用了之前博客中介绍的栈和队列的操作函数。只是修改下每个元素的结点类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #ifndef STACK_H_ #define STACK_H_
#include "tree.h"
#define LIST_INIT_SIZE 10 #define LISTINCREMENT 2
typedef BitTree StackType;
typedef struct stackNode { StackType *elem; int length; int listsize; }Stack;
……
#ifndef QUEUE_H_ #define QUEUE_H_
#include "tree.h"
typedef BitTree QueueType;
struct LinkQueue { QueueType key; struct LinkQueue *next; };
typedef struct queueNode { struct LinkQueue *head; struct LinkQueue *end; } Queue;
……
|
最后,附上头文件的定义,部分方法未实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
|
#include "stack.h" #include "queue.h"
#ifndef TREE_H_ #define TREE_H_
typedef char TreeType;
typedef struct BitNode { TreeType key; struct BitNode *left; struct BitNode *right; } BitTree;
void initBitTree(BitTree&);
void destoryBitTree(BitTree&);
void createBitTree(BitTree**);
void clearBitTree(BitTree&);
void preOrderTraverse(BitTree*);
void preOrderTraverseNormal(BitTree*);
void inOrderTraverse(BitTree*);
void inOrderTraverseNormal(BitTree*);
void postOrderTraverse(BitTree*);
void postOrderTraverseNormal(BitTree*);
void levelOrderTraverse(BitTree*);
int compareTreeNode(BitTree, BitTree);
void visit(BitTree);
#endif
|