【数据结构】树、二叉树、堆 + 堆的实现 + 堆排序TOP_k问题(附完整源码)

文章目录

前言

1、树

1.1、树的概念和结构

1.2、树的相关概念

 ​编辑

1.3、树的表示

2、二叉树

2.1、二叉树的概念和结构

​编辑

 2.2、特殊的二叉树

2.3、二叉树的性质

2.4、 二叉树的存储结构

3、堆

3.1、堆的概念和结构

 3.2、堆的实现

1、创建堆的结构

2、堆的初始化

3、堆的销毁

4、打印堆中的数据

5、向上调整建堆

6、向下调整建堆

7、插入元素

8、删除堆顶元素

9、获取堆顶元素

10、判空

3.3、堆实现完整源码

4、堆的排序

4.1、堆的简单排序

 4.2、建堆复杂度分析

1、向上调整建堆:O(N*logN)

2、向下调整建堆:O(N)

4.3、堆排序

1、升序——建大堆,降序——建小堆

2、TOP_k问题


前言

二叉树是树形数据结构中极其重要的组成,堆又是二叉树中一种更加特殊的数据结构。

1、树

1.1、树的概念和结构

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

下面我们对比一组图片来理解

 右边就是抽象出来的一个树形结构,其中,A为根结点,除根结点之外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。因此,树是递归定义的。

注意:树形结构中,子树之间不能有交集,否则就不是树形结构 。同时,除了根结点之外,其他结点有且只有一个父亲结点;一棵有N个结点的树有N-1条边(在外面抽象出来的结构中)。

1.2、树的相关概念

 

结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的为6

叶结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等结点为叶结点

非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等结点为分支结点

双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点

孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点

兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点

树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为6

结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推; 树的高度或深度:树中结点的最大层次; 如上图:树的高度为4

堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点

结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先

子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙

森林:由m(m>0)棵互不相交的树的集合称为森林;

注意:上面框起来的概念尤其需要理解!!!

1.3、树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。 

​typedef int DataType;
struct Node
{
    DataType data;         // 结点中的数据 
    struct Node* Child;   // 指向左边第一个孩子结点  
    struct Node* Brother;  //指向其右边第一个兄弟结点        
};

2、二叉树

2.1、二叉树的概念和结构

二叉树是树形结构的一种,其结构特点为每个结点最多只有两个子树(左子树与右子树),即二叉树不存在度大于2的结点。且二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

注意:对于任意的二叉树都是由以下几种情况复合而成的: 

下面我们看两个有趣的图片,现实中的二叉树

 2.2、特殊的二叉树

1、 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^K-1,则它就是满二叉树。

2、完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树

2.3、二叉树的性质

1、若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1) 个结点。

2、 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2^h-1个。

3、对任何一棵二叉树,如果度为0的叶结点个数为 n0,度为2的分支结点个数为 n2,则有

n0=n2+1。

证明如下

/*
* 假设二叉树有N个结点
* 从总结点数角度考虑:N = n0 + n1 + n2 ①

* 从边的角度考虑,N个结点的任意二叉树,总共有N-1条边
* 因为二叉树中每个结点都有双亲,根结点没有双亲,每个节点向上与其双亲之间存在一条边
* 因此N个结点的二叉树总共有N-1条边
* 
* 因为度为0的结点没有孩子,故度为0的结点不产生边; 度为1的结点只有一个孩子,故每个度为1的结
点产生一条边; 度为2的结点有2个孩子,故每个度为2的结点产生两条边,所以总边数为:n1+2*n2 
* 故从边的角度考虑:N-1 = n1 + 2*n2 ②
* 结合① 和 ②得:n0 + n1 + n2 = n1 + 2*n2 - 1
* 即:n0 = n2 + 1

4、若规定根结点的层数为1,具有n个结点的满二叉树的深度,h=log(n+1) (log以2 为底)

5、对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的父亲结点有:

(1) 左孩子序号为 2i+1 2i+1<n

(2)右孩子序号为 2i+2 (2i+2<n)

5、对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的孩子结点有:父亲结点的序号为 (i - 1)/ 2 ,i=0时,为根结点编号,无双亲结点。

2.4、 二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

1、 顺序存储 :顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

 2、链式存储: 二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。

3、堆

3.1、堆的概念和结构

堆是二叉树中更加特殊的一种结构,不仅要求是完全二叉树,而且要求父亲结点的值小于左孩子和右孩子的值(小堆/小根堆)或者父亲结点的值大于左孩子和右孩子的值(大堆/大根堆),同时,堆在物理结构(存储结构)上为顺序存储结构。

 3.2、堆的实现

堆是完全二叉树中的一种更加特殊的结构,所以我们用顺序存储的方式存储堆的数据,即与顺序表的结构相同。

头文件:#include"Heap.h"

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int HPDateType;

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

//初始化
void HPInit(HP* php);
//销毁
void HPDestry(HP* php);
//打印
void HPPrint(HP*php);
//插入元素并保持堆的结构
void HPPush(HP* php, HPDateType x);
//删除堆顶元素
void HPPop(HP* php);
////向上调整建堆
void AdjustUp(HPDateType* a, int child);
////向下调整建堆
void AdjustDown(HPDateType* a, int n, int parent);
//获取堆顶元素
//获取堆顶元素
HPDateType HPTop(HP* php);
//判空
bool HPEmpty(HP* php);

1、创建堆的结构

typedef int HPDateType;

typedef struct Heap
{
	HPDateType* arr;//数组,用来顺序存储
	int size;       //有效数据个数
	int capacity;   //容量大小
}HP;

2、堆的初始化

与顺序表的初始化相同

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

3、堆的销毁

数组的空间是动态申请的,在使用过后需要归还给操作系统。由于数组是一片连续的空间,之间释放就可以了。

//销毁
void HPDestry(HP* php)
{
	assert(php);
	free(php->arr);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

4、打印堆中的数据

打印堆中的数据就是打印数组中的内容。

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

5、向上调整建堆

思路:(1)我们可以用完全二叉树孩子结点和父亲结点的关系,将孩子结点与其对应的父亲结点向上比较,若孩子结点值的下标为 i,则父亲结点的下标为:(i - 1) / 2。

(2)如果建小堆,孩子结点<父亲结点,则将孩子结点的值赋给父亲结点,反之则不做处理;如果建大堆,孩子结点>父亲结点,则将孩子结点的值赋给父亲结点,反之则不做处理。

(3)循环结束条件,即当孩子结点已经到达堆顶时,则一趟比较结束。

这里涉及元素的交换,所以我们先实现元素的交换代码,形参的改变要影响实参,所以需要传递实参的地址。

////交换元素位置
void Swap(HPDateType* p1, HPDateType* p2)
{
	HPDateType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
////向上调整建堆
void AdjustUp(HPDateType*a,int child)
{
	int parent = (child - 1) / 2;
	while(child>0)
	{
		//建小堆:a[child] < a[parent]
		//建大堆:a[child] > a[parent]
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

6、向下调整建堆

思路:(1)我们可以用完全二叉树父亲结点和孩子结点的关系,将父亲结点与其对应的孩子结点向下比较,若父亲结点值的下标为 i,则孩子结点的下标为:2*i+1(左孩子)和 2*i+2(右孩子)

(2)如果建小堆,先找到左孩子和右孩子中较小的,再和父亲结点比较,孩子结点<父亲结点,则将孩子结点的值赋给父亲结点,反之则不做处理;如果建大堆,先找到左孩子和右孩子中较大的,再和父亲结点比较,孩子结点>父亲结点,则将孩子结点的值赋给父亲结点,反之则不做处理。我们可以用假设法来找到左孩子和右孩子中较小的或者较大的,先假设左孩子较小或者较大,然后再比较来确定真正较小的或者较大的,需要注意的一点是,如果最后一个孩子是左孩子,那么比较的时候就会存在越界访问的问题,所以我们还需要注意这种情况。

(3)结束条件,设数组长度为n,当child >= n时,说明调整到叶子了,就需要调整了,所以child<n时才进入循环。

////向下调整建堆
void AdjustDown(HPDateType* a,int n,int parent)
{
	//假设法:先假设左孩子较小(建小堆)
	      //  假设左孩子较大 (建大堆)
	int child = 2 * parent + 1;
	
	while (child<n)//child >= n时,说明调整到叶子了
	{
		//找真正的较小的孩子(建小堆):a[child] > a[child + 1]
		//找真正的较大的孩子(建大堆):a[child] < a[child + 1]
		//注意:最后一个孩子可能是左孩子,a[child+1]越界
		if (child + 1 < n && (a[child] > a[child + 1]))
		{
			child++;
		}
		if(a[child]<a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
			break;
	}
}

7、插入元素

插入元素,就要判断数组当前的空间是否足够,这扩容的思路与前面的顺序表扩容一致。但是,需要注意的一点是,在插入元素后,依旧要保持堆的结构(大堆或者小堆),就需要再次调整堆的结构。我们可以向上调整建堆。

//插入元素并保持堆的结构
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, sizeof(HPDateType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}
	php->arr[php->size] = x;
	php->size++;
	//向上调整
	AdjustUp(php->arr,php->size-1);
}

8、删除堆顶元素

删除堆顶元素后,堆的结构就会被破坏,我们可以将最后一个元素放到堆顶,然后向下调整建堆来重新调整堆的结构。

//删除堆顶元素
void HPPop(HP* php)
{
	assert(php);
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;

	AdjustDown(php->arr, php->size, 0);
}

9、获取堆顶元素

//获取堆顶元素
HPDateType HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->arr[0];
}

10、判空

bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

3.3、堆实现完整源码

实现代码:Heap.c

#include"Heap.h"

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

//销毁
void HPDestry(HP* php)
{
	assert(php);
	free(php->arr);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

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

////交换元素位置
void Swap(HPDateType* p1, HPDateType* p2)
{
	HPDateType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

////向上调整建堆
void AdjustUp(HPDateType*a,int child)
{
	int parent = (child - 1) / 2;
	while(child>0)
	{
		//建小堆:a[child] < a[parent]
		//建大堆:a[child] > a[parent]
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
//插入元素并保持堆的结构
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, sizeof(HPDateType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}
	php->arr[php->size] = x;
	php->size++;
	//向上调整
	AdjustUp(php->arr,php->size-1);
}

////向下调整建堆
void AdjustDown(HPDateType* a,int n,int parent)
{
	//如果要调整为小堆,就要找到两个孩子中较小的一个
	//假设法:先假设左孩子较小(建小堆)
	      //  假设左孩子较大 (建大堆)
	int child = 2 * parent + 1;
	
	while (child<n)//child >= n时,说明调整到叶子了
	{
		//找真正的较小的孩子(建小堆):a[child] > a[child + 1]
		//找真正的较大的孩子(建大堆):a[child] < a[child + 1]
		//注意:最后一个孩子可能是左孩子,a[child+1]越界
		if (child + 1 < n && (a[child] > a[child + 1]))
		{
			child++;
		}
		if(a[child]<a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}
//删除堆顶元素
void HPPop(HP* php)
{
	assert(php);
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;

	AdjustDown(php->arr, php->size, 0);
}

//获取堆顶元素
HPDateType HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->arr[0];
}

bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

4、堆的排序

4.1、堆的简单排序

我们知道在大堆中,堆顶的数据是最大的;小堆中,堆顶的数据是最小的。所以,我们可以将堆顶的数据拿下来,然后再重新调整堆,找到次小的或者次大的。

//升序
void test03()
{
	DateType a[] = { 3,5,4,6,8,9,1,6,7,15,45,34,10 };
	int sz = sizeof(a) / sizeof(DateType);
	HP php;
	HPInit(&php);
	//将数组中的元素一一拿过来建小堆
	for (int i = 0; i < sz; i++)
	{
		HPPush(&php,a[i]);
	}
	int j = 0;
	//不断将堆顶的元素拿下来
	while (!HPEmpty(&php))
	{
		a[j++] = HPTop(&php);
		HPPop(&php);
	}
	for (j = 0; j < sz; j++)
	{
		printf("%d ", a[j]);
	}
    HPDeatry(&php);
}

 4.2、建堆复杂度分析

1、向上调整建堆:O(N*logN)

从根结点的左孩子结点开始向上调整建堆

void test01()
{
	DateType a[] = { 3,5,4,6,8,9,1,6,7,15,45,34,10,6,7 };
	int sz = sizeof(a) / sizeof(DateType);
	for (int i = 1; i < sz; i++)
	{
		AdjustUp(a, i);
	}
}

由上图就可以得到, 

 

 由O的渐进表示法可得,复杂度为O(n*logn)

2、向下调整建堆:O(N)

从最后一个孩子结点的父亲结点开始向下调整建堆

void test02()
{
	DateType a[] = { 3,5,4,6,8,9,1,6,7,15,45,34,10 };
	int sz = sizeof(a) / sizeof(DateType);
	for (int i = (sz - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, sz, i);
	}
}

由上图可以得到,

通过两者的对比,我们就可以得出结论:向下调整建堆的算法更胜一筹。

4.3、堆排序

1、升序——建大堆,降序——建小堆

思考:升序,建小堆还是建大堆?降序,建小堆还是建大堆?

思路:

升序——建大堆:大堆的根结点对应最大的值,我们可以将根结点的值与最后一个结点的值交换,然后再次重新建大堆,找到次大的值与倒数第二个结点的值交换......。这样到最后就将一组数按照升序的规则排好了。

//堆排序
void HeapSort(int* a, int sz)
{
	//升序————建大堆:O(N)
	for (int i = (sz - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, sz, i);
	}

	// O(N*logN)
	while (sz - 1)
	{
		Swap(&a[0], &a[sz - 1]);
		AdjustDown(a, sz-1, 0);
		sz--;
	}
}

降序——建小堆:小堆的根结点对应最小的值,我们可以将根结点的值与最后一个结点的值交换,然后再次重新建小堆,找到次小的值与倒数第二个结点的值交换......。这样到最后就将一组数按照降序的规则排好了。

2、TOP_k问题

TOP_k问题即找一组数据中最大的前k个数据或者找最小的前k个,一般都数据量较大。

要解决这个问题,首先我们要考虑到,数据量较大时,由于存储空间的限制,我们不可能将所有的数据都放在数组中。

基本思路:(1)用一组数据的前k个数来建堆。如果,找最大的前k个,就建小堆;找最小的前k个,就建大堆。

(2)然后拿剩下的N-k个数据依次与根结点比较。找最大的前k个,就把比根结点大的拿来作为新的根结点;找最小的前k个,就把比根结点大的拿来作为新的根结点。最后堆中k个数就是最大的或者最小的前k个。

举例:找100000个数中最大的前10个

随机生成100000个数写入文件中

//创造数据
void CreatDdate()
{
	int N = 100000;
	srand(time(0));
	FILE* pf = fopen("date.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		exit(1);
	}
	for(int i=1;i<=N;i++)
	{
		int x = (rand() + i)%10000;
		fprintf(pf, "%d\n", x);
	}
	fclose(pf);
}

date.txt文件中随机生成的10000以内的数据 

读文件中的数据,并将剩下的N-k个与根结点比较。 

void PrintTop_k(int k)
{
	FILE* pout = fopen("date.txt", "r");
	if (pout == NULL)
	{
		perror("fopen");
		exit(1);
	}
	DateType* arr = (DateType*)malloc(sizeof(DateType) * k);
	if (arr == NULL)
	{
		perror("malloc");
		exit(1);
	}

	for (int j = 0; j < k; j++)
	{
		fscanf(pout, "%d", &arr[j]);
	}
	
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, k, i);
	}
	int x = 0;;
	while (fscanf(pout, "%d", &x) > 0)
	{
		if (x > arr[0])
		{
			arr[0] = x;
			AdjustDown(arr, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", arr[i]);
	}
}

输出结果

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值