树是一种非线性的数据结构,它是由n(n≥0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树有很多种存储形式,这里展示的是最经典的一种--孩子兄弟表示法
struct TreeNode{
int val;
struct TreeNode* firstchild;//代表树的第一个孩子结点
struct TreeNode* offspring;//孩子的兄弟结点
};
二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树
任何一个二叉树都是由下面的结构复合组成
性质1:二叉树的第i层上至多有2i-1(i≥1)个节点 [6] 。
性质2:深度为h的二叉树中至多含有2h-1个节点 [6] 。
性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1 [6] 。
性质4:具有n个节点的满二叉树深为log2n+1。
性质5:若对一棵有n个节点的完全二叉树进行顺序编号(从0开始),对于编号为i(i≥1)的节点,有以下几点性质:
(1)i的左孩子结点的编号是2*i+1
(2)i的右孩子的编号是2*i+1
(2)i的父节点的编号是(i-1)/2
一般有两种:一种为链式结构,一种为顺序结构
堆的性质:
堆中某个结点的值总是不大于或不小于其父结点的值;
堆总是一棵完全二叉树
//这里建立的是小堆---即所有孩子节点的值小于父节点的值
//大堆的代码与这个类似,留给读者思考
#include
#include
#include
#include
typedef int HPDataType;
typedef struct Heap {
HPDataType* a;
int size;
int capacity;
}HP;
//初始化
void HeapInit(HP* php);
//销毁
void HeapDestroy(HP* php);
//插入
void HeapPush(HP* php, HPDataType x);
//删除堆顶元素
void HeapPop(HP* php);
//取出堆顶的元素
HPDataType HeapTop(HP*php);
//判断堆是否为空
bool HeapEmpty(HP* php);
//得到堆的大小
int HeapSize(HP* php);
//向下调整堆
void AdjustDown(HPDataType* a, int n, int parent);
//向上调整堆
void AdjustUp(HPDataType* a, int child);
//交换
void Swap(HPDataType* p1, HPDataType* p2);
//初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
//销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
//交换
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 = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整堆
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child+1] < a[child])
child++;
if (a[child] < a[parent])//小堆
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//插入
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size++] = x;
AdjustUp(php->a, php->size - 1);
}
//删除堆顶元素
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
//取出堆顶的元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
//判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//得到堆的大小
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
基本思想:先建立一个堆,根据堆的性质,堆顶的元素是最大值或最小值,那么我们只要将堆顶的元素与数组的最后一个元素交换,然后根据HeapPop函数的思路,调整前面的数据使得它还是一个堆,如此循环,得到一个有序的序列
void HeapSort(int*a,int n)
{
//建立堆
//...
//调整堆
for(int i=n-1;i>0;i--)//只要调整n-1次
{
Swap(&a[0],&a[i]);
AdjustDown(a,i,0);
}
}
这里建立堆有两种思路,分别是AdjustUp和AdjustDown,即向上调整和向下调整
代码如下
void HeapSort(int*a,int n)
{
//建立堆
//AdjustUp
for(int i=1;i=0;i--)//从最后一个叶子节点的父节点开始
AdjustDown(a,n,i);
//调整堆
for(int i=n-1;i>0;i--)//只要调整n-1次
{
Swap(&a[0],&a[i]);
AdjustDown(a,i,0);
}
}
思路:假设在n个数据中找前k个最大值,先建一个大小为k的小堆(反之,建大堆),然后依次比较堆顶元素和n-k个数据,不断调整小堆,最后堆中存放的就是前k个最大值,但不有序
void CreateNDate()
{
//造数据
int n = 10000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
//将数据放入文件中
for (size_t i = 0; i < n; ++i)
{
int x = rand() % 1000000;//数据在0~1000000
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void PrintTopK(int k)
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
int* tmp = (int*)malloc(sizeof(int) * k);
//读取数据
for (int i = 0; i < k; i++)
{
fscanf(pf, "%d", &tmp[i]);
}
//建立小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(tmp, k, i);
}
//将堆顶元素和其他元素比较,如果大于则交换,调整堆
while (!feof(pf))
{
int val = 0;
fscanf(pf, "%d", &val);
if (val > tmp[0])
{
tmp[0] = val;
AdjustDown(tmp, k, 0);
}
}
for (int i = 0; i < k; i++)
printf("%d ",tmp[i]);
free(tmp);
}
int main()
{
int k = 10;
//CreateNDate();//这个函数只要调用一次
PrintTopK(k);
return 0;
}
typedef struct TreeNode{
int val;
struct TreeNode*left;
struct TreeNode*right;
}TreeNode;
代码如下
//先序
void PreOrder(TreeNode*root)
{
if(root==NULL)//结点为空,直接返回
return;
printf("%d",root->val);//遍历根结点
PreOrder(root->left);//遍历左子树
PreOrder(root->right);//遍历右子树
}
//中序
void InOrder(TreeNode*root)
{
if(root==NULL)//结点为空,直接返回
return;
InOrder(root->left);//遍历左子树
printf("%d",root->val);//遍历根结点
InOrder(root->right);//遍历右子树
}
//后序
void PostOrder(TreeNode*root)
{
if(root==NULL)//结点为空,直接返回
return;
PostOrder(root->left);//遍历左子树
PostOrder(root->right);//遍历右子树
printf("%d",root->val);//遍历根结点
}
PreOrder函数递归展开图
如上图,如果将函数递归展开,我们会发现这就是一颗树的形状,并且需要注意的是函数递归是经过NULL结点的!!!中序和后序的展开图与它类似,可以自己画画图加深对递归遍历的理解
二叉树的很多题目都是用递归来做的,本质的思想其实就是分治思想,即将一个问题转化成两个子问题。
很多人的第一想法是用之前的遍历二叉树,将打印的代码变成计数的代码,这种方法当然可以,但是它其实本质是个深度优先遍历的思想,我们现在要求用分治,代码格式如下
int TNodeSize(TreeNode*root){
}
思路:求一颗二叉树的结点数=>求它的左子树的结点数+求它的右子树的结点数+它本身,当然前提是它不是一个空树,这样我们就将问题转换成了两个相同的子问题,代码如下
int TNodeSize(TreeNode*root){
//如果为空,结点个数就是0
if(root==NULL)
return 0;
//如果不为空,求二叉树节点个数就可以拆成求两个子树结点数的问题,记得加1
return TNodeSize(root->left)+TNodeSize(root->right)+1;
}
分治的思想依旧如上:求一颗二叉树的叶子结点数=>求它的左子树的叶子结点数+求它的右子树的叶子结点数,而函数返回的条件是树为空,或找到叶子节点
代码如下
int BTLeafSize(TreeNode*root){
if(root==NULL)//空树
return 0;
if(root->left==NULL||root->right==NULL)//叶子节点
return 1;
return BTLeafSize(root->left)+BTLeafSize(root->right);
}
分治的思想依旧如上:求一颗树的高度=>max{ 左子树的高度,右子树的高度 } +1
代码如下
int BTreeHeight(TreeNode*root){
if(root==NULL)
return 0;
return fmax(BTreeHeight(root->left),BTreeHeight(root->right))+1;
}
下面还有几个题目留给读者思考:
typedef struct TreeNode{
int val;
struct TreeNode*left;
struct TreeNode*left;
}TreeNode;
//求第K层的结点数
int BTLevelSize(TreeNode*root,int k);
//查找结点
TreeNode*BTreeFind(TreeNode*root,int x);
//两棵树是否相等
bool isSame(TreeNode*p,TreeNode*q);
如果觉得这篇博客对你有所帮助的话,请一定不要吝啬你的点赞加评论哦!!!