Bootstrap

数据结构-堆(完全二叉树)

目录

树与二叉树

树的概念

树的结构

二叉树的概念

二叉树的结构

顺序存储

链式存储

堆的概念及结构

堆的基本操作

堆的初始化HeapInit与销毁HeapDestroy

入堆HeapPush---向上调整

出堆HeapPop---向下调整

获取堆顶数据HeapTop

堆的数据个数HeapSize​

判断堆是否为空HeapEmpty​

堆排序

代码


树与二叉树

树的概念

   树是由N(N>=0)个结点组成的一个具有层次关系的有限集合,因此树是一种逻辑上为非线性的数据结构。

  • 当N等于0时,称之为空树。
  • 当N等于1时,树的第一个结点称之为根结点,根结点是一个特殊的结点它没有前驱结点。
  • 当N大于1时,除根节点,其余结点可分为M(M>0)个互不相交的有限集T1,T2,T3 ... TM,其中每个集合本身又是一棵树,称之为根的子树。

   树的根结点没有前驱结点,除根结点以外的所有结点有且只有一个前驱结点。在树中所有结点可以与零个或者多个后继结点。子树是不相交的,并且在N个结点的树中有N-1条边,因此树是递归定义的。

树的结构

  • 结点的度
  •    实际上度分为入度与出度,在树中某结点的前驱结点个数为入度,后继结点的个数为出度。显然在树中根节点的入度为零,而除根节点以外的所有结点的入度为一。对于树来说结点的度实际上是指结点的出度。如A结点的度为3,B结点的度为2,C结点的度为1,F结点的度为0。
  • 双亲结点(父结点)
  •    若一个结点有直接前驱结点,则该结点称其直接前驱结点为双亲。如A是B的双亲结点或者父结点,C是G的双亲或者父结点。
  • 孩子结点(子女结点)
  •    若一个结点有直接后继结点,则该结点称其直接后继结点为孩子。如B是A的孩子结点或者子女结点,G是C的孩子或者子女结点。
  • 兄弟结点
  •    具有相同父结点的结点互称为兄弟节点。如B、C是兄弟结点;I,J是兄弟结点。
  • 叶子结点(终端结点)
  •    度为0的结点,也就是没有孩子结点的结点称之为叶子结点或者终端结点。如F,H,I,J,K,J,M。
  • 分支结点(非终端结点)
  •    度大于0的结点称之为分支结点或者非终端结点。如D,E。
  • 结点的层次
  •    结点的层次从树的根结点开始定义,根结点为第一层,它的孩子结点为第二层,以此类推。
  • 树的度
  •    一棵树中,最大的结点的度称为树的度。如A的度为3,B的度为2,C的度为1,树的度3。
  • 树的高度(深度)
  •    树中结点的最大层次。如上图,树的高度为4。
  • 堂兄弟结点
  •    双亲在同一层的结点互称为堂兄弟。如E,F,G,H互为堂兄弟。
  • 结点的祖先与子孙
  •    从根结点A到叶子结点I的唯一路径上的任意结点,称为结点I的祖先;如E,B,A都是I的祖先,同一棵树中根结点是所有结点的祖先。以某结点为根的子树,其中任一结点都称为该结点的子孙;同一棵树中所有结点都是根结点的子孙。
  • 树林
  •    m(m>=0)棵互不相交的树的集合称为森林。森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林。反之,只要给m 棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。

   树最常见的结构是孩子兄弟表示法,每一个结点都有存储数据的变量,指向左边第一个孩子结点的指针和指向右边相邻兄弟结点的指针。

二叉树的概念

   二叉树是一种特殊的树结构,其特点是每个结点至多只有两棵子树(即二叉树中不存在度大于
2 的结点);并且二叉树的子树有左右之分,其次序不能任意颠倒,因此二叉树是有序树。若将其左、右子树颠倒,则成为另一棵不同的二叉树。即使树中结点只有一棵子树,也要区分它是左子树还是右子树。

   与树相似,二叉树也以递归的形式定义。二叉树是N(N≥ 0)个结点的有限集合:

  • 空二叉树,即N = 0。
  • 由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

二叉树的结构

   二叉树的五种基本形态:

   几个特殊的二叉树:

  • 满二叉树:一棵高度为 h,且含有 2^h-1 个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点,满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点度数均为2。
  • 完全二叉树:高度为 h、有N个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。满二叉树是一种特殊的完全二叉树。
  • 二叉排序树:左子树上所有结点的数据均小于根结点的数据;右子树上的所有结点的数据均大于根结点的数据;左子树和右子树又各是一棵二叉排序树。
  • 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。

   二叉树的性质:

  • 若规定根节点的层数为1,非空二叉树上的叶子结点数等于度为 2 的结点数加 1,即 n0=n2+1。
  • 若规定根节点的层数为1,非空二叉树上第k层上至多有2^(k-1)个结点(k≥ 1)。
  • 若规定根节点的层数为1,高度为h的二叉树至多有2^h-1个结点(h≥1)。
  • 若规定根节点的层数为1,具有N个结点的满二叉树的高度h=log2 (N+1)。
  • 若根结点下标为0,则左孩子为leftChild=parent*2+1,右孩子为rightChild=parent*2+2,不管结点是左孩子还是右孩子其双亲为parent=(child-1)/2。
  • 若规定根节点的层数为1,高度为h的完全二叉树所拥有的结点个数为2^(h-1)<=N<=2^h-1。

顺序存储

   顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。所以在实际使用中只有堆才会使用数组来存储。

链式存储

   二叉树的链式存储有两种表示方式,一种是二叉链,也就是每一个结点中有存储数据的变量,指向左孩子的指针与指向右孩子的指针;另一种是三叉链,也就是每一个结点中有存储数据的变量,指向左孩子的指针,指向右孩子的指针以及指向双亲的指针。


堆的概念及结构

   堆是一种数据结构,它与操作系统的虚拟进程地址空间中的堆是两回事。堆的逻辑结构是一颗特殊的完全二叉树,它要求双亲结点中的数据要大于或者小于其左右孩子结点中的数据;而堆的物理结构是由动态数组来实现的。

   堆可以分为大根堆与小根堆。设数组a存储着堆中的数据,大根堆就是双亲结点的数据大于其左右孩子结点的数据(a i >= a 2*i + 1 && a i >= a 2*i + 2);小根堆就是双亲结点的数据小于其左右孩子结点的数据(a i <= a 2*i + 1 && a i <= a 2*i + 2)。

   根据堆的性质我们可以知道,在大根堆中根结点的数据值是最大的,而在小根堆中根结点的数据值是最小的。

堆的基本操作

   堆的基本操作,先用大根堆进行演示,操作小根堆与操作大根堆的思路是一样的,只需要将判断大小条件修改一下就可以进行大小根堆的切换。

堆的初始化HeapInit与销毁HeapDestroy

   因为堆的物理结构是数组,所以堆的初始化与销毁与动态顺序表一样。

入堆HeapPush---向上调整

   入堆的操作就是先将数据插入到数组最后一个位置上,数据插入完成后再进行向上调整。向上调整的基本思路就是让插入的数据与其双亲结点的数据进行比较,如果插入的数据比其双亲结点的数据大,则将插入的数据与双亲数据交换,直到有双亲数据比插入数据大或者插入数据已经换到根结点就停止。

void HeapPush(Heap* ph, HeapDataType x) // 入堆
{
	assert(ph);

	if (ph->size == ph->capacity) // 检查容量
	{
		int newCapacity = ph->capacity == 0 ? 4 : ph->capacity * 2;

		HeapDataType* tmp = (HeapDataType*)realloc(ph->a, sizeof(HeapDataType) * newCapacity);
		if (!tmp)
		{
			perror("realloc fail\n");
			return;
		}

		ph->a = tmp;
		ph->capacity = newCapacity;
	}

	ph->a[ph->size++] = x; // 先将数据插入数组
	 
	AdjustUp(ph->a, ph->size-1); // 进行向上调整
}

出堆HeapPop---向下调整

   出堆的操作不能直接将根结点的数据删除,否则会把数据打乱,造成堆不再是大根堆,而且再次调整成大根堆的代价很大。正确的出堆操作是将根结点的数据与数组中最后一个结点的数据进行交换,然后将换上来的结点数据与其左右孩子进行调整直到大根堆复原。向下调整的基本思路是先选出左右孩子中数据大的那一个结点(小根堆就是选出小的那一个结点),再将数据大的那一个结点与其双亲结点比较,如果选出来的那一个数据大的孩子结点比双亲结点的数据大则进行交换,直到换上来的结点成了叶子结点或者选出的那一个孩子结点比换上来的结点小就停止交换。

void HeapPop(Heap* ph) // 出堆
{
	assert(!HeapEmpty(ph)); // 如果堆为空则不能出堆
	assert(ph);

	HeapSwap(&(ph->a[0]), &(ph->a[ph->size - 1])); // 最后一个结点与根结点交换
	ph->size--;

	AdjustDown(ph->a, 0, ph->size); // 进行向下调整
}

获取堆顶数据HeapTop

堆的数据个数HeapSize

判断堆是否为空HeapEmpty

堆排序

   堆排序的操作只要用到向上调整与向下调整的思想就可以。堆排序的操作分为两步:第一步建堆,建堆可以向下建堆也可以向上建堆,但向下建堆的效率比向上建堆的效率高。第二步就是排序,将堆顶的元素与最后一个元素交换,交换之后采用向下调整让次大或者次小的元素到堆顶,依次往复,直到有序。所以,如果需要升序就建大根堆,需要降序就建小根堆。

代码

// 头文件
typedef int HeapDataType; // 数据类型

typedef struct Heap
{
	HeapDataType* a; // 数组首地址
	int size; // 数据个数
	int capacity; // 数组容量
}Heap;

void HeapSwap(HeapDataType* n1, HeapDataType* n2);
void HeapPrint(Heap* ph);
void HeapInit(Heap* ph);
void HeapDestroy(Heap* ph);
void HeapPush(Heap* ph, HeapDataType x);
void HeapPop(Heap* ph);
HeapDataType HeapTop(Heap* ph);
bool HeapEmpty(Heap* ph);
int HeapSize(Heap* ph);
void AdjustDown(HeapDataType* a, int parent, int size);
void AdjustUp(HeapDataType* a, int child);
void HeapSort(HeapDataType* a, int size);

// 源文件
void HeapSwap(HeapDataType* n1, HeapDataType* n2) // 交换结点的数据
{
	assert(n1 && n2);

	HeapDataType tmp = *n1;
	*n1 = *n2;
	*n2 = tmp;
}

void HeapPrint(Heap* ph) // 打印
{
	assert(ph);

	for (int i = 0; i < ph->size; i++)
	{
		printf("%d ", ph->a[i]);
	}

	printf("\n");
}
 
void HeapInit(Heap* ph) // 初始化
{
	assert(ph);

	ph->a = NULL;
	ph->size = ph->capacity = 0;
}

void HeapDestroy(Heap* ph) // 销毁
{
	assert(ph);

	if (ph->a)
	{
		free(ph->a);
		ph->a = NULL;
		ph->size = ph->capacity = 0;
	}
}

void AdjustUp(HeapDataType* a, int child) // 向上调整
{
	assert(a);

	int parent = (child - 1) / 2; // 获取双亲结点的下标
 
	// while (parent >= 0)这样的条件会多进入一次循环,并且if (a[child] >= a[parent])这样判断会死循环

	while (child > 0) // 如果孩子结点的下标小于等于0则停止,调整完成
	{
		if (a[child] >= a[parent]) // 如果插入的数据小于其双亲数据则进行调整否则跳出循环
		{
			HeapSwap(&(a[child]), &(a[parent])); // 交换数据

 			child = parent; // 迭代
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void HeapPush(Heap* ph, HeapDataType x) // 入堆
{
	assert(ph);

	if (ph->size == ph->capacity) // 检查容量
	{
		int newCapacity = ph->capacity == 0 ? 4 : ph->capacity * 2;

		HeapDataType* tmp = (HeapDataType*)realloc(ph->a, sizeof(HeapDataType) * newCapacity);
		if (!tmp)
		{
			perror("realloc fail\n");
			return;
		}

		ph->a = tmp;
		ph->capacity = newCapacity;
	}

	ph->a[ph->size++] = x; // 先将数据插入数组
	 
	AdjustUp(ph->a, ph->size-1); // 进行向上调整
}
 
void AdjustDown(HeapDataType* a, int parent, int size) // 向下调整
{
	assert(a);

	int child = parent * 2 + 1; // 先假设左孩子大

	while (child < size) // 结束条件不能是while (parent < size)否则会越界
	{
		if ((child + 1 < size) && a[child + 1] > a[child]) // 如果右孩子比左孩子大则孩子下标改为右孩子
		{
			child++;
		}

		if (a[child]>a[parent]) // 孩子比双亲大则交换
		{ 
			HeapSwap(&(a[parent]), &(a[child])); // 交换

			parent = child; // 迭代
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//void AdjustDown(HeapDataType* a, int parent, int size) // 向下调整--递归
//{
//	assert(a);
//	
//	int child = parent * 2 + 1;
//	
//	if (child + 1 < size && a[child + 1] < a[child])
//	{
//		child++;
//	}
//
//	if (child >= size)
//	{
//		return;
//	}
//
//	if (a[child] < a[parent])
//	{
//		HeapSwap(&(a[child]), &(a[parent]));
//	}
//
//	AdjustDown(a, child, size);
//}

void HeapPop(Heap* ph) // 出堆
{
	assert(!HeapEmpty(ph)); // 如果堆为空则不能出堆
	assert(ph);

	HeapSwap(&(ph->a[0]), &(ph->a[ph->size - 1])); // 最后一个结点与根结点交换
	ph->size--;

	AdjustDown(ph->a, 0, ph->size); // 进行向下调整
}

HeapDataType HeapTop(Heap* ph) // 获取根的数据
{
	assert(!HeapEmpty(ph));
	assert(ph);

	return ph->a[0]; // 根的数据就是存储在数组的第一个位置
}

bool HeapEmpty(Heap* ph) // 判空---若堆为空返回true否则返回false
{
	assert(ph);
	return ph->size == 0;
}

int HeapSize(Heap* ph) // 数据元素的个数
{
	assert(ph);

	return ph->size;
}

void HeapSort(HeapDataType* a, int size) // 堆排序---大根堆
{
	 建堆---向上建堆 O(N*logN)
	//for (int i = 1; i < size; i++)
	//{
	//	AdjustUp(a, i);
	//}

	// 建堆---向下建堆 O(N)
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, size);
	}

	// 排序 O(N*logN)
	int end = size - 1;
	while (end > 0)
	{
		HeapSwap(&(a[0]), &(a[end]));
		AdjustDown(a, 0, end);
		end--;
	}

}

 

;