堆和堆排序【数据结构】

news/2024/5/20 9:18:02 标签: 数据结构,

目录

  • 一、
    • 1. 的存储定义
    • 2. 初始化
    • 3. 销毁
    • 4. 的插入
      • 向上调整算法
    • 5. 的删除
      • 向下调整算法
    • 6. 获取顶数据
    • 7. 获取的数据个数
    • 8. 的判空
  • 二、Gif演示
  • 三、 排序
    • 1. 排序
      • (1) 建大
      • (2) 排序
    • 2.Topk问题
  • 四、完整代码
    • 1.的代码
      • Heap.c
      • Heap.h
      • test.c
    • 2. 排序的代码

前言:
什么是呢?
(Heap)是一种数据结构,它是 一种特殊的二叉树 ,其中父节点的键值总是大于或等于(或小于或等于)其任何一个子节点的键值。这意味着在中,根节点具有最大(或最小)键值。
:一般是数组数据看做一棵完全二叉树
完全二叉树的逻辑结构:
大<a class=堆01" />

  • 任意一个父结点 大于等于 子结点
    大<a class=堆02" />

  • 任意一个父结点 小于等于 子结点
    小<a class=堆" />
    数组存储完全二叉树
    在这里插入图片描述

一、

1. 的存储定义

因为存储结构,这里使用动态数组的形式来存放数据。但是也要注意其中的逻辑结构是完全二叉树。定义一个指针指向动态数组,定义存储的容量capacity,记录中的数据的个数size

代码

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;	//指向动态数组
	int capacity;	//的容量
	int size;		//中数据个数
}Heap;

2. 初始化

类似顺序表的初始化

代码

//初始化
void InitHeap(Heap* hp) 
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = 0;
	hp->size = 0;
}

3. 销毁

避免内存泄漏

代码

//销毁
void DestroyHeap(Heap* hp) 
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;	
}

4. 的插入

重点:
的插入前,我们需要注意的就是,首先判断其容量,然后使用realloc给数组分配空间
分配空间后,把数据插入但是数据在插入时,由于一般分为大根和小根,所以这里使用的大根。 大:父结点的值大于等于其孩子结点的值
但是数据的值不能确定,这个时候就需要我们使用 的向上调整算法

向上调整算法

在数组的末端插入元素,进行与其父结点进行比较,大的情况下,如果其孩子结点的值大于父亲结点的值时,把插入的数据向上调整,向上调整的方法是:把插入的数据与其父结点进行交换,交换后继续判断是否还需要向上调整。(使用向上调整算法的条件是前面结点的树是构成的)

这里是使用的是数组,所以当插入元素时,在数组的末端进行插入数据

物理存储:
在这里插入图片描述
逻辑存储情况:
在这里插入图片描述

在插入的数据时,我们就需要考虑一下,
1. 当插入的孩子结点的值大于其父亲结点的值时,就向上调整
思路:
首先是根据孩子结的下标找父结点的下标,(孩子结点下标-1)/2 == 父结点下标,因为可能调整所以将判断条件放到循环里面(当然也可以用递归),在循环里面切记一定要及时更新当前孩子结点的下标和父结点的下标,孩子结点的值大于父结点的值就向上调整,否则就跳出循环。当孩子结点的下标到0时,向上调整完成,循环结束。
在这里插入图片描述
2. 当小于等于时,不需要调整
在这里插入图片描述
代码

//向上调整
void AdjustUp(HPDataType * a,int child) 
{
	//先找到父结点的下标
	int parent = (child - 1) / 2;
	while (child > 0)	//child等于0时,说明已经调整ok了
	{
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			//可能会向上调整多次
			child = parent;
			parent = (parent - 1) / 2;
		}
		else 
		{
			break;
		}
	}
}

//的插入
void PushHeap(Heap* hp, HPDataType x)
{
	assert(hp);
	//满判断
	if (hp->capacity == hp->size) 
	{
		int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
		HPDataType* tmp = (HPDataType*)realloc(hp->a,sizeof(HPDataType)*newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	//元素的插入
	hp->a[hp->size] = x;
	hp->size++;
	//的向上调整
	AdjustUp(hp->a,hp->size-1);
}

调试,查看一下数据存储情况
在这里插入图片描述

5. 的删除

中元素的删除,发现直接删除尾结点是简单的(size减一即可),但是,一般,删除元素都是删除的头结点。
直接删除头结点时:发现逻辑结构上变成了两棵树,这样直接删除头结点的方法不推荐。
在这里插入图片描述
交换结点再删除
头尾结点交换后,再删除尾结点,然后头结点使用的向下调整算法,调
使用前提就是,当进行交换的时候,保证左右仍是。第一个结点与最后一个结点的值交换后,向下调整。

向下调整算法

这里调的(根结点的值大于左右孩子结点的值)

  • 第一步,找到第一个根结点的孩子结点,这里使用假设法,先让左孩子的值最大,再进行判断左孩子还是右孩子的值是最大的,找出大的。
  • 第二步与根结点进行比较,大于根结点就交换。
  • 及时更新父结点和孩子结点的下标
  • 注意当孩子结点值都小于父亲结点值就跳出循环;循环结束条件:孩子结点的下标大于数组最大的下标(就是孩子下标<数组的个数,child<size,大于等于时说明循环就结束了)。

过程:
在这里插入图片描述
调整后
在这里插入图片描述
这样就完成头结点的删除。
还需要注意的就是:
在这里插入图片描述

代码

//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
	//先去找根结点的较大的孩子结点
	int child = 2 * parent + 1;
	//可能会向下调整多次
	while (child<size) 
	{
		//这里使用假设法,先假设左孩子的值最大
		//如果不对就进行更新
		if ((child+1 < size)&&a[child] < a[child+1]) 
		{
			child++;
		}
		//根结点与其孩子结点中的较大的一个进行交换
		if(a[child] > a[parent]) 
		{
			swap(&a[child],&a[parent]);
			//更新下标
			parent = child;
			child = 2 * parent + 1;
		}
		else 
		{
			break; //调完
		}
	}
}
//的删除
void PopHeap(Heap* hp)
{
	assert(hp);
	assert(hp->size>0);
	//头尾交换
	swap(&hp->a[0],&hp->a[hp->size-1]);
	hp->size--;
	//向下调整
	AdjustDown(hp->a,hp->size,0);
}

调试一下:
在这里插入图片描述
上图中指向下标6其实有数据65的,但是数组的下标有效范围在0-5

6. 获取顶数据

前提:得有数据
代码

//获取顶数据
HPDataType TopHeap(Heap* hp) 
{
	assert(hp);
	assert(hp->size>0);
	return hp->a[0];
}

7. 获取的数据个数

代码

//获取的数据个数
int SizeHeap(Heap* hp)
{
	assert(hp);
	return hp->size;
}

8. 的判空

代码

//的判空
bool EmptyHeap(Heap* hp) 
{
	assert(hp);
	return hp->size == 0;
}

二、Gif演示

演示
<a class=堆动图gif" />

三、 排序

排序是一种选择排序。
排序:可以从小到大进行排序(使用大)。Top k 问题:取出最大的前k个值。

1. 排序

排序(Heap Sort)是一种基于完全二叉树的排序算法,它通过将待排序的元素建成一个二叉排序的时间复杂度为O(nlogn),它是不稳定排序算法。

排序的思路如下:

  1. 升序排序为例,先建立一个(父节点的值大于子节点的值),将待排序的元素都插入中。
  2. 顶元素(最大值)与末尾元素交换,然后将的大小减1。
  3. 顶元素向下调整操作,使得重新满足最大的性质。
  4. 重复2-3步,直到的大小为1。排序完成。

(1) 建大

使用 向下 调整算法来向上建:使用向下调整算法,把数组调成大
因为本身是一个完全二叉树,假设一共有h层,我们从第h-1层(即不是叶子结点的那一层开始)
因为是大,根结点的值大于孩子结点的值,从最下方使用向下调整来不断把较大的值来调到根节点。
注意:虽然使用的是向下调整算法,其实还是不断往上调整(把大的值调到上面)。
如图:
在这里插入图片描述
直到调整到第一层为止
时间复杂度:O(N)

//排序
void HeapSort(int* arr, int n) 
{
	int i = 0;
	//使用向下调整算法向上调整,把大的值调到上方。
	for (i = (n - 1 - 1) / 2; i >= 0;i--)
	{
		//先找到数组最后端的父结点的下标
		//父结点的下标减一就是另一个
		//使用向下调整算法进行调整
		AdjustDown(arr,n,i);
	}
}

当然也可以用向上算法进行向上建

思路:先让一个独自成,然后尾插一个结点,再进行与根结点进行比较,大于根结点的值就交换。
但是这个使用向上调整算法向上建的时间复杂度为:O(Nlog(N))

//向上调整算法进行排序
void HeapSort(int* arr, int n)
{
	int i = 0;
	//先让第一个结点独自成
	//再一次尾增结点进行向上调整
	for (i = 1; i < n; i++) 
	{
		AdjustUp(arr,i);
	}
}

(2) 排序

因为建成大后,将顶元素(最大值)与末尾元素交换

	//注意end 是从n-1开始的(数组最后一个元素的下标)
	int end = n-1;
	while (end > 0) 
	{
		//swap end = n-1 这表示下标
		swap(&arr[0],&arr[end]);
		//adjustdown 函数里面的end是元素的个数,所以不是先--end
		//所以
		AdjustDown(arr,end,0);
		end--;
	}

注意这里的end–,上述是从数组最后一个元素下标n-1 开始。的首元素与尾元素交换完后,接着就是的个数减1,然后下进行向下调整。这里的end–放在了最后。因为AdjustDown中的第二个参数是传的是的大小,正好数组下标n-1 , 由n减一也是 n -1。

下方给出了 end 从n 开始的优化,但是可读性就会下降

void HeapSort(int* arr, int n)
{
	int i = 0;
	//先建成一个大
	for (i = (n - 1 - 1) / 2; i >= 0;--i) 
	{
		AdjustDown(arr,n,i);
	}

	//顶元素与尾元素进行交换,进而把大的元素放到后面
	int end = n;
	while (end > 0) 
	{
		swap(&arr[--end],&arr[0]);
		AdjustDown(arr,end,0);
	}
}

2.Topk问题

topk问题,例如:在10000个数据排名中找出前10;或者在10000个数中找出最大的前10个

这里我们就以在10000个数中找出最大的前10(k = 10)个为例

首先应先准备数据,随机生成10000个数(注意srand函数只能生成30000多个随机数)
核心思想: 建一个可以存储k个数据的小。先把文件数据前10个数据读取到小中(进行向下调成小),然后再把文件中的其他数据一个一个读出与小的根结点的值进行比较,如果大于小的根结点,就进放入中,然后进行向下调

//创建数据
void Createdata() 
{
	int n = 10000;
	srand((unsigned)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) % 100000;
		//把随机生成的数据写到fin文件中去
		fprintf(fin,"%d\n",x);
	}
	fclose(fin);
}
void PrintTopK(int k) 
{
	//从文件中读出数据
	const char* file = "data.txt";
	FILE* fout = fopen(file,"r");
	if (fout == NULL)
	{
		perror("fout error");
		return;
	}
	//将数据读出到容量为k的动态数组中
	int* arr = (int*)malloc(sizeof(int)*k);
	if (arr == NULL)
	{
		perror("malloc error");
		exit(-1);
	}
	//先把前k个数据放入数组中
	for (int i = 0; i < k; i++)
	{
		//将数据读到数组中
		fscanf(fout,"%d",&arr[i]);

		//放数据的同时进行建
		AdjustUp(arr,i);
	}

	int x = 0;
	//当文件里面的数据读完后会返回EOF
	while (fscanf(fout, "%d", &x) != EOF) 
	{
		//当从文件拿出的数据大于小中的数据时
		//将数据放到小
		//并使用向下调整
		//这样每次来的比较大的数据就可以放到小
		if (x > arr[0]) 
		{
			arr[0] = x;
			AdjustDown(arr,k,0);
		}
	}

	//打印数据
	for (int i = 0; i < k;i++) 
	{
		printf("%d ",arr[i]);
	}
	fclose(fout);

}

在这里插入图片描述

四、完整代码

1.的代码

Heap.c

#include "Heap.h"

//初始化
void InitHeap(Heap* hp) 
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = 0;
	hp->size = 0;
}

//销毁
void DestroyHeap(Heap* hp) 
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;	
}

//交换两个数
void swap(HPDataType* s1,HPDataType* s2) 
{
	HPDataType tmp = *s1;
	*s1 = *s2;
	*s2 = tmp;
}
//向上调整
void AdjustUp(HPDataType * a,int child) 
{
	//先找到父结点的下标
	int parent = (child - 1) / 2;
	while (child > 0)	//child等于0时,说明已经调整ok了
	{
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			//可能会向上调整多次
			child = parent;
			parent = (parent - 1) / 2;
		}
		else 
		{
			break;
		}
	}
}

//的插入
void PushHeap(Heap* hp, HPDataType x)
{
	assert(hp);
	//满判断
	if (hp->capacity == hp->size) 
	{
		int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
		HPDataType* tmp = (HPDataType*)realloc(hp->a,sizeof(HPDataType)*newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	//元素的插入
	hp->a[hp->size] = x;
	hp->size++;
	//的向上调整
	AdjustUp(hp->a,hp->size-1);
}

//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
	//先去找根结点的较大的孩子结点
	int child = 2 * parent + 1;
	//可能会向下调整多次
	while (child<size) 
	{
		//这里使用假设法,先假设左孩子的值最大
		//如果不对就进行更新
		if ((child+1 < size)&&a[child] < a[child+1]) 
		{
			child++;
		}
		//根结点与其孩子结点中的较大的一个进行交换
		if(a[child] > a[parent]) 
		{
			swap(&a[child],&a[parent]);
			//更新下标
			parent = child;
			child = 2 * parent + 1;
		}
		else 
		{
			break; //调完
		}
	}
}
//的删除
void PopHeap(Heap* hp)
{
	assert(hp);
	assert(hp->size>0);
	//头尾交换
	swap(&hp->a[0],&hp->a[hp->size-1]);
	hp->size--;
	//向下调整
	AdjustDown(hp->a,hp->size,0);
}

//获取顶数据
HPDataType TopHeap(Heap* hp) 
{
	assert(hp);
	assert(hp->size>0);
	return hp->a[0];
}

//获取的数据个数
int SizeHeap(Heap* hp)
{
	assert(hp);
	return hp->size;
}

//的判空
bool EmptyHeap(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;	//指向动态数组
	int capacity;	//的容量
	int size;		//中数据个数
}Heap;

//初始化
void InitHeap(Heap* hp);

//销毁
void DestroyHeap(Heap* hp);

//的插入
void PushHeap(Heap* hp, HPDataType x);

//的删除
void PopHeap(Heap*hp);

//获取顶数据
HPDataType TopHeap(Heap* hp);

//获取的数据个数
int SizeHeap(Heap* hp);

//的判空
bool EmptyHeap(Heap* hp);

test.c

#include "Heap.h"

void Test1() 
{
	Heap hp;
	InitHeap(&hp);
	PushHeap(&hp,49);
	PushHeap(&hp,65);
	PushHeap(&hp,34);
	PushHeap(&hp,25);
	PushHeap(&hp,37);
	PushHeap(&hp,27);
	PushHeap(&hp,19);
	//删除65
	PopHeap(&hp);
	//printf("的个数:%d\n",SizeHeap(&hp));
	//while (!EmptyHeap(&hp)) 
	//{
	//	printf("%d-", TopHeap(&hp));
	//	PopHeap(&hp);
	//}

	DestroyHeap(&hp);
	//27,19,34,65,49,25,37
}
int main() 
{
	Test1();
	return 0;
}

2. 排序的代码

//排序
void HeapSort(int* arr, int n) 
{
	int i = 0;
	//使用向下调整算法向上调整,把大的值调到上方。
	for (i = (n - 1 - 1) / 2; i >= 0;i--)
	{
		//先找到数组最后端的父结点的下标
		//父结点的下标减一就是另一个
		//使用向下调整算法进行调整
		AdjustDown(arr,n,i);
	}

	//进行排序
	//因为是大,所以根结点的值是最值
	//把最值与的最后一个结点进行交换
	//再把交换后的根节点进行向下调整
	//然后的大小减一
	

	//注意end 是从n-1开始的(数组最后一个元素的下标)
	int end = n-1;
	while (end > 0) 
	{
		//swap end = n-1 这表示下标
		swap(&arr[0],&arr[end]);
		//adjustdown 函数里面的end是元素的个数,所以不是先--end
		//所以
		AdjustDown(arr,end,0);
		end--;
	}
}

http://www.niftyadmin.cn/n/5352551.html

相关文章

蓝桥杯官网填空题(平方拆分)

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 将 2019 拆分为若干个两两不同的完全平方数之和&#xff0c;一共有多少种不同的方法&#xff1f; 注意交换顺序视为同一种方法&#xff0c;例如 13^225^235^22019…

【JaveWeb教程】(35)SpringBootWeb案例之《智能学习辅助系统》登录功能的详细实现步骤与代码示例(8)

目录 案例-登录和认证1. 登录功能1.1 需求1.2 接口文档1.3 思路分析1.4 功能开发1.5 测试 案例-登录和认证 在前面的课程中&#xff0c;我们已经实现了部门管理、员工管理的基本功能&#xff0c;但是大家会发现&#xff0c;我们并没有登录&#xff0c;就直接访问到了Tlias智能…

如何使用Prompt快速压缩将RAG成本降低80%

英文原文地址&#xff1a;How to Cut RAG Costs by 80% Using Prompt Compression 通过即时压缩加速推理 2024 年 1 月 5 日 推理过程是使用大型语言模型时消耗资金和时间成本的因素之一&#xff0c;对于较长的输入&#xff0c;这个问题会更加凸显。下面&#xff0c;您可以…

第九章 动态规划part17(● 647. 回文子串 ● 516.最长回文子序列 ● 动态规划总结篇)

学习目标&#xff1a; ● 647. 回文子串 ● 516.最长回文子序列 ● 动态规划总结篇 学习内容&#xff1a; 647. 回文子串 动态规划解决的经典题目&#xff0c;如果没接触过的话&#xff0c;别硬想 直接看题解。 https://programmercarl.com/0647.%E5%9B%9E%E6%96%87%E5%AD%90…

【寒假每日一题·2024】AcWing 5415. 仓库规划(补)

文章目录 一、题目1、原题链接2、题目描述 二、解题报告1、思路分析2、时间复杂度3、代码详解 一、题目 1、原题链接 5415. 仓库规划 2、题目描述 二、解题报告 1、思路分析 思路参考y总&#xff1a;y总讲解视频 &#xff08;1&#xff09;由于每一个仓库均有一个m维向量的位…

网络通讯(20)-UDP协议应用实例网络聊天室

本文演示UDP协议应用,通过实例网络聊天室进行学习。 目录 实现多人聊天室的功能

计算机毕业设计 基于SpringBoot的校园闲置物品交易系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

每日OJ题_算法_前缀和④_力扣238. 除自身以外数组的乘积

目录 力扣238. 除自身以外数组的乘积 解析代码 力扣238. 除自身以外数组的乘积 238. 除自身以外数组的乘积 难度 中等 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数…