1. 树的基本概念

树与链表,栈和队列不同, 是一种非线性的数据结构, 它由n (n>=0) 个有限结点组成一个具有层次关系的集合

把它叫做树,是因为存储在内存中的数据, 在逻辑上呈现一种树的形态, 只是根在上,叶在下

其次, 每一棵树都可以分为根(根结点)和子树(子节点), 子树又可以分为根和子树

树的结点关系由人类亲缘关系来解释

  • 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林

其中, 粗字体表示需要重点理解, 剩下的作为了解

最后, 子树之间不能有交集,否则就是图

2. 二叉树

二叉树的概念

二叉树是一组结点的集合, 每一个结点的度可以是1,2, 但是绝对不能大于2, 否则就不是一颗二叉树 

什么是满二叉树/完全二叉树

满二叉树, 就是每一层的结点数量都达到最大值, 如上图

完全二叉树, 前h-1层是满的, 最后一层从左往右必须是连续的

如图, 如果是这样,就不是一颗完全二叉树

满二叉树结点个数和完全二叉树结点范围 

因为满二叉树的每一层都是满的, 所以结点的数量是固定的

假设满二叉树的高度为h, 推导公式:

F(h) = 2^0 + 2^1 + 2^2 ... 2^(h-2) + 2^(h-1)

用错位相减法解:

2*F(h) =         2^1 + 2^2 + 2^3 + ............. + 2^(h-1) + 2^(h)

-

F(h) =  2^0 + 2^1 + 2^2 + ..... + 2^(h-2) + 2^(h-1)

F(h) =   2^h - 1

高度为h的满二叉树, 有 2^h-1个结点

 

完全二叉树最多结点数量:  等于满二叉树结点数量,  2^h-1

完全二叉树最少结点数量: 最后一层最少一个结点, 前h-1层是满的

推导公式:

F(h) = 2^0 + 2^1 + ... + 2^h-2 + 1

还是用错位相减法先求出前h-1层的结点数量:

2 * F(h) =      2^1 + 2^2 + ... + 2^h-1

-

F(h) = 2^0 + 2^1 + ... + 2^h-2 

= -1 + 2^h-1

然后+1, 因为最后一层最少一个结点, 完全二叉树最少结点数量: 2^(h-1)

完全二叉树结点范围: [ 2^(h-1), 2^h - 1]

3. 堆的概念

堆的基本概念

通过观察完全二叉树, 可以发现完全二叉树非常适合用数组进行存储

更为重要的是, 通过数组的下标关系可以用孩子找到父亲,父亲也可以找到孩子

比如图中:

D(孩子)的下标(3)-1除2, 可以得到B(父亲)的下标

父亲(B) * 2 + 1, 得到D(孩子)下标

父亲(B) * 2 + 2, 得到E(孩子)下标

所以可以得出结论:

leftchild = parent*2+1

rightchild = parent*2+2

parent = (child-1) / 2

什么是堆

从本质上(物理结构)来看, 堆是一个数组, 但是从逻辑结构来看, 堆是一棵完全二叉树

数组都可以看作是一颗完全二叉树, 但是不能看作是堆

判断数组是否是一个堆, 必须满足一个条件: 所有父亲大于或者等于孩子 --- 大根堆 / 满足所有父亲小于或者等于孩子 --- 小根堆

4. 堆的实现

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

typedef int HPDataType;
typedef struct heap
{
	HPDataType* dys;
	int capcacity;
	int size;
}HP;

// 初始化与销毁
void HeapInit(HP* php);
void HeapDestroy(HP* php);

// 向上调整算法
void AdjustUp(HPDataType* dys, int child);
// 向下调整算法
void AdjustDown(HPDataType* dys, int parent, int sz);

void HeapPush(HP* php, HPDataType data);
void HeapPop(HP* php);

HPDataType HeapTop(HP* php);
bool isEmpty(HP* php);
heap.c
#include "heap.h"

// 初始化与销毁
void HeapInit(HP* php)
{
	assert(php);
	HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * 4);
	if (NULL == tmp)
	{
		perror("HeapInit::malloc fail");
		return;
	}
	php->dys = tmp;
	php->size = 0;
	php->capcacity = 4;
}
void HeapDestroy(HP* php)
{
	assert(php);
	free(php->dys);
	php->dys = NULL;
	php->capcacity = 0;
	php->size = 0;
}
void Swap(HPDataType* buf1, HPDataType* buf2)
{
	int tmp = *buf1;
	*buf1 = *buf2;
	*buf2 = tmp;
}
// 向上调整算法
void AdjustUp(HPDataType* dys, int child)
{
	assert(dys);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (dys[child] > dys[parent])
		{
			Swap(&dys[child], &dys[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
// 向下调整算法
void AdjustDown(HPDataType* dys, int parent, int sz)
{
	assert(dys);
	int child = parent * 2 + 1;
	while (child < sz)
	{
		// 假设左孩子大于右孩子
		if (child+1 < sz && dys[child + 1] > dys[child])
		{
			child++;
		}

		if (dys[child] > dys[parent])
		{
			Swap(&dys[child], &dys[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
// 进堆
void HeapPush(HP* php, HPDataType data)
{
	assert(php);
	if (php->size == php->capcacity)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->dys, php->capcacity * sizeof(HPDataType) * 2);
		if (NULL == tmp)
		{
			perror("HeapPush::realloc fail");
			return;
		}
		php->dys = tmp;
		php->capcacity *= 2;
	}
	php->dys[php->size] = data;
	php->size++;
	AdjustUp(php->dys, php->size - 1);
}
// 删除堆顶数据
void HeapPop(HP* php)
{
	assert(php);
	assert(!isEmpty(php));
	Swap(&php->dys[0], &php->dys[php->size - 1]);
	php->size--;
	AdjustDown(php->dys, 0, php->size);
}
bool isEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!isEmpty(php));
	return php->dys[0];
}
int HPSize(HP* php)
{
	assert(php);
	return php->size;
}
test.c
 #include "heap.h"

int main()
{
	HP heap;
	HeapInit(&heap);
	HeapPush(&heap, 1);
	HeapPush(&heap, 2);
	HeapPush(&heap, 3);
	HeapPush(&heap, 5);
	HeapPush(&heap, 6);
	while (!isEmpty(&heap))
	{
		printf("%d ", HeapTop(&heap));
		HeapPop(&heap);
	}
}

5. 堆的实际应用1 - 堆排序

通过数据结构堆进行排序

将数组元素依次插入到堆中, 模拟建队的过程

然后分别取出堆顶数据放回到数组中, 模拟排序

#include "heap.h"
void heapsort(int* array, int sz)
{
	HP heap;
	HeapInit(&heap);
	// 将数组元素依次入堆
	for (int i = 0; i < sz; i++)
	{
		HeapPush(&heap, array[i]);
	}
	// 分别取出堆顶数据, 然后入数组
	int i = 0;
	while (!isEmpty(&heap))
	{
		array[i++] = HeapTop(&heap);
		HeapPop(&heap);
	}
	HeapDestroy(&heap);
}
int main()
{
	int a[] = { 7,8,3,6,1,9,2,4 };
	heapsort(a, sizeof(a) / sizeof(int));
  }

 

如图, 通过堆进行了排序

但是这种方法并不好, 因为建堆开辟空间, 空间复杂度为O(N), 其次每次排序都需要一个数据结构堆, 很麻烦

下面来看一种最优的方法

向上/下调整建堆排序

#include "heap.h"
void heapsort(int* array, int n)
{
	 // 向上调整建堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(array, i);
	}
	// 向下调整建堆
	/*for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(array, i, n);
	}*/
	int end = n - 1;
	while (end > 0)
	{
		Swap(&array[0], &array[end]);
		AdjustDown(array, 0, end);
		end--;
	}
}
int main()
{
	int a[] = { 7,8,3,6,1,9,2,4 };
	heapsort(a, sizeof(a) / sizeof(int));
  }

向上调整建堆, 将数组从第2个元素(下标为1)开始依次向上调整,将数组建成一个堆

然后通过一个非常巧妙的方法, 模拟排序

按照这个原理, 最终数组会从小到大进行排序

最后根据这个例子可以得出一个结论:

排升序, 建大堆

排降序, 建小堆

下面再来看一下, 向下调整建堆

void heapsort(int* array, int n)
{
	 // 向上调整建堆
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(array, i);
	}*/
    
	// 向下调整建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(array, i, n);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&array[0], &array[end]);
		AdjustDown(array, 0, end);
		end--;
	}
}
int main()
{
	int a[] = { 7,8,3,6,1,9,2,4 };
	heapsort(a, sizeof(a) / sizeof(int));
  }

向下调整建堆, 首先需要找到第一个结点的父亲, 然后依次递减向下调整建堆, 如下图

现在知道向上/向下调整如何建堆, 建堆后如何排序, 然后排升序, 建大堆, 排降序, 建小堆

那么, 向上调整和向下调整在效率上有什么区别吗?

结论是, 向下调整在效率上更优, 下面进行证明

向上/向下调整建堆复杂度证明

首先, 计算向下调整建堆复杂度

                             

                                                       

logN可以忽略不计, 所以向下调整时间复杂度为O(N)

接下来, 计算向上调整:

                   

                                                             

如图向上调整建堆, 时间复杂度为O(N*logN), 所以向下调整建堆效率更优

最后, 计算建堆后, 堆排序的时间复杂度

堆排序的时间复杂度

通过观察发现, 堆排序本质上和向上调整建堆相同

所以, 整个堆排序(建堆+排序)(N+N*log(N)), 在N*logN这一个量级 

 

posted @ 2023-05-24 11:37  许木101  阅读(60)  评论(0)    收藏  举报