数据结构学习笔记---009
- 数据结构之二叉树
- 1、二叉树的概念和结构
- 1.1、回顾二叉树的重要性质
- 1.2、回顾二叉树的主要分类
- 1.1、如何实现二叉树?
- 2、二叉树的实现
- 2.1、二叉树的BinaryTree.h
- 2.2、二叉树的BinaryTree.c
- 2.2.1、二叉树的构建
- 2.2.2、二叉树销毁
- 2.2.3、二叉树前序、中序、后序遍历操作
- 2.2.4、二叉树取结点个数操作
- 2.2.5、二叉树取叶子结点个数操作
- 2.2.6、取二叉树的高度/深度操作
- 2.2.7、二叉树取第k层结点个数操作
- 2.2.8、二叉树取查找值为x的节点操作
- 2.2.9、二叉树层序遍历操作
- 2.2.10、二叉树判断是否为完全二叉树操作
- 2.2.11、二叉树的判空
- 2.3、二叉树的main.c
- 3、二叉树OJ巩固练习
- 3.1、练习题1:二叉树的深度
- 3.2、练习题2:单值二叉树
- 3.3、练习题3:相同的树
- 3.4、练习题4:二叉树的前序遍历 存入数组
- 3.5、练习题5:对称二叉树
- 3.6、练习题6:另一棵树的子树
- 3.7、练习题7:计算左叶子节点个数
数据结构之二叉树
前言:
前篇学习了 数据结构之树和二叉树的基本概念知识,那么这篇继续完善二叉树的相关知识并完成实现二叉树的构建和遍历的内容。
/知识点汇总/
1、二叉树的概念和结构
因为前篇已经非常详细的单独学习了数和二叉树的基本知识概念,那么这里就简单回顾一下即可。
概念:
二叉树(Binary Tree)是树形结构的一个重要类型。每个节点最多只能有两棵子树,且有左右之分。
它的定义可以概括为:一个n个有限元素的集合,该集合或者为空、或者由一个称为根的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成。
形象一点就是:
(1)空二叉树——(a);
(2)只有一个根结点的二叉树——(b);
(3)只有左子树——©;
(4)只有右子树——(d);
(5)完全二叉树——(e)
1.1、回顾二叉树的重要性质
1.二叉树第i层上的结点数目最多为2^(i-1) (i≥1)。
2.深度为k的二叉树至多有2^k-1个结点(k≥1)。
3.包含n个结点的二叉树的高度至少为log2(n+1)。
4.在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
5.如果有一颗n个结点的完全二叉树(其深度为⌊log2n⌋+1)的结点按层序编号(从第一层到最后一层,每层从左到右),对任一结点i(1≤i≤n):
1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点⌊i/2⌋。
2)如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子LCHILD(i)是结点2i。
3)如果2i+1>n,则结点i无右孩子,否则其右孩子RCHILD(i)是结点2i+1。
1.2、回顾二叉树的主要分类
1.满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,那么这棵二叉树就被称为满二叉树。
2.完全二叉树:除最后一层外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点。每个结点最多有两个子树,因此不存在度大于2的结点。左子树和右子树是有顺序的,次序不能任意颠倒。
3.二叉搜索树(二叉排序树):可以为空树,或者是具备如下性质:若它的左子树不空,则左子树上的所有结点的值均小于根节点的值;若它的右子树不空,则右子树上的所有结点的值均大于根节点的值,左右子树分别为二叉排序树。
4.平衡二叉树:这是一种概念,是二叉查找树的一个进化体,它有几种实现方式:红黑树、AVL树。平衡二叉树的特点是任意节点的子树的高度差都小于等于1。
1.1、如何实现二叉树?
二叉树的存储结构分类
1.二叉树的数组存储结构:
一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费等问题。
并且实际应用中,也只有堆会用数组的形式存储。因为堆满足完全二叉树。
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
2、二叉树的链式存储结构:
常用链表来指示元素的逻辑关系。
通常,以链表中的每一个结点,三个域组成,分别是数据域、左指针域和右指针域,左指针指向左孩子,右指针指向右孩子。
链式结构也分为二叉链和三叉链,一般是二叉链。高阶数据结构涉及三叉链。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}TreeNode;
2、二叉树的实现
2.1、二叉树的BinaryTree.h
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType Val;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树的高度/深度
int BinaryTreeHeight(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
//创建队列结构体成员与变量
typedef struct BinaryTreeNode* QDatetype;//注意这里的返回类型和参数与二叉树的接口类型要对应
typedef struct QueueNode
{
QDatetype val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
//队列初始化
void QueueInit(Queue* pq);
//队列销毁
void QueueDestory(Queue* pq);
//元素入队
void QueuePush(Queue* pq, QDatetype x);
//元素出队
void QueuePop(Queue* pq);
//获取队头元素
QDatetype QueueFront(Queue* pq);
//获取队尾元素
QDatetype QueueBack(Queue* pq);
//获取队列元素个数或队列大小
int QueueSize(Queue* pq);
//队列判断空
bool QueueEmpty(Queue* pq);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root);
//写法2:
//int BinaryTreeComplete2(BTNode* root);
2.2、二叉树的BinaryTree.c
2.2.1、二叉树的构建
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
assert(a);
if (a[*pi] == '#' || *pi >= n)
{
++(*pi);
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL)
{
perror("malloc fail");
exit(-1);
}
root->Val = a[*pi];
++(*pi);
root->left = BinaryTreeCreate(a, n, pi);
root->right = BinaryTreeCreate(a, n, pi);
return root;
}
2.2.2、二叉树销毁
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
{
return;
}
BinaryTreeDestory(&(*root)->left);
BinaryTreeDestory(&(*root)->right);
free(*root);
*root = NULL;
}
2.2.3、二叉树前序、中序、后序遍历操作
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
//printf("# ");
return;
}
printf("%c ", root->Val);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
//printf("# ");
return;
}
BinaryTreeInOrder(root->left);
printf("%c ", root->Val);
BinaryTreeInOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
//printf("# ");
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c ", root->Val);
}
2.2.4、二叉树取结点个数操作
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
2.2.5、二叉树取叶子结点个数操作
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
//为空返回0,不能assert,否则递归走不动
if (root == NULL)
{
return 0;
}
//只有根节点或叶子节点,则返回1
if (root->left == NULL && root->right == NULL)
{
return 1;
}
//其余情况的叶子节点=左子树叶子节点+右子树叶子节点
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
2.2.6、取二叉树的高度/深度操作
int BinaryTreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return fmax(BinaryTreeHeight(root->left), BinaryTreeHeight(root->right)) + 1;
}
2.2.7、二叉树取第k层结点个数操作
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k > 0);
//为空
if (root == NULL)
return 0;
//根节点
if (k == 1)
return 1;
//其余层,递归k-1
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
2.2.8、二叉树取查找值为x的节点操作
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
//参数为空
if (root == NULL)
{
return NULL;
}
//根节点匹配,则返回空节点
if (root->Val == x)
{
return root;
}
//遍历左右子树,找匹配值
BTNode* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = BinaryTreeFind(root->right, x);
if (ret2)
{
return ret2;
}
//遍历结束都没有匹配,则返回空
return NULL;
}
2.2.9、二叉树层序遍历操作
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
//队列销毁
void QueueDestory(Queue* pq)
{
assert(pq);
//工作指针,遍历销毁
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
//元素入队
void QueuePush(Queue* pq, QDatetype x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->next = NULL;
newnode->val = x;
//空队列的处理
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
//尾指针后移
pq->ptail = newnode;
}
pq->size++;
}
//元素出队
void QueuePop(Queue* pq)
{
assert(pq);
//判空,空队列不能出队
assert(pq->phead);
//兼容只有一个节点的情况
//delfront保存当前头指针地址,以便free掉,并防止内存泄漏
QNode* delfront = pq->phead;
//首元素出队,头指针直接后移即可
pq->phead = pq->phead->next;
free(delfront);
delfront = NULL;
//只有一个节点时,出队后,为空,那么尾指针也得置空
if (pq->phead == NULL)
{
pq->ptail = NULL;
}
pq->size--;
}
//获取队头元素
QDatetype QueueFront(Queue* pq)
{
assert(pq);
//判空,为空没有元素取
assert(pq->phead);
return pq->phead->val;
}
//获取队尾元素
QDatetype QueueBack(Queue* pq)
{
assert(pq);
//判空,为空没有元素取
assert(pq->phead);
return pq->ptail->val;
}
//获取队列元素个数或队列大小
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
//队列判断空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL;
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
assert(root);
//创建队列
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%c ", front->Val);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
QueuePop(&q);
}
printf("\n");
QueueDestory(&q);
}
2.2.10、二叉树判断是否为完全二叉树操作
判断完全二叉树的思路1;
1.如果树为空,直接返回错误;
2.如果树非空,层序遍历二叉树;
3.如果一个结点左右孩子均Not NULL时,则pop该结点,将其左右孩子入队列;
4.如果遇到一个结点,左孩子为空,而右孩子非空,则一定不为完全二叉树;
5.如果遇到一个结点,左孩子不为空,而右孩子为空;或者是左右孩子均为空,则结点之后的队列的结点都为叶子结点;
满足以上条件则为完全二叉树,否则不是完全二叉树。
核心思想:类似于层序一层层的节点构造而成,即最后一层的节点上面的层节点都是满的。前提是最后一层不是根节点,即第一层.
所以遍历不连续就不是完全二叉树,并且队列比栈适用于判断的重要原因之一就是,第一个NULL出队列时,所有的非空节点一定会进队列。
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
assert(root);
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//遇见空就结束
if (front == NULL)
{
break;
}
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
//因为出到空,退出循环后,再写一个循环判断队列里是否还有非空
//若有非空,则false,若没有非空或不进入循环,则true
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//遇见空就结束
if (front)
{
QueueDestory(&q);
return false;
}
}
QueueDestory(&q);
return true;
}
2.2.11、二叉树的判空
//二叉树判空
bool BinaryTreeEmpty(BTNode* root)
{
return root == NULL;
}
2.3、二叉树的main.c
#include "BinaryTree.h"
int main()
{
char str[] = "ABC##DE#G##F###";// ABC##DE#G##F###
//char str2[] = "ABCDEGF";
int len = strlen(str);
int NodeNum = 0;
BTNode* tree = BinaryTreeCreate(str, len, &NodeNum);
printf("前序遍历:");
// 二叉树前序遍历
BinaryTreePrevOrder(tree);
printf("\n");
printf("中序遍历:");
// 二叉树中序遍历
BinaryTreeInOrder(tree);
printf("\n");
printf("后序遍历:");
// 二叉树后序遍历
BinaryTreePostOrder(tree);
printf("\n");
// 二叉树节点个数
printf("节点个数:%d\n", BinaryTreeSize(tree));
printf("层序遍历:");
// 层序遍历
BinaryTreeLevelOrder(tree);
printf("\n");
//二叉树判空
if (BinaryTreeEmpty(tree))
{
printf("空树\n");
}
else
{
printf("非空树\n");
}
// 判断二叉树是否是完全二叉树
if (BinaryTreeComplete(tree))
{
printf("是完全二叉树\n");
}
else
{
printf("非完全二叉树\n");
}
// 二叉树叶子节点个数
printf("叶子节点:%d\n", BinaryTreeLeafSize(tree));
//计算二叉树的高度
printf("二叉树的高度:%d\n", BinaryTreeHeight(tree));
// 二叉树第k层节点个数
printf("第k层叶子节点:%d\n", BinaryTreeLevelKSize(tree, 3));
// 二叉树查找值为x的节点
printf("第k层叶子节点:%c\n", BinaryTreeFind(tree, 'E')->Val);
// 二叉树销毁
BinaryTreeDestory(&tree);
return 0;
}
运行结果,如图所示:
3、二叉树OJ巩固练习
3.1、练习题1:二叉树的深度
核心思想就是取左右子树叶子节点的高度,将比较大的值+根节点的第一层即可。因为计算是以0计算所以需要把第一层加回来。或者+1也可以理解为(考虑只有根节点),加自己本身这层,就像数数需要加上自己。
方法1:fmax调用库函数更方便,fmax用于比较出较大值并返回较大值。头文件:<math.h>
方法2:通过变量保存,递归的值,最后比较大小之后加回第一层。
//方法1:fmax函数
/**/
int calculateDepth(struct TreeNode* root)
{
if (root == NULL)
{
return 0;
}
return fmax(calculateDepth(root->left), calculateDepth(root->right)) + 1;
}
//方法2:保存变量
/**/
int calculateDepth(struct TreeNode* root)
{
if (root == NULL)
{
return 0;
}
int left = calculateDepth(root->left);
int right = calculateDepth(root->right);
return left > right ? left + 1 : right + 1;
}
3.2、练习题2:单值二叉树
单值二叉树(每个节点都具有相同的值) — 给定的二叉树是单值二叉树就返回true,否则就返回false
思路:排除法和分治法,检查每个节点和他的孩子的值是否匹配。
bool isUnivaTree(struct TreeNode* root)
{
if (root == NULL)
{
return true;
}
//排除法,排除左孩子值与其父节点值不等
if (root->left && root->lefd->val != root->val)
{
return false;
}
//排除法,排除右孩子值与其父节点值不等
if (root->right && root->right->val != root->val)
{
return false;
}
//执行递归,返回结果,若是不满足上述排除的情况,说明满足单值二叉树
return isUnivaTree(root->left) && isUnivaTree(root->right);
}
3.3、练习题3:相同的树
相同的树 — 给你两棵二叉树的根节点p和q,编写一个函数来检验这两棵树是否相同(如果这两棵树的结构和节点值相同,则认为是相同的)
思路:排除法和分治法,自己比,左子树比,右子树比
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
//都为空
if (p == NULL && q == NULL)
return true;
//其中一个为空
if (p == NULL || q == NULL)
return false;
//都不为空且不相等
if (p->val != q->val)
return false;
//递归遍历完,都不满足上述排除的情况,则满足相同的树。
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
3.4、练习题4:二叉树的前序遍历 存入数组
这道题的关键在于获取结点个数开辟数组,并涉及前序遍历的参数设计细节问题。
方法1:递归法
方法2:全局变量法(不推荐)
方法3:指针法
int TreeSize(struct TreeNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
void preorder(struct TreeNode* root, int* a, int i)
{
if (root == NULL)
return;
a[i++] = root->val;
preorder(root->left,a,i);
preorder(root->right,a,i);
}
//全局变量法
//int i = 0;
//void preorder(struct TreeNode* root, int* a)
//{
// if (root == NULL)
// return;
// a[i++] = root->val;
// preorder(root->left, a);
// preorder(root->right, a);
//}
//指针法
void preorder(struct TreeNode* root, int* a, int* pi)
{
if (root == NULL)
return;
a[(*pi)++] = root->val;
preorder(root->left, a, pi);
preorder(root->right, a, pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
int n = TreeSize(root);
int* a = (int*)malloc(sizeof(int) * n);
*returnSize = n;
//方法1:递归法
preorder(root, a, 0);
//方法2:全局变量法(不推荐)
//i= 0; --- 必须置0
//preorder(root, a); -- 全局变量法,但尽量不用
//方法3:指针法
//int i = 0;
//preorder(root, a, &i);
return a;
}
3.5、练习题5:对称二叉树
对称(镜像)二叉树 — 给你一个二叉树的根结点root,检查它是否轴对称。
思路:设计一个复用函数,继续利用排除法和递归遍历,排除不成立的结果,能成功返回的对称二叉树。
bool _isSymmetric(struct TreeNode* root1, struct TreeNode* root2)
{
if (root1 == NULL && root2 == NULL)
{
return true;
}
//一个为空,另一个不为空
if (root1 == NULL || root2 == NULL)
{
return false;
}
//都不为空
if (root1->val != root2->val)
{
return false;
}
return _isSymmetric(root1->left, root2->right) && _isSymmetric(root1->right, root2->left);
}
bool isSymmetric(struct TreeNode* root)
{
return _isSymmetric(root->left, root->right);
}
3.6、练习题6:另一棵树的子树
另一棵树的子树:给两颗二叉树,检验一棵树是否包含另一棵树的子树。
思路:结合上面相同的树的方法,去匹配子树
//相同的树
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
//都为空
if (p == NULL && q == NULL)
return true;
//其中一个为空
if (p == NULL || q == NULL)
return false;
//都不为空且不相等
if (p->val != q->val)
return false;
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
//另一棵树的子树
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
//为空没有子树,匹配失败
if (root == NULL)
return false;
//根节点匹配,调用相同的树方法,去匹配
if (root->val == subRoot->val)
return isSameTree(root, subRoot);
//递归前序遍历匹配
return isSameTree(root, subRoot) ||
isSameTree(root->left, subRoot) ||
isSameTree(root->right, subRoot);
}
3.7、练习题7:计算左叶子节点个数
计算二叉树中左叶子节点的个数。
思路:递归遍历左右子树,累计左叶子节点个数即可。
1.判断是否为空
2.判断是否为左叶子节点
3.递归累加。
// 二叉树左叶子节点个数
int BinaryTreeLeftLeafSize(BTNode* root)
{
//为空返回0
if (root == NULL)
{
return 0;
}
//判断有左孩子,并是左叶子节点
if (root->left && root->left->left == NULL && root->left->right == NULL)
{
return 1;
}
//其余情况的左叶子节点=左子树叶子节点+右子树叶子节点
return BinaryTreeLeftLeafSize(root->left) + BinaryTreeLeftLeafSize(root->right);
}