【数据结构】堆和二叉树详解(下)
前言:上期我们介绍了一些二叉树的概念,以及详细介绍了了堆的概念结构和实现,这期我们就着重来详细介绍二叉树的实现。
上一篇文章的在这:堆和二叉树(上)
本篇文章的笔记在这:笔记
文章目录
- 一,实现链式结构的二叉树
- 二,二叉树的前中后序遍历
- 三,二叉树的结点个数、查找、高度、销毁
- 1,二叉树的结点个数
- 2,二叉树求叶子结点的个数
- 3,二叉树求第K层结点的个数
- 4,二叉树的高度/深度
- 5,二叉树的查找
- 6,二叉树的销毁
- 四,二叉树的层序遍历
- 五, 判断是否为完全二叉树
- 七,二叉树选择题
一,实现链式结构的二叉树
上一期我们在实现堆的时候实现的是顺序结构的二叉树,下面我们就来实现一下链式结构的二叉树。链式结构就是底层是一个链表,这个链表与单链表不同,含有两个指针域和一个数据域,左右指针分别指向左右孩子,数据域用来存储数据。
下面我们就来手动创建一个二叉树。
<定义二叉树的结构>
typedef char BTDataType;
typedef struct BinaryTreeNode
{BTDataType Data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;<创建一个申请结点的函数>
//开辟结点空间的函数
BTNode* BuyNode(char x)
{BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL){perror("malloc fail!");exit(1);}//开辟成功 初始化、node->Data = x;node->left = node->right = NULL;//左孩子和右孩子都为空 为根结点//返回根结点return node;
}<接下来就是创建多个结点将它们链接起来>
BTNode* CreatBinaryTree()
{BTNode* nodeA = BuyNode('A');BTNode* nodeB = BuyNode('B');BTNode* nodeC = BuyNode('C');BTNode* nodeD = BuyNode('D');BTNode* nodeE = BuyNode('E');BTNode* nodeF = BuyNode('F');nodeA->left = nodeB;nodeA->right = nodeC;nodeB->left = nodeD;nodeC->left = nodeE;nodeC->right = nodeF;return nodeA;}
这样我们就创建好了一个二叉树。
再来回顾一下二叉树的概念:
二叉树分为空树和非空二叉树,非空二叉树由根结点、根结点的左子树、根结点的右子树组成的。而根结点的左右子树又是由子树结点,子树结点的左子树,子树结点的右子树构成的,因此二叉树的定义是按照递归来定义的。
前面说了,二叉树的插入是没有意义的,在数据结构初阶的二叉树与C++的二叉搜索树有些不同,C++的二叉搜索树的子树是有一定的要求的所以插入删除是有意义的。
而这里的二叉树是没有限制的,所以无意义。
那就会有人问既然没有意义那为什么还要学习它呢?
在数据结构初阶二叉树的学习相较于其他的数据结构有些不一样,二叉树虽然没有插入删除的方法,但是还有遍历,查找等方法是值得学习的。
二,二叉树的前中后序遍历
学习二叉树就离不开树遍历的操作,所以我们要重点来学习。
先来看看遍历规则:
1)前序遍历(Preorder Traversal 亦称先序遍历):访问根结点的操作发⽣在遍历其左右⼦树之前
访问顺序为:根结点、左⼦树、右⼦树
2)中序遍历(Inorder Traversal):访问根结点的操作发⽣在遍历其左右⼦树之中(间)
访问顺序为:左⼦树、根结点、右⼦树
3)后序遍历(Postorder Traversal):访问根结点的操作发⽣在遍历其左右⼦树之后
访问顺序为:左⼦树、右⼦树、根结点
遍历的时候谁谁在前面就先打印谁,比如前序遍历,根-左-右就先打印根,再打印根的左,再打印根的左,直到最后一个根结点的左孩子被打印完了就开始打印最后一个根结点的右孩子然后一直回归。从左边打印到右边。
//前序遍历 根-左-右
void preOrder(BTNode* root)
{//判断根结点是否为空if (root == NULL){printf("NULL ");return;}//不为空printf("%c ", root->Data);preOrder(root->left);preOrder(root->right);
}
中序遍历和后序遍历:
下面来看看递归分析图,下图如果实在看不清可以去看我后面发的笔记。
//中序遍历 左-根-右
void inOrder(BTNode* root)
{//判断根结点是否为空if (root == NULL){printf("NULL ");return;}//不为空preOrder(root->left);printf("%c ", root->Data);preOrder(root->right);
}//后序遍历 左-右-根
void postOrder(BTNode* root)
{//判断根结点是否为空if (root == NULL){printf("NULL ");return;}//不为空preOrder(root->left);preOrder(root->right);printf("%c ", root->Data);
}
紧接着我们来学习一下二叉树的其他操作:
三,二叉树的结点个数、查找、高度、销毁
1,二叉树的结点个数
我们知道如果是一个满二叉树有n层那么总结点个数可以直接算出来为 2^n-1
个结点。但如果是一个完全二叉树呢?我们就不能直接算出它有多少个结点了。下面我们就来看看如何得到二叉树结点的个数。
- 方法一,定义一个全局变量size每遍历一个结点就加加一下。
从上面的分析可以看到在第一次调用的时候是可行的,但是如果想要多次调用的话就会出问题,那有没有什么方法可以解决呢?来看看法二:
// ⼆叉树结点个数 版本1 存在问题 当多次调用的时候size会叠加
int size = 0;
int BinaryTreeSize(BTNode* root)
{//判断根结点是否为空if (root == NULL){//printf("NULL ");return 0;}//不为空size++;BinaryTreeSize(root->left);BinaryTreeSize(root->right);return size;
}
- 方法二,使用一个指针,让形参影响实参。
法二我们可以看到已经解决了法一存在的问题每次调用都是6,但每次调用函数之前都需要手动的将size置为0很麻烦其实也没有完全解决法一存在的问题,接下来我们就来看看法三。
//版本二 借助一个指针 通过形参的改变影响实参
int BinaryTreeSize2(BTNode* root, int* psize)
{if (root == NULL){return 0;}//注意优先级(*psize)++;BinaryTreeSize2(root->left,psize);BinaryTreeSize2(root->right,psize);return *psize;
}
3.方法三,递归
在前面回顾二叉树概念和定义的时候我们说过二叉树是按照递归定义的,所以与二叉树相关的操作都可以使用递归,这里递归依旧使用大化小的思想,逐一去找未知。
如果对递归还是不太熟悉的可以去看我之前讲递归的文章。
传送门:递归
//版本三 大化小递归 找未知
int BinaryTreeSize3(BTNode* root)
{//递归结束条件if (root == 0){return 0;}//结点个数=根节点+左子树结点总数+右子树结点总数return 1 + BinaryTreeSize3(root->left) +BinaryTreeSize3(root->right);
}
2,二叉树求叶子结点的个数
只要是求个数的都可以使用递归,与上面法三的思路一样递归都是大化小的思想。
// ⼆叉树叶⼦结点个数
int BinaryTreeLeafSize(BTNode* root)
{if (root == 0){return 0;}//根结点的左右孩子为空 则一定为叶子结点if (root->left == NULL && root->right == NULL){return 1;}//叶子结点=左叶子结点总数+右叶子结点总数return BinaryTreeLeafSize(root->left) +BinaryTreeLeafSize(root->right);
}
3,二叉树求第K层结点的个数
求个数用递归,大化小的思想,将第K层结点的个数传化成,左子树第K层结点的个数与右子树第K层结点个数的和。
代码逻辑:如果没有递归到第K层,就继续往下递归;递归到了第K层的是时候就看第K层的结点是否为空为空就说明没有结点返回0,不为空且当前层数为第K层时就返回一,之后往上回归就一直做一个累加的动作,直到累加到最上面第一个结点。
// ⼆叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{if (root == NULL){return 0;}if (k == 1){return 1;}return BinaryTreeLevelKSize(root->left,k-1) +BinaryTreeLevelKSize(root->right,k-1);
}
4,二叉树的高度/深度
二叉树的高度当然是要找左右子树中最长,最高的那一棵了,来看看思路:
//⼆叉树的深度/⾼度
int BinaryTreeDepth(BTNode* root)
{if (root == NULL){return 0;}//高度=根结点+max(左子树高度,右子树高度)int leftdepth = BinaryTreeDepth(root->left);int rightdepth = BinaryTreeDepth(root->right);//累加操作return 1 + (leftdepth > rightdepth ? leftdepth : rightdepth);
}
5,二叉树的查找
// ⼆叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL){ return 0;}//判断x是否等于结点值 这是一个重要的递归结束条件if (root->Data == x){return root;//这个root是广义的根结点 不是二叉树的第一个结点 }//左子树查找BTNode* leftFind = BinaryTreeFind(root->left,x);if (leftFind){return leftFind;}//右子树查找BTNode* rihgtFind = BinaryTreeFind(root->right,x);if (rihgtFind){return rihgtFind;}//左右子树都遍历完后就说明找不到了return NULL;
}
6,二叉树的销毁
在前面学了这么多的数据结构的操作中都有销毁操作,是因为这些结点都是在堆上申请内存的所以在我们不使用的时候就要手动的销毁释放掉,避免造成空间的浪费。
// ⼆叉树销毁 使用后序遍历销毁 左-右-根
void BinaryTreeDestory(BTNode** root)
{if (root == NULL){return;}//递归左子树 (*root)->left 拿到的是左子树这个成员BinaryTreeDestory(&(*root)->left);BinaryTreeDestory(&(*root)->right);//递归查找完后 将根节点的值释放掉free(*root);//释放后置空*root = NULL;
}
以上就是二叉树的基本操作了,下面我们再来介绍一个与前面遍历方式不同的遍历,这种遍历需要借助数据结构队列来完成。
四,二叉树的层序遍历
除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。层序遍历顾名思义就是一层一层的遍历,一次得到一层所有根结点的值。但是需要借助数据结构队列来实现,下面我们来分析一下思路你就知道为什么使用队列了。
具体思路:
定义一个队列,每次将根结点放入队列中,开始时将第一个根结点放入队列,在循环中将这个根结点全都打印后就 出队列! 同时判断刚刚被出队列的根结点有无左右孩子有则继续入队列,没有就不入队,队列为空跳出循环。这其实就是一种一层带着一层入队的思路。
//层序遍历
void leverOrder(BTNode* root)
{//创建一个队列Queue q;//队列的初始化QueueInit(&q);//入队 插入根结点QueuePush(&q, root);while (!QueueEmpty(&q)){//取队头,打印队头 出对头BTNode* top = QueueFront(&q);//top拿到了printf("%c ", top->Data);QueuePop(&q);//出完队判断top(根结点)的左右孩子是否为空 不为空则入队//判断左孩子if (top->left)QueuePush(&q,top->left);//判断右孩子if (top->left)QueuePush(&q,top->left);}QueueDestroy(&q);
}
如果对于队列的实现还不清楚的可以去看我之前讲栈和队列的文章! 传送门:栈和队列详解
五, 判断是否为完全二叉树
判断是否为完全二叉树的方法与上面二叉树层序遍历类似,下面我们来看思路:
//判断是否为完全二叉树
bool BinaryTreeComplete(BTNode* root)
{Queue q;QueueInit(&q);//将根节点root入队列保证队列不为空QueuePush(&q, root);//第一次循环while (!QueueEmpty(&q)){//取队头,出队头BTNode* top = QueueFront(&q);QueuePop(&q);//判断出出来的队头是否为空 为空则跳出循环 不为空则继续讲根的左右孩子入队列 无论左右孩子是否存在if (top == NULL){break;}QueuePush(&q, root->left);QueuePush(&q, root->right);}//跳出第一层循环 此时队列不一定为空 再判断一次while (!QueueEmpty(&q)){//取队头,出队头BTNode* top = QueueFront(&q);QueuePop(&q);//如果第二次循环还能出倒不为空的结点就不是完全二叉树 返回falseif (top != NULL){QueueDestroy(&q);return false;}}QueueDestroy(&q);//到这里第二次循环都已经结束了 队列彻底为空了 就说明没有出到非空结点 那么就是完全二叉树返回truereturn true;
}
以上就是这期文章所讲的二叉树的所有操作了,下面我们在来介绍一些有关二叉树常考的选择题。
七,二叉树选择题
在做题之前我们首先来讲一下二叉树的性质:
对任何⼀棵⼆叉树, 如果度为其叶结点个数为 n0, 度为2(有两个孩子的根结点)的分支结点个数为n2 ,则有n0=n2+1。
下面我们来证明一下这个结论:
1. 某⼆叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该⼆叉树中的叶⼦结点数为( )
A 不存在这样的⼆叉树
B 200
C 198
D 199
选B,由上面推导出的公式可知,n0=n2+1 叶子结点个数=199+1=200
2.在具有 2n 个结点的完全⼆叉树中,叶⼦结点个数为( )
A n
B n+1
C n-1
D n/2
选A
3.⼀棵完全⼆叉树的结点数位为531个,那么这棵树的⾼度为( )
A 11
B 10
C 8
D 12
这里直接使用带入法,在求满二叉树的时候我们知道,满二叉树的高度为:2^h-1=n
h=log(n+1); 所以相同高度的完全二叉树的结点肯定少于满二叉树的结点个数,所以直接带入2^x>=531 解出x=10 选B
4.⼀个具有767个结点的完全⼆叉树,其叶⼦结点个数为()
A 383
B 384
C 385
D 386这一题与第二题一样就是把2n换成了767,所以(1)n0=(767-1)/2 (2)n0=(767)/2
结果选B。
链式二叉树遍历选择题:
1.某完全⼆叉树按层次输出(同⼀层从左到右)的序列为 ABCDEFGH 。该完全⼆叉树的前序序列为(
) A
ABDHECFG
B ABCDEFGH
C HDBEAFCG
D HDEBFGCA
选A:
2.⼆叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则⼆叉树根结点为
()
A E
B F
C G
D H
前面说过,先序遍历是根左右,根结点(二叉树第一个结点)是第一个遍历的,所以E就是根结点,选A
3.设⼀课⼆叉树的中序遍历序列:badce,后序遍历序列:bdeca,则⼆叉树前序遍历序列为____。
A adbce
B decab
C debac
D abcde
选D:
4.某⼆叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同⼀层从左到右)
的序列
为 A
FEDCBA
B CBAFED
C DEFCBA
D ABCDEF
这题大家可以尝试使用上面的方法不难得出答案选A,但是还有一种更加巧妙的方法,我们可以直接从后序遍历得出根结点为F看选项直接选A。
以上就是本章的全部内容啦!
最后感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!