c语言实现数据结构--树

主要内容:

  • 树的基础概念以及存储方式
  • 二叉树的概念、性质以及结构
  • 二叉树的顺序存储–堆的操作
  • 建堆的时间复杂度分析

一.树

1.1.树的定义
树是一种非线性的数据结构,它是由n(n>=0)个节点组成的具有层次关系的结构。它的层次关系看起来像一颗倒着的树,因此将它叫做树。一颗树 = 根节点 + n颗子树(n>=0),而子树又可以按上述定义,因此树这种结构是递归定义的。

图片.png

1.2.树的概念

图片.png

  • 节点的度:一个节点含有的子树个数。如E的度=2
  • 树的度:一棵树中度最大的节点的度就是一棵树的度。如上述:树的度=6
  • 叶节点或终端节点:度为0的节点。如上述:B,C,H…
  • 分支节点或非终端节点:度不为0的节点。如上述:D,E,J…
  • 父节点或双亲节点:若一个节点含有子节点,则该节点就是子节点的双亲节点。如D是H的父节点
  • 孩子节点:若一个节点含有子节点,则子节点就是该节点的孩子节点。如H是D的孩子节点
  • 兄弟节点:若两个节点含有共同的父节点,则这两个节点是兄弟节点。如I和J是兄弟节点
  • 堂兄弟节点:双亲在同一层次的两个节点就是堂兄弟节点。如H和I是堂兄弟节点
  • 节点的层次:节点的层次有两种划分方式:1.根节点为第0层,2.根节点为第1层.一般以第二种为主
  • 树的高度或深度:节点的最大层次。如按照根为第一层,则这棵树的高度为:4
  • 节点的祖先:从该节点到根节点的所有节点都是该节点的祖先。如:p的祖先:AEJ
  • 节点的子孙:以某个节点为根的子树中所有的节点都是该节点的子孙。如所有节点都是A的子孙
  • 森林:m颗互不相交的树的集合。
1.3.树的物理存储

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

typedef int DataType;
struct Node
{
 struct Node* child; // 第一个孩子结点
 struct Node* brother; // 指向其下一个兄弟结点
 DataType data; // 结点中的数据域
};  

如下图:左边的为真正的逻辑结构,右边的为孩子兄弟表示后的逻辑结构。
图片.png

1.4.树的应用

如:Linux的目录结构就是一颗树,Windows的目录结构是森林,森林中树的表示为孩子兄弟表示法。
图片.png

二.二叉树

2.1.二叉树的定义

二叉树就是度最大是2的树。二叉树可以看作为:根+左子树+右子树。而左子树也可看作根+左子树+右子树
图片.png对于任何一颗二叉树都是由以下几种情况组合而成:
图片.png

  • 二叉树的左右子树有次序之分,所以二叉树是有序树
2.2.特殊的二叉树

1.满二叉树:
一个二叉树,如果每一层次的节点数都达到最大值,那么该二叉树就是满二叉树。
下面这颗树的高度为3,第一层有20个节点,第二层有21个节点,第三层有2^2个节点

最后一层的节点数大于前面k-1层的节点总数

图片.png

假设一颗满二叉树的高度为k,则第k层有2^(k-1)个节点
总共的节点数:N = 2^0 + 2^1 + 2^2 + … + 2^(k-1) = 2^k - 1

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

  • 如果中间有空节点则不是完全二叉树,如下图

图片.png

对于高度为k的完全二叉树,其节点数量的范围:【2^(k-1), 2^k - 1】
证明:
前k-1层为满二叉树,所以前k-1层的节点总数:N = 2^(k-1)-1,而第k层的节点数至少为1,
所以下限=2^(k-1)-1+1 = 2^(k-1); 当第k层的节点数满时,上限:2^k - 1;

2.3.二叉树的性质
  1. 若规定根节点的层次为1,则一颗非空二叉树第i层的节点数,最多为2^(i-1)
  2. 若规定根节点的层次为1,则深度为h的二叉树,最多有2^h - 1个节点
  3. 对于任何一颗二叉树,如果度为0的节点为n0,那么度为2的节点数为:n2 = n0 - 1;
  4. 若规定根节点的层次为1,则有n个节点的满二叉树的高度:h=log2(n+1)

证明:假设满二叉树的高度为h,则2h-1 = n => 2h = n + 1 两边同时取对数: h = log2(n+1)

  1. 对于具有n个节点的完全二叉树,如果按照从上到下,从左到右的数组顺序对所有节点从0开始编号,则对于序号为i的节点有:
    1. i的双亲节点:parent = (i - 1)/2;
    2. i的左孩子节点:leftchild = 2*i + 1;
    3. i的右孩子节点:rightchild = 2*i + 2;
2.4.二叉树的存储结构
  1. 顺序存储

顺序存储就是使用数组来存储二叉树,一般只有完全二叉树才会使用顺序存储,因为非完全二叉树会存在空间浪费的情况。而现实中只有堆才会使用数组来存储。逻辑结构是树形结构,物理结构是顺序存储

  • 对于任何一个数组,都可以看作是一颗完全二叉树的物理存储

完全二叉树的节点是从上到下,从左向右依次存储的,所以可以依次存放到数组中。
非完全二叉树中节点不是依次存储的,需要给空节点留位置,因此会造成空间的浪费。

图片.png

  1. 链式存储

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

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{

 struct BinTreeNode* left; // 指向当前节点左孩子
 struct BinTreeNode* right; // 指向当前节点右孩子
 BTDataType data; // 当前节点值域
}

// 三叉链
struct BinaryTreeNode
{
 struct BinTreeNode* parent; // 指向当前节点的双亲
 struct BinTreeNode* left; // 指向当前节点左孩子
 struct BinTreeNode* right; // 指向当前节点右孩子
 BTDataType data; // 当前节点值域
}

图片.png

三.二叉树的顺序存储–堆

3.1.堆的概念

如果有一个关键码的集合K = { k1,k2 ,k3 ,…,},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: ki<=k2i+1 且 ki<=k2i+2 ( ki>=k2i+1 且ki >=k2i+2 ) ,i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

  • 所有的父节点>=子节点 (父节点<=子节点)
3.2.堆的性质
  1. 堆总是一颗完全二叉树
  2. 堆中某个节点总是小于(大于)其父节点的值
  3. 其父节点和子节点的下标符合完全二叉树的性质
3.3.堆的结构图

图片.png

3.4.堆的实现–大堆
  1. 结构体以及接口的定义
typedef int HPDataType;
typedef struct heap
{
	HPDataType* data;
	int size;
	int capacity;
}heap;

//建立
void HeapCreate(heap* php, HPDataType* nums, int numsLen);
//初始化
void HeapInit(heap* php);
//销毁
void HeapDestroy(heap* php);
//入堆
void HeapPush(heap* php, HPDataType x);
//删除堆顶
void HeapPop(heap* php);
//获得堆顶数据
HPDataType HeapTop(heap* php);
//求堆中数据量
int HeapSize(heap* php);
//判断是否为空
bool HeapEmpty(heap* php);
//向下调整算法
void AdjustDown(HPDataType* data, int n, int parent);
//向上调整算法
void AdjustUp(HPDataType* data, int child);
//交换函数
void Swap(HPDataType* p1, HPDataType* p2);

  1. 堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。
下面先介绍两种建堆算法:1.向上调整算法 2.向下调整算法
:::tips
向上调整算法:将数据插入一个堆中,该数据与父节点的数据比较,若该数据大于父节点则交换,反之不交换,向上调整直至该数组变为堆。
:::
图示:使用向上调整算法的前提就是插入数据前数组已经是一个堆
图片.png
代码实现:

void AdjustUp(HPDataType* data, int child)
{
	assert(data != NULL);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (data[child] > data[parent])
		{
			Swap(data + child, data + parent);
			child = parent;
			parent = (child - 2) / 2;
		}
		else
		{
			break;
		}
	}
} 

向下调整算法:找出子节点中最大的那个节点,然后与根节点比较,如果大于根节点则进行交换,然后按照上述方法向下调整,直到到达数组尾部。

图示:使用向下调整算法的前提就是左右子树均为堆
图片.png代码实现:

// n为data数组的长度,parent为要调整的双亲节点
void AdjustDown(HPDataType* data, int n, int parent)
{
	assert(data != NULL);
	//假设左孩子最大
	int child = parent * 2 + 1;
	while (child < n)
	{
        //验证是否左孩子最大,注意没有右孩子的情况
		if (child + 1 < n && data[child + 1] > data[child])
		{
			child++;
		}
        //如果孩子大于双亲,则交换,继续向下调整
		if (data[child] > data[parent])
		{
			Swap(data + child, data + parent);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}  

铺垫完成,正式开始建堆
已知下面的数组,由此建堆。
int array[] = {27,15,19,18,28,34,65,49,25,37};
我们可以用向上调整算法建堆,也可以用向下调整算法建堆,就这两种方法来看使用向下调整算法建堆更好,具体为什么可以看两种时间复杂度的推导。
使用向上调整算法建堆:
由于向上调整算法的前提条件是数组已经是堆,因此我们从数组中第一个数开始插入,因为空树也可认为是堆,每次插入后数组还是堆,重复上述步骤,直到数组中的数据全部插入,至此堆建立完成。

//php为指向堆结构体的指针,nums为数组,numsLen为数组长度
void HeapCreate(heap* php, HPDataType* nums, int numsLen)
{
	assert(php != NULL);

	php->data = (HPDataType*)malloc(sizeof(HPDataType) * numsLen);
	if (php->data == NULL)
	{
		perror("Create");
		exit(-1);
	}
	php->size = numsLen;
	php->capacity = numsLen;
	memcpy(php->data, nums, numsLen * sizeof(HPDataType));

	//1.使用向上调整建堆
	int i = 0;
	for (; i < numsLen; i++)
	{
		AdjustUp(php->data, i);
	}
} 

该方法建堆的时间复杂度推导:

假设所建的堆为一颗高度为h的满二叉树,节点数为:n
那么第1层有20 个节点,需要向上调整0次
第2层有21个节点,调整1次
第3层有22个节点, 调整2次

第h层有2h-1个节点, 调整h-1次
则所有节点总共调整次数:F(h) = 211 + 222 +…+2h-1*(h-1), 由此可以看出该式子是等差等比形式,所以我们可以用错位相减法来求和
2F(h) = 2^2 * 1 + 2^3 * 2 + … + 2^(h-1) * (h-2)+ 2^h * (h-1) —(1)
F(h) = 2^1 * 1 + 2^2 * 2 +…+2^(h-1) * (h-1) —(2)
用(1)-(2)式:可得: F(h) = - 2^1 * 1 - (2^2 +2^3+ …+2^(h-1)) + 2^h * (h-1)
= -(2^1+ 2^2 +2^3+. … + 2^(h-1)+ 2^h) + 2^h * h
= 2 - 2^(h+1) + 2^h * h
又因为 h = log2(n+1)
所以: F(h) = 2 - (n+1) * 2+(n+1) * log2(n+1) = (n+1)log2(n+1) - 2 * n
所以:O(n) = n
log2n
当然还有一种简单算法:时间复杂度取影响最大的那一项,那么F(h)= 2^1 * 1 + 2^2 * 2 +…+2^(h-1) * (h-1)中,毫无疑问
,2h-1*(h-1) 比前面h-1项加起来还大:因为第h层的节点数大于前面h-1层的节点总数,所以计算时间复杂度时只看最后一项也是可以的,
将h = log2(n+1)代入2^h-1 * (h-1)中:也可以得到O(n) = n*log2n

使用向下调整算法建堆:
由于向下调整算法的前提条件是左右子树都是堆,那么我们可以从最后一个元素开始调整,因为最后一个元素没有子树,空树可以看作是堆, 但是叶子节点本来就是堆,所以没必要调整了,所以我们改为从最后一个分支节点开始调整,这时也满足左右子树均为堆

void HeapCreate(heap* php, HPDataType* nums, int numsLen)
{
	assert(php != NULL);

	php->data = (HPDataType*)malloc(sizeof(HPDataType) * numsLen);
	if (php->data == NULL)
	{
		perror("Create");
		exit(-1);
	}
	php->size = numsLen;
	php->capacity = numsLen;
	memcpy(php->data, nums, numsLen * sizeof(HPDataType));

	//2.使用向下调整建堆
	int i = 0;
    //最后一个节点的下标为:numsLen-1, 则其双亲节点的下标:(numsLen-1-1)/2
	for (i = (numsLen - 2) / 2; i >= 0; i--)
	{
		AdjustDown(php->data, php->size, i);
	}
}

该算法的时间复杂度推导:

假设所建的堆为一颗高度为h的满二叉树,节点数为:n
那么第1层有20 个节点,需要向调下整h-1次
第2层有21个节点,调整h-2次
第3层有22个节点, 调整h-3次

第h-1层有2h-2个节点, 调整1次
第h层有2h-1个节点, 调整0次
则所有节点总共调整次数:F(h) = 20*(h-1) + 21*(h-2) +…+2h-21+2h-10, 由此可以看出该式子是等差等比形式,所以我们可以用错位相减法来求和
F(h) = 2^0 * (h-1) + 2^1 * (h-2) +…+2^(h-2) * 1 —(1)
2
F(h) = 2^1 * (h-1)+2^2 * (h-2) + … + 2^(h-1) —(2)
用(2)-(1) 可得: F(h) = -2^0 * (h-1) + 2^ 1 + 2 ^2 + … + 2^(h-2)+ 2^(h-1)
= 2^0 + 2^1 + 2^2 + … + 2^(h-2) +2^ (h-1) - h
= 2^(h- 1) - h
又 h = log2(n+1)
所以可得F(n) = n+1-1-log2(n+1) = n - log2(n+1)
故O(n) = n

总结:由于向上调整算法建堆的时间复杂度为n*log2n ,而向下调整算法建堆的时间复杂度为n
所以我们选择用向下调整算法建堆, (简单理解,使用向下调整算法的时候节点多调整少,向上则是节点多调整多)

  1. 堆的插入

在插入前,数组已经是堆,故可以使用向上调整算法,调整次数为高度次,即:h = log2(n+1)
图示:
图片.png代码实现:

void HeapPush(heap* php, HPDataType x)
{
	assert(php != NULL);

	//检查是否需要扩容
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* temp = (HPDataType*)realloc(php->data, sizeof(int) * newCapacity);
		if (temp == NULL)
		{
			perror("push::");
			exit(-1);
		}
		php->data = temp;
		php->capacity = newCapacity;
	}

    //先插入
	php->data[php->size++] = x;
    //然后调整,size指向的是末尾元素的下一个
	AdjustUp(php->data, php->size-1);
}
  1. 堆的删除

     删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,这时,左右子树均为堆,因此可以使用向下调整算法。
    

图示:
图片.png代码实现:

void HeapPop(heap* php)
{
	assert(php != NULL);

	Swap(php->data, php->data + php->size - 1);
	php->size--;
	AdjustDown(php->data, php->size, 0);

}

5.求堆的长度以及判断是否为空

int HeapSize(heap* php)
{
	assert(php != NULL);

	return php->size;
}
bool HeapEmpty(heap* php)
{
	assert(php != NULL);

	return php->size == 0;
}
3.5.堆的应用
  1. 堆排序

堆排序分为两个步骤: 1.建堆(升序建大堆,降序建小堆), 2.利用堆删除来达到有序的目的

  1. 求top-k问题,求数据集合中前k个最大或者最小的数据,一般数据量较大.

    (1)如果数据量较小,可以全部放入内存中,则将这些数据进行建堆, 然后利用堆删除的思想,选出k个数即可
    时间复杂度:n+klog2(n+1) (其中n为建堆的时间,每次选一个数,要调整高度次,选k个调整k高度)
    空间复杂度:1
    (2)问题: 数据量:10亿个整数,需要4G的内存空间,要求在其中选出最大的k个数
    数据量较大,不能全部放入内存中,则将前k个数在内存中建立一个小堆,然后遍历剩下的数据,如果比堆顶的数据大,则将该数放入堆顶,然后进行向下调整,直到文件中的数据全部遍历完成,此时内存中的k个数的小堆,存放的就是文件中的最大的k个数
    时间复杂度:k + (n-k)log2(k+1)
    空间复杂度:k, 因为要在内存中建立k个数的小堆

  • 15
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值