文章目录
前言
在介绍堆之前需要介绍一下树和二叉树的概念,因为堆是一种数据结构,是将集合中所有的元素按完全二叉树的顺序存储方法存储在一个数组中
一、什么是树
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树,也就是它是根朝上,叶子朝下的
- 有一个特殊的结点,称为根节点,根节点没有前驱节点
- 每棵子树的根节点有且只有一个前驱,可以有0个或多个后驱
1.1 树的相关概念
节点的度: 一个节点含有的子树的个数,如上图:A的度为6;
叶节点或终端节点:度为0的节点称为叶节点,如上图:B、C、H、I…等节点为叶节点;
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有一个子节点,则这个节点称为其子节点的父节点,如上图:A是B的父节点;
孩子节点或子节点:一个节点含有的子树的根根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点,如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度,如上图:树的度为6;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次,如上图:树的高度为4;
堂兄弟节点双亲在同一层的节点互为堂兄弟,如上图:H、I互为兄弟节点;
节点的祖先:从根到该节点所经分支上的所有节点,如上图:A是所有节点的祖先;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙,如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;
1.2 树的表示
实际中树有很多种表示方式如:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法等。这里就简单了解其中最常用的孩子兄弟表示法
typedef int DataType;
struct Node
{
struct Node* _firstChild1; //第一个孩子节点
struct Node* _pNextBrother; //指向其下一个兄弟节点
DataType _data; //节点中的数据
};
1.4 树在实际中的应用
表示文件系统的目录树结构
二、什么是二叉树
一个二叉树是节点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两颗别称为左子树和右子树的二叉树组成
注:二叉树不存在度大于2的节点
2.1 特殊的二叉树
- 满二叉树: 一个二叉树,如果每一层的节点都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且节点总数是2^k-1,则它就是满二叉树;
- 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。深度为k且有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从1至n的节点一一对应时称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树;
2.2 二叉树的性质
- 若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个节点
- 若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2^h - 1
- 对任何一颗二叉树,如果度为0其叶节点个数为n0,度为2的分支节点个数为n2,则n0=n2+1
- 若规定根节点的层数为1,具有n个节点的满二叉树的深度,h=log(n+1).(log(n+1)是log以2为底,n+1为对数)
- 对具有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否则无右孩子
三、堆
3.1 堆的结构
堆的性质:
- 堆中某个节点的值总是不大于或者不小于其父节点的值;
- 堆总是一颗完全二叉树
堆有小堆和大堆之分:
- 小堆:父节点总是小于它的子节点
- 大堆:父节点总是大于它的子节点
3.2 堆的实现
3.2.1 堆向下调整算法
给出一个数组,逻辑上看做一颗完全二叉树。通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法的前提:左右子树必须是一个堆,才能调整
int array[] = {
27,15,19,18,28,34,65,49,25,37};
具体实现:
void Swap(int* left, int* right)
{
int tmp = *left;
*left = *right;
*right = tmp;
}
void AdjustDown(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (a[child] > a[child + 1])
{
child = child + 1;
}
if (hp->_a[child] < hp->_a[parent])
{
Swap(&hp->_a[child], &hp->_a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
3.2.2 建堆
建堆时,根节点左右子树不是堆,该怎么调整呢?这里从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆了
int a[] = {
1,5,3,8,7,6};
建大堆:
void Swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void AdjustDown(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if ((child+1)< size && a[child] < a[child + 1])
child = child + 1;
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
void Print(int a[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
int main()
{
int a[] = {
1,5,3,8,7,6 };
int size = sizeof(a) / sizeof(a[0]);
//建大堆
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdjustDown(a, size, i);
}
Print(a,size);
return 0;
}
运行结果:
建小堆:
void Swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void AdjustDown(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if ((child+1)<size && a[child] > a[child + 1])
child = child + 1;
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
void Print(int a[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
int main()
{
int a[] = {
1,5,3,8,7,6 };
int size = sizeof(a) / sizeof(a[0]);
//建小堆
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdjustDown(a, size, i);
}
Print(a,size);
return 0;
}
运行结果:
建堆的时间复杂度为O(N).
3.2.3 堆的插入
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆
具体代码实现:
void Swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void Print(int a[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
void AdjustUp(int a[], int size,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;
}
}
}
int main()
{
int a[] = {
15,18,19,25,28,34,65,49,27,37,10 };
int size = sizeof(a) / sizeof(a[0]);
AdjustUp(a, size, size-1);
Print(a, size);
return 0;
}
运行结果:
3.2.4 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据和最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法
3.2.5 数据结构和接口(声明)
//#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
//向下调整
void AdjustDown(Heap* hp,int child);
//向下调整
void AdjustUp(Heap* hp,int parent);
void HeapPrint(Heap* hp);
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
3.2.6 接口函数的定义(实现)
#include "Heap.h"
void Swap(int* left, int* right)
{
int tmp = *left;
*left = *right;
*right = tmp;
}
//向下调整,建立大堆
void AdjustDown(Heap* hp,int parent)
{
int child = parent * 2 + 1;
while (child < hp->_size)
{
if ((child+1)< hp->_size && hp->_a[child] < hp->_a[child + 1])
{
child = child + 1;
}
if (hp->_a[child] > hp->_a[parent])
{
Swap(&hp->_a[child], &hp->_a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
//向上调整
void AdjustUp(Heap* hp , int child)
{
int parent = (child - 1) / 2;
while (child != 0)
{
if (hp->_a[child] > hp->_a[parent])
{
Swap(&hp->_a[child], &hp->_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 HeapCreate(Heap* hp, HPDataType* a, int n)
{
assert(hp && a);
hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * (n+1));
hp->_capacity = n+1;
memcpy(hp->_a, a, n*sizeof(HPDataType));
hp->_size = n;
//建大堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(hp, i);
}
}
// 堆的销毁
void HeapDestory(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)
{
HPDataType* pa = (HPDataType*)realloc(hp->_a, sizeof(HPDataType)\
* hp->_capacity * 2);
if (pa == NULL)
{
perror(" realloc fail \n");
exit(-1);
}
else
{
hp->_a = pa;
}
}
hp->_a[hp->_size++] = x;
AdjustUp(hp, hp->_size - 1);
}
// 堆的删除
void HeapPop(Heap* hp)
{
assert(hp);
Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
hp->_size--;
AdjustDown(hp, 0);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
return hp->_a[0];
}
//堆的数据个数
int HeapSize(Heap* hp)
{
return hp->_size;
}
// 堆的判空
int HeapEmpty(Heap* hp)
{
if (hp->_size == 0)
return 1;
return 0;
}
3.2.7 堆的调用
void TestHeap()
{
int a[] = {
1,5,3,8,7,6 };
int size = sizeof(a) / sizeof(a[0]);
Heap p;
HeapCreate(&p, a, size);
HeapPrint(&p);
HeapPush(&p, 10);
HeapPrint(&p);
HeapPop(&p);
HeapPrint(&p);
int Top = HeapTop(&p);
int sz = HeapSize(&p);
printf("%d\n", Top);
printf("%d\n", sz);
HeapDestory(&p);
}
运行结果如下:
3.3 堆的应用
3.3.1 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
- 升序:建大堆
- 降序:建小堆
- 利用堆删除思想来进行排序
建堆和堆删除都用到了向下调整,因此掌握了向下调整,就可以完成堆排序
3.3.2 TOP-K问题
基本思路如下:
- 用数据集合中前K哥元素来建堆
1. 前K个最大的元素,则建小堆
2. 前K个最小的元素,则建大堆 - 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素