数据结构 – 二叉树
文章目录
一、树的概念及结构
1.数的概念
树是一种非线性的数据结构,由n(n > 0)个有限结点组成的一个有层次关系的集合,像一颗倒挂的树;
1.有一个根结点,根结点没有前驱结点;
2.除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、… Tm,其中每一个集合Ti(1<=i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继 ;
3.树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构;
2.树的相关概念
1.结点的度:一个结点含有的子树的个数称为该结点的度;
2.叶结点:度为0的结点称为叶结点;
3.父结点: 若一个结点含有子结点,则这个节点称为其子结点的父结点;
4.孩子结点:一个结点含有的子树的根结点称为该结点的子结点;
5.树的度:一棵树中,最大的结点的度称为树的度;
6.结点的层次:从根开始定义起,根为第1层,根的子结点为第2层;
7.树的高度或深度:树中结点的最大层次;
8.结点的祖先:从根到该结点所经分支上的所有结点;
9.子孙:以某结点为根的子树中任一结点都称为该结点的子孙;
10.结点个数与结点边的关系:N个结点的树有N-1个边;
3.树的表示
孩子兄弟表示法:
//左孩子右兄弟
typedef int DataType;
struct TreeNode
{
struct TreeNode* pFirstChild1; //指向第一个孩子结点
struct TreeNode* pNextBrother; //指向下一个兄弟结点
DataType Date; //数据
}
二、二叉树的概念及结构
1.概念
一棵二叉树是结点的一个有限集合,该集合
1.或者为空
2.由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
1.二叉树不存在度大于2的结点;
2.二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树注意;
对于任意的二叉树都是由以下几种情况复合而成的:
2.特殊二叉树
1.满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^k- 1,则它就是满二叉树。
2.完全二叉树:对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树;要注意的是满二叉树是一种特殊的完全二叉树。
3.二叉树的性质
1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点;
2.若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h - 1;
3.对任何一棵二叉树,如果度为0的结点个数为n0,度为2的结点个数为n2,则有n0=n2+1;(度为0的结点比度为2的结点多一个)
4.若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n + 1);
5.对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
(1)若i>0,i位置节点的父节点序号:(i-1)/2;i=0,i为根节点编号,无双亲节点;
(2)若2i+1<n,左孩子序号:2i+1;2i+1>=n,则无左孩子;
(3)若2i+2<n,右孩子序号:2i+2;2i+2>=n,则无右孩子;
例题:
4.二叉树的存储结构
1.顺序结构
用数组来存储,一般只适合表示完全二叉树,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树;
2.链式存储
用链表来表示一颗二叉树,使用二叉链表,包括两个指针域和一个数据域,左右指针分别指向左右孩子节点;
三、二叉树的顺序存储
1.堆的概念及结构
如果有一个关键码的集合K ={k0, k1, k2, … , kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中并满足: Ki <= K2i+1 且 Ki <= K2i+2(Ki >= K2i+1且K i>= K2i+2)i=01,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆;
堆的性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
小根堆中所有的父亲都是小于等于孩子;
大根堆中所有的父亲都是大于等于孩子。
2.堆的实现
Heap.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
//初始化
void HPInit(HP* php);
//销毁
void HPDestroy(HP* php);
//打印
void HPPrint(HP* php);
//交换
void Swap(HPDataType* p1, HPDataType* p2);
//向上调整算法
void AdjustUp(HPDataType* a, int child);
//插入
void HPPush(HP* php, HPDataType x);
//向下调整算法
void AdjustDown(HPDataType* a, int size, int parent);
//删除
void HPPop(HP* php);
Heap.c
#include"Heap.h"
//初始化
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
//销毁
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
//打印
void HPPrint(HP* php)
{
assert(php);
int i = 0;
for (i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)//孩子到根节点是最后一个结点
{
if (a[child] < a[parent])//孩子小于父亲就交换,这是建小堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
//插入
void HPPush(HP* php, HPDataType x)
{
assert(php);
//扩容
if (php->capacity == php->size)
{
int newcapacity = (php->capacity == 0) ? 4 : 2 * (php->capacity);
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realcoc");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
//插入
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
//堆顶元素
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//大小
int HPSize(HP* php)
{
assert(php);
return php->size;
}
//向下调整算法
void AdjustDown(HPDataType* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
//选出左右孩子中小的那一个
if (a[child + 1] < a[child] && (child + 1 < size))
{
++child;
}
//孩子跟父亲比较
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else
{
break;
}
}
}
//删除
void HPPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);//首尾元素交换,删除尾部
php->size--;
AdjustDown(php->a, php->size, 0);//向下调整
}
向上调整算法:
1.用于堆插入数据时,对所插入数据进行位置调整,将其放在正确的位置,让整个数据结构满足大堆或小堆;
2.其思想是(建大堆):输入参数是数组名和刚插入的孩子结点的下标,利用该孩子节点找到其父节点,对比父子两节点,如果孩子>父亲,就将孩子与父亲交换,然后更新孩子节点的下标为刚交换的父亲节点的下标,再算出下一个父亲节点的下标,继续比较,直到孩子节点小等于当前的父节点,就跳出循环,或者孩子节点的下标不再大于0,结束循环,该节点调整完毕;
建小堆就是把大于号和小于号交换一下;
3.每次的插入数据都是将数据放在数组尾部,然后执行一次向上调整算法;
4.时间复杂度:logN
向下调整算法:
1.用于删除堆顶数据,将堆顶数据和其孩子节点进行比较和交换,将其放在正确的位置,让整个数据结构满足大堆或小堆;
2.向下调整算法有前提条件:该节点的左右子树都是大堆(或小堆)才能向下调整;
3.其思想是(建大堆):输入参数是数组名、数组大小和父节点下标,用父节点下标算出其左孩子下标,选出左右孩子中较大的那一个,跟父节点比较,如果孩子>父亲,就将孩子与父亲交换,更新父节点下标为刚交换的孩子节点下标,然后算出新的左孩子结点下标,继续比较,直到直到孩子节点小等于当前的父节点,就跳出循环,或者孩子节点下标大于数组大小,结束循环,该节点调整完毕;
建小堆就是把大于号和小于号交换一下;
4.删除堆顶数据就是将数组第一个元素与最后一个元素交换,然后删除最后一个元素,再进行一次向下调整算法;
5.时间复杂度:logN
3.堆的应用 - 堆排序
1.排升序:建大堆
因为如果建小堆,第一次建堆后,取出最小的数,剩下的数关系乱了,还要继续建堆时间复杂度为O(N^2),效率很低。没有使用到堆的优势。建大堆,把最大的数选出来,和最后的数交换,然后不看做堆的一部分,之后向下调整一次就可以选出次大的数,与删除的思想一致,时间复杂度为O(N * logN);
排降序: 建小堆;
2.建堆方式
(1)向上调整建堆
从堆中的最后一个孩子节点开始,依次向前进行向上调整,时间复杂度为:O(N*logN);
(2)向下调整建堆
由于向下调整算法需要左右子树都是堆,因此从堆中的最后一个非叶子节点开始,依次向前进行向下调整,时间复杂度为:O(N)
对比下来,向下调整建堆的方式更好。
3.排序思想:排升序,先建大堆,堆顶数据就是所有元素中最大的数,交换数组头尾元素,最大的数就到了数组尾部,然后进行一次向下调整,保证剩下的数据还是大堆,再将指向队尾的指针向前移动,队尾不算进堆中,继续循环,直到所有的数据都排序过。
4.实现:
void HeapSort(int* a, int n)
{
//建堆方式1:每次插入都向上调整
//O(N * logN)
//for (int i = 0; i < n; i++)
//{
// AdjustUp(a, i);
//}
//建堆方式2:每次插入都向下调整
//O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; i--)//找对后一个叶子结点的父节点,因为向下调整需要左右子树都为堆,所以先将左右子树调整为堆
{
AdjustDown(a, n, i);
}
//不断向下调整排序
//O(N * logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);//交换第一个和最后一个数据
AdjustDown(a, end, 0);//向下调整一次
end--;
}
}
TOP - K问题就可以使用堆排序来求解。
四、二叉树的链式存储
1.二叉树的遍历
1.前序遍历(Preorder Traversal 亦称先序遍历)一一访问根结点的操作发生在遍历其左右子树之前;
2.中序遍历(Inorder Traversal)一一访问根结点的操作发生在遍历其左右子树之中 (间);
3.后序遍历(Postorder Traversal)一一访问根结点的操作发生在遍历其左右子树之后;
前序遍历是最符合深度优先遍历的,中序和后序也是深度优先;
2.二叉树链式结构的实现
BinaryTree.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
//结构
typedef int BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;
//创建结点
BTNode* BuyNode(BTDataType x);
//前序遍历
void PreOrder(BTNode* root);
//中序遍历
void InOrder(BTNode* root);
//后序遍历
void PostOrder(BTNode* root);
//结点个数
int TreeSize(BTNode* root);
//叶子节点个数
int TreeLeafSize(BTNode* root);
//第k层的结点个数
int TreeKLevel(BTNode* root, int k);
//找值为x的结点
BTNode* TreeFind(BTNode* root, BTDataType x);
//树的高度
int TreeDepth(BTNode* root);
//销毁
void TreeDestroy(BTNode* root);
BinaryTree.c
1.创建节点:
//创建结点
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
assert(node);
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
2.前序遍历、中序遍历和后序遍历:
//前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
//中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
//后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
这三个算法属于分治算法,利用递归来求解问题。
结点个数:
int count = 0;
void TreeSize(BTNode* root)
{
if (root == NULL)
{
return;
}
++count;
TreeSize(root->left);
TreeSize(root->right);
}
上面的方法每次调用前都要去清空count,多线程调用会出现问题;
更好的方法:分治思想
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
结点个数 = 左子树的结点个数 + 右子树的结点个数
3.叶子结点数量
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
4.第k层的结点数
转换思路:求左子树的第k - 1层 + 右子树的第k - 1层
int TreeKLevel(BTNode* root, int k)
{
assert(k >= 1);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}
5.找值为x的结点
//找值为x的结点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data = x)
{
return root;
}
BTNode* ret1 = TreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = TreeFind(root->right, x);
if (ret2)
{
return ret2;
}
}
注意:返回值不能直接返回到最外层,要层层返回才能把最终值带出去;
递归调用图:
6.树的高度
int TreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int LeftDepth = TreeDepth(root->left);
int RightDepth = TreeDepth(root->right);
return LeftDepth > RightDepth ? LeftDepth + 1 : RightDepth + 1;
}
7.销毁(后序遍历)
void TreeDestroy(BTNode* root)
{
if (root == NULL)
{
return;
}
TreeDestroy(root->left);
TreeDestroy(root->right);
free(root);
}
3.层序遍历(广度优先遍历)
层序遍历就是一层一层的从左到右输出数据;
借助队列,先将根节点入队,若队列不为空,则进入循环,先输出根节点的数据,也就是队头数据,然后入队根结点的左右不为空的子树,继续循环;
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))//当队列不为空
{
BTNode* front = QueueFront(&q);//取队头数据
printf("%d\n", front->data);
QueuePop(&q);//出队
if (front->left)//左子树不为空,进队
{
QueuePush(&q, front->left);
}
if (front->right)//右子树不为空,进队
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestory(&q);
}
4.判断一棵树是否为完全二叉树
使用层序遍历,将空结点也入队,遇到第一个空结点后,后面就不能再有非空的节点了,否则就不是完全二叉树;
bool TreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)//根节点入队
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);//取队头结点
QueuePop(&q);//出队
if (front)//若队头结点不为空,则入队其左右子节点,空结点也入队
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
else//若front是空结点,则表明所有结点均已入队(包括叶子节点下的空结点),跳出循环
{
break;
}
}
//此时的队列中,队头就是第一个出现的空结点
//开始判断此时队列里第一个NULL后,是否还有非空结点
//如果后面全是空,则该树是完全二叉树,否则不是
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);//取队头结点
QueuePop(&q);//出队
if (front)
{
QueueDestory(&q);
return false;
}
}
QueueDestory(&q);
return true;
}