数据结构初阶——堆

二叉树的顺序结构

堆是一种特殊的数据结构,通常可以被看做一棵树的数组对象。普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结 构,一个是操作系统中管理内存的一块区域分段。

堆的概念及结构

堆将所有的元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足如下的性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;若节点的值都小于父节点,则为大堆,否则为小堆。

数据结构初阶——堆_第1张图片

左图就是一个逻辑意义上的小堆的形式,而下图就是该小堆在内存中存储的结构。

 

通过观察上述的图片我们可以发现一些重要的结论:

当我们得到了一个父节点的位置时,例如10的存储下表是[0]。如果我们需要求解它的子节点的小标我们只需要将父节点的下标的值*2+1,这样得到的就是左孩子的下标。+2得到的就是右孩子的坐标。同样如果我们需要通过子节点下标得到父节点的下标,只需要将(子节点的下标-1)/2。

leftchild = parent / 2 + 1;

rightchild = parent / 2 + 2;

parent = (child - 1) / 2;

至此我们就可以通过上述的公式来实现一个堆的结构。

由于堆的结构是以数组为基底,因此我们可以直接写出其结构。

堆的实现

下面的数据结构都已实现小堆为例子:

定义堆的结构体

typedef int HPDataType;

//定义堆的结构体
typedef struct Heap
{
	HPDataType* _data;
	int _size;
	int _capacity;
}Heap;

堆的插入

假设我们已经有了第一个数据,其已经成为了一个大堆,现在需要插入一个新的数据,有两种可能性:第一种比前一个数据小,这样就不需要改变位置;第二种比前一个数据大,这样就需要改变两个数据的位置,那么通过前面的父子节点的关系,就可以改变他们的位置,以形成一个大堆。将这种调整的行为,称为向上调整。

按照之前动态顺序表的编写方法,先初始化堆节点,然后若是size与capacity大小相等,那么我们就需要扩容,将数据存储在使用*_data指针指向的数据中,然后对size与capacity分别进行修改。然后我们就需要对堆新插入的数据进行处理,进行上述的向上调整处理。按照下图的例子可以表示其过程:

数据结构初阶——堆_第2张图片 上述是小堆的逻辑结构,5在逻辑上是插入在34之后,但在物理存储上是存放在28之后的位置,因为堆使用的是数据结构是线性表。

//初始化堆
void HeapInit(Heap* php)
{
	assert(php);

	php->_data = NULL;
	php->_size = 0;
	php->_capacity = 0;
}
//交换函数
void Swap(HPDataType* num1, HPDataType* num2)
{
	int tmp = *num1;
	*num1 = *num2;
	*num2 = tmp;
}

//向上调整,最后形成小堆
void AdjustUp_Small(HPDataType* data, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//在这里由于是从下往上的不会有,没有有节点的影响
		if (data[child] < data[parent])
		{
			Swap(&data[child], &data[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

// 堆的插入 - 小堆
void HeapPush_Small(Heap* php, HPDataType x)
{
	assert(php);

	//扩容
	if (php->_size == php->_capacity)
	{
		int newcapaticy = php->_capacity == 0 ? 4 : php->_capacity * 2;
		HPDataType* newnode = (HPDataType*)realloc(php->_data, sizeof(HPDataType) * newcapaticy);
		if (newnode == NULL)
		{
			perror("realloc failed");
			exit(-1);
		}

		php->_data = newnode;
		php->_capacity = newcapaticy;
	}

	php->_data[php->_size] = x;
	php->_size++;

	AdjustUp_Small(php->_data, php->_size - 1);
}

有了上述的函数,我们就可以通过插入来简单地形成一个小堆。

数据结构初阶——堆_第3张图片 

堆的删除

动态线性表的删除非常的简单只要删除末尾,但是堆的删除一般删除堆顶的元素,这样我们就需要讨论一下具体的操作,若是我们直接删除堆顶的元素,然后按照线性表的操作将数据往前移,这样的操作是不可行的,它会直接将整个堆的形态破坏。因为在线性表中删除使用尾删,操作非常的简单,那么我们就假想将堆顶的元素与堆尾的元素进行位置互换,这样以后堆顶元素的删除就完成,换到堆顶的元素由于原先是处于末尾,那么它一定不是最小的元素,此时我们就需要将该元素往下进行调整,与向上调整类似。

向下调整就是以小标为0的点为父节点,将其与较大小的一个子节点进行交换,已交换的子节点再次作为父节点,重复上述过程直至该节点都小于其子节点或者超出数组的size大小时停止。在这里有一点需要注意,就是向下调整的时候可能会遇到父节点是没有右孩子的,需要添加条件进行判断。数据结构初阶——堆_第4张图片

 

// 向下调整,最后形成小堆
void AdjustDown_Small(HPDataType* data, int size, int parent)
{
	int child = parent * 2 + 1;

	while (child < size)
	{
        //在这里需要注意堆假想成的树可能没有右孩子,需要添加条件进行判断。
		if ((data[child + 1] < data[child]) && (child + 1 < size))
		{
			child++;
		}

		if (data[child] < data[parent])
		{
			//交换数据
			Swap(&data[child], &data[parent]);
			//迭代祖孙
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

// 堆的删除 - 小堆
void HeapPop_Small(Heap* php)
{
	assert(php);
	assert(php->_size > 0);

	Swap(&php->_data[php->_size - 1], &php->_data[0]);
	php->_size--;

	AdjustDown_Small(php->_data, php->_size, 0);
}

堆创建的方式

在前面的内容中,我们讲述过堆的插入,通过堆的插入可以创建出一个堆,但是这种方式的时间复杂度为O(N*logN),我们还可以使用一种时间复杂度为O(N)的方法。

第一种方法就是上文中的向上调整法:数据结构初阶——堆_第5张图片

在这种方案中我们可以了解到一点,在二叉树中最后一层的节点数是上一层的两倍,从第一层开始每一层都需要往上进行移动,移动的次数依次加1,最后形成了上述与树的高度有关的时间复杂度公式,在上述的时间复杂度的公式中,简单地看最后一项2^(h-1)*(h-1),由一颗满二叉树共有2^h-1个节点可以将最后一项化为((N+1)*log(N+1)-1)/2,单看这一项复杂度就为O(N*logN)。

第二种方法就是向下调整法:

数据结构初阶——堆_第6张图片

我们如果有一颗树,只有顶部的元素不同,左右两个子树已经成为了堆,若要得到一个堆,此时我们就需要对堆顶元素进行向下调整。再回到这里,现在左右两颗子树都不是堆,那么我们就要将其变成堆,堆左右子树的左右子树进行向下调整,那么我们就找到最后的一颗子树,按上图所示,先是以6、7为子节点的子树,然后是以4、5为子节点的子树,最后是整个树。树的高度为h,从顶层开始,最顶层需要移动h-1次,有2^0个节点,然后向下调整节点个数依次增加,移动的层数依次减少,最后可得到上述F(h)的时间复杂度。经过计算可以得到时间复杂度F(N)=N-long(N+1),由于N的数值很大的时候log(N+1)可以近似于没有。

	//建堆算法
	//向下调整建堆
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_Big(php->_data, php->_size, i);
	}

	//向上调整建堆
	for (int i = 1; i < size; i++)
	{
		AdjustUp_Big(php->_data, i);
	}

一些其他的函数

堆的销毁

// 堆的销毁
void HeapDestroy(Heap* php)
{
	assert(php);

	free(php->_data);
	php->_data = NULL;

	php->_size = php->_capacity = 0;
}

堆的数据个数 

// 堆的数据个数
int HeapSize(Heap* php)
{
	assert(php);

	return php->_size;
}

 堆的判空

// 堆的判空
bool HeapEmpty(Heap* php)
{
	assert(php);

	return php->_size == 0;
}

堆的应用

堆排序

若是想要将一些数据进行排序,可以运用堆的思想。若是我们要升序排列一些数据,我们就可以将待排的数组组成一个大堆的形式,通过堆的删除的方法,依次将大的数据存放在数组的末尾。最后可以形成一个升序数组。降序的也是同理,将数据建立一个小堆,然后依次进行堆的删除。

// 升序的堆排序
void HeapSort_Up(int* a, int size)
{
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_Big(a, size, i);
	}
	HeapPrint(&a);

	int end = size - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown_Big(a, end--, 0);
	}
	HeapPrint(&a);
}

// 降序的堆排序
void HeapSort_Down(int* a, int size)
{
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_Small(a, size, i);
	}
	HeapPrint(&a);

	int end = size - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown_Small(a, end--, 0);
	}
	HeapPrint(&a);
}

TopK问题

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

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

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

模拟随机文件的实现

// 创建随机数字的文件
void CreateFileData(int n, int k)
{
	FILE* fin = fopen("data.txt", "w");
	srand(time(0));
	if (fin == NULL)
	{
		perror("fopen failed");
		exit(-1);
	}
	
	for (int i = 0; i < n; i++)
	{
		int val = rand()%10000;

		if ((val % 11 == 0) && k>0)
		{
			val = val + 100000;
			k--;
		}
		fprintf(fin, "%d\n", val);
	}

	fclose(fin);
}

测试从文件中读取数据TopK算法 - 最大的K个

// 测试从文件中读取数据TopK算法 - 最大的K个
void TestTopK_Max()
{
	int n = 0;
	int k = 0;

	printf("请输入n和k:>");
	scanf("%d%d", &n, &k);

	CreateFileData(n, k);

	FILE* fout = fopen("data.txt", "r");
	if (fout == NULL)
	{
		perror("fopen failed");
		exit(-1);
	}

	HPDataType* a = (HPDataType*)malloc(sizeof(HPDataType) * k);
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &a[i]);
	}

	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown_Small(a, k, i);
	}

	int val = 0;
	while ((fscanf(fout, "%d", &val)) != EOF)
	{
		if (val > a[0])
		{
			a[0] = val;
			AdjustDown_Small(a, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d\n", a[i]);
	}

	HeapSort_Up(a, k);

	fclose(fout);
}

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