探索数据结构:堆的具体实现与应用

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog

1. 堆的概念

堆(Heap)是计算机科学中一类特殊的数据结构。堆通常是一个可以被看作一棵完全二树的数组对象,若满足:

  • 任意节点的值>=其子节点的值。则称为大根堆
  • 任意节点的值<=其子节点的值。则称为小根堆

img

img

2. 堆的实现方式

虽然堆是一种特殊的二叉树,它既可以用数组存储也可以用链式存储。但是考虑到其完全二叉树的特性,我们最好采用数组存储的方式,因为这样既方便访问,也并不会浪费格外的空间。

img

假设某个合法下标为i:

  • 若双亲节点存在,下标为(i-1)/2。
  • 若孩子节点存在,左孩子下标为2i+1,右孩子为2i+2。

3. 堆的功能

  1. 堆的初始化。
  2. 堆的插入。
  3. 堆的删除。
  4. 获取堆顶的元素。
  5. 堆的元素个数。
  6. 堆的判空。
  7. 输出堆。
  8. 建堆。
  9. 销毁堆。

4. 堆的声明

因为我用数组实现堆,所以堆的声明与顺序表类似。

typedef int HpDataType;
typedef struct Heap 
{
	HpDataType* a;//存储数据
	int size;//大小
	int capacity;//容量
}Heap;

5. 堆的实现

5.1. 堆的初始化

5.1.1. 代码实现
void HeapInit(Heap* hp)//堆的初始化
{
	assert(hp);
	hp->a = NULL;
	hp->size = hp->capacity = 0;
}
5.1.2. 复杂度分析
  • 时间复杂度:没有额外的时间消耗,时间复杂度为O(1)。
  • 空间复杂度:没有额外的空间消耗,空间复杂度为O(1)。

5.2. 堆的插入

当我们堆进行插入时可能会破坏堆的原有结构,这时就需要我们对其进行向上调整。

img

5.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 HeapPush(Heap* hp, HpDataType x)//堆的插入
{
	assert(hp);
	if (hp->size == hp->capacity)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HpDataType* tmp = (HpDataType*)realloc(hp->a, newCapacity * sizeof(HpDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newCapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp, hp->size - 1);//向上调整
}
5.2.2. 复杂度分析
  • 时间复杂度:假设有N个节点,高度为h,2h -1=N。至少调整log2(N+1)-1次,所以时间复杂度为logN。
  • 空间复杂度:没有开辟额外的空间,空间复杂度为O(1)。

5.3. 堆的删除

堆的删除是指删除堆顶的数据,如果我们删除堆顶元素并往前覆盖就可能打乱原有的亲缘关系。所以我们可以先将堆顶的元素与末尾元素交换,然后再进行向下调整·。

img

5.3.1. 代码实现
void swap(HpDataType* x1, HpDataType* x2)
{
	HpDataType tmp = *x1;
	*x1 = *x2;
	*x2 = tmp;
}
void 
void AdjustDown(int* 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;
		}
	}
}

void HeapPop(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);//向下调整
}
5.3.2. 复杂度分析
  • 时间复杂度:假设有N个节点,高度为h,2h -1=N。至少调整log2(N+1)-1次,所以时间复杂度为logN。
  • 空间复杂度:没有开辟额外的空间,空间复杂度为O(1)。

5.4. 获取堆顶元素

5.4.1. 代码实现
HpDataType HeapTop(Heap* hp)//获取堆顶元素
{
	assert(hp);
	assert(hp->size > 0);
	return hp->a[0];
}
5.4.2. 复杂度分析
  • 时间复杂度:没有额外的时间消耗,时间复杂度为O(1)。
  • 空间复杂度:没有额外的空间消耗,空间复杂度为O(1)。

5.5. 获取堆的元素个数

5.5.1. 代码实现
size_t HeapSize(Heap* hp)//堆的大小
{
	assert(hp);
	return hp->size;
}
5.5.2. 复杂度分析
  • 时间复杂度:没有额外的时间消耗,时间复杂度为O(1)。
  • 空间复杂度:没有额外的空间消耗,空间复杂度为O(1)。

5.6. 判断堆是否为空

5.6.1. 代码实现
bool HeapEmpty(Heap* hp)//判断堆是否为空
{
	assert(hp);
	return hp->size == 0;
}
5.6.2. 复杂度分析
  • 时间复杂度:没有额外的时间消耗,时间复杂度为O(1)。
  • 空间复杂度:没有额外的空间消耗,空间复杂度为O(1)。

5.7. 输出堆

5.7.1. 代码实现
void HeapDisplay(Heap* hp)//堆的打印
{
	for (int i = 0; i < hp->size; ++i)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}
5.7.2. 复杂度分析
  • 时间复杂度:遍历整个数组,时间复杂度为O(N)。
  • 空间复杂度:没有额外的空间消耗,空间复杂度为O(1)。

5.8. 建堆

5.8.1. 代码实现
void HeapCreatUp(Heap* hp,HpDataType* arr,int n)//向上调整建堆
{
	assert(hp && arr);
	for (int i = 0; i < n; i++)
	{
		HeapPush(hp, arr[i]);
	}
}
void HeapCreatDown(Heap* hp, HpDataType* arr, int n)//向下调整建堆
{
	assert(hp && arr);
	HpDataType* tmp = (HpDataType*)malloc(sizeof(HpDataType) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp->a = tmp;
	memcpy(hp->a, arr, sizeof(HpDataType) * n);
	hp->size = n;
	hp->capacity = n;
	for (int i = ((n - 1) - 1) / 2; i >= 0; i--)//从最后一个元素开始
	{
		AdjustDown(hp->a, n, i);
	}
}
5.8.2. 复杂度分析

假设高度为h,节点个数为N。如果是向上调整建堆:

img

F ( N ) = 2 1 × 1 + 2 2 × 2 + . . . + 2 h − 1 × ( h − 1 ) 2 F ( N ) = 2 2 × 1 + 2 3 × 2 + . . . + 2 h − 1 × ( h − 1 ) + 2 h × ( h − 1 ) 2 F ( N ) − F ( N ) = − 2 1 − 2 2 − 2 3 − . . . 2 h − 1 + 2 h × ( h − 1 ) = − 2 h + 2 − 2 h + 2 h × h F ( N ) = 2 h × ( h − 2 ) + 2 , N = 2 h − 1 F ( N ) = ( N + 1 ) × ( l o g 2 ( N + 1 ) − 2 ) + 2 F(N)=2^1×1+2^2×2+...+2^{h-1}×(h-1)\\ 2F(N)=2^2×1+2^3×2+...+2^{h-1}×(h-1)+2^h×(h-1)\\ 2F(N)-F(N)=-2^1-2^2-2^3-...2^{h-1}+2^h×(h-1)=-2^h+2-2^h+2^h×h\\ F(N)=2^h×(h-2)+2,N=2^h-1\\ F(N)=(N+1)×(log2(N+1)-2)+2 F(N)=21×1+22×2+...+2h1×(h1)2F(N)=22×1+23×2+...+2h1×(h1)+2h×(h1)2F(N)F(N)=212223...2h1+2h×(h1)=2h+22h+2h×hF(N)=2h×(h2)+2,N=2h1F(N)=(N+1)×(log2(N+1)2)+2

如果是向下调整建堆:

img
F ( N ) = 2 h − 2 × 1 + 2 h − 3 × 2 + . . . + 2 0 × ( h − 1 ) 2 F ( N ) = 2 h − 1 × 1 + 2 h − 2 × 2 + . . . + 2 1 × ( h − 1 ) 2 F ( N ) − F ( N ) = 2 h − 1 + 2 h − 2 + . . . 2 1 − 2 0 × ( h − 1 ) = 2 h − 1 − h F ( N ) = 2 h − 1 − h , N = 2 h − 1 F ( N ) = N − l o g 2 ( N + 1 ) F(N)=2^{h-2}×1+2^{h-3}×2+...+2^0×(h-1)\\ 2F(N)=2^{h-1}×1+2^{h-2}×2+...+2^1×(h-1)\\ 2F(N)-F(N)=2^{h-1}+2^{h-2}+...2^1-2^0×(h-1)=2^h-1-h\\ F(N)=2^h-1-h,N=2^h-1\\ F(N)=N-log2(N+1) F(N)=2h2×1+2h3×2+...+20×(h1)2F(N)=2h1×1+2h2×2+...+21×(h1)2F(N)F(N)=2h1+2h2+...2120×(h1)=2h1hF(N)=2h1h,N=2h1F(N)=Nlog2(N+1

  • 时间复杂度:向上调整建堆最后一排调整h-1次,倒数第二排调整h-2次…时间复杂度为NlogN。向下调整建堆倒数第二排调整1次,倒数第二排调整2…第一排调整h-1次。时间复杂为O(N)。
  • 空间复杂度:无论是向上调整建堆还是向下调整建堆都需开辟N个空间,所以空间复杂度为O(N)。

5.9. 销毁堆

5.9.1. 代码实现
void HeapDestroy(Heap* hp)//销毁堆
{
	assert(hp);
	free(hp->a);
	hp->size = hp->capacity = 0;
}
5.9.2. 复杂度分析
  • 时间复杂度:没有额外的时间消耗,时间复杂度为O(1)。
  • 空间复杂度:没有额外的空间消耗,空间复杂度为O(1)。

5.10. 完整代码

5.10.1. Heap.h
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
typedef int HpDataType;
typedef struct Heap 
{
	HpDataType* a;//存储数据
	int size;//大小
	int capacity;//容量
}Heap;
void HeapInit(Heap* hp);//堆的初始化
void AdjustUp(Heap* hp, int child);//向上调整
void HeapPush(Heap* hp, HpDataType x);//堆的插入
bool HeapEmpty(Heap* hp);//判断堆是否为空
size_t HeapSize(Heap* hp);//堆的大小
void AdjustDown(int* a, int n, int parent);//向下调整
void HeapPop(Heap* hp);//删除堆顶元素
HpDataType HeapTop(Heap* hp);//获取堆顶元素
void HeapDisplay(Heap* hp);//堆的打印
void HeapDestroy(Heap* hp);//销毁堆
void HeapCreatUp(Heap* hp,HpDataType* arr, int n);//向上调整建堆
void HeapCreatDown(Heap* hp,HpDataType* arr, int n);//向下调整建堆
5.10.2. Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void HeapInit(Heap* hp)//堆的初始化
{
	assert(hp);
	hp->a = NULL;
	hp->size = hp->capacity = 0;
}
void swap(HpDataType* x1, HpDataType* x2)
{
	HpDataType tmp = *x1;
	*x1 = *x2;
	*x2 = tmp;
}
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 HeapPush(Heap* hp, HpDataType x)//堆的插入
{
	assert(hp);
	if (hp->size == hp->capacity)
	{
		int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HpDataType* tmp = (HpDataType*)realloc(hp->a, newCapacity * sizeof(HpDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newCapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp, hp->size - 1);//向上调整
}
void AdjustDown(int* 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;
		}
	}
}

void HeapPop(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 HeapTop(Heap* hp)//获取堆顶元素
{
	assert(hp);
	assert(hp->size > 0);
	return hp->a[0];
}

bool HeapEmpty(Heap* hp)//判断堆是否为空
{
	assert(hp);
	return hp->size == 0;
}

size_t HeapSize(Heap* hp)//堆的大小
{
	assert(hp);
	return hp->size;
}

void HeapDisplay(Heap* hp)//堆的打印
{
	for (int i = 0; i < hp->size; ++i)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}
void HeapCreatUp(Heap* hp,HpDataType* arr,int n)//向上调整建堆
{
	assert(hp && arr);
	for (int i = 0; i < n; i++)
	{
		HeapPush(hp, arr[i]);
	}
}
void HeapCreatDown(Heap* hp, HpDataType* arr, int n)//向下调整建堆
{
	assert(hp && arr);
	HpDataType* tmp = (HpDataType*)malloc(sizeof(HpDataType) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp->a = tmp;
	memcpy(hp->a, arr, sizeof(HpDataType) * n);
	hp->size = n;
	hp->capacity = n;
	for (int i = ((n - 1) - 1) / 2; i >= 0; i--)//从最后一个元素开始
	{
		AdjustDown(hp->a, n, i);
	}
}
void HeapDestroy(Heap* hp)//销毁堆
{
	assert(hp);
	free(hp->a);
	hp->size = hp->capacity = 0;
}

6. Top-K问题

6.1. 问题分析

Top-K问题简单来说就是求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。这个问题在我们日常生活中非常常见,比如说:游戏中活跃度前十的玩家,世界五百强企业等等。

解决这个问题常见的思路就是遍历或者排序,但是当数据量较大时这种方法就并不适用了。这时我们就需要建堆来处理,具体操作方法如下:

  1. 用数据集合中前K个元素来建堆。
  • 前k个最大的元素,则建小堆。
  • 前k个最小的元素,则建大堆。
  • 用剩余的N - K个元素依次与堆顶元素来比较,不满足条件则替换堆顶元素。
void TopK(int* a, int n, int k)
{
	//建堆
	int* kminHeap = (int*)malloc(sizeof(int) * k);
	if (kminHeap == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	//将前k个数据放入堆中
	for (int i = 0; i < k; i++)
	{
		kminHeap[i] = a[i];
	}
	//向下调整法建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(kminHeap, k, i);
	}
	//依次比较
	for (int i = k; i < n; i++)
	{
		if (a[i] > kminHeap[0])
		{
			kminHeap[0] = a[i];
			AdjustDown(kminHeap, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminHeap[i]);
	}
	printf("\n");
	free(kminHeap);
}
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;
	TopK(a, n, 10);
}

img

6.2. 复杂度分析

  • 时间复杂度:建堆时间为K,向下调整的最坏时间为(N-K)*logK。所以时间复杂度为NlogK。
  • 空间复杂度:建堆会开辟K的个空间,所以空间复杂度为logK。
  • 109
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 76
    评论
### 回答1: 《Android深度探索:系统应用源代码分析与ROM定制》是一本介绍Android系统应用源代码分析和ROM定制领域的专业书籍。本书由作者对Android系统的深入研究和实践经验进行总结,内容包括系统应用源代码分析、ROM定制的实践案例和技术点等。 书籍的第一部分主要讲解了Android系统应用源代码分析的方法和技巧。通过对Android系统应用的分析,读者可以了解和掌握系统应用的运行原理和内部逻辑。书中详细介绍了系统应用的组成结构、数据存储方式、界面布局等核心知识,让读者能够更深入地理解Android系统应用的工作机制。 第二部分则介绍了ROM(Read-Only Memory)定制的相关内容。ROM定制是指通过修改Android系统的源代码以达到个性化需求的目的。书中详细讲解了ROM定制的原理和方法,包括修改系统应用、替换系统资源、自定义主题等操作。此外,书中还提供了大量实际案例供读者参考,帮助读者在实践中掌握ROM定制的技巧。 本书适合对Android系统应用开发和ROM定制有一定基础的读者阅读。读者通过学习本书可以进一步提升对Android系统的理解和应用开发能力,同时也能够掌握ROM定制的相关技术,为个性化修改系统做出自己的贡献。 综上所述,《Android深度探索:系统应用源代码分析与ROM定制》涵盖了系统应用源代码分析和ROM定制的相关知识,适合对Android系统开发和ROM定制感兴趣的读者学习和参考。通过本书的学习,读者可以更加深入地理解Android系统的内部机制,同时也可以通过定制ROM实现个性化的Android系统定制。 ### 回答2: 《Android深度探索: 系统应用源代码分析与ROM定制PDF》这本书是一本探究Android系统应用源代码和ROM定制的指南。本书的目标是帮助读者深入了解Android系统,并探索系统应用源代码的细节。我们将介绍如何分析系统应用的源代码,并讨论ROM定制的过程。 这本书分为几个部分,包括系统应用源代码分析、ROM定制的基本概念和步骤以及一些实际的案例分析。首先,我们将介绍如何获取和编译Android系统的源代码,并讨论如何使用Android Studio等工具来分析系统应用的源代码。通过深入研究系统应用的源代码,读者可以了解系统应用的内部机制和实现细节。 接下来,我们将介绍ROM定制的基本概念和步骤。ROM定制是指根据个人或特定需求对Android系统进行修改和定制的过程。这本书将介绍如何根据自己的需求修改系统应用或添加自定义功能。我们还将讨论如何制作自己的ROM,并介绍一些常见的ROM定制技巧和工具。 最后,本书还包括一些实际的案例分析,通过实际的项目展示如何进行系统应用源代码分析和ROM定制。这些案例分析将帮助读者更好地理解和运用书中所介绍的知识。 总之,《Android深度探索: 系统应用源代码分析与ROM定制PDF》是一本深入探索Android系统并学习系统应用源代码和ROM定制的指南。通过阅读本书,读者将能够了解Android系统的内部机制、掌握系统应用的源代码分析技巧,并能够进行自己的ROM定制。无论是对于Android开发者还是ROM爱好者来说,这本书都是一本难得的参考书籍。 ### 回答3: 《Android深度探索:系统应用源代码分析与ROM定制PDF》是一本涉及Android系统应用源代码分析和ROM定制的电子书。这本书共分为两个主要部分,分别是系统应用源代码分析和ROM定制。 在第一部分,书中详细探讨了Android系统应用的源代码分析。作者通过对Android系统的应用源代码进行逐个解析和讲解,帮助读者了解应用的结构、实现原理以及各个模块之间的关系。阅读本部分可以使读者更加深入地了解Android系统应用的工作方式,并且能够根据源代码进行调试和自定义开发。 在第二部分,书中介绍了ROM定制的相关内容。ROM是指Android系统的固件,而ROM定制就是对Android系统进行个性化定制。作者从ROM定制的基本概念开始讲解,然后逐步介绍了ROM的制作和修改过程,包括系统应用的替换、桌面主题的更换、开机画面的定制等。通过这部分内容的学习,读者可以掌握ROM定制的方法和技巧,实现个性化定制的Android系统。 这本书适合对Android开发有一定基础的读者,尤其是对Android系统应用开发和ROM定制感兴趣的人群。通过阅读本书,读者可以深入了解Android系统应用和ROM定制的内部原理,提升自己的技术水平。无论是作为学习材料还是作为参考工具,这本书都会对读者有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 76
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值