从二叉树到堆排序

一:树

1.树的基本概念

请添加图片描述

  • 如图,这就是一颗普通的树,它是由一个根节点(A)和若干个子节点组成的,每个子节点也可能会有自己的子节点,就像树的树干上长出来许多分支一般,我们把这种数据结构叫做树。
  • 子树是不相交的,如果相交了就不是树,这也是区别树与非树的重要特点
  • 请添加图片描述
  • 如图,有子树相交,那么这个结构就不是树
  • 除了根节点外,每个节点仅有一个父节点
  • 一个有N个节点的树有N-1条边
  • 度:一个节点含有的子树的个数,比如A节点的度为5,因为它有A,B,C,D,E五个子树
  • 叶节点或终端节点:度为0的节点称为叶节点或终端节点,比如节点B,E,G,H,I,J,K,L,它们都没有子树,度为0,就是叶节点。
  • 双亲节点或父节点:A是B,C,D,E,F的父节点,C是G,H的父节点,很好理解
  • 孩子节点或子节点:B,C,D,E,F为A的子节点,G,H是C的子节点
  • 兄弟节点:具有相同父节点的节点互称兄弟节点,比如B,C,D,E,F为兄弟节点
  • 树的度:树中最大节点的度称为树的度,图中树的度为5
  • 树的高度或深度,树中节点的最大层次,图中树的高度为3
  • 森林:由m棵互不相交的树组合为森林

2.二叉树

  • 一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
  • 二叉树的特点:
  1. 每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
  2. 二叉树的子树有左右之分,其子树的次序不能颠倒。
  • 下面几个树都是二叉树
  • 请添加图片描述
    请添加图片描述

请添加图片描述

(1)最常见表示二叉树的方法

  • 现在我们知道了树这个数据结构,但如何表示又成了一个问题
  • 而最常用的表示方法就是双亲表示法
  • 在一个节点中储存数据和左右两个孩子节点
    请添加图片描述
    请添加图片描述
  • 结构体定义如下
typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	BTDataType data;
}BTNode;

(2)满二叉树

  • 定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
  • 下图就是一颗满二叉树。
    请添加图片描述

(3)完全二叉树

  • 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
  • 并且完全二叉树的前K-1层均满,最后一层可以不满,但从左到右必须是连续的
    • 这么说或许不太清晰,还是用图片来理解的快。请添加图片描述
  • 上图中左边的树不是完全二叉树,而右边的树是完全二叉树
  • 因为左边那棵树最后一层不是从左到右连续的,而右边的树是从左到右连续的

(4)二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点.
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1.
  3. 对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1
  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=LogN

(5)存储结构

  • 二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
  • 顺序结构:
  • 顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,马上我们就能看到。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
  • 下图是一颗完全二叉树的顺序存储
    请添加图片描述
  • 链式结构:(即上文的双亲表示法)
  • 二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。

二:堆

1.逻辑结构与存储结构

  • 前面讲的关于树的概念都是为了堆作铺垫
  • 因为堆的逻辑结构其实就是一颗完全二叉树
  • 而堆的存储结构则是数组

2.堆的特性

  • 结构性:结构上是完全二叉树
  • 有序性:任意节点的值是其子树所有节点的最大(最小)值(不满足这点的只是普通的完全二叉树而不是堆)

堆分为大根堆和小根堆:

  • 小根堆:父节点小于等于子节点
  • 大根堆:父节点大于等于子节点
  • 如图就是一个小根堆

请添加图片描述

  • 如果假设父节点在数组中存储的下标为parent
  • 则左子节点下标leftchild=parent*2+1
  • 右子节点下标rightchile=parent*2+2
  • parent=(child-1)/2

3.向下调整算法

  • 此算法使用的前提是二叉树的左右子树恰好都为堆(大堆或小对均可),通过一次向下调整,可将此二叉树变为大堆或小堆
  • 举个栗子(以小堆为栗)
    请添加图片描述
  • 如图,根节点27的左右子树均为小堆
  • 向下调整算法可分为这几步:
  • (1)选出左右子节点中小的那一个
  • (2)将小的子节点与父节点比较
    a.如果小的子节点比父节点小,则跟父节点交换,并且把原来子节点的位置当做父节点继续往下调整
    b.如果小的子节点比父节点大,则不需要处理,此时整个树已经是小堆了
    请添加图片描述

代码如下:

void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;
		}
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

4.建堆

  • 可以看到,向下调整算法虽然能够帮助我们将普通的完全二叉树变为堆,但是必须要求左右子树均为小堆
  • 那么遇到不满足这样条件的完全二叉树如何将其变为堆呢?
  • 可以从最后一个非叶子节点,从后往前,依次进行向下调整
  • 如图,从最后一个非叶子节点34开始往前依次进行向下调整4次,即可将最初的完全二叉树变为一个大堆
    请添加图片描述
  • 代码如下:
int main()
{
	int a[] = {37,49,28,34,27,19,25,15,65 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
}

5.堆排序

  • 重头戏来了,我们现在终于建好了堆,可以进行堆排序了
  • 先想想,如果我们想要升序排序,应该建大堆还是小堆呢?
  • 如果建小堆,我们可以得到一个最小的数,但剩下的数据如何排序,再次建堆得出次小的数?假设我们已知建堆的时间复杂度为O(N),排一个数就建一次堆,那最终堆排序的时间复杂度就为O(N2)了,那么为什么不直接选择排序或者冒泡排序呢?
  • 所以我们升序排序需要建大堆
  • 建完大堆后将最大的数(根节点)和最后一个数互换,把剩下N-1个数看成新的堆,进行一次向下调整,选出次大的数,一次向下调整复杂度为O(logN),一共需要调整N-1次,加上开始建堆的O(N),最终时间复杂度为O(NlogN)
  • 下图为堆排序过程

请添加图片描述

  • 代码如下:
int main()
{
	int a[] = {37,49,28,34,27,19,25,15,65 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		swap(&(a[0]), &(a[end]));
		AdjustDown(a, end, 0);
		end--;
	}
  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dhdw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值