目录
二叉树概念及结构
形如下图结构的被称为二叉树。
二叉树特点:
1、二叉树不存在大于2的结点。
2、二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
满二叉树与完全二叉树
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^k -1 ,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
二叉树的性质
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1)个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h-1 .
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为 n2,则有 n0=n2 +1
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1) . (ps:log2(n+1) 是log以2为底,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否则无右孩子
堆的概念及结构
堆的性质:
1、堆中某个节点的值总是不大于或不小于其父节点的值;
2、堆总是一棵完全二叉树。
堆的实现
堆的创建
根据存储结构可知,我们可以用一个数组来存储这些数据,然后进行调整(具体调整步骤后面会讲),直到它成为一个堆。
我们用typedef来将int重命名以便以后类型转换的便利。
typedef int HPDataType;
既然本质是一个数组,那么其实创建的过程跟我们之前学习的顺序表没有什么太大区别。
代码如下:
// 堆的构建
typedef int HPDataType;//后面的int都用HPDataType 来代替
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
堆的销毁
既然创建跟顺序表类似,那么销毁也没太大区别。
代码如下:
//堆的销毁
void HeapDestroy(Heap* hp)
{
assert(hp);
free(hp->a);
hp->capacity = hp->size = 0;
}
交换函数
便于交换数据,代码如下:
//交换数据
void Swap(HPDataType* x, HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
堆的打印
打印也如此,直接上代码:
//堆的打印
void HeapPrint(Heap* hp)
{
for (int i = 0; i < hp->size;i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
堆的插入
特点:
在插入的过程中要有重要的调整步骤,此步骤也是堆的关键步骤,也是我们学习堆的主要学习思想,堆的特性也是在插入的过程中不断的调整从而实现。
这里我把堆的特性再讲一下:
1、堆中某个节点的值总是不大于或不小于其父节点的值;
2、堆总是一棵完全二叉树。
主要是第一点,也就是插入的过程中必须保证这第一点,那么怎么才能保证这第一点呢?
我们首先看一下堆插入的代码:
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
HPDataType newCapacity = hp->capacity == 0 ? 4 : 2 * (hp->capacity);
HPDataType* tmp = realloc(hp->a, sizeof(HPDataType)*(newCapacity));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = newCapacity;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);//向上调整函数
}
聪明的你肯定发现了,这个操作其实与顺序表的插入并没有太大区别,但是插入过后要进行重要的一步,那就是向上调整函数。
向上调整算法
这里我们以小堆为例子,当我们插入一个数的时候,要让这个数与它的双亲进行比较,由于是小堆,必须保证双亲必须小于孩子,那么如果双亲大于孩子,那么就要进行交换。
如图是将一个10插入到一个堆里面,然后进行向上调整的过程。
代码如下:
//向上调整算法
//双亲序列(i-1)/2
void AdjustUp(HPDataType *a, HPDataType 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 HeapPop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
向下调整算法
我们这里还是以小堆为例,但是这里有一点需要注意的是,当根节点与孩子交换的时候,要找数值较小的那个数据进行交换,这样能保证根节点是两个孩子当中最小的。
向下调整算法详解图:
代码如下:
//向下调整算法
//左孩子=(2*parent+1) 右孩子=(2*parent+2)
void AdjustDown(HPDataType* a, HPDataType n, HPDataType root)
{
int parent = root;
int child = 2 * parent + 1;
while (child < n)
{
//这里的条件是小堆
//如果要实现大堆 将&&前边条件中的<改为>
if ((a[child + 1] <a[child]) && (child + 1) < n)
{
child++;
}
//这里的条件是小堆
//如果要实现大堆 将<改为>
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
获取堆顶元素
如果数组不为空的话直接返回数组下标为0的数据就可以了。
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
return hp->a[0];
}
获取堆的数据个数
直接返回size就可以了。
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
堆的判空
只要size为0,堆就为空。
// 堆的判空
bool HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
完整代码
Heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
HPDataType size;
HPDataType capacity;
}Heap;
//交换数据
void Swap(HPDataType* x, HPDataType* y);
// 堆的构建
void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestroy(Heap* hp);
//堆的打印
void HeapPrint(Heap* hp);
//向上调整算法
void AdjustUp(HPDataType* a, HPDataType child);
//向下调整算法
void AdjustDown(HPDataType* a, HPDataType n, HPDataType root);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
Heap.c
#include"Heap.h"
//交换数据
void Swap(HPDataType* x, HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
// 堆的构建
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
//堆的销毁
void HeapDestroy(Heap* hp)
{
assert(hp);
free(hp->a);
hp->capacity = hp->size = 0;
}
//堆的打印
void HeapPrint(Heap* hp)
{
for (int i = 0; i < hp->size;i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
//向上调整算法
//双亲序列(i-1)/2
void AdjustUp(HPDataType *a, HPDataType 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;
}
}
}
//向下调整算法
//左孩子=(2*parent+1) 右孩子=(2*parent+2)
void AdjustDown(HPDataType* a, HPDataType n, HPDataType root)
{
int parent = root;
int child = 2 * parent + 1;
while (child < n)
{
//小根堆
//由于是小根堆 所以找出左右孩子小的那个孩子
if ((a[child + 1] <a[child]) && (child + 1) < n)
{
child++;
}
//小根堆
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
HPDataType newCapacity = hp->capacity == 0 ? 4 : 2 * (hp->capacity);
HPDataType* tmp = realloc(hp->a, sizeof(HPDataType)*(newCapacity));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = newCapacity;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);
}
// 堆的删除
void HeapPop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
return hp->a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
// 堆的判空
bool HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
test.c
#include"Heap.h"
void TestHeap()
{
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]);
}
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
HeapDestroy(&hp);
}
int main()
{
TestHeap();
return 0;
}
TopK问题
TopK问题:找出N个数里面最大/最小的前K个问题。
比如:长葛排名前10的胡辣汤,郑州科技学院大学王者荣耀排名前10的韩信,全国排名前10的李白。等等问题都是Topk问题,
需要注意:
找最大的前K个,建立K个数的小堆
找最小的前K个,建立K个数的大堆
解析:
假设我们需要求N个数里面最大的前k个问题,我们就要建一个小堆,那么为什么我们不建大堆呢? 倘若我们建了大堆,如果第一个就是我们所要找的最大数,那么无论怎么向下调整,其他数都进不去。
代码如下:
void PrintTopK(int* a, int n, int k)
{
Heap hp;
HeapInit(&hp);
//创建一个有k个数据小堆
for (int i = 0; i < k; i++)
{
HeapPush(&hp, a[i]);
}
for (int i = 0; i < n; i++)
{
if (a[i] > HeapTop(&hp))
//如果遍历的数据大于堆顶
//就将其插入
{
HeapPop(&hp);
HeapPush(&hp, a[i]);
}
}
HeapPrint(&hp);
HeapDestroy(&hp);
}
void TestTopk()
{
int n = 1000000;
int* a = (int*)malloc(sizeof(int) * n);//为a创建空间
if (!a)
{
printf("malloc failed");
exit(-1);
}
srand((unsigned)time(0));
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
// 再去设置10个比100w大的数
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[5355] = 1000000 + 3;
a[51] = 1000000 + 4;
a[15] = 1000000 + 5;
a[2335] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[3144] = 1000000 + 10;
PrintTopK(a, n, 10);
}
堆排序
具体思想:
排升序,建大堆。
排降序,建小堆。
这里我们用降序,建小堆为例:
给定一个数组,我们要对所有的结点进行向下调整,但是由于根节点没有孩子,所以我们从最后一个非叶子结点进行向下调整,并且依次向根节点遍历,直到根节点,在此之间遍历的所有结点都进行向下调整。
如图所示:
由此可见我们用上述方法成功的遍历出了数组中最小的那个值,因此我们接下来就是要逐次遍历出倒数第二小的值,倒数第三小的值。。。。等等,直到将数组成功排序。
方法如下:
将根节点与最后一个节点进行交换,然后将除了最后一个节点的其他节点再看成一个新堆,再进行遍历,那么就能成功的遍历出倒数第二小的数了,依次类推。
堆排序详解图:
入图所示:
最终堆将以降序排列,依次输出就可以啦。
代码如下:
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1)/2; i >= 0; i--)
{
AdjustDown(a, n , i);
}
for (int end = n - 1; end > 0; end--)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
}
}
int main()
{
int a[] = { 16,17,5,20,3,4 };
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]);
}
printf("\n");
HeapSort(a, sizeof(a) / sizeof(a[0]));
for (int i = 0; i < sizeof(a) / sizeof(a[0]);i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}