数据结构-堆及堆排序

1.堆的定义

  • 堆(Heap)是一种数据结构,通常是一个完全二叉树。在堆中,每个节点都有一个与其相关的值,并且满足堆的性质。堆分为两种类型:大堆和小堆。
  • 大堆:在大堆中,对于每个非叶子节点,其值都大于或等于它的子节点的值。也就是说,根节点的值是整个堆中的最大值。
  • 小堆:与大堆相反,在小堆中,对于每个非叶子节点,其值都小于或等于它的子节点的值。根节点的值是整个堆中的最小值。

数据结构-堆及堆排序_第1张图片

左边的这幅图就是大堆,大堆中所有的父结点都大于子结点,并且是完全二叉树。右边这幅图就是小堆,小堆所有的父结点都小于子结点。

因为堆也就是完全二叉树的性质,我们可以用数组来实现,在数组中呢,所有的数据都是按顺序排的,相比于非完全二叉树,空间浪费的情况会好很多,这也就是实现二叉树中的顺序存储。

2.代码示例

因为堆分为大堆和小堆,所以实现哪一种都行,这里我实现的是大堆。(这会影响下面的向上调整算法向下调整算法)

typedef int HPDateType;
typedef struct HeapNode
{
	HPDateType* arr;
	int size;
	int capacity;
}HP;

堆的结构

因为底层结构是数组,所以堆的结构就和顺序表一样,定义一个数组arr,数组中有效数据的个数size和数组的容量capacity。

void HPInit(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

堆的初始化

1.判断传入的指针是否为假

2.初始化跟顺序表也一样,数组置为空,size和capacity也都初始化为0

void HPDestory(HP* php)
{
	assert(php);
	if (php->arr != NULL)
	{
		php->arr = NULL;
		php->size = php->capacity = 0;
	}
}

销毁堆

1.判断传入的指针是否为假

2.判断数组此时是否为空,非空的话就将数组置为空,size和capacity置为0

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

交换函数

因后面的入堆操作会用到,所以将交换功能封装成一个函数。

因为形参相当于实参的一份临时拷贝,所以形参的改变那不会影响实参,故我们如果想要交换两个数,需要传地址过去,也就是传址调用,这样解引用过后还是实参,之后就进行交换即可。

void HPAdjustUp(HPDateType* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//小堆:<
		//大堆:>
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

向上调整算法(关键!!!)

向上调整算法和向下调整算法是实现堆的核心,所以我讲这个功能封装成一个函数。向上调整算法主要用于入堆操作,在解释这段代码之前,先解释一下这个算法是干什么的。 

前提:使用向上调整算法的前提是你插入的数据会破坏堆的结构,例如:插入到尾端的数据比它的父结点要大,那么此时这颗子树就不是大堆,而整体也就不是大堆。就是不管插入还是删除数据,都必须严格遵守大堆或小堆的结构,而让数组保持大堆或小堆就要用到向上调整算法来调整数组的结构,使其发生改变后也能调整回大堆或小堆。

向上调整算法(大堆):顾名思义,就是让子结点和父结点进行比较。如果子结点比父结点要大,我们就要交换子结点和父结点的值,这样这颗子树才是大堆,之后让新的子结点,也就是之前那棵子树的父结点继续向上比较,直到子结点转移到根结点时结束,因为根结点无父结点。

在实现向上调整算法时我们要用到子结点(child)和父结点(parent)

1.因为是入堆操作,所以传入的子结点(child)就是最后一个数据,而它的父结点就是:(下标-1)/2

2.因为要不断向上比较,当子结点到根结点时就要停止,所以循环条件是child>0,然后根据入堆的数据来和它的父结点进行比较,如果小于父结点,我们就不需要调整,此时还是大堆。如果大于它的父结点,就要交换两者的值,并将child移到原父结点的位置,再根据新的child来找到新的parent以此类推

注意:传入的数据只需要数组arr和子结点的下标即可,不需要父结点的下标

void HPPush(HP* php, HPDateType x)
{
	assert(php);
	//判断空间是否充足
	//空间不足
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDateType* tmp = (HPDateType*)realloc(php->arr, newcapacity * sizeof(HPDateType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}
	//空间充足
	php->arr[php->size] = x;
	//向上调整算法
	HPAdjustUp(php->arr, php->size);
	php->size++;
}

入堆

1.判断传入的指针是否为假

2.分情况讨论。空间不足时需要增容,增容操作我在线性表那一章讲过,故这里就不再赘述

3.空间充足时,直接将数据放到下标为size的位置处

4.利用向上调整算法来调整整个数组的结构

5.size++

注意:size++这步操作要放在向上调整算法之后,如果放在向上调整算法之前,那么最后一个数据的下标就是size-1,到时我们传入的child就要是size-1

void HPPrint(HP* php)
{
	assert(php);
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->arr[i]);
	}
	printf("\n");
}

打印堆中的数据

1.判断传入的指针是否为假

2.遍历整个数组,打印出每个位置的数据

3.换行操作

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

判断堆是否为空

因为下面的出堆操作要判断堆是否为空,故将这个功能封装成一个函数。

1.判断传入的指针是否为假

2.判断size是否等于0,等于0说明数组为空,不等于0说明数组非空

void HPAdjustDown(HPDateType* arr, int parent, int n)
{
	int child = parent * 2 + 1;
	//默认是左子树
	//循环条件是左子树不能越界访问
	while (child < n)
	{
		//比较左子树和右子树的大小
		//注意右子树不能越界访问
		if (child + 1 < n && arr[child] < arr[child + 1])
		{
			child = child + 1;
		}
		//向下调整
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

向下调整算法

前提:因出堆操作是把堆顶的数据给删除掉,而在出堆操作之后,剩下的结构会被打乱,这是我们就要用向下调整算法来调整剩下的结构,使其重新调整为大堆或小堆。

向下调整算法(大堆):顾名思义,就是让父结点和子结点进行比较。因为是向下调整,所以我们要从根结点开始,首先让根结点和子结点进行比较,而在和子结点比较之前,要先比较两个子结点,也就是左子树和右子树,让较大的那个子结点和根结点进行比较,这么做是因为较大的子结点如果比根结点要大的话,那么交换两者的值之后,以根结点为父结点的子树就符合大堆的结构,所以没必要两个都比较。比较一次之后,让父结点转移到原较大子结点的位置,再找到新的子结点,再进行比较,而结束的条件是左子树要小于size,不能越界访问。

1.根据父结点的下标,让其*2+1就找到了左子树,而右子树就是左子树的下标。而默认求左子树是因为堆是完全二叉树,结点从左到右是依次排列的,如果一个父结点有右子树,那么这颗树一定有左子树,而有左子树,不一定有右子树,故默认求的是左子树

2.循环条件就是左子树的下标要小于size,进入到循环里面,先比较的就是左右子树的大小,找到大的那个,再与父结点进行比较。如果比父结点小,直接退出循环即可。如果比父结点大,就交换两者的值,然后父结点转移到原较大子结点的位置,再找到新的子结点,再进行比较,以此类推。

注意:在比较左右子树时要考虑没有右子树的情况,所以在判断左右子树的条件中要加上child+1要小于size,不能越界访问。

void HPPop(HP* php)
{
	assert(!HPEmpty(php));
	//出堆出的是堆顶的数据
	//先将堆顶和堆底的数据进行交换
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;
	//向下调整算法
	HPAdjustDown(php->arr, 0, php->size);
}

出堆

1.判断堆是否为空

2.交换堆顶和堆底的数据

3.size--

4.调用向下调整算法

注意:size--要放在向下调整算法之前,不然的话先调用向下调整算法,会使数组变为原数组。

HPDateType HPTop(HP* php)
{
	assert(php);
	return php->arr[0];
}

取堆顶的数据

1.判断传入的指针是否为假

2.直接返回数组中下标为0的数据即可

注意:上面的向上调整算法和向下调整算法如果要实现小堆的话,只需要将>改为<即可。

3.堆排序

这篇既然讲到了堆,而后面我们学排序时会学堆排序,故趁热打铁直接把堆排序给讲完。

堆排序:顾名思义,就是利用堆的结构来将数组中的数据进行排序,而堆排序的时间复杂度是要比冒泡排序的时间复杂度要低的,比冒泡排序更高效。

堆排序我依旧以大堆为例。

堆排序思想:将一个随机的数组通过向下调整算法或向上调整算法,将数组的结构转变为堆结构。之后再将堆顶的数据和堆底的数据进行交换,固定好一个值,再将前面的数据调整为堆结构,再次交换堆顶和堆底的数据,一直循环,直到排好序为止。

可能会有人疑惑为什么这么做就能排序?因为堆顶的数据一定是当前数组中最大或最小的数据,再利用堆排序,每次都能把当前数组中最大或最小的数据排在后面,等排之后,数组就会变为有序数组。

void HeapSort(int* arr, int n)
{
	//创建堆
	//从最后一棵树开始
	//利用向下调整算法创建堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		HPAdjustDown1(arr, i, n);
	}
	//堆排序
	int end = n - 1;
	for (int i = end; i > 0; i--)
	{
		Swap1(&arr[0], &arr[end]);
		HPAdjustDown1(arr, 0, end);
		end--;
	}
}

1.将随机数组调整为堆结构。这一步的思想是先找到最后一颗树,先对最后一颗树进行调整,再调整下一棵树,以此类推,当i=0是,也就是到了根结点,在对整棵树进行调整,这样就能将数组调整为堆结构

2.将堆顶和堆底的数据进行交换,这里我是用一个临时变量end来记录最后一个数据的下标,因为一次固定一个数据,故循环次数就是size-1,也就是end,交换之后利用向下调整算法,把前面的数据(也就相当于一个新的数组)调整为堆结构,最后让end--,以此类推,重复上述过程。

这里我用的是向下调整算法来实现堆排序,也可以用向上调整算法来实现,大家可以去试试。

通过排序我们可以发现,如果你建的是大堆,那么排序的结果就是升序,建的是小堆,排序的结果就是降序。

4.总结

堆和堆排序的关键就是要掌握向上调整算法和向下调整算法,把这两个掌握,堆和堆排序实现起来就会轻松很多,最后还是实现一个功能,检验一个功能。

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