分享缩略图

分享到:
链接已复制
首页> 新闻中心>

数据结构——二叉树之c语言实现堆与堆排序

2025-06-24 12:27:54

来源:新华网

字体:

目录

前言:

1.二叉树的概念及结构

1.1 特殊的二叉树 

1.2 二叉树的存储结构

   1.顺序存储

2.链式存储 

2. 二叉树的顺序结构及实现 

2.1 堆的概念 

  ​编辑

2.2 堆的创建

3.堆的实现

3.1 堆的初始化和销毁 

初始化:

销毁: 

插入:

向上调整:

删除: 

向下调整: 

堆顶元素: 

判空: 

 4.堆排序

4.1排序实现

 


前言:

   在上一期我们介绍了有关于树的基础概念,了解了关于树的各名称的含义,然而在现实中树被用得最多的场景还是在我们计算机中的资源管理器的文件存储结构中,在其他场景被使用的情况很少,所以我们这一期要介绍一种被广泛使用的树型结构——二叉树。

1.二叉树的概念及结构

  顾名思义,二叉树是由一个根结点和两棵子树构成,二叉树的每个结点最多只有两个结点:

从上图可以看出:

1. 二叉树不存在度大于2的结点

2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树  

二叉树是由以下几种情况复合而成的:

 

现实中的二叉树:

1.1 特殊的二叉树 

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

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

1.2 二叉树的存储结构

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

   1.顺序存储

  顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺 序存储在物理上是一个数组,在逻辑上是一颗二叉树。 

2.链式存储 

    二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程 学到高阶数据结构如红黑树等会用到三叉链。

 

2. 二叉树的顺序结构及实现 

  普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

2.1 堆的概念 

  

堆的性质:

1.堆中某个结点的值总是不大于或不小于其父结点的值。

2.堆总是一棵完全二叉树。 

 

2.2 堆的创建

  下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算 法,把它构建成一个堆。根结点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点的树,就可以调整成堆。

3.堆的实现

 介绍完堆的概念和性质之后,我们接下来就要来用代码实现堆及堆的各个方法。由于堆是顺序结构实现的,所以我们选择使用顺序表来实现它:

typedef int HPDataType;typedef struct Heap{ 	HPDataType* a;	int size;	int capacity;}HP;

3.1 堆的初始化和销毁 

  堆是用顺序表来实现的,而顺序表的空间都是我们手动在内存中的堆中开辟的,所以也需要手动释放,而在程序最初运行时我们也要对它进行初始化。

初始化:

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

销毁: 

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

插入:

 在插入数据之前,我们选确定空间够不够,如果city等于cpapcity,我们就判断空间满了,需要扩容,然后插入数据,而要实现建堆的话,我们还需要使用向上调整方法实现:

void HPPush(HP* php, HPDataType x){ 	assert(php);	if (php->capacity == php->size)	{ 		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);		if (tmp == NULL)		{ 			perror("realloc fail!");		}		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){ 	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;		}	}}

删除: 

  有插入操作就必然有删除操作,那么我们如何实现删除操作呢?如果我们直接进行头删,那么我们建的堆就会被破环,如果尾删的话,那么堆就没有意义了(后面详细解释),所以我们先让堆中第一个元素与最后一个元素交换,然后再让size减一,而这时我们建的堆被破环了,所以还需要使用向下调整方法来重新建堆,而向下建堆的算法也比较简单,先找出第一个结点更小的那个子结点,只要这个结点比它的父结点小就让它们交换位置,如此循环往复,直到走到堆尾:

删除代码实现:

void HPPop(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 n, int parent){ 	//假设更小的孩子是左孩子	int child = parent * 2 + 1;	while (child=n说明孩子已经不存在	{ 		if (child+1

堆顶元素: 

HPDataType HPTop(HP* php){ 	assert(php);	assert(php->size > 0);	return php->a[0];}//堆顶元素

判空: 

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

 4.堆排序

 堆排序是一种速度很快的排序算法,冒泡排序的时间复杂度为O(N^2),而堆排序的时间复杂度仅为O(logN),学完堆,我们就可以来试着实现堆排序了。

4.1排序实现

 我们先创建一个无序数组:

int a[] = {  8,6,5,3,9,0,7,1,4,2 };

现在这个数组不是堆,我们堆排序的第一步就是先建堆呢,可以使用向下调整吗,答案是不可以,只有下面的子树都是堆时才可以使用,而现在这棵树仅是一个无序数组,所以我们选择从后往前建堆,什么意思呢,我们可以把这组树看成一棵一棵树:

 

我们发现,从9开始,往上每一个结点都有自己的子结点,这就意味着从就开始,每往前走一步就是一棵树,所以我们只要从9开始使用向下调整建堆,每往前走一步就可以实现一棵树的建堆,而走到8时,整棵树也就完成了建堆:

int a[] = {  8,6,5,3,9,0,7,1,4,2 };int len = sizeof(a) / sizeof(int);for (int i = (len - 1 - 1) / 2; i >= 0; i--){ 	AdjustDown(a, len, i);}//建堆

这个算法到底怎么样呢?我们运行一下程序看看:

 

我们将这些数字摆成一棵二叉树:

 

从上图可以看出,这组数字摆成一棵二叉树它就是一个标准的堆。 

     成功建堆之后,我们就可以来使用堆来排序了,从上图可以看出,我们建的是小堆,如果我们要实现降序,可以使用小堆实现吗?答案是可以,而且经过实验,我们得出结论:升序:建大堆降序:建小堆,所以我们使用小堆来实现降序是没有问题的。如何实现呢,我们可以先创建一个变量end指向最后一个结点,然后让第一个结点和尾结点交换,因为第一个结点是整个堆最小的数,交换位置之后,最小的数就在最后一个结点了,我们让end向前走一步,然后使用向下调整让堆第二小的数字走到第一个结点,然后再和end指向的结点交换,循环往复之后最大的数就走到了第一个结点,而我们也完成了降序排序:

while (end > 0)    {         swap(&a[0], &a[end]);        end--;        AdjustDown(a, end, 0);            }//调整

来看看结果:

 

 下面是完整代码:

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>#include<stdlib.h>#include<stdbool.h>void swap(int* p1, int* p2){ 	int tmp = *p1;	*p1 = *p2;	*p2 = tmp;}void AdjustDown(int* a, int n, int parent){ 	int child = parent * 2 + 1;	while (child<n)	{ 		if (child+1<n&&a[child + 1] < a[child])		{ 			child++;		}		if (a[child] < a[parent])		{ 			swap(&a[child], &a[parent]);			parent = child;			child = parent * 2 + 1;		}		else		{ 			break;		}	}}void test(){ 	int a[] = {  8,6,5,3,9,0,7,1,4,2 };	int len = sizeof(a) / sizeof(int);	for (int i = (len - 1 - 1) / 2; i >= 0; i--)	{ 		AdjustDown(a, len, i);	}//建堆		int end = len - 1;	while (end > 0)	{ 		swap(&a[0], &a[end]);		end--;		AdjustDown(a, end, 0);			}//调整		for (int i = 0; i < len; i++)	{ 		printf("%d ", a[i]);	}}int main(){ 	test();	return 0;}

到这里我们的堆就结束了,我将代码放在下面,感兴趣的小伙伴可以试试哦。

Heap.h :

#pragma once#include#include#include#includetypedef int HPDataType;typedef struct Heap{ 	HPDataType* a;	int size;	int capacity;}HP;void HPInit(HP* php);//初始化void HPDestroy(HP* php);//销毁void HPPush(HP* php, HPDataType x);//插入void HPPop(HP* php);//删除HPDataType HPTop(HP* php);//堆顶元素void AdjustUp(HPDataType* a, int child);//向上调整void AdjustDown(HPDataType* a, int n,int parent);//向下调整bool HPEmpty(HP* php);//判空

Heap.c :

#define _CRT_SECURE_NO_WARNINGS 1#include"Heap.h"void HPInit(HP* php){ 	assert(php);	php->a = NULL;	php->size = php->capacity = 0;}//初始化void swap(HPDataType* p1, HPDataType* p2){ 	HPDataType tmp = *p1;	*p1 = *p2;	*p2 = tmp;}//交换void AdjustDown(HPDataType* a, int n, int parent){ 	//假设更小的孩子是左孩子	int child = parent * 2 + 1;	while (child=n说明孩子已经不存在	{ 		if (child+10)	{ 		if (a[child] < a[parent])		{ 			swap(&a[child], &a[parent]);			child = parent;			parent = (child - 1) / 2;;					}		else		{ 			break;		}	}}//向上调整void HPPush(HP* php, HPDataType x){ 	assert(php);	if (php->capacity == php->size)	{ 		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);		if (tmp == NULL)		{ 			perror("realloc fail!");		}		php->a = tmp;		php->capacity = newcapacity;	}	php->a[php->size] = x;	php->size++;	AdjustUp(php->a, php->size-1);//向上调整}//插入void HPPop(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);//向下调整}//删除HPDataType HPTop(HP* php){ 	assert(php);	assert(php->size > 0);	return php->a[0];}//堆顶元素bool HPEmpty(HP* php){ 	assert(php);	return php->size = 0;}//判空void HPDestroy(HP* php){ 	assert(php);	free(php->a);	php->a = NULL;	php->capacity = php->size = 0;}//销毁

test.c :

#define _CRT_SECURE_NO_WARNINGS 1#include"Heap.h"void test(){ 	int a[] = {  4,9,0,2,5,3,7,1,8,6 };	HP hp;	HPInit(&hp);	for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)	{ 		HPPush(&hp, a[i]);	}	while (hp.size)	{ 		printf("%d ", hp.a[hp.size - 1]);		hp.size--;	}	HPDestroy(&hp);}void test02(){ 	int a[] = {  4,9,0,2,5,3,7,1,8,6 };	HP hp;	HPInit(&hp);	for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)	{ 		HPPush(&hp, a[i]);	}	while (hp.size>0)	{ 		int top = HPTop(&hp);		printf("%d ", top);		HPPop(&hp);	}	HPDestroy(&hp);}void test03(){ 	int a[] = {  4,9,0,2,5,3,7,1,8,6 };	size_t len = sizeof(a) / sizeof(int);	for (int i = (len - 1 - 1) / 2; i >= 0; i--)	{ 		AdjustDown(a, len, i);	}	int end = len - 1;	while (end>0)	{ 		swap(&a[0], &a[end]);		AdjustDown(a, end, 0);		end--;	}	for (int i = 0; i < len; i++)	{ 		printf("%d ", a[i]);	}}int main(){ 	//test02();	test03();	return 0;}

【责任编辑:新华网】