图说数据结构---堆

你要批评指点四周风景,你首先要爬上屋顶。 – 歌德
图说数据结构---堆_第1张图片

目录

      • 一. 堆的概念及结构
    • 1.概念
    • 2.性质
    • 3.结构
      • 二.堆的创建(小堆)
    • 1.堆的向下调整算法
    • 2.堆的向上调整算法
    • 3.向上调整建堆
    • 4.向下调整建堆
    • 5.建堆的时间复杂度
      • 三.堆的应用
    • 1.堆排序
    • 2.Top-k问题
      • 四. 堆的其他一些接口
    • 1.堆的定义
    • 2.堆的初始化
    • 3.堆的销毁
    • 4.堆的插入
    • 5.堆的删除
    • 6.取堆顶元素

一. 堆的概念及结构

1.概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

2.性质

堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。

3.结构

图说数据结构---堆_第2张图片

二.堆的创建(小堆)

1.堆的向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提: 左右子树必须是一个堆,才能调整。 \color{#FF0000}{左右子树必须是一个堆,才能调整。} 左右子树必须是一个堆,才能调整。

int array[] = {27,15,19,18,28,34,65,49,25,37};

下面是图示过程:
图说数据结构---堆_第3张图片
主要思想:
1.让parent标记需要调整的结点(图中27),child标记parent的孩子中值最小的那个结点(图中15)。
2.比较parent结点和child结点的值,若parent小于child,让parent和child交换对应结点的值,不小于直接结束。
3.parent标记结点的新位置(即child处),child再次标记parent的孩子中值最小的那个结点(如果孩子存在的话),以此类推。

具体代码:

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] > a[child + 1])
		{
			child++;
		}
		if (a[child] < a[parent])
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

2.堆的向上调整算法

主要思想:
1.先用child标记我们要向上调整的结点,然后用parent找到child的父节点
2.比较child结点和parent结点的值,若小于parent则交换两结点的值,不小于直接结束
3.child标记结点的新位置(即parent处),parent标记child的父节点(若存在)以此类推。
图示:
图说数据结构---堆_第4张图片
我们再看代码:

void AdjustUp(HPDataType* a,int child)
{
	int parent = (child - 1) / 2;
	//while(parent >= 0)   不好
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

当child等于0时,parent = (child - 1)/2,parent依旧等于0,因此循环条件while(parent >= 0) 不好

3.向上调整建堆

主要思想:
从第二个结点开始,依次进行堆的向上调整,直到最后一个结点。
图示:
图说数据结构---堆_第5张图片
代码:

void HeapCreate(HP* php, HPDataType* a, int n)
{
	assert(php);
	HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	php->a = tmp;
	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->size = php->capacity = n;

	//向上调整建堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(php->a, i);
	}

}

4.向下调整建堆

主要思想:
从下到上,依次找到各个子树的父节点,将各个子树调整为小堆
图示:
图说数据结构---堆_第6张图片
代码:

void HeapCreate(HP* php, HPDataType* a, int n)
{
	assert(php);
	HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	php->a = tmp;
	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->size = php->capacity = n;
	
	//向下调整建堆
	for (int i = (n-1-1)/2; i >= 0; i--)
	{
		AdjustDown(php->a, n, i);
	}
}

5.建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
下面是向下建堆的时间复杂度计算:
图说数据结构---堆_第7张图片
因此:向下调整建堆的时间复杂度为O(N)。
下面是向上建堆的时间复杂度计算:
图说数据结构---堆_第8张图片
此时间复杂度明显大于向下调整建堆。
因此: 向下调整建堆的时间复杂度更优,为 O ( N ) 。 \color{#FF0000}{向下调整建堆的时间复杂度更优,为O(N)。} 向下调整建堆的时间复杂度更优,为O(N)

三.堆的应用

1.堆排序

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

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

2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
以升序为例:
1.将第一个结点(所有元素中值最大的)与最后一个结点交换值,再对第一个结点进行向下调整,这样最大的数就到了最后面,我们只需要排前n-1个数。
2.再将第一个结点(次大的)与倒数第二个结点交换值,对第一个结点进行向下调整,以此类推。

图说数据结构---堆_第9张图片
代码

void HeapSort(HP* php)
{
	int end = php->size - 1;

	//升序建大堆
	while (end)
	{
		swap(&php->a[end], &php->a[0]);
		AdjustDown(php->a, end, 0);
		end--;
	}


}

2.Top-k问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆

前k个最大的元素,则建小堆
前k个最小的元素,则建大堆

  1. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    若求前k个最大的元素,则遍历数据,比堆顶大就替换堆顶,再向下调整。
    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
    图说数据结构---堆_第10张图片

代码:

//topk问题(数多,且pop少)
void topk()
{
	int minheap[5];
	FILE* f = fopen("Data.txt", "r");
	if (f == NULL)
	{
		perror("fopen fail");
		exit(-1);
	}
	
	int k = 5;
	for (int i = 0; i < k; i++)
	{
		fscanf(f, "%d", &minheap[i]);
	}

	//建小堆
	for (int i = (k-1-1)/2; i >=0; i--)
	{
		AdjustDown(minheap, k, i);
	}
	
	//从文件中取
	int val = 0;
	while (fscanf(f, "%d", &val) != EOF)
	{
		if (val > minheap[0])
		{
			minheap[0] = val;
			AdjustDown(minheap, k, 0);
		}
	}
	//打印小堆
	for (int i = 0; i < k; i++)
	{
		printf("%d \n", minheap[i]);
	}

}

四. 堆的其他一些接口

1.堆的定义

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

2.堆的初始化

void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

3.堆的销毁

void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->size = php->capacity = 0;
	
}

4.堆的插入

主要思想:
在数组最后插入一个数据,在对这个数据进行向上调整。
图示:
图说数据结构---堆_第11张图片

void HeapPush(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 error");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->size] = x;
	php->size++;


	AdjustUp(php->a,php->size-1);

}

5.堆的删除

图示:
图说数据结构---堆_第12张图片

void HeapPop(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);

}

6.取堆顶元素

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];

}

你可能感兴趣的:(数据结构,数据结构,算法)