【数据结构】二叉树——堆


一、二叉树

首先,什么是树,数学定义是连通且不含回路的图。通俗点来讲,有点像一棵树倒过来,根在上,也在下。
(更加严谨的定义,请学习离散数学相关知识)
而二叉树就是每个节点的度数最多不大于2。

现实中的二叉树
二叉树

特殊的二叉树

  • 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。满二叉树很像细胞分裂,一个变两个,两个变四个。
  • 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。满二叉树也是一种完全二叉树。

二、堆

堆便是一种完全二叉树。不过堆中某个节点的值总是不大于或不小于其父节点的值。父节点总是大于或等于子节点称之为大堆,反之,若父节点总是小于或等于其子节点称之为小堆。

那么我们在计算机中怎么表示一个堆呢?
答案是数组

父子节点关系

想要知道如何用一个数组来表示堆,首先我们得知道父节点和子节点的关系。
我们假设父节点是parent,左子节点是leftchild,又子节点是rightchild。

parent = (child-1)/2; //这里的孩子,左孩子和右孩子随便哪一个都可以
leftchild = parent*2+1;
rightchild = parent*2+2;

这样我们就能在数组中通过这样的关系来找到每一个节点了。

堆的创建

堆的初始化和销毁

//相关定义
typedef int HPDataType;
typedef struct Heap
{
    
    
	HPDataType* a;
	int size;
	int capacity;
}HP;

//初始化
void HeapInit(HP* php)
{
    
    
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}
//销毁
void HeapDestroy(HP* php)
{
    
    
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity;
}

//直接创建,不需初始化
void HeapCreate(HP* php, HPDataType* array, int size)
{
    
    
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * size);
	if (php->a == NULL)
	{
    
    
		perror("malloc fail");
		exit(-1);
	}
	memcpy(php->a, array, sizeof(HPDataType) * size);
	php->size = php->capacity = size;
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
    
    
		AdjustDown(php->a, size, i);
	}
}

增加数

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 fail");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;
	//向上调整
	AdjustUp(php->a, php->size - 1);
}

由于增加那个数是直接插在最后面的,不一定能构成堆,所以需要向上调整。

void AdjustUp(HPDataType* a, int child)
{
    
    
	assert(a);
	//找到父节点
	int parent = (child - 1) / 2;
	while (child > 0)
	{
    
    
	    //这里是建大堆,如果想建小堆,改成“<”即可
		if (a[child] > a[parent])
		{
    
    
			//交换函数,很简单就不再展示了
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
    
    
			//如果子节点比父节点要小,就无需再调整,直接跳出循环
			break;
		}
	}
}

删除数

删除数一般是删除堆顶元素

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);
}

删除数是将堆顶元素和最后一个元素交换,然后删除掉最后一个元素。然后通过向下调整使之重新构成堆。

void AdjustDown(HPDataType* a, int size, int parent)
{
    
    
	//找到左孩子
	int child = parent * 2 + 1;
	while (child < size)
	{
    
    
		//一共有两个孩子,通过进一步比较选出较大的孩子。
		//由于有左孩子不一定有右孩子,加一个判断避免越界
		if (child + 1 < size && a[child] < a[child + 1])
		{
    
    
			child++;
		}
		//这里同样是建大堆,建小堆改“<”即可
		//同时上面选的便不再是较大的孩子,而应该是较小的孩子
		//所以同样要改成“<”
		if (a[child] > a[parent])
		{
    
    
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}
}

其他相关函数

//取堆顶元素
HPDataType HeapTop(HP* php)
{
    
    
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}
//求堆的元素个数
int HeapSize(HP* php)
{
    
    
	assert(php);
	return php->size;
}
//判断堆是否为空
bool HeapEmpty(HP* php)
{
    
    
	assert(php);
	return php->size == 0;
}

堆排序

堆排是首先把数据建成堆,然后再进行排序。

1. 建堆

  • 向上调整建堆
//向上调整建堆的思想是第一个数已经是堆了,从第二个数开始向上调整建堆
//最终的时间复杂度是O(N*logN)
	for (int i = 1; i < n; ++i)
	{
    
    
		AdjustUp(a, i);
	}
  • 向下调整建堆
//向下调整建堆的思想是从最后一个节点的父节点开始向下调整,
//然后-1找到前一个父节点,再向下调整,不断循环,找到根节点
//时间复杂度是O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
    
    
		AdjustDown(a, n, i);
	}

所以就建堆而言,向下调整建堆是明显由于向上调整建堆的。

2. 排序

升序建大堆,降序建小堆

//这里假设是升序,建大堆
//通过把堆顶和最后一个元素交换,因为堆顶元素是最大的,
//然后向下调整建堆
//再把次大的放到倒数第二的位置
//通过不断的循环,便把大的全部放到后面,这样便做到了升序
//若想降序,建小堆即可,这样是把小的元素放到后面
//整体的时间复杂度是O(N*logN)
//冒泡排序的时间复杂度是O(N^2),可见堆排是效率很高的算法
	int end = n - 1;
	while (end > 0)
	{
    
    
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

完整的堆排序函数

//直接传数组地址和数组元素的个数过来
void HeapSort(int* a, int size)
{
    
    
	//建堆
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
    
    
		AdjustDown(a, size, i);
	}
	//排序
	//升序建大堆,降序建小堆
	//这里是升序建大堆
	int end = size - 1;
	while (end)
	{
    
    
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

总结

本文介绍了堆的增删查改和堆排序,希望对大家有所帮助。

猜你喜欢

转载自blog.csdn.net/lyq2632750277/article/details/128496651