栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【C语言】数据结构——树

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【C语言】数据结构——树

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

一、树的概念 概念: 

※ 节点的度:一个节点 含有的子树的个数 称为该节点的度; 如上图:A的度为2。 ※ 叶节点或终端节点: 度为0的节点称为叶节点 ; 如上图:E、F、G、H...等节点为叶节点。 非终端节点或分支节点:度不为0的节点; 如上图:B、C、D...等节点为分支节点 ※ 双亲节点或父节点:若 一个节点含有子节点,则这个节点称为其子节点的父节点 ; 如上图:A是B的父节点 ※ 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点 ※ 兄弟节点:具有 相同父节点的节点互称为兄弟节点 ; 如上图:B、C是兄弟节点。 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为2。 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推; ※ 树的高度或深度: 树中节点的最大层次 ; 如上图:树的高度为4 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:D、F互为堂兄弟节点。 ※ 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先。 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。 森林:由m(m>0)棵互不相交的树的集合称为森林; PS:有的地方根节点的层次是设为0的,那么要注意空树就为-1。

PS:树形结构中子树是不能有交集的!否则不是树形结构。

(要注意,虽然不是树,但是其并不是错误的,是别的数据结构。) 

左孩子右兄弟表示法:
typedef int DataType;
struct tree
{
	struct treee* firstchild1; //指向孩子节点
    struct tree* pNextBrother; //指向兄弟
    DataType _data;
};

        第一个指针,只指向子节点的左孩子节点,让第二个指针指向其兄弟节点,这样就能把树的结构表现出来。这里使用的是类似链表的方式。

        这里A的孩子指针指向B。B的孩子指针指向D,兄弟指针指向C,C的孩子指针指向F、G,兄弟指针指向NULL。

        孩子指针只指向第一个孩子节点,这里B只指向D,而E由D的兄弟指针指向。

(只养老大,老二让老大去带,老三让老二去带。)

PS:如果节点后面没有子节点或者兄弟节点了,其指针指向空。

二、二叉树 概念:

二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。

1. 或者为空 2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

PS:

  • 1. 二叉树不存在度大于2的结点
  • 2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

满二叉树:除在二叉树最下层的节点外,每层的节点都有两个子节点。

完全二叉树:除最后一层外,其他各层的节点数都达到最大个数,且最后一层节点是从左到右的连续存在。

PS:满二叉树是一种特殊的完全二叉树。

二叉树的性质:

下标(数组)计算父子间的关系:

                                     leftchild = parent*2+1;

                                     rightchile = parent*2+2;

                                     parent= (child-1)/2 ; (不用考虑小数,整数运算会忽略小数)

PS:h为二叉树的高度,n是节点数。

 (指数用^的话不太明确,这里就用图片了) 

Q:具有2k个节点的完全二叉树中,叶子节点个数为?

A: n0+n2+n1=2k;  且 n2=n0 - 1;所以n0+n0 -1+n1=2k;

       2n0-1+n1=2k; 这里度为1的节点(n1)要么为1要么为0,如果这里为0,n0是小数,但是节点数不可能是小数,所以这里n1为1。

       所以n0=k;所以叶子节点数为k个。

顺序存储:

       顺序结构存储就是使用数组来存储,如果节点为空,要把空间留出来。会造成空间浪费,适合存储满二叉树和完全二叉树。 

链式存储:

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{ 
 struct BinTreeNode* _pLeft; // 指向当前节点左孩子
 struct BinTreeNode* _pRight; // 指向当前节点右孩子
 BTDataType _data; // 当前节点值域
}

         二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。

 

三、数据结构-堆

小根堆:

            1.是个完全二叉树。

            2.树中所有的父亲都是小于等于孩子。

大根堆:

            1.是个完全二叉树。

            2.树中所有的父亲都是大于等于孩子。

        这里是用的数组来模拟堆的结构。

插入数据:

        因为使用的顺序表,其真实的存储是直接插入到尾部就可以了,但是怎么让其逻辑上成为一个堆呢?

        现在我们给出一个数组,逻辑上看做一颗完全二叉树。新添加一个数据,使用向上调整算法可以把它调整成一个小堆。

int array[] = {10,15,19,18,28,34,65,49,25,37};

​​​​​(这里用画图快速模拟一下堆)

        这里假设已经有一个正确结构的堆,插入一个节点27,这里堆就不匹配了。那么这里就需要使用向上调整算法进行调整。

        插入的节点跟父节点进行比较如果,父节点比较大,就把自己跟父节点进行交换。然后继续跟新的父节点进行比较,直到遇到比自己小的父节点。

 

        这样堆就正确排列了。

PS:调整节点比较大小时,只需要跟相同祖先的节点进行比较,兄弟节点之间的大小不影响堆的建立。       

代码:

//向上调整
void AdjustUp(int* arr,int pos)
{

	int child = pos;
	int parent = (child - 1) / 2; //父节点公式。

	while ( child > 0 ) {

		parent = (child - 1) / 2;
		
		if (arr[child] < arr[parent]) {
			Swap(&arr[child],&arr[parent]);
		}
		else {
			break;
		}

		child = parent;
	}
}

        一开始是没数据的,每次插入一个数调整一下,这样堆就创建出来了。

        这里求父节点的下标的时候,因为其是整数做除法,小数点会被忽略,子节点 4 和 5,算出来的父节点都是 2,刚好对应上。

        这里最坏的情况是交换到根节点,那么就可以判断是否到了根节点来结束循环。注意不要使用parent>=0来判断,因为到最后 parent = -1/2 ,其结果为0,会造成死循环。

PS:这里创建的是小堆,如果要创建大堆,把父子节点的比较换成大于符号就可以了。

删除数据:

        删除堆的根节点,不能直接删除,不然其逻辑结构失效,堆会被打乱。

        首先要把根节点跟最后一个叶子节点进行交换,然后删除。再把新的根节点进行向下调整。

        选择较小的子节点,如果其也比自己小,那么进行交换。如果其比自己大那么结束交换。(生成大堆的时候选择较大的数,如果比自己大进行交换)

        并不是每个节点都有右节点,如果只有一个左节点直接交换就行了。如果自己孩子节点的下标超过了堆的大小,说明自己已经是叶子节点了,就要退出循环。

PS:向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

代码:
void AdjustDown(int *arr ,int size,int parent)
{
	int child = parent * 2 + 1;

	while ( child < size ) {

		//防止越界,因为有左孩子节点,不一定有右孩子节点。
		if ( child + 1 < size && arr[child + 1] 

        有了插入和删除就可以写出堆的结构了。

 堆代码实现:

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

//交换
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType temp = *a;
	*a = *b;
	*b =  temp;
}

//向上调整
void AdjustUp(int* arr,int pos)
{
	//建立小堆,如果新加入的数据比父节点小,就交换,比父节点大就退出。
	int child = pos;
	int parent = (child - 1) / 2;

	while ( child > 0 ) {

		parent = (child - 1) / 2;
		
		if (arr[child] < arr[parent]) {
			Swap(&arr[child],&arr[parent]);
		}
		else {
			break;
		}

		child = parent;
	}
}

//向下调整
void AdjustDown(int *arr ,int size,int parent)
{
	//取较小的子节点交换数据。
	int child = parent * 2 + 1;

	while ( child < size ) {

		//防止越界,因为有左孩子节点,不一定有右孩子节点。
		if ( child + 1 < size && arr[child + 1] _a = NULL;
	hp->_capacity = 0;
	hp->_size = 0;
}

//创建堆
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	assert(hp);
	assert(a);

	for (int i = 0; i < n;i++) {
		HeapPush(hp, a[i]);
	}
}

//销毁
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->_a);
	hp->_a = NULL;
	hp->_capacity = 0;
	hp->_size = 0;
}

//插入数据
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	int newCpacity = 0;

	if (hp->_capacity==hp->_size) {
		newCpacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HPDataType* temp=(HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newCpacity);
		if (temp==NULL) {
			perror("temp为NULL");
			exit(-1);
		}

		hp->_a = temp;
		hp->_capacity = newCpacity;
	}

	hp->_a[hp->_size] = x;
	hp->_size++;


	//向上调整,构建堆
	AdjustUp(hp->_a,hp->_size-1);
}

//删除数据
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	//交换首尾元素,size--,然后把根节点的元素向下调整。

	Swap(&hp->_a[0],&hp->_a[hp->_size-1]);
	hp->_size--;

	AdjustDown(hp->_a,hp->_size,0);

}

//取根数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->_a[0];
}

//堆的大小
int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->_size;
}

//堆是否为空
bool HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->_size==0;
}

测试: 

       

        画图来快速判断一下结构是否正确。

        由于实际使用的是顺序表进行存储的,所以大多数代码的写法跟顺序表差不多。这里主要的是插入数据和删除数据的逻辑和思路。

堆排序:

        堆优势是选数的速度很快,那么可以就使用堆来进行排序。

        直接利用上面已经写好的堆。

void HeapSort(int* a, int n)
{
	Heap hp;
	HeapInit(&hp);
	for (int i = 0; i < n;i++) {
		HeapPush(&hp,a[i]);
	}

	int i = 0;
	while (!HeapEmpty(&hp)) {
		a[i++] = HeapTop(&hp);
		HeapPop(&hp);
	}
}

测试:

         这种写法有缺点,使用堆排序的时候,不用一定要把堆这个数据结构全部写出来,只需要用数组和算法来模拟出堆的结构就可以。

        而且这里还使用了两个数组,空间复杂度也提升了。

        这里可以直接使用向上调整算法和向下调整算法来对数组进行调整。

//方式1:
//直接从数组第二个元素开始向上调整构建堆 ,此时的时间复杂度为O(nlogn);
for (int i = 1; i < n;i++) {
	  AdjustUp(a, i);
}

         直接把给进来的数组的数据向上调整,调整出来的是数组就是堆的结构,构成堆就可进行排序了。

PS:这里构建堆,要从第二个元素开始向上调整,一直到最后。

//方式2:
//直接从最后的叶子节点的父节点,往下调整构建堆,时间复杂度为O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
	  AdjustDown(a, n, i);
}

        向下调整,需要节点左右有堆,这里第一个有堆的节点,是最后一个叶子节点的父节点。然后依次往上调整,调整到最后堆的结构就建立起来了。

 PS:最后一个节点的下标就是数组的长度-1,然后它的父节点就是第一个开始调整的节点。

        最后把调整出来的堆进行排序,这直接就可以用一个数组进行排序。

        其思路是把跟节点跟最后一个节点的数据进行交换,然后把堆的大小减小一个(这样就遍历不到最后一个节点了),因为把数据交换到了根节点,所以要重新调整建堆,直接把新的根数据向下调整,然后继续交换,直到堆的大小变为0。

        从上面的思路就可以知道,假设建立的是小堆,其根节点是堆中最小的数,把他放到数组后面,然后继续建堆,找到次小的数,再放到后面。最后会发现,放置的数据在数组中是从大到小排列,构成了一个降序的数组。(根据其逻辑可以知道,建立降序数组用小堆,建立升序数组,用大堆)

void HeapSort(int* a, int n)
{
    //i是最后一个叶子节点的父节点
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(a, n, i);
	}

	//注意升序建立小堆,降序建立大堆。
	//把收尾数据交换,然后把根数据向下调整
	int size = n-1;
	while (size > 0) {
		Swap(&a[0], &a[size]);
		AdjustDown(a,size,0);
		size--;
	}

}
堆排序的时间复杂度:

向上调整的时间复杂度:

    向上调整,开始的节点是第根节点的左节点,根节点不用调整。然后依次往下走。其最后一层占一半的节点,所以消耗比较多。

总的(最坏)调整次数T(h): 

公式是等比值*等差值,这里使用错位相减法来化简。

 

 可以发现前面就是一个等比数列,可以利用等比公式化简:

满二叉树的节点数就是 2^h-1,二叉树的高度 h=log(n+1);所以可得: 

 这里影响n*logn大小是其最大的影响因素,所以其时间复杂度为O(n*logn);

向下调整的时间复杂度:

        向下调整,开始的节点是第一个非叶子节点,然后依次往上走。所以开始h层的数据不用调整,是从h-1层开始调整的。

        这里第一个层有2^0个节点,最多调整h-1层(不包括自己这层)。h-1层有 2^(h-2)个节点,向下调整1层。

可以归纳出公式:

总的(最坏)调整次数T(h): 

公式是等比值*等差值,这里使用错位相减法来化简。

        前面是0-1*(h-1);得1-h。中间系数相减都为1。后面是2^(h-1)-0;得2^(h-1)。

         可以发现前面就是一个等比数列,可以利用等比公式化简:

 

        满二叉树的节点数就是 2^h-1,二叉树的高度 h=log(n+1);所以可得: 

 

         这里n的大小是其最大的影响因素,所以其时间复杂度为O(n)。


转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/925554.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号