数据结构(初阶)笔记归纳9:堆

目录

一、树的概念和结构

1.1.树的概念

1.2.树的相关概念

1.3.树的递归定义

1.4.区分树和非树

1.5.树的节点定义

1.5.1.明确树的度为N

1.5.2.没有明确树的度

1.6.树的应用

二、二叉树的概念及结构

2.1.二叉树的概念

2.1.二叉树的性质

2.2.特殊的二叉树

三、二叉树与数组

3.1.完全二叉树与数组

3.2.非完全二叉树与数组

四、堆

4.1.堆的概念

4.2.堆的特点

4.3.堆的实现

4.3.1.堆文件结构

4.3.2.头文件编写

4.3.2.源文件编写

4.3.2.1.头文件包含

4.3.2.2.堆的初始化

4.3.2.3.堆的销毁

4.3.2.4.堆的插入

4.3.2.5.向上调整

4.3.2.6.堆的删除

4.3.2.7.向下调整

4.3.2.8.堆顶元素

4.3.2.9.堆的判空

4.3.3.测试文件编写

4.4.堆排序

4.5.TopK问题


一、树的概念和结构

1.1.树的概念

一种非线性的数据结构

1.2.树的相关概念

**根节点:**唯一没有前驱节点的特殊节点

**子树:**某个节点及其后代组成的子结构

**节点的度:**该节点的子树个数

**叶节点(终端节点):**度为0的节点

**分支节点(非终端节点):**度不为0的节点

**父节点:**直接包含子节点的上级节点

**子节点:**作为某个节点子树根节点的直接下级节点

**兄弟节点:**具有相同父节点的同级节点

**堂兄弟节点:**双亲在同一层的节点

**树的度:**最大节点的度

**节点的层次:**以根节点为第1层,根的子节点为第2层,以此类推

**树的深度:**节点的最大层次

**节点的祖先:**从根到该节点所经分支上的所有节点

**子孙:**以某节点为根的子树中任一节点

**森林:**多棵不相交的树的集合(并查集)

1.3.树的递归定义

每棵树都可以看作由一个根和若干个子树组成

1.4.区分树和非树

子树不相交:

除了根节点外,每个节点有且仅有一个父节点:

一个N节点的树有N-1条边:

1.5.树的节点定义

1.5.1.明确树的度为N
cpp 复制代码
#define N 4

struct TreeNode
{
	int val;
	struct TreeNode* subs[N];
};
1.5.2.没有明确树的度

左孩子右兄弟表示法

cpp 复制代码
struct TreeNode
{
	int val;
	struct TreeNode* leftchild;
	struct TreeNode* rightBrother;
};

无论一个父节点有多少子节点

child指向左边开始第一个孩子

1.6.树的应用

Linux树状目录结构

二、二叉树的概念及结构

2.1.二叉树的概念

一种特殊的树结构

由一个根节点及左子树和右子树构成

允许为空树

2.1.二叉树的性质

不存在度大于2的节点

子树有左右之分,次序不能颠倒

2.2.特殊的二叉树

**满二叉树:**每层节点数都为最大值

**完全二叉树:**除最后一层外都为满二叉树,且最后一层从左到右必须连续

三、二叉树与数组

3.1.完全二叉树与数组

用下标算父子关系:

算孩子:

  • 假设父亲在数组中的下标是:i
  • 左孩子在数组中的下标是:i * 2 + 1
  • 右孩子在数组中的下标是:i * 2 + 2

算父亲:

  • 假设孩子在数组中的下标是:j
  • 父亲在数组中的下标:(j - 1) / 2

3.2.非完全二叉树与数组

非完全二叉树可以用数组存储

但是会造成空间浪费,所以不适合

四、堆

4.1.堆的概念

大堆:完全二叉树,每个父亲节点的值都大于等于其子节点的值

小堆:完全二叉树,每个父亲节点的值都小于 等于其子节点的值

4.2.堆的特点

小堆根最小,谁小谁当爹

大堆根最大,谁大谁当爹

排序效率高

4.3.堆的实现

4.3.1.堆文件结构
  • 头文件(Heap.h):顺序表的结构创建,顺序表的方法声明
  • 源文件(Heap.c):顺序表的方法实现
  • 测试文件(test.c):测试数据结构的方法
4.3.2.头文件编写
cpp 复制代码
//头文件包含
#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 Swap(HPDataType* p1, HPDataType* p2);
//向上调整
void AdjustUp(HPDataType* a,int child);
//向下调整
void AdjustDown(HPDataType* a,int n,int parent);
//堆的初始化
void HPInit(HP* php);
//堆的销毁
void HPDestroy(HP* php);
//堆的插入
void HPPush(HP* php, HPDataType x);
//堆的删除
void HPPop(HP* php);
//堆顶元素
HPDataType HPTop(HP* php);
//堆的判空
bool HPEmpty(HP* php);
4.3.2.源文件编写
4.3.2.1.头文件包含
cpp 复制代码
#include "Heap.h"
4.3.2.2.堆的初始化
cpp 复制代码
void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}
4.3.2.3.堆的销毁
cpp 复制代码
void HPDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}
4.3.2.4.堆的插入
cpp 复制代码
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void HPPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail!");
			return;
		}

		php->capacity = newcapacity;
		php->a = tmp;
	}
	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);
}
4.3.2.5.向上调整
cpp 复制代码
#define MIN_HEAP 1
//#define MAX_HEAP 1

void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
#ifdef MIN_HEAP
		if (a[child] < a[parent])//小堆:子 < 父则交换
#else
		if (a[child] > a[parent])//大堆:子 > 父则交换
#endif
        {
            Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

解析:

从某个子节点的下标向上调整

(子节点下标 - 1)/ 2 == 父节点的下标,依次与祖先节点比较

如果是小堆,子节点值小于父节点时,将子节点与父节点值交换

并且更新子节点的下标,再次找新的父节点的下标继续进行比较

4.3.2.6.堆的删除
cpp 复制代码
void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	AdjustDown(php->a,php->size,0);
}

**注:**堆的删除只能从堆顶元素删除,随机删除会破坏堆的结构

4.3.2.7.向下调整
cpp 复制代码
#define MIN_HEAP 1
//#define MAX_HEAP 1

void AdjustDown(HPDataType* a, int n, int parent)
{
	//小堆:假设左孩子小 大堆:假设左孩子大
	int child = parent * 2 + 1;
	while (child < n)
	{
		//找出较小的孩子(右孩子要存在,否则会越界)
#ifdef MIN_HEAP
		if (child + 1 < n && a[child + 1] < a[child])
#else   //找出较大的孩子(右孩子要存在,否则会越界)
        if (child + 1 < n && a[child + 1] > a[child])
#endif
		{
			++child;
		}

#ifdef MIN_HEAP
		if (a[child] < a[parent])//小堆:父 > 子则交换
#else
        if (a[child] > a[parent])//大堆:父 < 子则交换
#endif
		{
			Swap(&a[child], &a[parent]);
			parent = child;//更新父节点下标
			child = parent * 2 + 1;//算出新的孩子下标
		}
		else
		{
			break;
		}
	}
}

解析:

从根节点的下标向下调整

父节点的下标 * 2 + 2 == 右孩子的下标

父节点的下标 * 2 + 1 == 左孩子的下标

假设法判断较小的子节点的下标值

交换根节点与子节点的元素

将新的根节点进行向下调整

4.3.2.8.堆顶元素
cpp 复制代码
HPDataType HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}
4.3.2.9.堆的判空
cpp 复制代码
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
4.3.3.测试文件编写
cpp 复制代码
void TestHeap01()
{
	int a[10] = { 4,2,8,1,5,6,9,7 };
	HP hp;
	HPInit(&hp);
	for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		HPPush(&hp, a[i]);
	}

    //找出最小的前k个
    int k = 0;
    scanf("%d",&k);
    while(k--)
    {
        printf("%d ",HPTop(&hp));
        HPPop(&hp);
    }

    HPDestroy(&hp);
}

int main()
{
	TestHeap01();
	return 0;
}

4.4.堆排序

降序(建小堆):

向上调整建堆:

cpp 复制代码
void HeapSort(int* a, int n)
{
	//建堆
    for (int i = 0; i < n; i++)
    {
	    AdjustUp(a, i);
    }
    int end = n - 1;
    //排序
    while (end > 0)
    {
    	Swap(&a[0], &a[end]);
        AdjustDown(a,end,0);
	    --end;
    }
}

void TestHeap02()
{
	int a[] = { 4,2,8,1,5,6,9,7 };
	HeapSort(a, sizeof(a) / sizeof(int));
}

向下调整建堆:

cpp 复制代码
void HeapSort(int* a, int n)
{
	//建堆(从最后一个节点(下标为n-1)的父节点开始)
    for (int i = (n-2)/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;
    }
}

void TestHeap02()
{
	int a[] = { 4,2,8,1,5,6,9,7 };
	HeapSort(a, sizeof(a) / sizeof(int));
}

**注:**降序建小堆,升序建大堆

向下调整建堆:

假设树的高度为h:

需要移动节点的总步数:

T(h) = 2^0 * (h - 1) + 2^1 * (h - 2) + 2^2 * (h - 3) +...... + 2^(h - 3) * 2 + 2^(h - 2) * 1

错位相减可得:

T(h) = 2^0 + 2^2 + ...... + 2^(h - 2) + 2^(h - 1) - 2^0 * (h - 1)

= 2^1 + 2^2 + ...... + 2^(h - 1) + 2^0 * h

= 2^h - 1 - h

满二叉树:最后一层满的高度

F(h) = 2^0 + 2^1 + ...... + 2^(h - 2) + 2^(h - 1) = 2^h - 1 = N

h = log2(N + 1)

最少情况:最后一层只有一个的高度

F(h) = 2^0 + 2^1 + ...... + 2^(h - 2) + 1= 2^(h - 1) = N

h = log2(N) + 1

综上所述:

满二叉树的总步数为:T(N) = N - log2(N + 1)

时间复杂度为:O(N),建堆效率远高于向上调整

**注:**节点数量多的层调整次数少,节点数量少的层调整次数多

向上调整建堆:

T(h) = 2^1 * 1 + 2^2 * 2 + ...... + 2^(h - 2) * (h - 2) + 2^(h - 1) * (h - 1)

错位相减可得:

T(h) = - (2^2 + 2^3 + ...... + 2^(h - 1)) + 2^h * (h - 1) - 2^1

= - (2^0 + 2^1 + 2^2 + 2^3 + ...... + 2^(h - 1)) +2^h * (h - 1) + 2^0

= - (2^h - 1) +2^h * (h - 1) + 2^0

满二叉树:最后一层满的高度

F(h) = 2^0 + 2^1 + ...... + 2^(h - 2) + 2^(h - 1) = 2^h - 1 = N

h = log2(N + 1)

综上所述:

满二叉树的总步数为:T(N) = - N + (N + 1) * (log2(N + 1) - 1) + 1

时间复杂度为:O(N*logN)

**注:**节点数量多的层调整次数多,节点数量少的层调整次数少

堆排序的整体时间复杂度为O(N*logN)

4.5.TopK问题

N个数找最大的前K个(假设N远大于k)

方法1:

建一个N个数的大堆,时间复杂度为O(N)

Popk次,时间复杂度为O(K * logN)

总时间复杂度为:O(N + K * logN)

方法2:

用前k个数,建一个小堆,时间复杂度为O(K)

剩下数据跟堆顶数据比较,如果比堆顶数据大,就替代堆顶进堆

覆盖根的位置,然后向下调整,时间复杂度为O(logK * (N - K))

总时间复杂度为:O(K + (N - K)*logK) ≈ O(N*logK)

cpp 复制代码
#include "Heap.h"
#include "time.h"

//造数据
void CreateNData()
{
	int n = 100;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; ++i)
	{
		int x = (rand() + i) % 10000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

void TestHeap3()
{
	int k;
	printf("请输入k:");
	scanf("%d", &k);
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc fail!");
		return;
	}

	//打开文件读取
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
        free(kminheap);
		return;
	}

	//读取文件中前k个数
	for (int i = 0; i < k; i++)
	{
		fscanf(fout,"%d", &kminheap[i]);
	}

	//建k个数的小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(kminheap, k, i);
	}

	//读取剩下的N-K个数
	int x = 0;
	while (fscanf(fout,"%d", &x) > 0)
	{
		if (x > kminheap[0])
		{
			kminheap[0] = x;
			AdjustDown(kminheap, k, 0);
		}
	}

	printf("最大的前%d个数:",k);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");

    free(kminheap);
    fclose(fout);
}

int main()
{
	//CreateNData();
	TestHeap3();
}
相关推荐
Prince-Peng4 小时前
技术架构系列 - 详解Redis
数据结构·数据库·redis·分布式·缓存·中间件·架构
只是懒得想了4 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
ruxshui5 小时前
个人笔记: 星环Inceptor/hive普通分区表与范围分区表核心技术总结
hive·hadoop·笔记
码农水水5 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
慾玄5 小时前
渗透笔记总结
笔记
m0_736919105 小时前
模板编译期图算法
开发语言·c++·算法
dyyx1115 小时前
基于C++的操作系统开发
开发语言·c++·算法
m0_736919105 小时前
C++安全编程指南
开发语言·c++·算法
CS创新实验室5 小时前
关于 Moltbot 的学习总结笔记
笔记·学习·clawdbot·molbot
蜡笔小马5 小时前
11.空间索引的艺术:Boost.Geometry R树实战解析
算法·r-tree