树,是一种常用到的数据结构,可以用来模拟具有树状结构性质的数据集合.在树的结构中,每个节点包含一个节点的值和所有节点的列表.从图的观点来看,树也可以看作是一个由N个节点和N-1条边组成的有向无环图。
在树中,应用最广泛的是二叉树.正如名字中所描述的一样,二叉树的每个节点最多由两个节点(子树结构),习惯上成为左子树和右子树.
二叉树的数据结构一般使用结构体来进行描述
struct TreeNode {
ValueType value; // 当前节点的值
struct TreeNode * left; // 左节点
struct TreeNode *right; // 右节点
};
这样在初始化一个节点时,就需要使用:
struct TreeNode *node = malloc(sizeof(struct TreeNode *));
node->value = ...;
node->left= ...;
node->right = ...;
更多的时候为了书写方便,会使用关键字typedef进行简化:
typedef struct TreeNode TreeNode;
这样就可以将初始化节点的操作简化为:
TreeNode *node = malloc(sizeof(TreeNode *));
node->value = ...;
node->left= ...;
node->right = ...;
二叉树的遍历是使用最广泛的的操作之一,最常使用到的遍历顺序有四种:
对于下图中的二叉树,
先序遍历的过程:
中序遍历过程:
后序遍历的过程:
中序和后序遍历的顺序大致相同,只是遍历根节点的顺序不一样,由此也可发现其实所谓的先序,中序,后序只是在遍历过程中根节点的访问顺序不同而已,习惯上称这三种方式为根序遍历.
树的节点结构具有相似性,因此在遍历的过程中经常采用循环和递归的方式来实现遍历.
先序遍历是按照根节点,左子树和右子树的顺序访问所有节点。由于二叉树遍历的定义所具有的递归特性,在遍历过程中可常使用递归和循环来进行树的遍历.
递归实现先序遍历实现大概这个样子:
void preorderTraversal(struct TreeNode *root) {
if (!root) {
// 递归结束条件
return;
}
// 访问当前根节点
printf("%d", root->value);
// 访问左子树
preorderTraversal(root->left);
// 访问右子树
preorderTraversal(root->right);
}
同理可以使用递归实现中序遍历和后续遍历:
// 中序遍历
void inorderTraversal(struct TreeNode *root) {
if (!root) {
// 递归结束条件
return;
}
// 访问左子树
preorderTraversal(root->left);
// 访问当前根节点
printf("%d", root->value);
// 访问右子树
preorderTraversal(root->right);
}
// 后序遍历
void postorderTraversal(struct TreeNode *root) {
if (!root) {
// 递归结束条件
return;
}
// 访问左子树
preorderTraversal(root->left);
// 访问右子树
preorderTraversal(root->right);
// 访问当前根节点
printf("%d", root->value);
}
除了递归之外,还可以使用循环的方式来实现二叉树的先序遍历:
void preorderTraversal(TreeNode *root) {
// 可以使用数组,栈,双向链表或者其他自定义结构来存储尚未遍历右子树的节点
TreeNode *nodes[10000] = {0};
int index = -1;
TreeNode *current = root;
// 只要数组不为空或者当前节点不为空则循环
while (index != -1 || current) {
if (current) {
// 先访问根节点
printf("%d", current->value);
nodes[++index] = current;
// 然后左节点
current = current->left;
} else {
// 最后是右节点
current = nodes[index--]->right;
}
}
}
中序遍历:
// 中序遍历
void inorderTraversal(TreeNode *root) {
TreeNode *nodes[1000] = {0};
int top = -1;
TreeNode *treeNode = root;
while (treeNode || top != -1) {
// 获取所有的左子树节点
while (treeNode) {
nodes[++top] = treeNode;
treeNode = treeNode->left;
}
// 每次获取栈顶元素
treeNode = nodes[top--];
printf("%d", treeNode->value);
if (treeNode->right) {
treeNode = treeNode->right;
} else {
// 防止遍历根节点时循环
treeNode = NULL;
}
}
}
对于后序遍历会比较麻烦一点:
void postorderTraversal(TreeNode *root) {
TreeNode *treeNode = root;
TreeNode *lastVisit = NULL;
TreeNode *nodes[1000] = {0};
int top = -1;
while (treeNode || top != -1) {
while (treeNode) {
nodes[++top] = treeNode;
treeNode = treeNode->left;
}
// 获取栈顶元素
treeNode = nodes[top--];
if (treeNode->right && treeNode->right != lastVisit) {
nodes[++top] = treeNode;
treeNode = treeNode->right;
} else {
printf("%d", treeNode->value);
lastVisit = treeNode;
treeNode = NULL;
}
}
}
看起来这样是实现了节点的先序访问,但是却没有对访问的节点序列进行保存,就没有办法继续访问遍历结果或者对遍历结果做进一步的操作.
为了解决这一问题,常用的操作是添加一个数组来保存遍历的结果序列.由于事先不太可能提前准确预判节点的数量,所以对于这个需要保存结果序列的数组,有两种常用的初始化方式:
void subpreorderTraversal (struct TreeNode* root, int *result, int *returnSize) {
if (!root) {
return;
}
result[*returnSize] = root->value;
(*returnSize) += 1;
subpreorderTraversal(root->left, result, returnSize);
subpreorderTraversal(root->right, result, returnSize);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
*returnSize = 0;
int *result = malloc(sizeof(int) * 10000); // 这里初始化了一个可以保存10000个整数的数组
subpreorderTraversal(root, result, returnSize);
return result;
}
// 定义一个数组结构
struct TreeNodeArray {
int size; // 当前数组的最大容量
int current; // 下一个将要保存的元素的索引
int *values; // 数组的指针
};
// 初始化数组结构
struct TreeNodeArray *init() {
struct TreeNodeArray *arr = malloc(sizeof(struct TreeNodeArray *));
arr->current = 0;
arr->size = 4;
arr->values = malloc(sizeof(int) * arr->size);
return arr;
}
// 使用该函数来保存节点
void save(struct TreeNodeArray *array, int value) {
if (array->current >= array->size) {
// 当数组容量不足时进行扩容
array->size *= 2;
// 注意这里使用的都是字节,所以要使用 size(数据类型) * 数据个数 的形式
array->values = realloc(array->values, sizeof(int) * array->size);
}
array->values[array->current] = value;
array->current += 1;
}
这样就可以使用save方法进行遍历结果的保存:
void preorderTraversal(TreeNode *root, struct TreeNodeArray *arr) {
if (!root) {
return;
}
save(arr, root->value);
preOrder(root->left, arr);
preOrder(root->right, arr);
}
对比以上两种方式就会发现:
第一种方式简单粗暴的增大存储空间,这样就可以直接直接进行数据的存储节约了存取的时间,而第二种方式需要在存取数据时频繁地进行扩容,通过精细化的扩容算法可以适当地减少内存的使用. 不过一般在对于内存要求不高的情况下,更加倾向于使用暴力开辟内存的方法来进行数据存取.