树是数据结构里面比较复杂的一种结构,很多科班出身的程序员都不一定搞得清楚其具体的结构与原理,本人本科使用的是严的《数据结构》,大二时开始学习的,当时编程能力尚处在起步阶段,对于书中一些数据结构和算法没有能力切切实实的实现。然而,作为一名合格的程序员,有些知识是我们应该具备的。有人说,我不会栈,队列,更不会任何一种树,不也是好好的当我的程序员吗?道理很简单,书到用时方恨少,未雨绸缪。如果你理解了一些树的原理和用途,说不定会给你遇到的问题带来另外一种更好的解决方式。
栈结构,队列结构,树结构,栈和队列比较简单一些,树结构比较复杂,而且树的类型也很多,变种更多,如BST(二叉搜索树),AVL(自平衡二叉搜索)树,RB-Tree(红黑树),B-Tree(多路搜索树),R-Tree(索引树)等,要想弄明白这些树结构的实现原理和操作方法,我们首先需要掌握理解一些树的基本知识,如建树过程,树的各种遍历,树的遍历包括先根遍历(DLR),中根遍历(LDR),后根遍历(LRD)(有的叫前序、中序、后序遍历,但是不如先根,中根,后根更能表达出遍历的规则),广度优先搜索,深度优先搜索。
下面本文就以分析代码的形式来理解掌握一棵树的建立过程,以及用先根、中根、后根方式遍历一颗树。提到树的遍历,也许很多人都会想到递归,递归使得代码非常的简洁,然而对于我们理解学习原理来说不太适合,非递归方式虽然代码繁多,但是能给人更加直观的感受。本文将会给出递归实现和非递归实现的两种版本。非递归实现要借助栈结构,相比大多数人都知道栈就是LIFO(后进先出),对于理解本文代码就足够了,本文代码中使用的栈结构是自己实现的,而且尝试用纯C模仿一下C++的有函数的结构体,具体请参考“纯C模仿C++有函数的结构体”一文。
/*此头文件的引用以及头文件中函数的实现请查阅“从零学习Tree0—栈和队列”一文*/
/*BST.h*/
#ifndef __BST__H
#define __BST__H
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int RESULT;
#ifndef SUCCESS
#define SUCCESS 0
#endif
#ifndef FAILURE
#define FAILURE -1
#endif
typedef struct _Node
{
struct _Node* plc; /*左字结点指针*/
struct _Node* prc; /*右子结点指针*/
int key; /*key字段*/
int val; /*val字段*/
int cnt; /*计数*/
}Node;
#define ELEM_TYPE Node /* 堆栈所存储的值的数据类型 */
/*建树 插入节点*/
RESULT insert(Node** root, int key, int val);
/*递归遍历*/
void DLR(const Node* root);/*先根*/
void LDR(const Node* root);/*中根*/
void LRD(const Node* root);/*后根*/
/*非递归遍历*/
void _DLR(Node* root);/*先根遍历*/
void _LDR(Node* root);/*中根遍历*/
void _LRD(Node* root);/*后根遍历*/
/*非递归广度优先搜索*/
void BFS(Node* root);
/*非递归深度优先遍历*/
void DFS(Node* root);
#endif//__BST__H
/*BST.c*/
#include “BST.h”
#include "stack.h"
/*创建节点*/
Node* createnode(int key,int val)
{
Node* node = (Node*)malloc(sizeof(Node));
node->key = key;
node->val = val;
node->cnt = 1;
node->plc = NULL;
node->prc = NULL;
return node;
}
/*插入节点*/
RESULT insert(Node** root, int key, int val)
{
Node* node = createnode(key, val);
if (NULL == *root)
{
if (NULL == node)
{
perror("createnode error!!!\n");
return FAILURE;
}
*root = node;
}
else
{
Node* tmp = *root;
while (1)
{
if (key == tmp->key)
{
tmp->cnt++;
return SUCCESS;
}
else if (key < tmp->key)
{
if (tmp->plc == NULL)
{
tmp->plc = node;
return SUCCESS;
}
tmp = tmp->plc;
continue;
}
else
{
if (tmp->prc == NULL)
{
tmp->prc = node;
return SUCCESS;
}
tmp = tmp->prc;
continue;
}
}
}
return SUCCESS;
}
/*递归遍历*/
void DLR(const Node* root)
{
if (root != NULL)
{
printf("%d\t", root->val);
DLR(root->plc);
DLR(root->prc);
}
}
void LDR(const Node* root)
{
if ( root != NULL )
{
LDR( root->plc );
printf("%d\t",root->val);
LDR( root->prc );
}
}
void LRD(const Node* root)
{
if (root != NULL)
{
LRD(root->plc);
LRD(root->prc);
printf("%d\t", root->val);
}
}
/*非递归遍历*/
void _DLR(Node* root)
{
Node* tmp = root;
Stack stack;
registstack(&stack);
stack.init_size(&stack,1000);
if (NULL == root)
{
printf(" empty tree!!!\n ");
return;
}
//
while (tmp || !stack.is_empty(&stack))
{
while (tmp)/*将根节点,根节点的左子节点,左子节点的左子节点,一直到最左子节点,全部入栈*/
{
stack.push(&stack, tmp);
printf( "%d\t",tmp->val );/*入栈是输出当前结点的val*/
tmp = tmp->plc;
}
tmp = stack.top(&stack);/*获取栈顶的结点,栈顶结点是没有左子树(最左子节点)或者左子树已经遍历完毕的结点*/
stack.pop(&stack);/*清除栈顶元素*/
tmp = tmp->prc;/*遍历当前栈顶元素的右子树*/
}
stack.destroy(&stack);
}
/*中序遍历和前序遍历的过程是一样的,只是访问结点val的时机不一样
最左子节点:访问时一直向左访问,知道不能向左访问时,此时的结点就是最左子节点*/
void _LDR( Node* root )
{
Node* tmp = root;
Stack stack;
registstack(&stack);
stack.init_size(&stack, 1000);
if (NULL == root)
{
printf(" empty tree!!!\n ");
return;
}
while (tmp || !stack.is_empty(&stack))
{
while (tmp)
{
stack.push(&stack, tmp);
tmp = tmp->plc;
}
tmp = stack.top(&stack);
stack.pop(&stack);
printf("%d\t", tmp->val);/*第一次输出的肯定是最左子节点,此时最左子节点要么是叶子节点,要么是只有右子树的结点*/
tmp = tmp->prc;
}
stack.destroy(&stack);
}
void _LRD( Node* root )
{
int flag[1000];/*标记结点是否已经被访问过*/
Stack stack;
registstack(&stack);
stack.init_size(&stack, 1000);
if (NULL == root)
{
printf(" empty tree!!!\n ");
return;
}
Node* tmp = root;
/*将所有的左子节点入栈*/
while (tmp)
{
stack.push(&stack,tmp);
flag[stack.size] = 0;
tmp = tmp->plc;
}
/*循环处理栈内结点*/
while (!stack.is_empty(&stack))
{
tmp = stack.top(&stack);/*取栈顶结点,第一次被取出来的肯定是最左子节点*/
while ( tmp->prc && flag[stack.size] == 0) //如果最左子节点有右子节点并且当前节点的右子节点尚未被访问*/
{
flag[stack.size] = 1;/*标记为1说明当前结点的右子树开始被访问*/
tmp = tmp->prc;
while (tmp)/*将当前结点的右子树的全部左子节点入栈,*/
{
stack.push(&stack,tmp);
flag[stack.size] = 0;
tmp = tmp->plc;
}
tmp = stack.top(&stack);
}
tmp = stack.top(&stack);
printf("%d\t",tmp->val);
stack.pop(&stack);
}
stack.destroy(&stack);
}
/*test.c*/
#include “BST.h”
#include "stack.h"
int main()
{
Node* root = NULL;
int array[9] = {20,15,13,14,16,28,30,22,25};
int index = 0;
do{
insert(&root, array[index], array[index]);
} while (++index < 9);
printf(" \n前序遍历(DLR)\n ");
DLR(root);
printf(" \n非递归前序遍历(DLR)\n ");
_DLR(root);
printf(" \n中序遍历(LDR)\n ");
LDR(root);
printf(" \n非递归中序遍历(LDR)\n ");
_LDR(root);
printf(" \n后序遍历(LRD)\n ");
LRD(root);
printf(" \n非递归后序遍历(LRD)\n ");
_LRD(root);
printf("\n");
return 0;
}
打印出来结果,我们发现,中根遍历输出结果是从小到大的顺序,造成这种结果的原因就是我们一开始建的是一颗BST(二叉搜索树),所以中根遍历才是有序序列。对于BST,后续的篇章我们将介绍其特点和一些其他操作,比如删除节点操作。
代码只是为了更好的说明先根、中根、后根遍历的过程所用,错误之处请不吝指正。