树的几个重要定义
1.树=根+子树=根+亲缘关系
2.节点的度:有几个子树或根有几个孩子
3.叶子节点:没有孩子的终端节点 度为0
4.分支节点:度不为0的节点
5.树=叶子+分支节点
6.父亲节点/双亲节点
7.子节点
8.树的度:最大节点的度就是树的度
9.树的层:一般从第一层开始数,也有从0层开始数的,但是多数情况下从第一层开始
10.树的高/深度:一共多少层
11.空树的度:0
为什么会有0层这个少数概念?
数组我们是从下标为0开始的
为什么呢—>为了方便计算
我们都是知道 数组名=首元素地址
a[i]=*(a+i);
数组从零开始就是为了方便计算
12.森林:多颗互不相交的树----->并查集
树是由递归定义的:任何一棵树=[根+N颗子树(N>=0)]
注意:树的子树之间是不相交的
若子树相交则为—>图
一颗有N个节点的数有N-1条边
树给如何代码定义呢?
这里循序渐进的介绍三种方法,其中最后一种是最绝妙最常用的方法
(1).
//1.明确度 N=4
#define N 4
struct TreeNode
{
int val;
struct TreeNode* sub[N];//指针数组
};
(2).
//2.未明确度
SeqList subs;//顺序表内部存struct TreeNode* 在C语言就有些麻烦
//C++中有 vector<struct TreeNode*>suns;
(3).
//3.左孩子右兄弟法
struct TreeNdoe
{
int val;
struct TreeNode* leftchild;
struct TreeNode* rightbrother;
};
/*
不管我有几个孩子
我都只想左边第一个孩子
child 指向左孩子
brother指向兄弟
*/
这是树的定义
在树的结构中我们最常用的是二叉树
二叉树的定义就只有左孩子和右孩子,他至多两个孩子,不存在多个兄弟
二叉树:满二叉树 和 完全二叉树
满二叉树:高度为H ,一共有2^H-1个节点
完全二叉树:前H-1层都是满的,最后一层不满,但从左到右必须连续存在
节点个数范围:2^H~2 ^H-1
所以满二叉树是特殊的完全二叉树
二叉树是能存非常多的节点的
假设有N个节点
2^H-1=N
H=log(N)+1
H=20 能存100W+个节点
H=30 能存10亿+个节点
二叉树的存储—数组
假设父亲在数组的下标是i
左孩子=2i+1
右孩子=2i+2
假设孩子在数组的下标是j
需要判断孩子是左孩子还是有孩子吗---->不用判断
除法运算直接取整
父亲=(j-1)/2
关于数组的存储方式是只适用于完全二叉树的
非完全二叉树倒是也能用就是不适合
非完全二叉树通常用链式结构存储
堆
1.堆是一个完全二叉树----->数组存储
2.大堆:任何一个父亲都大于等于孩子
小堆:任何一个父亲都小于等于孩子
注意:大堆小堆都不是严格的升序降序,因为孩子之间并无大小关系
但是小堆的根是最小的,大堆的根是最大的
堆在逻辑上:是一棵树
物理上:是一个数组
定义堆的代码
Heap.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.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 HPPush(HP* php,HPDataType x);
void HPPop(HP* php);
Heap.c
#include"Heap.h"
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(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 HPPush(HP* php,HPDataType x)
{
//利用向上调整法 插到堆尾
//根据是大堆小堆 与父亲比较调整位置
assert(php);
//先看看有没有空间插入
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
//原来空间就为0先给4个,原来有空间现在没地方插直接扩2倍
HPDataType* tmp = (HPDataType*)realloc(php->a,newcapacity * sizeof(HPDataType));
//判断扩容成功与否
if (tmp = NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
//x插到堆尾
php->a[php->size] = x;
php->size++;
Adjustup(php->a, php->size - 1);//把堆尾元素当做孩子与父亲比较进行位置调整
}
以上就只有插入及插入所需要的向上调整算法
通过一个数组实现堆:
#include"Heap.h"
void TestHeap01()
{
int a[] = { 4,2,8,1,5,6,7,9 };
HP hp;
HPInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&hp, a[i]);//插入并及时调整位置
}
}
向下调整算法
//向下调整算法
void Adjustdown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//n就是数组大小
//向上调整找父亲,向下调整找孩子
//找那个孩子呢? 假设法假设左孩子小,找左孩子
//判断下到底是哪个孩子小
if (child + 1 < n && a[child + 1] < a[child])
//if (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 HPPop(HP* php)
{
/*
将堆顶与堆尾互换,删除堆尾,再利用向下调整算法调整位置
*/
assert(php);
assert(php->size > 0);
Sawp(&php->a[0], &php->a[php->size - 1]);//顶尾交换
//删除尾部就很简单直接--
php->size--;
Adjustdown(php->a, php->size, 0);//从堆顶一个一个向下比较调整
}
注意:在这里我们得到的都是小堆
实现堆:是由数组一个一个的插入(插入中包含向上调整算法)
我们要控制得到的是大堆还是小堆:
可以通过控制两个调整算法的判断比较条件来实现
以上示例只能怪都是<号
若想得到大堆就改成>号
注意:若想使用向下调整,左右子树必须是小堆
返回堆顶元素
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
Push尾插 利用向上调整法
Pop尾删 利用向下调整法
实现一个打印有序(并不是严格的排序)
1.实现一个堆
2.因为通过调整算法后一定是有序的
所以打印顶,再删除顶,在继续打印出来就一定是有序的
但这并不是堆排序
int main()
{
int a[] = { 4,2,8,1,5,6,9,7 };
HP hp;
HPInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&hp, a[i]);//插入并及时调整位置
}
while (!HPEmpty)
{
printf("%d", HPTop(&hp));
HPPop(&hp);
}
return 0;
}
注意:这只是打印出来是有序的,同样到底想升序还是降序去改变两种调整算法的判断条件
Top K----->相同的逻辑
寻找最大的前K个
int main()
{
int a[] = { 4,2,8,1,5,6,9,7 };
HP hp;
HPInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&hp, a[i]);//插入并及时调整位置
}
int k;
scanf("%d", &k);
while (k--)
{
printf("%d", HPTop(&hp));
HPPop(&hp);
}
return 0;
}
这个算法的时间复杂度:log(N)
算是很快的算法
10亿个数据 只需要调30次
前面我们只是实现了打印有序,并未让数组变为有序
接下来的代码我们将让数组a变为有序数组
int main()
{
int a[] = { 4,2,8,1,5,6,9,7 };
HP hp;
HPInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&hp, a[i]);//插入并及时调整位置
}
int i = 0;
while (!HPEmpty)
{
a[i++] = HPTop(&hp);//直接把顶放进数组中
HPPop(&hp);
}
//出循环就整个数组都是有序的了
return 0;
}
如何建堆
向上调整法建堆
时间复杂度:O(N*logN)
void HeapSort(int *a,int n)
{
for (int i = 1; i < n; i++)
{
Adjustup(a, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
Adjustdown(a, end, 0);
--end;
}
}
向下调整法建堆
时间复杂度:O(N)
从最后一个分支节点开始调
也就是最后一个叶子的父亲
一个子树一个子树的调
4 2 8 1 5 6 9 7 2 7 9
最后一个分支节点是5
从5的位置开始–,像前面依次调整位置
void HeapSort(int* a, int n)
{
for (int i = (n-1-1)/2; i >=0; i--)
{
Adjustdown(a,n,i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
Adjustdown(a, end, 0);
--end;
}
}