【数据结构】树详解——二叉树——堆

目录

  • 一、前言
  • 二、树的概念及其结构
    • 1.树直接的关系
    • 2.数的概念
    • 3.树的基本概念
    • 4.多叉树的的表示
    • 5.树的应用
  • 三、二叉树的概念及结构
    • 1.概念
    • 2.特殊的二叉树
  • 四、完全二叉树(堆)的顺序结构及其实现
    • 1.完全二叉树的顺序结构
    • 2.完全二叉树的数组存储关系
    • 3.堆的结构以及概念
    • 3.堆的实现
      • 堆的初始化
      • 堆的插入及向上调整
      • 堆的删除以及向下调整
      • 堆的创建
    • 4.堆的应用
      • 堆排序
      • topK


一、前言

往期数据结构文章可点击下列链接

【数据结构】时间复杂度
【数据结构】顺序表
【数据结构】链表——增、删、查、改
【数据结构】双向循环链表
【数据结构】栈和队列详解
有兴趣的同学可以点击前往支持一下


二、树的概念及其结构

1.树直接的关系

【数据结构】树详解——二叉树——堆_第1张图片


2.数的概念

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

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
  • 因此,树是递归定义的
    【数据结构】树详解——二叉树——堆_第2张图片
    注意:树形结构中,子树之间不能有交集,否则就不是树形结构
    【数据结构】树详解——二叉树——堆_第3张图片

3.树的基本概念

【数据结构】树详解——二叉树——堆_第4张图片

  • 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林;
- 标红的概念请大家熟练掌握。-

4.多叉树的的表示

  • 左孩子右兄弟表示法
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};

【数据结构】树详解——二叉树——堆_第5张图片


5.树的应用

在linux操作系统下的目录系统就是用树结构表示的:

我们可以下载一个tree软件查看Linux下的目录:

sudo apt install tree

【数据结构】树详解——二叉树——堆_第6张图片


三、二叉树的概念及结构

1.概念

树的每一个结点的度都不大于2的树即为二叉树

一个二叉树由根节点左子树右子树组成:
【数据结构】树详解——二叉树——堆_第7张图片

注意

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

2.特殊的二叉树

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

【数据结构】树详解——二叉树——堆_第8张图片


四、完全二叉树(堆)的顺序结构及其实现

1.完全二叉树的顺序结构

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

【数据结构】树详解——二叉树——堆_第9张图片


2.完全二叉树的数组存储关系

【数据结构】树详解——二叉树——堆_第10张图片
由图可推知结点父子关系:

  • 父结点:parent = (child - 1) / 2(其中child可为左右结点下标,parent为父结点下标)
  • 左子结点:child = 2 * parent + 1
  • 右子结点:child = 2 * parent + 2

3.堆的结构以及概念

堆的性质:

  • 堆总是一棵完全二叉树
  • 堆中某个节点的值总是不大于或不小于其父节点的值

【数据结构】树详解——二叉树——堆_第11张图片

即如图所示10小于15和56, 15小于25和30, 56小于70每个结点都小于等于其子结点。且该树为完全二叉树, 所以第一个二叉树为小堆。


3.堆的实现

堆的初始化

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

void HeapInit(Heap* hp)
{
	assert(hp);
	hp->_a = NULL;
	hp->_capacity = hp->_size = 0;
}

堆的插入及向上调整

【数据结构】树详解——二叉树——堆_第12张图片

插入:

void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->_size == hp->_capacity)
	{
		int newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HPDataType* new = realloc(hp->_a, sizeof(HPDataType) * newcapacity);
		if (new == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		hp->_a = new;
		hp->_capacity = newcapacity;
	}

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

	AdjustUp(hp->_a, hp->_size - 1);
}

向上调整:

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

堆的删除以及向下调整

堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

【数据结构】树详解——二叉树——堆_第13张图片
堆的向下调整法:

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

而在删除的过程中我们采用了交换法,没有改变左右子树的结构顺序,他们仍然是一个堆,所以可以对根结点使用向下调整法。

向下调整法是利用根结点与左右子树的根较小的值进行比较,如果比较小值大则不满足堆的调节进行交换。直到交换到满足堆的条件。
【数据结构】树详解——二叉树——堆_第14张图片

删除:

void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->_size > 0);
	hp->_a[0] = hp->_a[hp->_size - 1];
	hp->_size--;
	AdjustDown(hp->_a, hp->_size, 0);
}

向下调整:

void AdjustDown(HPDataType* a, int n, int parent)
{
	//左孩子
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child = child + 1;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

堆的创建

如果我们有一个数组,想让它以堆的顺序进行排列,那我们应该怎么办呢?一个无序的数组的根节点左右子树不是一个堆,所以我们不能对根使用向下调整。那么我们可以换一种思路对最后一个非叶子结点向下调整,直到调整到根节点,就可以调整成堆。

int a[] = {1,5,3,8,7,6};

【数据结构】树详解——二叉树——堆_第15张图片
这样建堆的时间复杂度达到了O(n)!


4.堆的应用

堆排序

这是一种时间复杂度达到O(n*logn)的排序算法

  1. 建堆
    • 升序 :建大堆
    • 降序 :建小堆
  2. 利用堆删除思想来进行排序

【数据结构】树详解——二叉树——堆_第16张图片

void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	
	//利用堆删除思想来进行排序
	for (int i = 0; i < n; i++)
	{
		Swap(&a[0], &a[n - i - 1]);
		AdjustDown(a, n - i - 1, 0);
	}
	
	//打印
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

topK

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    • 前k个最大的元素,则建小堆
    • 前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
void PrintTopK(int k)
{
	//data.txt里有10000个数据
	const char* file = "data.txt";
	FILE* fin = fopen(file, "r");
	if (fin == NULL)
	{
		ferror("fopen fail");
		exit(-1);
	}

	int* minheap = (int*)malloc(sizeof(int) * k);
	
	for (int i = 0; i < k; i++)
	{
		fscanf(fin, "%d", &minheap[i]);
	}

	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minheap, k, i);
	}

	int x = 0;
	while (fscanf(fin, "%d", &x) != EOF)
	{
		if (x > minheap[0])
		{
			minheap[0] = x;
		}
		AdjustDown(minheap, k, 0);
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
	printf("\n");

	fclose(fin);
}

你可能感兴趣的:(数据结构,数据结构)