简单介绍树的概念 ,比较重要的是堆的实现、堆排序、TOP-K问题、二叉树的遍历(前、中、后序及层序遍历)以及二叉树功能的实现
目录
一、树的概念及结构
1.1树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。
- 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继 因此,树是递归定义的。
树的例图:
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
1.2树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及左孩子右兄弟表示法等。我们这里就了解其中最常用左孩子右兄弟表示法。具体结构如下:
typedef int DataType;
struct Node
{
struct Node* child; // 第一个孩子结点
struct Node* brother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};
树在实际中的运用就有: 表示文件系统的目录树结构等等
二、二叉树概念及结构
2.1二叉树概念:
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空 2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
1. 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:
————————————————————————————————————————————
有两个特殊的二叉树:
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的
注意:满二叉树是一种特殊的完全二叉树
2.2二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空 间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
三、二叉树的顺序结构及实现
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质: 1.堆中某个节点的值总是不大于或不小于其父节点的值
2.堆总是一棵完全二叉树
3.1 堆的实现:
下面都是实现小堆
注意事项:
1.堆的插入之后必须保证堆还是小堆,所以插完数据之后要进行向上调整,确保堆依然是小堆
tip:已知孩子下标为child(上图的10就是孩子节点),不管其是左孩子还是右孩子,parent=(child-1)/2
2.堆的删除是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法(向下调整算法有一个前提:左右子树必须是一个堆,才能调整) , 保持堆还是小堆
tip: parent是父亲节点的下标(上图28就是父亲节点)
左孩子下标: leftchild=parent*2+1, 右孩子下标: rightchild=parent*2+2
heap.h头文件:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
#include<time.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
// 堆的构建
void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestroy(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
//向上调整
void Adjustup(HPDataType*a, int child);
//向下调整
void Adjustdown(HPDataType*a, int size, int parent);
//打印堆
void HeapPrint(Heap*hp);
代码实现(heap.c):
#include"heap.h"
void swap(HPDataType*a, HPDataType*b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
void Adjustup(HPDataType*a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
//大堆的话把a[child] < a[parent]中的‘<’换成‘>’
if (a[child] < a[parent])
{
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void HeapPrint(Heap*hp)
{
for (int i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
void Adjustdown(HPDataType*a, int size, int parent)
{
//向下调整,把他调成小堆,跟左右孩子中最小的那个交换
//结束条件:父亲节点的值<=小的孩子的值 或者 直接调整到叶子
assert(a);
int child = parent * 2 + 1;
while (child<size)
{
//如果右孩子比左孩子小,那么就让孩子下标++,使其变为右孩子
//child+1保证孩子下标在有效范围内
if (child + 1 < size&&a[child + 1] <a[child])//大堆的话把a[child + 1] <a[child]中的‘<’换成‘>’
{
child++;
}
if (a[child]<a[parent])//大堆的话把a[child]<a[parent]中的‘<’换成‘>’
{
swap(&a[child ], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
// 堆的构建
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
// 堆的销毁
void HeapDestroy(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
// 堆的插入 (小堆)
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)
{
//判断是初始化(初始化时capacity为0)还是空间满了需要增容
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType*tmp = realloc(hp->a, sizeof(HPDataType)*newcapacity);
if (tmp == NULL)
exit(-1);
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
//插完数据之后,向上调整,调的是尾部所以下标就是size-1
Adjustup(hp->a, hp->size - 1);
}
// 堆顶部的删除
void HeapPop(Heap* hp)
{
assert(hp);
assert(hp->size > 0);
swap(&hp->a[0], &hp->a[hp->size - 1]);//顶部与尾部交换
hp->size--;
//向下调整,调的是堆顶,所以父亲下标就是0
Adjustdown(hp->a, hp->size, 0);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
return hp->a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
return hp->size;
}
// 堆的判空
bool HeapEmpty(Heap* hp)
{
return hp->size == 0;
}
代码测试:
#include"heap.h"
int main()
{
int a[] = { 70, 56, 30, 25, 15, 10, 75 };
Heap hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(a[0]);i++)
{
HeapPush(&hp, a[i]);
}
printf("push建立小堆:");
HeapPrint(&hp);
printf("删除堆顶:");
HeapPop(&hp);
HeapPrint(&hp);
return 0;
}
3.2堆排序(堆的应用)
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
代码:
#include"heap.h"
void HeapSort(int*a, int n)
{ //升序建大堆,记得把向上调整和向下调整符号转变一下
//因为用的是向下调整,所以必须确定根的左右子树都为大堆,把n-1当做孩子节点下标找父亲节点,从堆的后面开始遍历
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
Adjustdown(a, n, i);
}
//for (int i = n - 1; i >0; i--)
//{
// Adjustup(a, i);
//}
堆顶与尾部交换,最大的数在尾部
for (int i = 0; i < n; i++)
{
swap(&a[0], &a[n - 1 - i]);
//交换之后,因为是大堆所以向下调整找次大的数,尾部被交换的数不计入调整的长度
Adjustdown(a, n - 1 - i, 0);
}
//for (int end = n - 1; end > 0; --end)
//{
// swap(&a[end], &a[0]);
// Adjustdown(a, end, 0);
//}
}
int main()
{
int a[] = { 70, 56, 30, 25, 15, 10, 75 ,33, 50, 69};
printf("排序前:");
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
printf("%d ", a[i]);
printf("\n");
printf("升序排大堆:");
HeapSort(&a,sizeof(a)/sizeof(a[0]));
printf("向下调整:");
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
3.3TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素 , 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
代码:
#include"heap.h"
void PrintTopK(int* a, int n, int k)
{
//找最大元素用小堆,找最小元素用大堆
// 1. 建小堆--用a中前k个元素建堆
Heap hp;
HeapInit(&hp);
int i = 0;
for (i = 0; i < k; i++)
{
//小堆,把向下调整和向上调整里面变小于号
HeapPush(&hp, a[i]);
}
// 2. 将剩余n-k个元素依次与堆顶元素比较,比堆顶大就替换掉堆顶元素
for (int j = k; j < n; j++)
{
if (a[j]>HeapTop(&hp))
{
hp.a[0] = a[j];
Adjustdown(hp.a, hp.size, 0);
/*HeapPop(&hp);
HeapPush(&hp, a[i]);*/
}
}
HeapPrint(&hp);
HeapDestroy(&hp);
}
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int)*n);
srand(time(0));
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[1] = 1000000 + 5;
a[2335] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 11;
a[3144] = 1000000 + 10;
PrintTopK(a, n, 10);
}
int main()
{
printf("前十个最大的数");
TestTopk();
return 0;
}
四、二叉树的链式结构
4.1二叉树的遍历
所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);
我们创建一颗简单二叉树来进行前中后序的遍历
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode*left;
struct BinaryTreeNode*right;
BTDataType data;
}BTNode;
BTNode* BuyNode(BTDataType x)
{
BTNode*node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
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;
}
//前序
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;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
//后序
void PostOrder(BTNode*root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
int main()
{
BTNode*root = CreatBinaryTree();
printf("前序:");
PreOrder(root);
printf("\n中序:");
InOrder(root);
printf("\n后序:");
PostOrder(root);
printf("\n");
return 0;
}
层序遍历:
设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历,刚好符合队列特性所以用队列实现即可
如下图,层序遍历的结果:A B C D E F (遇到NULL跳过)
代码:
注意:这里需要用到Queue.h和Queue.c(里面有队列的实现代码),需要的伙伴到栈和队列的文章取一下,需要把Queue.h稍微修改一下如下图
//层序遍历 用队列
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode*left;
struct BinaryTreeNode*right;
BTDataType data;
}BTNode;
void LevelOrder(BTNode* root)
{
if (root == NULL)
return false;
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode*front = QueueFront(&q);
QueuePop(&q);
printf("%c ", front->data);
//孩子带进队列
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q);
}
代码检测时记得创建二叉树,像前文前中后序代码段里面那样
4.2二叉树的功能实现
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
//二叉树的深度/高度
int BinaryTreeDepth(BTNode* root);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root);
代码实现:
//二叉树节点个数
//void BinaryTreeSize(BTNode* root, int*pn)
//{
//
// if (root == NULL)
// return;
// ++(*pn);
// BinaryTreeSize(root->left,pn);
// BinaryTreeSize(root->right,pn);
//
//}
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
return 0;
return BinaryTreeSize(root->left) +
BinaryTreeSize(root->right) + 1;
}
二叉树的叶子节点个数
int BinaryTreeLeafSize(BTNode*root)
{
if (root == NULL)
return 0;
if (root->left == NULL&&root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
二叉树的第k层节点数
int BinaryTreeLeveKSize(BTNode*root, int k)
{
assert(k >= 1);
if (root == NULL)
return 0;
if (k == 1)
return 1;
//root不为空,k也不是1,说明k层在root的子树里
//那就把root的左右子树各看为根,那原本求root的k变成求左右子树的k-1
//(对root来讲就是k,对root的子树来讲就是k-1)
return BinaryTreeLeveKSize(root->left,k-1) + BinaryTreeLeveKSize(root->right,k-1);
}
二叉树的深度/高度
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
return 0;
//求左右子树的深度,然后返回最大的+1
int left = BinaryTreeDepth(root->left);
int right = BinaryTreeDepth(root->right);
return left > right ? left + 1 : right + 1;
}
//二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
//先找左再右
BTNode*left = BinaryTreeFind(root->left, x);
if (left)
return left;
BTNode*right = BinaryTreeFind(root->right, x);
if (right)
return right;
//都没找到返空
return NULL;
}
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
return;
BinaryTreeDestory((*root)->left);
BinaryTreeDestory((*root)->right);
free(root);
*root = NULL;
}
//判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
//用层序遍历的思想,不管是否为空都入队列,完全二叉树的非空节点是连续的
//非完全二叉树的非空节点不是连续的
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode*front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
break;
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//遇到空以后,检查队列中剩下的节点
//1.剩下全是空,则是完全二叉树
//2.剩下的存在非空节点,则不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode*front = QueueFront(&q);
QueuePop(&q);
if (front)
return false;
}
QueueDestroy(&q);
return true;
}