树型结构
前言
树型结构在生活中是非常常见的一种结构,应用范围很广,就用一个简单的例子来说。计算机中的文件目录就是一个树型结构,一般创建一个文件,如果文件中没有文件那么就相当于一个空树,如果里面有文件,就相当于这个文件的子树,以此类推,就形成了树型结构的文件目录。
在学习中我们主要学习二叉树的一些特性,把树细化学习。
二叉树的概念
二叉树是结点的有限集合,该集合或者为空,或者由一个
根节点加上两颗对称的左子树和右子树的二叉树组成。
二叉树的每个结点上最多有两个子树
二叉树有五种形态,及只有根结点,空树,只有左子树,只有右子树,左右子树都有。
二叉树的分类
二叉树有完全二叉树和满二叉树两种
- 满二叉树是所有分支节点都存在左右子树,并且叶子结点在同一层上。
完全二叉树是具有N个结点的二叉树的结构与满二叉树前N个结点的结构相同
怎么理解呢?
左图为满二叉树,有图为完全二叉树。二叉树性质
二叉树最大结点数:若规定只有一个根结点的时候,深度为1,那么深度为K的树,最大结点数为 (2^K) -1 (K>=0)
二叉树的第i层上的结点数:若规定只有一个根结点的时候,层数为1,那么一颗非空树的第i层最多有 2^(i-1) (i>0)
叶子结点和非叶子结点的关系:对于任何一颗二叉树,如果叶子结点个数为N0,度为2的非叶子结点个数为N1,则有 N1 = N0 + 1;
关于完全二叉树的深度:具有N结点的二叉树的的深度K为log2(n+1)上取整数
顺序存出中父结点和子结点的关系:子结点要找到对应的父结点,(i - 1)/2;父找到左孩子i*2 + 1;找到右孩子i*2 + 1;
二叉树的创建
创建一颗二叉树,以顺序表,通过先序遍历的方式还原一颗二叉树(顺序表中必须有标识NULL的特殊字符存在)
具体思路是采用递归的方式,用一个index来记录树创建过程中创建到数组中的哪个位置,递归的用先序遍历的方式去创建。
具体代码:// 创建一个结点 TreeNode* CreateTreeNode(TreeType value) { TreeNode* new_node = (TreeNode*)malloc(sizeof(TreeNode)); // 用assert来判断new_node空间是否开辟成功。 // 在这里我们直接让程序挂掉。(不同场景处理方式不同) assert(new_node); new_node->data = value; new_node->lchild = NULL; new_node->rchild = NULL; return new_node; } // 二叉树递归体,递归的建立一颗二叉树 TreeNode* _CreateTree(TreeType arr[], size_t size, int* index, TreeType nulltype) { if (index == NULL || size < 1) { return NULL; } if (arr[*index] == nulltype) { return NULL; } // 先创建根结点 TreeNode* root = Create Tree Node(TreeType value); ++(*index); // 递归的创建左子树 TreeNode* root->lchild = _CreateTree(arr, size, index, nulltype); ++(*index); // 递归的创建右子树 TreeNode* root->rchild = _CreatTree(arr,size, index, nulltype); return root; } // 函数主体 TreeNode* CreateTree(TreeType arr[], size_t size, TreeType nulltype) { if (size < 1) { return NULL; } // 用来记录创建到数组中的哪个元素上 int index = 0; return _CreateTree(arr, size, &index, nulltype); }
二叉树的遍历
创建好二叉树后,我们访问二叉树中的某个结点,就必须经过遍历。
我们实现一下,二叉树的递归版的前、中、后序遍历和非递归版的前、中、后遍历。
递归版
先序遍历(前序遍历)
void PreOrder(TreeNode* root) { if (root == NULL) { return; } // 先打印根结点 printf("%c ", root->data); // 递归的打印左右子树 PreOrder(root->lchild); PreOrder(root->rchild); }
中序遍历
void InOrder(TreeNode* root) { if (root == NULL) { return; } // 先递归的找到最左边的孩子 InOrder(root->lchild); // 打印最左边的孩子,递归栈出栈打印根结点 printf("%c ", root->data); // 递归进右子树 InOrder(root->rchild); }
后续遍历
void PostOrder(TreeNode* root) { if (root == NULL) { return; } // 先递归,找最左的孩子,打印,然后递归栈出栈,再进行最左子树的右子树进行递归 PreOrder(root->lchild); PreOrder(root->rchild); printf("%c ", root->data); }
非递归版
注意:这里我们在写非递归版,直接用顺序栈的函数接口,不做栈的实现。
如果不知道栈的实现,戳这里栈的实现前序遍历
前序遍历非递归采用手动栈来进行操作,栈的特性是先入后出,取栈顶元素,所以我们抓住这两个特性。 前序遍历,是根左右顺序遍历。
1)先让根结点入栈,取栈顶元素打印,成功进(2),失败退出循环。
2)进行出栈
3)再进行入右孩子,入左孩子。
就这样循环,直到栈为空,也就是取栈顶失败。遍历完成。
代码如下:void PreOrederByLoop(TreeNode* root) { if (root == NULL) { return; } // 创建一个栈。 SeqStack stack; // 初始化一个栈(用C语言实现的栈) InitStack(&stack); // 先入根结点 PushStack(&stack, root); TreeNode* top = NULL; while (FindTopStack(&stack, &top) { printf("%c ", top->data); PopStack(&stack); // 先入右孩子,后入左孩子 if (top->rchild != NULL) { PushStack(&stack, top->rchild); } if (top-lchild != NULL) { PushStack(&stack, top->lchild); } } }
中序遍历
中序遍历,是采用手动栈。将函数放入一个while(1)的循环中。
1)先循环的去从根结点到最左孩子的入栈。
2)进行取栈顶元素,并判断是否取栈顶元素成功,
3)如果失败break,退出while(1)训话。如果成功,打印栈顶元素,并出栈
4)进行右孩子的判断是否为空,如果不为空,进行让循环指针指向取栈顶元素的右孩子。
代码:
void InOrderByLoop(TreeNode* root)
{
if (root == NULL)
{
// 非法输入
return;
}
SeqStack Stack;
InitStack(&stack);
TreeNode* cur = root;
while (1)
{
// 从根结点到最左边的孩子,并逐一入栈
while (cur != NULL)
{
PushStack(&stack, cur);
cur = cur->lchild;
}
// 取栈顶元素
TreeNode* top = NULL;
FindTopStack(&stack, &top);
if (top == NULL)
{
// 取失败,退出循环。因为因为已经遍历完了。
break;
}
// 打印并出栈。
printf("%c ", top->data);
PopStack(&stack);
// 让cur尝试的去找最左孩子的右孩子。
cur = top->rchild;
}
}
- 后序遍历
后序遍历,借助手动创建的栈,采用while(1)循环作为大的循环条件。
1)循环的从根结点到最左孩子,并且逐一入栈。
2)取栈顶元素。如果取失败就break;
3)判断是否存在最左孩子是否存在右孩子。还要判断右孩子是否等于上一个取栈顶的元素,这样做是防止重复遍历。
4)如果不存在右孩子或者右孩子不等于上次取栈顶元素,那么可以打印并且出栈。
5)否则就让cur 指向栈顶元素的右孩子
代码:
void PostOrder(TreeNode* root)
{
if (root == NULL)
{
// 非法输入
return;
}
SeqStack satck;
InitStack(&stack);
// 用来记录上一个top的元素
TreeNode* pre = NULL;
TreeNode* cur = root;
while (1)
{
// 循环从根到最左孩子的入栈
while (cur != NULL)
{
PushStack(&stack, cur);
cur = cur->lchild;
}
// 取栈顶元素
TreeNode* top = NULL;
FindTopStack(&stack, &top);
if (top == NULL)
{
// 取失败退出,说明遍历结束
break;
}
// 满足个两个条件中的一个就可以打印并且出栈。
if (top->rchild == NULL || top->rchild == pre)
{
printf("%c ", top->data);
PopStack(&stack);
pre = top;
}
else
{
// 存在右孩子,继续入右孩子。
cur = top->rchild;
}
}
}
- 层序遍历
对于层序遍历,也是一个很重要的点,需要掌握。层序遍历就是,从上到下,从左到右依次的进行遍历。那么前面我们说的,前中后序遍历,都是借助栈的先进后出,可以访问栈顶元素的特点来进行的遍历。然而对于层序遍历用队列的先进先出,访问队首的方式遍历。对队列不熟悉的可以戳这里链式队列的实现,循环顺序队列的实现
1)先让根结点入队。
2)以取队守元素为条件进行循环
3)打印并出队
4)判断是否左孩子为空,不为空就让左孩子入队,
5)再判断右孩子是否为空,不为空就让右孩子入队
代码:
void LevelOrder(TreeNode* root)
{
if (root == NULL)
{
// 非法判断
return;
}
SeqQueue queue;
InitQueue(&queue);
PushQueue(&queue, root);
// 取栈顶元素,并且取队守元素是否为空。
TreeNode* head = NULL;
while (FindHead(&stack, &head))
{
// 打印并且出队
printf("%c ", head->data);
PopQueue(&queue);
if (head->lchild != NULL)
{
PushQueue(&queue, head->lchild);
}
if (head->rchild != NULL)
{
PushQueue(&queue, head->rchild);
}
}
}
判断是否是完全二叉树
判断是否是完全二叉树,更多的是在层序遍历的基础上变化而来,那么我们也需要借助队这个数据结构来进行。判断完全二叉树的依据就是在满二叉树的下一层,从左到右,要么只有左孩子,要么左右都有,不能只有右没有左。
1)我们需要创建队列,进行初始化,将root结点入队
2)取队首元素,判断是否为NULL,如果为NULL,直接break。
3)出队。入左孩子和右孩子。
4)循环判断队列是否大于0
如果是,就让原来的队列出队,并且取队首元素,判断是否为空,是就返回0。不是就继续
如果不是就返回1表示是完全二叉树
代码实现:int IsCompleteTree(TreeNode* root) { if (root == NULL) { // 非法判断 return 0; } SeqQueue queue; InitQueue(&queue); // 用来取队首元素 TreeNode* head = NULL; PushQueue(&queue, root); while (SizeQueue(&queue) > 0) { FindHeadQueue(&queue, head); if (head == NULL) { break; } PopQueue(&queue); PushQueue(&queue, head->lchild); PushQueue(&queue, head->rchild); } while (SizeQueue(&queue) > 0) { // 当上面循环跳出,进入本循环,队首元素为NULL先出队后取 PopQueue(&queue); FindHeadQueue(&queue, &head); if (head != NULL) { reuturn 0; } } return 1; }
以上为二叉树基础知识整理。