【数据结构】堆

堆的概念及结构

堆也是完全二叉树,只不过堆专门是用顺序表的形式来存储的。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

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

【数据结构】堆_第1张图片
【数据结构】堆_第2张图片

这里有几道选择题来帮助大家进一步理解堆的概念:

  1. 下列关键字序列为堆的是:( A )
    A 100,60,70,50,32,65
    B 60,70,65,50,32,100
    C 65,100,70,32,50,60
    D 70,65,100,32,50,60
    E 32,50,100,70,65,60
    F 50,100,70,65,60,32
  2. 已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是:( C )
    A 1
    B 2
    C 3
    D 4
  3. 一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为:( C )
    A(11 5 7 2 3 17)
    B(11 5 7 2 17 3)
    C(17 11 7 2 3 5)
    D(17 11 7 5 3 2)
    E(17 7 11 3 5 2)
    F(17 7 11 3 2 5)
  4. 最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是:( C )
    A[3,2,5,7,4,6,8]
    B[2,3,5,7,4,6,8]
    C[2,3,4,5,7,8,6]
    D[2,3,4,5,6,7,8]

堆的实现(以大根堆为例)

堆其实是将所有元素按完全二叉树的顺序存储方式存储在一个一维数组中的。

我们先来给一组数据:

a[6] = { 27, 39, 18, 5, 9, 32};

该完全二叉树的初始状态如下:

【数据结构】堆_第3张图片

我们有两种方法将这组数据来建堆:

  • 向上调整算法
  • 向下调整算法

堆向上调整算法

我们要从数组中第0个元素开始建堆。我们每次选第i个数(i从0开始),将这个数与先前建好的堆进行构建新堆,不断往堆顶挪,直到到达堆顶或者是遇到了比自己大的数就停止,这样调整堆的方法就叫做向上调整。

【数据结构】堆_第4张图片

代码实现:

//向上调整
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	//确定范围
	while (child > 0)
	{
		//儿子比父亲大就交换
		if (a[child] > a[parent])
			swap(&a[child], &a[parent]);

		//调整儿子与父亲的位置
		child = parent;
		parent = (child - 1) / 2;
	}
}


堆向下调整算法

从倒数第一个非叶子节点开始,将该数与其下方的数进行对比,若出现比两个子树元素大的情况,就将子树中最大的挪到父结点处直至越界停止,然后再判断非叶子节点的前一个结点不断重复直至到堆顶,这样的调整就叫做向下调整。

【数据结构】堆_第5张图片

代码实现:

//向下调整
void AdjustDown(HPDataType* a, int parent, int n)
{							//这里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;
	}
}

堆的创建

建堆的时候,是要将堆中的数组进行调整,一步一步调整成大堆/小堆的。

// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	//上面的这些就是把数组中的元素放到堆里,没什么用,下面的两个调整堆才有用。
	/*hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (hp->_a == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	for (int i = 0; i < n; i++)
	{
		hp->_a[i] = a[i];
	}

	hp->_size = n;
	hp->_capacity = n;*/

	//向上调整建堆
	for (int i = 1; i < hp->_size; i++)
	{
		AdjustUp(hp->_a, i);
	}

	//向下调整建堆
	//这里i的初始值就是第一个非叶子节点的下标。
	for (int i = (hp->_size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(hp->_a, i, hp->_size);
	}
}

建堆的时间复杂度

向下调整和向上调整建堆的时间复杂度:

【数据结构】堆_第6张图片

我们可以看到,向下调整建堆时,最后一层的结点不需要向下移动,而满二叉树中最后一层的结点占了总结点个数的一半+1;但是向上调整的时候最后一层的结点还需要移动,只有堆顶的数不需要移动。所以向下调整是要比向上调整更简单一些的。

向上调整的时间复杂度计算方法类似,这里就直接给出结果:O(N*logN)

所以总的来说,建堆还是推荐使用向下调整建堆。

堆的插入

插入数的时候是在数组的末尾插的,所以我们只需要将该节点与其祖先结点进行调整就可以了,也就是说只需要调一次。

比如说我现在在末尾插入一个57,那么:

【数据结构】堆_第7张图片

代码实现:

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	//上面这些是判断当前堆中的数组是否满了
	/*if (hp->_capacity == hp->_size)
	{
		int NewCapacity = hp->_capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->_a, NewCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}

		hp->_a = tmp;
		hp->_capacity = NewCapacity;
	}*/

	hp->_a[hp->_size] = x;
	hp->_size++;
	//插入是在数组的尾插入的,所以要用到向上调整
	AdjustUp(hp->_a, hp->_size - 1);
}

堆的删除

删除删的是堆顶元素,我们需要先将堆顶元素与数组中的最后一个元素交换,然后再从堆顶开始调整堆。

【数据结构】堆_第8张图片

上面这些基本上就是堆实现的方法了,最主要掌握的就是向上调整和向下调整。

堆的应用

堆排序

首先堆排序,得先建堆,建大堆还是建小堆是由排升序还是排降序来决定的。

排升序就建大堆,排降序就建小堆。

如果你想对一个数组排序,那么数组本身就是一个堆,所以我们就可以直接对数组进行调整,而不是再创建一个堆来进行堆排序。

怎么调整呢,和调整堆中的数组一样的方法,向上调整或者向下调整,这两个方法都可以建堆。上面也讲到了,推荐使用向下调整建堆。所以我下面就使用向下调整来建堆了。

代码实现:

	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}

建完堆之后就要开始堆排序了。

这是一个循环,每次都要将头(0)尾(n-1)进行交换,将尾–,然后再从头开始向下调整,直到当尾变成1就停止。

【数据结构】堆_第9张图片

代码实现:

void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}

	//首尾交换,向下调整
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, 0, end + 1);
	}
}

TOP-K 问题

// TopK问题:找出N个数里面最大/最小的前K个问题。

首先还是建堆
如果要的是N个数中最大的前K个数,那就建小堆。
如果要的是N个数中最小的前K个数,那就建大堆。

以大的前K个数为例:

先搞一个大小为K的数组,然后把N个数中的前K个数放到数组中,然后把大小为K的数组建堆成小堆,然后将后N - K个数与堆顶进行比较,如果比堆顶数小,就比较下一个,如果比堆顶数大,就替换掉堆顶(这样能保证前K个数能进入堆内)。不断遍历,直到N个数遍历完毕。

如果你建的是大堆的话,当我们没有遍历完N个数时,最大的数可能已经占到堆顶位置了,所以无论再怎么遍历,都无法将前K大的数入队。

代码实现:

void PrintTopK(int* a, int n, int k)
{
	int* Top = (int*)malloc(k * sizeof(int));
	if (Top == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//搞出存放前K个数的数组
	for (int i = 0; i < k; i++)
	{
		Top[i] = a[i];
	}

	//建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(Top, i, k);
	}

	for (int i = k; i < n; i++)
	{
		if (a[i] > Top[0])
			Top[0] = a[i];

		AdjustDown(Top, 0, k);
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", Top[i]);
	}

}

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