0
点赞
收藏
分享

微信扫一扫

Day63:Bellman_ford队列优化算法 判断负权回路 单源有限最短路

草原小黄河 2024-07-25 阅读 27

前言

在二叉树_堆(上卷)中我们已经讲了堆的初始化、销毁和插入数据,现在让我们从堆的删除数据开始,接着往下学习堆的其他方法和堆排序。

正文

堆的删除数据

不同于我们的顺序表,我们的出堆(删除堆里的数据)出的是堆顶的数据。而在出堆时,如果我们像顺序表一样删掉下标为0的数据然后让后面的数据整体前移一位是不行的,因为这样会让“父子关系”乱套。

首先,我们让10和70交换位置,然后size–,10这个数据就被我们出堆了。

此时这个堆不再是小堆了,70不该在这个位置,但是其他数据没有问题。

所以要对70进行调整,可以看出应该是向下去调整。由于我们要调小堆,所以如果往下找孩子结点比自身小,就要交换。

向下调整算法

我们此时已知父节点下标为0,根据二叉树性质,左孩子child结点下标就为2*0+1即1,也就是15,15比70小。虽然这个例子中左孩子15是比右孩子56小的,但是我们仍然要写左右孩子比较的一步代码。**为什么比较左右孩子,把更小的拿来交换呢?**这是因为我们要调整的是小堆,而小堆的堆顶必须是堆中的最小值,所以我们要确保换上去的是最小的。

所以我们要先将左右孩子进行比较,再将较小的那一个与父节点比较,然后根据情况有可能交换。

而以上这个过程也是要循环进行直到父节点不再比孩子结点大。

删除数据(包含向下调整算法)的代码:

void AdjustDown(HPDataType* arr, int parent, int n)
{
	int child = parent * 2 + 1;//左孩子
	while (child<n)
	{
		//找左右孩子中最小的那个
		if (child+1 < n && arr[child] > arr[child + 1])//child+1<n这个条件不能忘,否则右孩子就越界了
		{
			child++;//让child变为右孩子下标,而不再是左孩子下标
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HPPop(HP* php)//删除数据
{
	assert(php && php->arr);//要有数据才能删除
	//先交换首和尾
	Swap(&php->arr[0], &php->arr[php->size-1]);
	php->size--;
	//堆顶的向下调整
	AdjustDown(php->arr, 0, php->size);
}

然后在Test.c里测试一下:

可以看到size为5,确实删掉了一个数据;再看数组里的数据,也仍然是小堆。说明删除数据成功。

取堆顶数据

这个方法的代码就比较简单,直接展示:

HPDataType HPTop(HP* php)//取堆顶数据
{
	assert(php && php->size);//没有数据无法出堆
	return php->arr[0];
}
堆的判空

因为我们要返回的是布尔值,所以要加入对应头文件:

在Heap.h中:

#include<stdbool.h>

在Heap.c中:

bool HPEmpty(HP* php)//判空
{
	assert(php);
	return php->size == 0;
}

现在我们有取堆顶和出堆(出的是堆顶数据)、判空三个方法,配合使用就可以循环打印堆中数据。

//出堆测试
//HPPop(&hp);
while (!HPEmpty(&hp))
{
	printf("%d ", HPTop(&hp));
	HPPop(&hp);
}

由于小堆的堆顶总是最小值,我们每次出堆后会重新调整为新的有效小堆,然后我们打印堆顶,就是当前的最小值。所以打印完我们看到的是一个升序数组:

堆排序

因为上面这个现象,我们很难不想到排序,其实确实有这样用堆来排序的排序方法,叫做堆排序。

但是真正的堆排序并不是像我们这样:建堆然后循环去取堆顶和出堆。毕竟这样做我们只是打印结果是有序的,并没有将原本数组里的数据排成有序。

当然我们也可以取堆顶后不打印而是插入数组,循环后我们的数组会被覆盖为排好序的。

:🐙:我们做的事情是先有一个乱序数组,然后创建一个堆结构,将数组数据循环入堆得到一个有效的小堆(没排序),然后再通过取堆顶和出堆的循环覆盖掉原来数组,得到排好序的数组。

但是这样排序的问题在于我们不得不借助一个堆的数据结构,我们可以将其与我们再熟悉不过的冒泡排序进行比较。

可以看到,我们的冒泡排序使用的是两层循环,而没有使用额外的数据结构。

在C语言阶段,如果我们想要使用堆这样的数据结构,我们必须提前把这样的结构实现出来。如果我们像刚才讲的那种“堆排序”来堆排序,就得先把堆这样的结构实现好,还得把要用到的HPPush、HPTop、HPPop方法都准备好。

而且,我们会要创建额外的堆的空间,空间复杂度就是O(N),而冒泡排序的空间复杂度为O(1),现在我们希望堆排序的空间复杂度为O(1),也就是说不借助额外的数据结构。

那么,怎么堆排序呢?
//堆排序
//空间复杂度为O(1)
void HeapSort(int* arr, int n)
{
	
}

(和冒泡排序一样,我们的形参为要排序的数组和数组的数据个数)

我们回忆一下HPPush,我们其实是在数组的最后插入数据然后将其进行向上调整。而我们在堆排序中形参也有数组,同样是在数组中进行插入操作。所以我们可以直接在arr中进行调整使其变为有效的堆。(我们最初的“堆排序”是额外创建一个数组调整为堆)

怎么直接在数组中调整为堆呢(以小堆为例)?

我们循环去取数组里的数据,然后将其向上调整。

(例子)

可以看到,我们一个个取数组里的数据然后向上调整,就可以完成建堆操作。

代码:

Heap.c中的AdjustUp方法:

可以看到,我们就建堆成功了。

那么,接下来要怎么对这个堆排序呢?

而且,我们要就在这个数组里进行排序。

我们可以利用堆的堆顶为最大值(大堆)或最小值(小堆)这一点,来进行排序。

如果要排升序,我们就用大堆;如果要排降序,我们就用小堆。为什么这么说呢?

:🐙:比如现在我们有一个小堆,将堆顶数据与最后一个数据进行交换,最后一个数据就变成了数组里的最小值,而数组最后一个数据为最小值说明这应该是一个降序的数组。我们此时让size–,然后将堆顶数据向下调整,调整完又得到一个有效小堆,然后再重复将堆顶数据与当前的最后一个数据交换,这时整个数组倒数第二个位置就是数组中倒数第二小的数据……

重复这样的操作,我们就得到一个降序的数组。

所以,如果要排升序,我们就用大堆;如果要排降序,我们就用小堆

代码:

//堆排序
//空间复杂度为O(1)
void HeapSort(int* arr, int n)
{
	//建堆
    //此时我们的AdjustUp方法是建小堆的
	for (int i = 0; i < n; i++)
	{
		AdjustUp(arr, i);
	}
	//循环将堆顶数据与当前的最后一个数据交换
	int end = n - 1;
	while (end>0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);//注意我们的最后一个参数要传的是有效数据个数,所以就是end
		end--;
	}
}

打印结果,我们得到的是一个降序的数组。

如果现在我们想要改为排升序,我们要改动哪些地方呢?
  1. 注意我们前面在写AdjustUp方法时交换的判断条件是孩子结点比父节点小就交换,所以我们得到的是小堆,如果我们现在想要排升序,也就是说我们需要大堆,我们就要修改这个判断条件。

    有了前面第一步我们得到了大堆,但是我们在堆排序中有两步,第一步是建堆(取每个数据,向上调整),第二步是循环交换首尾,每次交换完要end–并且重新调整为堆(将堆顶向下调整),也就是说我们的向下调整也要改动。

  2. 现在我们不再找孩子结点中小的与父节点交换,而是找孩子结点里大的与父节点交换(因为我们要的是大堆);然后不再是孩子结点小于父结点交换,而是孩子结点大于父节点交换。

这样,我们就将堆排序改为了排升序。

本文到此结束,但是关于堆排序和堆的内容还没有讲完,敬请期待后文。

( ̄︶ ̄)↗

举报

相关推荐

0 条评论