数据结构初阶(8)二叉树的顺序结构 && 堆

3.1 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。

现实中我们通常把(一种二叉树),使用顺序结构的数组来存储。

需要注意的是

这里的堆和操作系统虚拟进程地址空间中的堆是两回事:

一个是数据结构;一个是操作系统中管理内存的一块区域分段。

3.2 堆的概念及结构

如果有一个关键码的集合K = { k0,k1,k2,..., kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:

,i = 0,1,2......

则称为小堆(或大堆)

大根堆(最大堆):根节点最大的堆。(任何一个父亲 ≥ 孩子)

小根堆(最小堆):根节点最小的堆。(任何一个父亲 ≤ 孩子)

堆的性质

  • 任何一个父节点 ≤ 左右孩子(小堆)------任何一个父节点 ≥ 左右孩子(大堆)
  • ++堆总是一棵完全二叉树。++

堆是一个完全二叉树,所以堆特别适合用数组进行存储(随机访问、空间浪费小)

++物理上是一个数组,逻辑上是一个二叉树。++

堆在数组里面不一定有序------只能保证首元素(根节点)最大 / 小

30和50交换一下还是堆。

++堆只规定了父亲和孩子的关系,左孩子和右孩子的关系没有规定,即没有规定两个兄弟之间的关系,叔侄之间也没有规定大小关系。++

这里就不是单纯地存储数据,还要形成一些特定的性质。

之前的搜索二叉树可以用于查找,这里的堆:

大堆:找最大值;小堆:找最小值。

堆的应用:1.堆排序;2.找top-k。(找top1,删top1,剩下组成新堆,找top2.....)


3.3 堆的实现

3.2.1 堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。

向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

cpp 复制代码
int array[] = {27,15,19,18,28,34,65,49,25,37};

3.2.2 堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。

根节点左右子树不是堆,我们怎么调整呢?

这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。

cpp 复制代码
int a[] = {1,5,3,8,7,6};

3.2.3 建堆时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

因此:建堆的时间复杂度为O(N)

3.2.4 堆的插入

先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。

3.2.5 堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

3.2.6 堆的代码实现

(1)Heap.h

cpp 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<string.h>    //memcpy
#include<time.h>      //rand随机数

typedef int HPDataType;
//定义一个堆这样的数据结构
typedef struct Heap
{
	HPDataType* a;
	//插入数据,空间不够要扩容,就需要size和capcity
	int size;
	int capacity;
}HP;
//底层就是一个数组(相比于链式结构------存储两个节点,更加简单,但是也更加抽象)

// 堆初始化------传参版
void HPInit(HP* php);
// 堆初始化------返回版
HP* HPInit();

// 堆的构建------用一个数组来初始化堆
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);
// 堆的判空
//bool HeapEmpty(Heap* hp);
int HeapEmpty(Heap* hp);


void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
void AdjustDown_min(HPDataType* a, int n, int parent);//小堆

void Swap(HPDataType* px, HPDataType* py);

(2)Heap.c

① 默认初始化

逻辑分析

初始化函数的两种形式

  • HeapInit:函数外创建一个堆,把堆的地址传给初始化函数进行初始化------传参初始化
  • HeapCreat:函数内创建一个堆,返回给函数外,用指针接收------返回初始化

初始化的两种方式

  • 默认初始化:使用默认值NULL、0给堆结构进行初始化。
  • 传参初始化:增加一个数组参数,接收一个数组,用这个数组来初始化堆。
cpp 复制代码
// 堆的初始化------传参版------默认初始化
void HPInit(HP* php);
// 堆的初始化------返回版
HP* HPCreat();

// 堆的创建------传参版------传参初始化
void HeapCreate(Heap* hp, HPDataType* a, int n);

代码实现

虽然是堆(树)------底层控制的还是数组。

cpp 复制代码
//初始化
void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}
② 销毁
cpp 复制代码
//销毁
void HPDestroy(HP* php)
{
	assert(php);
	//释放
	free(php->a);
	//释放完之后置空
	php->a = NULL;
	php->capacity = 0;
	php->size = 0;
}
③ 插入(堆尾)

以插入小堆为例进行分析。

逻辑分析

逻辑上控制的是二叉树:比较直观;

物理上控制的是数组:比较抽象;

  1. 逻辑上控制的数组,元素的插入只能选择尾插
  2. 尾插完不会影响其他节点,只会影响祖先
  3. 所以需要使用向上调整算法。

(取到父亲→和父亲比较→比父亲小就交换,直到根 or 比父亲大为止)

使用数组的优势:随机访问(相比于链式结构)

代码实现

cpp 复制代码
//插入数据------物理上控制的是数组,逻辑上控制的是二叉树
void HPPush(HP* php, HPDataType x)
{
	assert(php);

	//如果满了
	if (php->size == php->capacity)
	{
		//就扩容
		size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		
		php->a = tmp;
		php->capacity = newCapacity;
	}

	//插入到尾上(尾插)------有左先插左
	php->a[php->size] = x;        //有size个数据,则size是最后一个数据的下一个位置的下标
	php->size++;

	//是顺序表到此就写完了,但是这里是堆(大堆),对数据是有要求的

	//向上调整·算法
	AdjustUp(php->a, php->size - 1);
}
x.3.1 向上调整算法

参数:数组、插入元素的下标。

原则:能用循环简单实现,就不使用递归。

cpp 复制代码
//值交换
void Swap(HPDataType* px, HPDataType* py)
{
	HPDataType tmp = *px;
	*px = *py;
	*py = tmp;
}
//向上调整·算法
void AdjustUp(HPDataType* a, int child)
{
	//1.获取父亲下标
	int parent = (child - 1) / 2;
	
    //循环 "比较 + 交换"
    while (child > 0)
	{
		//2.比较------如果孩子比父亲小(小堆),就交换
		//if (a[child] < a[parent])
		
		//2.比较------如果孩子比父亲大(大堆),就交换
		if (a[child] > a[parent])
		{
            //3.交换
			Swap(&a[child], &a[parent]);

			//4.迭代------交换下标,计算下标
			child = parent;
			parent = (parent - 1) / 2;
		}

        //循环结束条件
        //(1)parent的0给到了child
        //(2)孩子比父亲大(小堆)、孩子比父亲小(大堆)
		else
		{
			break;
		}
	}
}

//一处区别
a[child] < a[parent]    a[child] > a[parent]

循环结束条件

while (parent >= 0),即当parent < 0结束------不可行。

因为:当p指向根(0),c指向1或2,再让c指向0(把p赋给c),p算得-0.5,虽然为负,但是会提升到0,就无法结束,会进入下一次循环。

但是下一次循环,由于if条件判断是 > 而不是 >= ,故会到else跳出循环。
(巧合地结束了,如果是>=就死循环了)

就算巧合地结束了,也不要采取这种方式,最好采取child来判断------child等于0就结束了。

函数应用

给一个数组→判断是不是堆

若不是→把它变成一个堆

思路1:排序------排完序一定是堆,但是堆不一定是排序。

思路2:建堆

x.3.2 时间复杂度

完全二叉树的结点数目N在两个极端之间------最后一层:满 / 只有一个。

满二叉树 ● 高度h和结点数目N的关系

  • h = log2(N+1)

完全二叉树 ● 高度h和最少结点数目N的关系

  • h = log2(N)+1

完全二叉树 ● 高度h和数目N的关系

  • h的取值在上面两个h取值之间

堆插入算法的时间复杂度取决于向上调整算法的时间复杂度:

  • 最好情况:不需要调整。
  • 最坏情况:每一层都需要调整。

时间复杂度O(logN)

④ 取堆顶(取最小值、最大值)
cpp 复制代码
//取堆顶元素(小堆------最小值,大堆------最大值)
HPDataType HPTop(HP* php)
{
	assert(php);

	return php->a[0];
}

思考------如何取次小的数据

第2小→一定是第2层2个孩子中一个;

第3小→一定是第3层4个孩子中一个吗→就不一定了,可能是第2层的另一个孩子。

方法

提供一个pop函数,取到最小值,就pop最小值重新建堆,就能取到次小值,以此类推......

⑤ 删除(堆顶)

堆的插入、删除,都要保证操作完之后,还是一个堆。

逻辑分析

删除堆顶,然后挪动覆盖可以吗?显然不可以。

思路1------挪动覆盖的方式去删除堆顶数据。

问题:
1、挪动覆盖时间复杂度是O(N)。
2、堆结构被破坏,父子变兄弟,兄弟变父子------需要重新建堆。

思路2------向下调整算法

  1. 首尾数据交换
  2. 删除尾部数据*(--size,不用抹除数据)*
  3. 向下调整算法

路径

  • 向上调整的路径是唯一的------沿祖先的路径,小于祖先,就交换。
  • 向下调整的路径不是唯一的------大于孩子,就需要选择左右孩子较小的那个孩子作交换

核心思想

  • 核心思想都是要保证:父亲 ≤ 任意一个左右孩子。

堆尾插入一个数据→影响祖先→和祖先进行比较(路径唯一)
堆头交换一个数据→>影响子孙→>和子孙进行比较

左孩子和右孩子,和谁进行比较?

→答:小堆和小的那个比较交换,保证交换上去的双亲比"两个孩子"都小(大堆同理可得)

代码实现

cpp 复制代码
//弹出------规定是删除堆顶的数据
void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	//1.交换首尾
	Swap(&php->a[0], &php->a[php->size - 1]);
	//2.删除堆尾
	php->size--;

	//3.向下调整·算法
	AdjustDown(php->a, php->size, 0);//参数传数组,而不传结构体------>使函数的可复用性大大提高
}
x.5.1 向下调整算法

参数:数组、数组大小(用于判定调整到了叶子结点)、

参数传数组,而不是传结构体,使函数的可复用性大大提高

循环(调整)结束条件

(1)调整来到了叶子结点------计算其左孩子存不存在。

(2)不需要再往下,已经是一个小/大堆。

cpp 复制代码
//向下调整·算法
//大堆
void AdjustDown(HPDataType* a, int n, int parent)
{
    //1.找出左右孩子中较大的那一个,用于比较
	//假设修正法·初始化
    //假设child(大/小孩子)是左孩子
	int child = parent * 2 + 1;

	//当到叶子------叶子用左孩子不存在来判定(完全二叉树,左孩子不存在,则右孩子一定不存在)
    //孩子下标超出数组范围,就结束
	while (child < n)
	{
        //if (a[child + 1] > a[child])
        //while判断的是child<n,不能保证child+1<n,所以这样写是有bug的
		
        //如果有右孩子,并且右孩子大于左孩子(注意"逻辑短路",先判断存在右孩子)
		if (child + 1 < n && a[child + 1] > a[child])
		{
			//就纠正假设
			++child;        //(大/小孩子)是右孩子
		}

        //2.比较
		//如果自己和两孩子比,不是最大值------较大的孩子比自己大
		if (a[child] > a[parent])
		{
			//3.交换
			Swap(&a[child], &a[parent]);
			//4.迭代
			parent = child;
			child = parent * 2 + 1;//默认还是算到左孩子上面
		}
		else
		{
			break;
		}
	}
}

//向下调整算法
//小堆
void AdjustDown_min(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}

        //如果自己和两孩子比,不是最小值------较小的孩子比自己小
		if (a[child] < a[parent])
		{
			//就交换
			Swap(&a[child], &a[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;//默认还是算到左孩子上面
		}
		else
		{
			break;
		}
	}
}

//两处区别
//a[child + 1] > a[child]    a[child + 1] < a[child]
//a[child] > a[parent]    a[child] < a[parent]
x.5.2 时间复杂度

堆删除算法的时间复杂度取决于向下调整算法的时间复杂度:

  • 最好情况:不需要调整。
  • 最坏情况:每一层都需要调整。

时间复杂度O(logN)

所以不选择挪动覆盖法------时间复杂度O(N),还需要重新建堆。

所以用堆取top-k,需要先建堆时,使用向下调整建堆才有优势,O(N)+O(K * logN)。

用向上调整建堆+取top-k,就没有优势了,光是向上调整建堆就已经是N*logN了。

⑥ 判空
cpp 复制代码
bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}
⑦ 堆的创建(传参初始化)

逻辑分析

  1. 创建数组空间。
  2. 把数组的数据拷贝过来。
  3. 建堆

建堆

之前是遍历数组,调用push------插入一个、调整一个,的方式进行建堆。

而这里是给了一个数组,应该怎样建堆呢?

思路1------模拟push插入的方式,遍历数组,向上调整,建堆
------几乎和之前一模一样,只是换用了memcpy一次性插入(拷贝)全部数据,实际上还是要对每个数据进行向上调整。
------从数组的第二个数据开始向上调整建堆

时间复杂度:

不能直接简单粗暴地计算,用循环次数N*每次执行次数logN得到O(N*logN)。

因为只有最后一层才是logN,即核心原因在于每次循环,向上调整算法的最坏执行次数是变化的。

++每层节点的向上调整次数,是和当前层的高度相关的:当前层节点数 * 当前层向上调整次数。++

++即总的调整次数------F(h),是一个高度的函数:∑ 当前层节点数 * 当前层向上调整次数。++

N是数据个数。

++F(h)需要转换成F(N),因为时间复杂度是问题规模N的函数。++

思路2------向下调整建堆

向下调整算法有条件:要求待调整数据下层的左右子树都是小/大堆。

(而向上调整算法是要求待调整数据上层的左右子树都是小/大堆)

由此特性可知,向上调整算法,在建堆的时候,可以直接从上往下用。

++(保证当前节点之上是已调整好的堆)++

而向下调整算法,在建堆时候,需要从最后一个节点依次往前调整,而最后一层都不需要再继续向下调整,故从倒数第二层开始调整。

++(保证当前节点之下是已调整好的堆)++

从倒数第二层的"相对"最后一个元素------即倒数第一个非叶子,即最后一个结点的父亲,开始调整。

最后一个节点(size-1)的父亲,(size-1-1)/2,不一定是倒数第二层的最后一个元素,但是一定是倒数第二层的"相对"最后一个元素。

时间复杂度:

但看循环可能会以为是循环次数 N/2 乘以 向下调整次数 logN,即O(N*logN)。

实际上是O(N)。

代码实现

cpp 复制代码
// 堆的创建------用数组初始化堆
void HPInitArray(HP* php, HPDataType* a, int n)
{
	assert(php);
	//开辟数组空间
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	//再把数组拷贝过来
	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->capacity = php->size = n;

	// 建堆
	// 思路1.------从第二个数组数据开始向上调整,建堆
	//for (int i = 1; i < php->size; i++)
	//{
	//	AdjustUp(php->a, i);    //i是插入数据的位置(孩子的位置)------memcpy是一次性把所有数据都插好了
	//}
	//时间复杂度O(N*logN)

	// 思路2.------从倒数第一个非叶子开始,向下调整,建堆 
	for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
        //size------数据个数,用来判断到没到叶子------孩子超出了数组范围
        //即使建好的堆只有底下几层,叶子的下标还是那些,堆的孩子的下标范围还是那个
	}
	//时间复杂度O(N)
}

结论

向上调整建堆是吃亏的,因为满二叉树最后一层节点个数占N/2 - 1,而且向上调整建堆------>对于节点数目多的层,每个节点的调整次数也多,相当于**"多*多"**。

最后一层的调整次数就已经是N/2 * logN,即最后一层的时间复杂度就已经是O(N*logN)这个量级了

向下调整则是,最后一层节点数目最多,但是不需要调,倒数第二层节点占大约25%,但是每个节点最多只用调整1次,相当于**"多*少"**,所以就从O(N*logN)降到O(N)了。


最后这也解释了为什么不使用之前的一个一个push------相对于向上调整建堆。

如果不把AdjustDown函数声明到头文件,就需要前置声明 ,或直接把定义移上去,因为编译器只会向上查找(往回找),不会向下兼容。


所以用堆取top-k,需要先建堆时,使用向下调整建堆才有优势,O(N)+O(K * logN)。

用向上调整建堆+取top-k,就没有优势了,光是向上调整建堆就已经是N*logN了。

(3)Test1.c

测试接口1~7。

堆特别适合用来选数据,测试选最大、次大、再大、......

cpp 复制代码
void test1()
{
	//给一个数组------>把它变成一个堆(排完序一定是堆,但堆不一定有序)
	//int a[] = { 50,100,70,65,60,32 };
	int a[] = { 60,70,65,50,32,100 };
	
	HP hp;            //定义一个堆

	//初始化
	HPInit(&hp);
	//遍历数组
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		//将数组元素插入堆------>变数组为堆
		HPPush(&hp, a[i]);            //大堆还是小堆,取决于push函数里面的向上调整算法
	}

    //更好的初始化方式------时间复杂度O(N)
	//HPInitArray(&hp, a, sizeof(a) / sizeof(int));

	//打印堆顶数据
	printf("%d\n", HPTop(&hp));
	//弹出堆顶
	HPPop(&hp);
	//打印堆顶数据
	printf("%d\n", HPTop(&hp));
    //弹出堆顶
	HPPop(&hp);

	while (!HPEmpty(&hp))
	{
		//取堆顶打印------最大、次大、......
		printf("%d\n", HPTop(&hp));
        //弹出堆顶
		HPPop(&hp);
	}
	//发现这里循环"取堆顶、打印、弹出堆顶",出来就是有序的。
    //但这并不是堆排序,这里仅仅是用一个堆将一个数组给降序 / 升序打印出来了
	
    //堆:push和pop的时间复杂度都非常优秀

	//销毁
	HPDestroy(&hp);
}

int main()
{
	test1();

	return 0;
}

3.4 堆的应用

3.4.1 堆排序-Test2.c

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

(1)建堆

  • 升序:建大堆
  • 降序:建小堆

(2)利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

代码实现

代码测试

这是一个简单版的堆排序------用给的数组创建大/小堆,再逐个放回原数组。

时间复杂度0(N*logN)。

缺陷

  1. 这个堆排序使用之前,需要堆的数据结构------手搓一大段代码。
  2. 空间复杂度 O(N)。

故不推荐这么写。


更推荐的写法:直接在原数组的空间上建堆

问题------升序,建大堆还是小堆呢?

++堆排序的本质就是选择,小堆选择当前的最小值放在前面,大堆就是选择当前的最大值放在后面。++

第一直觉是升序当然建小堆,小堆更符合整体升序,最小的数就在堆顶。 那次小的数呢?

实则:升序建大堆。

降序建小堆(使用小堆的向下调整建)+ 使用小堆的向下调整删

代码实现

while循环的时间复杂度计算:

大堆最底层(数组的后半部分)排序好------把当前最大值交换到最后一层,交换得到的较小值向下调整,这一层的消耗就是 结点数 * 层数,即2^(h-1) * (h-1)。

和向上调整建堆的消耗是一样的,是一个"多 * 多"。

3.4.2 TOP-K问题-Test3.c

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序

但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。

最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆

topk_max,则建小堆

topk_min,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

**替换:**直接覆盖堆顶元素,向下调整(把大的数沉下去),使得被替换掉的一定不是前K大的。

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

建的是小堆,第5大的数据进入了之后不会挡在堆顶阻止第6~10大的数据进入。

注意建大堆的最后一个元素不一定是整个大堆中的最小值。

cpp 复制代码
void PrintTopK(int* a, int n, int k)
{
 // 1. 建堆--用a中前k个元素建堆
 
 // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
}
void TestTopk()
{
 int n = 10000;
 int* a = (int*)malloc(sizeof(int)*n);
 srand(time(0));
 for (size_t i = 0; i < n; ++i)
 {
     a[i] = rand() % 1000000;
 }
 a[5] = 1000000 + 1;
 a[1231] = 1000000 + 2;
 a[531] = 1000000 + 3;
 a[5121] = 1000000 + 4;
 a[115] = 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);
}

Test3.c

数据创建。

cpp 复制代码
void CreateNDate()
{
	// 一直以为int是-32768------32767
	// 实际上在16位机器上,才是这样,int为2字节,16位,取值范围2^10 * 2^6 = 65536,有符合位就是2^10 * 2^5 = 32768
	// 在32/64位机器上,int为4字节,32位,取值范围1024*1024*1024*4=10亿多*4=42亿多,有符号位就是21亿多
	// 造数据
	int n = 100000;                  //写10万个随机数
	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();            //产生随机数据
		int x = (rand() + i) / 1000000;    //产生100万以内随机数据------rand随机数最大32767
		fprintf(fin, "%d\n", x);           //将数据写入文件------而且每写一个数据都给一个换行
                                           //加换行或空格------形成分割
                                           //不加分割就类似于"3425124789......"就不知道每个数是多少
		//换行相对于空格的优势在于可以根据行数判断数据个数,空格的优势在于省空间
		//但是用fscanf(..."%d"...)就不用换行fscanf(..."%d\n"...)了,默认遇到空格或换行就是多个值之间的分割
		//结论:写的时候需要加,读的时候不需要加
	}

	fclose(fin);
}

测试Top-k

cpp 复制代码
//目的:取出文件中的topk
void test3()
{
	printf("请输入要获取前几大的值:");
	int k = 0;
	scanf("%d", &k);//输入希望取到的topk

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

	//手动建堆------不使用之前代码------使用之前代码,意味着以后遇到题目,要先写一整个堆数据结构,非常麻烦且繁琐,故不要依赖数据结构------而且手动建堆并不麻烦,写个向下调整就解决了
	//1.申请堆空间
	int* minheap = malloc(sizeof(int) * k);
	if (minheap == NULL);
	    if (fout == NULL)
	    {
		    perror("malloc error");
		    return;
	    }

	//2.读取k个数据到堆空间
	int i = 0;
	for (i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minheap[i]);
	}

	//3.建k个数据的小堆
	for (i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_min(minheap, k, i);    //从后往前,向下调整建堆
	}

	//读取剩余数据
	int val = 0;                //读取到的值
	while (fscanf(fout, "%d", &val) != EOF)    //读到返回1,没读到就结束了
	{
		//如果大于堆顶的值
		if (val > minheap[0])
		{
			//就替换进堆
			minheap[0] = val;
			AdjustDown_min(minheap, k, 0);
		}
	}

    //打印top-k
	for (i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}

	fclose(fout);
}

int main()
{
	//先建一些数据
	CreateNDate();
	test3();

	return 0;
}

如何验证取出来的确实是topk?

在数据中手动地制造一些样本:手动编辑.txt文件,离散地在一些几万的数字后加一些数字使成为几亿的样本数据,看topk能否将这些样本数据找出来。

相关推荐
xiaobobo333042 分钟前
C语言中关于普通变量和指针变量、结构体包含子结构体或包含结构体指针的一些思考
c语言·开发语言·结构体指针
数据智能老司机1 小时前
图算法趣味学——最大流算法
数据结构·算法·云计算
秋难降2 小时前
【数据结构与算法】———深度优先:“死磕 + 回头” 的艺术
数据结构·python·算法
数据智能老司机2 小时前
图算法趣味学——图着色
数据结构·算法·云计算
数据智能老司机2 小时前
图算法趣味学——启发式引导搜索
数据结构·算法·云计算
时光の尘2 小时前
ESP32入门开发·VScode空白项目搭建·点亮一颗LED灯
c语言·ide·vscode·freertos·led·esp32-s3·esp32-idf
SimonSkywalke3 小时前
基于知识图谱增强的RAG系统阅读笔记(七)GraphRAG实现(基于小说诛仙)(一)
算法
程序猿编码3 小时前
基于LLVM的memcpy静态分析工具:设计思路与原理解析(C/C++代码实现)
c语言·c++·静态分析·llvm·llvm ir
再睡一夏就好4 小时前
【排序算法】④堆排序
c语言·数据结构·c++·笔记·算法·排序算法