二叉树的概念和应用:
- 一.树的概念和结构:
- 一.树的概念和结构:
- 1.树的概念:
- 2.树的相关概念:
- 3.树的表示:
- 二.二叉树的概念和结构:
- 1.概念:
- 2.两种特殊的二叉树:
- 1.完全二叉树:
- 2.满二叉树:
- 3.二叉树的性质:
- 题目一:
- 题目二:
- 题目三:
- 题目四:
- 题目五:
- 4.二叉树的存储结构:
- 三.二叉树的顺序存储结构和实现:
- 1.二叉树的顺序结构:
- 2.堆的概念和结构:
- 1.插入数据
- 2获取堆顶数据(数据交换和数据减少)
- 3.堆的代码实现:
- 四.堆的应用:
- 1.堆排序:
- 1.建堆:
- 2.利用堆的删除思想去进行排序(这是一种原地排序)
- 3.整体代码:
- 2.TOPK问题:
一.树的概念和结构:
一.树的概念和结构:
1.树的概念:
我们之前学习过了顺序表和链表这样比较简单的数据结构这些结构是一种线性结构,关于树他是一个树形结构,一个数有n个节点(n>=0),这些节点按层去排列,构成了一个树的结构。但是呢关于一个树他是根在上叶子在下的。
对于一颗树:
1.根节点:一个树只有一个根节点,他本身是没有前驱节点。
2.子树:除了根节点之外的其他的节点在数形结构中构成的集合,每一个子树的根节点有且只有一个前驱,和多个后继。
3.树是由递归==(把一个比较复杂的问题经过一次一次的递归去简化问题的复杂度)==定义的:
eg:
补充:在树形结构中,树形结构不能有子树相交。
必须满足一个根有一个前驱和多个后继当然也可以没有后继:
2.树的相关概念:
补充:层数的概念提供了两个记录的方法:
1.层数从1开始。
2.层数从0开始。
1.使用0和1分别作为起始的层数。
2.分别对上面的情况进行观察对比:
节点的度:一个节点含有的子树的个i树就是度。(A度是6)。
叶节点或终端节点:度为0的节点(只有一个前驱和一个后继)。
非终端节点或分支节点:度不为0的节点。
双亲节点或父节点:一个节点有自己的子节点,这个节点就是他子节点的父节点。
孩子节点或子节点:一个节点的子树的根节点,就是他的孩子节点。
兄弟节点:有相同父节点的子节点,这些子节点之间的关系就是兄弟关系。
树的度:对于一个树,找到一个节点有这这颗树最大的度(代表这个树的度)。
节点的层次:前面补充了:从1开始。
树的高度或深度:树的最大层次。
堂兄弟节点:双亲节点在同一层的节点之间被称为堂兄弟。
节点的祖先:从这个节点到我们1层的根节点中路径上的所有节点。
子孙:从这个节点下面的所有节点都是子孙。
森林:由m个不相交的树称为集合。
3.树的表示:
1.孩子兄弟表示法在这里是最常用的一个:
相当于把一个一个链表的头使有第一个孩子节点的方法连接在一起多个链表,构成了这样的逻辑结构。
二.二叉树的概念和结构:
1.概念:
一颗二叉树是一个节点的有限集合(对于集合来说)。
1.集合为空(根节点没有子树):
2.由一个根节点加上两颗(二叉树)子树构成的集合:
1.二叉树不存在度大于二的节点。
2.二插树的左右子树有次序之分,因此二叉树是一个有序树。
3.二叉树的复合(由下面几种复合而成:)。
2.两种特殊的二叉树:
1.完全二叉树:
一个二叉树每一层的节点都达到了最大,这个二叉树就是满二叉树。
计数节点个数,观察发现一个二叉树(层数为k)的节点个数为2^k-1.
通过等比数列的求和公式计算。
2.满二叉树:
完全二叉树是一种特殊的满二叉树(假设二叉树有K层)
1.要求二叉树的前面的K-1层必须是满的。
2.第K层必须要有一个节点到满层的一个范围。
3.总结:[2 ^ k-1(-1+1) 2^k-1 ]
3.二叉树的性质:
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(h-1) 个结点.
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h - 1.
- 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有n0 = n2+1
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log(n+1) . (ps:log(n+1) 是log以2为底,n+1为对数)
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
- 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
- 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
- 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
题目一:
1 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( B)
A 不存在这样的二叉树
B 200
C 198
D 199
题目二:
2.下列数据结构中,不适合采用顺序存储结构的是( A)
A 非完全二叉树
B 堆
C 队列
D 栈
题目三:
3.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2
题目四:
4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( B)
A 11
B 10
C 8
D 12
题目五:
5.一个具有767个节点的完全二叉树,其叶子节点个数为(B)
A 383
B 384
C 385
D 386
4.二叉树的存储结构:
二叉树可以通过两种形式去存储数据(顺序结构:链式结构)
1.顺序存储:
顺序结构就是拿数组去存储数据,这样的结构只适合用来存储完全二叉树。
如果二叉树种间存在一个节点只连接一个节点的情况会导致数组结构的问题:
1:数组下标为0的去存储我们的根节点:
2.从左到右依次存储:
3.子和父节点的下标计算:
leftchild=parent2+1
rightchild=parent2+2
parent=(child-1)/2
如图:
三.二叉树的顺序存储结构和实现:
1.二叉树的顺序结构:
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
2.堆的概念和结构:
如果有一个数据的一维数组K={k1,k2,k3,k4,k5……kn};我们把这个数组按照完全二叉树去构建一个逻辑结构,如果一维数组中满足(堆适合使用堆去存储):
ki<=k2i+1ork2i+2 —>小堆
ki>=k2i+1ork2i+2---->大堆
补充:
1.小堆的根是这个集合中最小的值:
2.堆的应用:
–1.topk问题(取排行榜的前面数据)
–2.堆排序O(n*logn)
1.插入数据
1-1.一个一个数据的插入保证每一个插入之后变成堆的结构都是一个大堆或者小堆。所以我们需要一个向上调整算法,新插入的数据需要和前面的比较(以下的举例都是小堆)
1-2:时间复杂度:
//3-1:进行向上调整:
void AdjustHeapUp(struct Heap* hp)
{
assert(hp);
//排一个升序(空复是N)(建立一个小堆)
//找父节点:parent=child-1/2;
int child = (hp->size) - 1;
int parent = (child - 1) / 2;
//向上调整如何结束:
while (child>0)
{
//进行单次的调整:
if (hp->str[child] < hp->str[parent])
{
swap(&hp->str[child], &hp->str[parent]);
}
else
{
break;
}
//向上调整数据的更新:
child = parent;
parent = (child - 1) / 2;
}
}
//3.插入数据
void HeapPush(struct Heap* hp, HeapData x)
{
assert(hp);
//边插入数据边调整:
//1.开始的时候没有开辟空间://2.后续的增容:
if (hp->size == hp->capacity)
{
hp->capacity = (hp->str == NULL ? 4 : hp->capacity * 2);
HeapData* tmp = (HeapData*)realloc(hp->str, sizeof(HeapData) * hp->capacity);
if (tmp == NULL)
{
perror("realloc filad");
exit(-1);
}
hp->str = tmp;
}
//2.插入数据:
hp->str[hp->size] = x;
hp->size++;
//3进行向上调整:
AdjustHeapUp(hp);
}
2获取堆顶数据(数据交换和数据减少)
1.获取堆顶数据:
2.交换堆顶数据和最后一个数据:
3.再减少堆中的一个数据:
4.向下调整(数据交换之后需要保持小堆)
时间复杂度的计算:
//4.获取(堆顶)数据
HeapData heapGetTop(struct Heap* hp)
{
//不可以指针为空,或者没有数据:
assert(hp);
assert(hp->size != 0);
//保存一下数据:
HeapData n= hp->str[0];
//交换:
int tmp = (hp->size)-1;
swap(&hp->str[0], &hp->str[tmp]);
return n;
}
//5-1:向下调整:
void AdjustHeapDown(struct Heap* hp)
{
assert(hp);
int parent = 0;
int child = (parent * 2) + 1;
while (child <= hp->size-1)
{
//不知道左右节点哪一个小假设:
//child+1 有可能会越界
if ((child + 1) < hp->size)
{
hp->str[child] < hp->str[child + 1] ? child : child++;
if (hp->str[child] < hp->str[parent])
{
swap(&hp->str[child], &hp->str[parent]);
}
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
}
//5.删除数据:
void Heappop(struct Heap* hp)
{
assert(hp);
assert(hp->size != 0);
//1.减去最后一个:
hp->size--;
//2.向下调整,变回小堆:
AdjustHeapDown(hp);
}
3.堆的代码实现:
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<string.h>
#include<stdlib.h>
typedef int HeapData;
//堆的结构
struct Heap {
HeapData* str;
int size;
int capacity;
}Hp;
//初始化:
void HeapInit(struct Heap* hp);
//销毁:
void HeapDestory(struct Heap* hp);
//插入数据
void HeapPush(struct Heap* hp, HeapData x);
//获取(堆顶)数据
HeapData heapGetTop(struct Heap* hp);
//删除数据:
void Heappop(struct Heap* hp);
//打印一下堆:
void Heapprint(struct Heap* hp);
//判断堆是否为空:
bool ispowe(struct Heap* hp);
#define _CRT_SECURE_NO_WARNINGS 1
#include"heap.h"
//1.初始化:
void HeapInit(struct Heap* hp)
{
//防止为空指针:
assert(hp);
//不想在初始化的时候开辟空间数据:
hp->str = NULL;
hp->capacity = 0;
hp->size = 0;
}
//2.销毁:
void HeapDestory(struct Heap* hp)
{
//防止为空指针:
assert(hp);
free(hp->str);
hp->str = NULL;
hp->capacity = 0;
hp->size = 0;
}
//3-1-1:交换函数:
void swap(HeapData* a, HeapData* b)
{
HeapData tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
}
//3-1:进行向上调整:
void AdjustHeapUp(struct Heap* hp)
{
assert(hp);
//排一个升序(空复是N)(建立一个小堆)
//找父节点:parent=child-1/2;
int child = (hp->size) - 1;
int parent = (child - 1) / 2;
//向上调整如何结束:
while (child>0)
{
//进行单次的调整:
if (hp->str[child] < hp->str[parent])
{
swap(&hp->str[child], &hp->str[parent]);
}
else
{
break;
}
//向上调整数据的更新:
child = parent;
parent = (child - 1) / 2;
}
}
//3.插入数据
void HeapPush(struct Heap* hp, HeapData x)
{
assert(hp);
//边插入数据边调整:
//1.开始的时候没有开辟空间://2.后续的增容:
if (hp->size == hp->capacity)
{
hp->capacity = (hp->str == NULL ? 4 : hp->capacity * 2);
HeapData* tmp = (HeapData*)realloc(hp->str, sizeof(HeapData) * hp->capacity);
if (tmp == NULL)
{
perror("realloc filad");
exit(-1);
}
hp->str = tmp;
}
//2.插入数据:
hp->str[hp->size] = x;
hp->size++;
//3进行向上调整:
AdjustHeapUp(hp);
}
//4.获取(堆顶)数据
HeapData heapGetTop(struct Heap* hp)
{
//不可以指针为空,或者没有数据:
assert(hp);
assert(hp->size != 0);
//保存一下数据:
HeapData n= hp->str[0];
//交换:
int tmp = (hp->size)-1;
swap(&hp->str[0], &hp->str[tmp]);
return n;
}
//5-1:向下调整:
void AdjustHeapDown(struct Heap* hp)
{
assert(hp);
int parent = 0;
int child = (parent * 2) + 1;
while (child <= hp->size-1)
{
//不知道左右节点哪一个小假设:
//child+1 有可能会越界
if ((child + 1) < hp->size)
{
hp->str[child] < hp->str[child + 1] ? child : child++;
if (hp->str[child] < hp->str[parent])
{
swap(&hp->str[child], &hp->str[parent]);
}
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
}
//5.删除数据:
void Heappop(struct Heap* hp)
{
assert(hp);
assert(hp->size != 0);
//1.减去最后一个:
hp->size--;
//2.向下调整,变回小堆:
AdjustHeapDown(hp);
}
//打印一下堆:
void Heapprint(struct Heap* hp)
{
assert(hp);
for (int i = 0; i < hp->size; i++)
{
printf("%d ", hp->str[i]);
}
printf("\n");
}
//判断堆是否为空:
bool ispowe(struct Heap* hp)
{
assert(hp);
if (hp->size == 0)
{
return false;
}
return true;
}
#include"heap.h"
int main()
{
HeapData arr[] = { 2,3,1,8,10,5,12,19,8 };
int n = sizeof(arr) / sizeof(arr[0]);
//初始化:
struct Heap hp;
HeapInit(&hp);
for (int i = 0; i < n; i++)
{
HeapPush(&hp, arr[i]);
}
//打印看一下小堆:
Heapprint(&hp);
//进行升序打印
for (int j = 0; j < n; j++)
{
printf("%d ", heapGetTop(&hp));
//删除数据:
Heappop(&hp);
}
//Heapprint(&hp);
HeapDestory(&hp);
return 0;
}
四.堆的应用:
1.堆排序:
1.建堆:
*升序:建立大堆
*降序:建立小堆
2.利用堆的删除思想去进行排序(这是一种原地排序)
1.建堆和堆的删除都使用了向下调整的算法只要会向下调整就可以拿捏堆排序:
2.排序:
1.给函数传一个数组。
2.函数原地的去排序。
3.把数组的内容排成一个需要的序:
4.(这是一种原地排序)
问题一(建堆):
1.开始顺序的调整:
从后往前第一个非叶子节点开始向下调整:
#include"heap.h"
//3-1-1:交换函数:
void swapQ(HeapData* a, HeapData* b)
{
HeapData tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
}
//5-1:向下调整:
void AdjustQsortHeapDown(HeapData* str,int n)
{
assert(str);
while (n>=1)
{
//arr是数组,n是元素个数:
int parent = (n - 1 -1)/2;
int child = parent * 2 + 1;
//孩子作为根的时候说明建堆完成:
//child的范围:
while (child <= n-1)
{
//不知道左右节点哪一个大:假设:
//child+1 有可能会越界
if (((child) <= n - 1) )
{
//产生数组越界问题:
if (child + 1 < n)
{
(str[child] > str[child + 1] ? child : child++);
}
if (str[child] > str[parent])
{
swapQ(&str[child], &str[parent]);
}
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
n--;
}
}
问题二(位置的调整)
1.把堆顶和堆尾进行交换:
2.减少堆的数据个数减少范围:
3.再一次调整重复之前的操作:
void HeapQsort(HeapData* arr,int n)
{
while (n)
{
//向下调整
AdjustQsortHeapDown(arr, n);
//进行首尾的交换,把大的放到应该的位置。
swapQ(&arr[0], &arr[n - 1]);
n--;
}
}
3.整体代码:
#include"heap.h"
//3-1-1:交换函数:
void swapQ(HeapData* a, HeapData* b)
{
HeapData tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
}
//5-1:向下调整:
void AdjustQsortHeapDown(HeapData* str,int n)
{
assert(str);
while (n>=1)
{
//arr是数组,n是元素个数:
int parent = (n - 1 -1)/2;
int child = parent * 2 + 1;
//孩子作为根的时候说明建堆完成:
//child的范围:
while (child <= n-1)
{
//不知道左右节点哪一个大:假设:
//child+1 有可能会越界
if (((child) <= n - 1) )
{
//产生数组越界问题:
if (child + 1 < n)
{
(str[child] > str[child + 1] ? child : child++);
}
if (str[child] > str[parent])
{
swapQ(&str[child], &str[parent]);
}
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
n--;
}
}
//2.堆排序的功能函数:
void HeapQsort(HeapData* arr,int n)
{
while (n)
{
//向下调整
AdjustQsortHeapDown(arr, n);
//进行首尾的交换,把大的放到应该的位置。
swapQ(&arr[0], &arr[n - 1]);
n--;
}
}
2.TOPK问题:
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
2.前k个最大的元素,则建小堆
3.前k个最小的元素,则建大堆- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
int main()
{
/*srand((unsigned int)time(NULL));
1.打开文件用来写数据:
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen filed");
exit(-1);
}
int num = 100000;
while (num--)
{
2.写数据:
int n = rand() % 1000000;
fprintf(pf, "%d\n", n);
}
关闭数据:
fclose(pf);*/
//1.打开文件用来写数据:
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen filed");
exit(-1);
}
//2.数据写到堆中:
int num = 100000;
//3.前面k个建堆:
int k = 10;
int* arr = (int*)malloc(sizeof(int) * k);
if (arr == NULL)
{
perror("malloc file");
exit(-1);
}
for(int i=0;i<k;i++)
{
int tmp = 0;
fscanf(pf,"%d", &tmp);
//插入法建堆:
arr[i] = tmp;
creatHeap(arr,i);
}
int number = num - k;
//4.后面N-k个比较:
for (int j = 0; j< number; j++)
{
int tp = 0;
fscanf(pf, "%d", &tp);
AdjustTOPtHeapDown(arr, k ,tp);
}
for (int a = 0; a < k; a++)
{
printf("%d ", arr[a]);
}
//关闭数据:
fclose(pf);
return 0;
}
1.插入建堆:
//1.插入法建堆:
void creatHeap(HeapData* arr, int k)
{
assert(arr);
//排一个升序(空复是N)(建立一个小堆)
//找父节点:parent=child-1/2;
int child = k - 1;
int parent = (child - 1) / 2;
//向上调整如何结束:
while (child > 0)
{
//进行单次的调整:
if (arr[child] < arr[parent])
{
swapQ(&arr[child], &arr[parent]);
}
else
{
break;
}
//向上调整数据的更新:
child = parent;
parent = (child - 1) / 2;
}
}
2.比较插入第一给个(进行向下调整)
//2.向下调整:
void AdjustTOPtHeapDown(HeapData* arr, int n, HeapData tmp)
{
assert(arr);
if (tmp > arr[0])
{
arr[0] = tmp;
//arr是数组,n是元素个数:
int parent = 0;
int child = parent * 2 + 1;
//孩子作为根的时候说明建堆完成:
//child的范围:
while (child <= n - 1)
{
if (child <= n - 1)
{
if (child + 1 < n)
{
(arr[child] < arr[child + 1] ? child : child++);
}
if (arr[child] < arr[parent])
{
swapQ(&arr[child], &arr[parent]);
}
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
}
}