【数据结构】非线性结构之树结构(含堆)

在这里插入图片描述

前言

前面的三篇文章已经将线性结构讲述完毕了,下面的文章将会为大家将讲点新东西:非线性结构中的树结构。萌新对这里的知识点相对陌生,建议反复观看!!
关于线性结构的三篇文章放在下面:
线性表之顺序表
线性表之链表
线性表之栈、队列


目录

  • 前言
  • 一、树的概念及结构
    • 1. 树的概念及结构
    • 2. 树的相关概念
    • 3. 树的表示
  • 二、二叉树的概念及结构
    • 1. 二叉树的概念
    • 2. 程序员的吉祥物
    • 3. 特殊的二叉树
    • 4. 二叉树的性质
  • 三、二叉树的顺序存储
    • 1. 二叉树树的顺序存储
    • 2. 堆的概念及实现
      • 2.1 堆的概念
      • 2.2 堆的实现
        • 2.2.1 堆的向上调整算法
        • 2.2.2 堆的向下调整算法
        • 2.2.3 堆的构建
          • a. 堆构建的方法方法
          • b. 建堆的时间复杂度
        • 2.2.4 堆的插入
        • 2.2.5 堆的删除
        • 2.2.6 堆代码的总体实现
      • 2.3 堆的应用
        • 2.3.1 堆排序的实现
        • 2.3.2 TopK的实现
    • 3. 二叉树的链式存储
      • 3.1 二叉树的构建
      • 3.2 二叉树的前序遍历
      • 3.3 二叉树的中序遍历
      • 3.4 二叉树的后序遍历
      • 3.5 二叉树的节点个数
      • 3.6 二叉树的叶子节点个数
      • 3.7 二叉树的第k层节点个数
      • 3.8 二叉树中寻找值为x的节点
      • 3.9 二叉树的层序遍历
      • 3.10 判断是否为完全二叉树
  • 结尾

一、树的概念及结构

1. 树的概念及结构

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

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点。
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、…Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。
  • 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,树是递归定义的。

【数据结构】非线性结构之树结构(含堆)_第1张图片
注意树结构中,子树之间不能相交,否则不能叫做树结构
【数据结构】非线性结构之树结构(含堆)_第2张图片


2. 树的相关概念

【数据结构】非线性结构之树结构(含堆)_第3张图片

注意:这里使用 ▲ 标注的都是比较重要的概念

  • 节点的度:一个节点含有的子树的个数称为该节点的度;如上图: R 的为6
  • 叶节点或终端节点:度为0的节点称为叶节点;如上图:j、k、m、l!..等节点为叶节点
  • 非终端节点或分支节点:度不为0的节点;如上图:b、c、d、h…等节点为分支节点
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如上图: R 是 b 的父节点
  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;如上图: b 是 R 的孩子节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点;如上图:a、b是兄弟节点
  • 树的度:一棵树中,最大的节点的度称为树的度;如上图:树的度为3
  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  • 树的高度或深度:树中节点的最大层次;如上图:树的高度为4
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:k、m互为堂兄弟节点
  • 祖先:从根到该节点所经分支上的所有节点╱如上图: A 是所有节点的祖先
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是 R 的子孙
  • 森林:由m (m>0)棵互不相交的树的集合称为森林;

3. 树的表示

这里使用孩子兄弟表示法:根节点左指针指向孩子,右指针指向兄弟。

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

【数据结构】非线性结构之树结构(含堆)_第4张图片
实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法


二、二叉树的概念及结构

1. 二叉树的概念

二叉树是一种特殊的树结构:

  • 节点的度最多为二
  • 由根节点连接着左子树和右子树构成(子树可以为空)

二叉树可以分为以下五种情况:

【数据结构】非线性结构之树结构(含堆)_第5张图片
每一颗二叉树都可以分为三个部分:根 、左子树、右子树。而子树还可以继续往后分。
【数据结构】非线性结构之树结构(含堆)_第6张图片


2. 程序员的吉祥物

【数据结构】非线性结构之树结构(含堆)_第7张图片


3. 特殊的二叉树

满二叉树:对于满二叉树而言,它的每一层节点个数都会达到最大值( 叶子节点只能是最后一层的节点,除叶子节点外,其余的所以节点都含有左右孩子 ) 。
完全二叉树:对于完全二叉树而言,它除了最后一层可以不为满,其他层必须为满,且最后一层节点必须是从左向右排列,且两节点之间不可有空再插入一个节点。
【数据结构】非线性结构之树结构(含堆)_第8张图片


4. 二叉树的性质

  1. 若规定根节点的层数为 1,则一棵非空二叉树的第i层上最多2^(i-1)个结点.
  2. 若规定根节点的层数为 1,则深度为h的二叉树的最大结点数2^h - 1.
  3. 对任何一棵非空二叉树,如果度为 0 其叶结点个数为 M,度为 2 的分支结点个数为 N,
    则有M = N + 1
  4. 若规定根节点的层数为 1,具有 n 个结点的满二叉树的深度,h = log以2为底,n+1为对数)
  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从 0 开始编号,则对于序号为i的结点有:
    1.若i > 0,i位置节点的双亲序号:(i - 1) / 2; 若i = 0,则无双亲节点
    2.若2i+1 < n,左孩子序号:2i + 1,2i + 1 >= n否则无左孩子
    3.若2i+2 < n,右孩子序号:2i + 2,2i + 2 >= n否则无右孩子

三、二叉树的顺序存储

1. 二叉树树的顺序存储

二叉树树的顺序存储是以数组为物理结构存储的一种结构,而画出来的是逻辑结构。
树的顺序存储是依照上面树的父子关系:

  • parent = (child - 1) / 2 ( 这里由于整形相除得到整形的机制,使得不需要要分辨左右孩子 )
  • leftchild = parent * 2 + 1 ( 左孩子的下标是父亲下标的两倍加一 )
  • rightchild = parent * 2 + 2 ( 右孩子的下标是父亲下标的两倍加二)

而我们看下面的图就可以知道,并不是所有的二叉树都适合使用二叉树的顺序存储。
下面非完全二叉树一共十三个空间就浪费了五个,再别说更极端的树了。
所以得出结论只有完全二叉树适合使用二叉树的顺序存储!!
【数据结构】非线性结构之树结构(含堆)_第9张图片


2. 堆的概念及实现

2.1 堆的概念

如果有一个关键码的集合,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:根节点的关键码大于等于左右子树的关键码(根节点的关键码大于等于左右子树的关键码),则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

关键码(key) 是堆中每个元素的一个属性,用于比较元素的大小。
根据元素的关键码,可以决定堆的类型:

  • 大根堆:每个节点的关键码都大于或等于其子节点的关键码,即根节点具有最大的关键码。
  • 小根堆:每个节点的关键码都小于或等于其子节点的关键码,即根节点具有最小的关键码。

【数据结构】非线性结构之树结构(含堆)_第10张图片

2.2 堆的实现

  • 关于堆的一些接口:Heap.h
#pragma once

#include 
#include 
#include 
#include 

typedef int HPDataType;

typedef struct Heap
{
	int* arr;
	int size;
	int capacity;
}Heap;

// 堆的构建
void HeapCreate(Heap* php, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* php);
// 堆的插入
void HeapPush(Heap* php, HPDataType x);
// 堆的删除
void HeapPop(Heap* php);
// 取堆顶的数据
HPDataType HeapTop(Heap* php);
// 堆的数据个数
int HeapSize(Heap* php);
// 堆的判空
int HeapEmpty(Heap* php);

2.2.1 堆的向上调整算法

使用堆的向上调整算法的前提是:在插入新元素之前,前面的元素就已经满足了堆的性质!!
堆的向上调整(heapify up)算法的思想是:(假设建大堆

  1. 将新元素插入到叶子节点。
  2. 与父节点进行比较。如果新元素的值大于父节点的值,则需要进行交换。
  3. 将新元素看作父节点,重复上述过程。

直到新元素值小于父节点为止,此时已经满足堆的性质。

void AdjustUp(int* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (arr[child] > arr[parent])
		{
			HPDataType tmp = arr[child];
			arr[child] = arr[parent];
			arr[parent] = tmp;
		}

		child = parent;
		parent = (child - 1) / 2;
	}
}

2.2.2 堆的向下调整算法

使用堆的向下调整算法的前提是:根节点的左右子树都满足堆的性质
堆的向下调整(Heapify Down)算法的思想是:

  • 与左右子节点进行比较。
  • 如果子节点中有比当前节点更大的值(小根堆)或者更小的值(大根堆),则与最大(小根堆)或最小(大根堆)的子节点交换。
  • 将交换后的子节点作为新的父节点,重复上述过程。
    【数据结构】非线性结构之树结构(含堆)_第11张图片
//从从后往前的第一个非叶子节点开始向下调整
void AdjustDown(int* arr, int N, int parent)
{
	int child = parent * 2 + 1;
	//这里假设左孩子为小的/大的一个
	while (child < N)
	{
		//假设这里建小堆
		//假设有右孩子并且如果右孩子比左孩子小的
		if (child + 1 < N && arr[child + 1] > arr[child])
		{
			child++;
		}
		//如果孩子比双亲小,则将双亲和孩子交换
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //当孩子比双亲大时,则不需要调整了
		{
			break;
		}
	}
}

2.2.3 堆的构建

a. 堆构建的方法方法
  1. 堆的向上调整算法建堆
  2. 堆的向下调整算法建堆

下面用两张图片来演示这两种构建堆的方法:

堆的向上调整算法建堆:
使用向上调整算法建堆需要从头开始插入元素,必须保证每次插入新元素使用向上调整算法,使得满足堆的条件。
【数据结构】非线性结构之树结构(含堆)_第12张图片


堆的向下调整算法建堆:
向下调整算法的前提是左右子树都符合堆的条件,而下面这颗完全二叉树的左右子树并不满足这一条件,那么左右子树任何满足堆的条件呢?那就需要左右子树的左右子树满足堆的条件,这样一直想下去可以的得出想要对根节点使用向下调整算法,必须让根节点以下的所有子树都满足堆的条件,显然从头开始是不能完成的,那么只能从尾部开始向下调整,而后又发现叶子结点本就符合堆的条件,那么只需要从最后一个节点的父亲节点开始从后往前使用向下调整算法即可完成建堆。

小结:使用向下调整算法建堆,需要从最后一个节点的父亲节点开始从后往前使用向下调整算法即可完成建堆。
【数据结构】非线性结构之树结构(含堆)_第13张图片

// 建堆--向上调整建堆 :注意此方法建堆需要申请和数组一样大的空间,空间复杂度为O(N)
// 这里只是演示一下可以有这种使用方法,不推荐!!!!!!!!
// 若是完成堆排序、TopK等操作不推荐,能够在数组上直接建堆,没有空间的消耗
void HeapCreate(Heap* php, HPDataType* a, int n)
{
	assert(php);
	//堆的初始化
	php->capacity = 0;
	php->size = 0;
	php->arr = 0;
	
	// 此操作是针对堆的,就需要使用这个
	for (int i = 0; i < n; i++)    
	{							   
		HeapPush(php, a[i]);       //HeapPush()函数在下面讲述
	}
	
	//下面两种方法是任何情况都可以使用,只需要修改一些参数
	//而上面的只能在有堆的情况才能使用
	/*// 建堆--向上调整建堆 --O(N*logN) -- log以2为底N的对数
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
	
	// 建堆--向下调整建堆 --O(N)
	for (int i = (n-1-1)/2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}*/
}
b. 建堆的时间复杂度
  1. 向上调整算法建堆的时间复杂度
    【数据结构】非线性结构之树结构(含堆)_第14张图片
  2. 向下调整建堆的时间复杂度
    【数据结构】非线性结构之树结构(含堆)_第15张图片
    小结
    向上调整算法建堆和向下调整算法建堆相比还是稍逊一筹。
    所以这里推荐使用向下调整算法建堆!!

2.2.4 堆的插入

当有新元素插入时,该元素需要与它的父亲进行比较。如果新元素的值大于父节点的值,则需要进行交换。 将新元素看作父节点,重复上述过程。(假设这里建大堆

void HeapPush(Heap* php, HPDataType x)
{
	assert(php);

	if (php->capacity == php->size)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		int* tmp = realloc(php->arr, sizeof(int) * newcapacity);
		if (tmp == NULL)
		{
			perror("malloc");
			return;
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}

	php->arr[php->size] = x;
	php->size++;
	AdjustUp(php->arr, php->size-1);
}

2.2.5 堆的删除

//从从后往前的第一个非叶子节点开始向下调整
void AdjustDown(int* arr, int N, int parent)
{
	int child = parent * 2 + 1;
	//这里假设左孩子为小的/大的一个
	while (child < N)
	{
		//假设这里建小堆
		//假设有右孩子并且如果右孩子比左孩子小的
		if (child + 1 < N && arr[child + 1] > arr[child])
		{
			child++;
		}
		//如果孩子比双亲小,则将双亲和孩子交换
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //当孩子比双亲大时,则不需要调整了
		{
			break;
		}
	}
}
// 堆的删除
void HeapPop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;
	AdjustDown(php->arr, php->size, 0);
}

【数据结构】非线性结构之树结构(含堆)_第16张图片

2.2.6 堆代码的总体实现

  • 头文件Heap.h的实现
#pragma once

#include 
#include 
#include 
#include 

typedef int HPDataType;

typedef struct Heap
{
	int* arr;
	int size;
	int capacity;
}Heap;

// 堆的构建
void HeapCreate(Heap* php, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* php);
// 堆的插入
void HeapPush(Heap* php, HPDataType x);
// 堆的删除
void HeapPop(Heap* php);
// 取堆顶的数据
HPDataType HeapTop(Heap* php);
// 堆的数据个数
int HeapSize(Heap* php);
// 堆的判空
int HeapEmpty(Heap* php);
  • 接口Heap.c的实现
#include "Heap.h"

void AdjustUp(int* arr, int child);

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

// 建堆--向上调整建堆 :注意此方法建堆需要申请和数组一样大的空间,空间复杂度为O(N)
// 这里只是演示一下可以有这种使用方法,不推荐!!!!!!!!
// 若是完成堆排序、TopK等操作不推荐,能够在数组上直接建堆,没有空间的消耗
void HeapCreate(Heap* php, HPDataType* a, int n)
{
	assert(php);
	//堆的初始化
	php->capacity = 0;
	php->size = 0;
	php->arr = 0;
	
	// 此操作是针对堆的,就需要使用这个
	for (int i = 0; i < n; i++)    
	{							   
		HeapPush(php, a[i]);       //HeapPush()函数在下面讲述
	}
	
	//下面两种方法是任何情况都可以使用,只需要修改一些参数
	//而上面的只能在有堆的情况才能使用
	/*// 建堆--向上调整建堆 --O(N*logN) -- log以2为底N的对数
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
	
	// 建堆--向下调整建堆 --O(N)
	for (int i = (n-1-1)/2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}*/
}
	
void HeapDestroy(Heap* php)
{
	assert(php);

	free(php->arr);
}

void AdjustUp(int* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (arr[child] > arr[parent])
		{
			HPDataType tmp = arr[child];
			arr[child] = arr[parent];
			arr[parent] = tmp;
		}

		child = parent;
		parent = (child - 1) / 2;
	}
}

//从从后往前的第一个非叶子节点开始向下调整
void AdjustDown(int* arr, int N, int parent)
{
	int child = parent * 2 + 1;
	//这里假设左孩子为小的/大的一个
	while (child < N)
	{
		//假设这里建小堆
		//假设有右孩子并且如果右孩子比左孩子小的
		if (child + 1 < N && arr[child + 1] > arr[child])
		{
			child++;
		}
		//如果孩子比双亲小,则将双亲和孩子交换
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //当孩子比双亲大时,则不需要调整了
		{
			break;
		}
	}
}

void HeapPush(Heap* php, HPDataType x)
{
	assert(php);

	if (php->capacity == php->size)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		int* tmp = realloc(php->arr, sizeof(int) * newcapacity);
		if (tmp == NULL)
		{
			perror("malloc");
			return;
		}
		php->arr = tmp;
		php->capacity = newcapacity;
	}

	php->arr[php->size] = x;
	php->size++;
	AdjustUp(php->arr, php->size-1);
}


// 堆的删除
void HeapPop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	php->size--;
	AdjustDown(php->arr, php->size, 0);
}

// 取堆顶的数据
HPDataType HeapTop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	return php->arr[0];
}

// 堆的数据个数
int HeapSize(Heap* php)
{
	assert(php);
	
	return php->size;
}

// 堆的判空
int HeapEmpty(Heap* php)
{
	assert(php);

	return php->size == 0;
}

2.3 堆的应用

  1. 堆排序
  2. TopK

2.3.1 堆排序的实现

堆排序实现思想:

  1. 先将数组建成堆(升序建大堆,降序建小堆)
  2. 交换堆顶元素和堆尾元素(数组首尾元素)
  3. 对数组向下调整,使其保持堆的形式
  4. 调整元素个数,使调整过的元素在正确位置,不再改变
  5. 重复 2-4 步,直到调整元素为 1 时结束,排序成功

【数据结构】非线性结构之树结构(含堆)_第17张图片

#include 
#include 
#include 
#include 

typedef int HPDataType;

typedef struct Heap
{
	int* arr;
	int size;
	int capacity;
}Heap;

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整 O(N*logN)
void AdjustUp(int* arr, int child)
{
	//将传入函数中的child向他的双亲进行对比
	int parent = (child - 1) / 2;
	//循环条件为
	while (child > 0)
	{
		//若child比parent大(建大堆) / 小(建小堆) 则进行交换
		if (arr[child] < arr[parent])
		{
			HPDataType tmp = arr[child];
			arr[child] = arr[parent];
			arr[parent] = tmp;
			//child移到parent的位置,继续与他的parent进行对比
			child = parent;
			parent = (child - 1) / 2;
		}
		else //否则则证明该数组已经是堆,不需要再进行调整
		{
			break;
		}
	}
}

//向下调整算法 O(N)
//从从后往前的第一个非叶子节点开始向下调整
void AdjustDown(int* arr, int N, int parent)
{
	int child = parent * 2 + 1;
	//这里假设左孩子为小的/大的一个
	while (child < N)
	{
		//假设这里建大堆
		//假设有右孩子并且如果右孩子比左孩子大(建大堆)/小(建小堆)的
		if (child + 1 < N && arr[child + 1] > arr[child])
		{
			child++;
		}
		//如果孩子比双亲大(建大堆)/小(建小堆),则将双亲和孩子交换
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //当孩子比双亲大时,则不需要调整了
		{
			break;
		}
	}
}


void Print(int* arr, int N)
{
	int i = 0;
	for (i = 0; i < N; i++)
	{
		printf("%d ", arr[i]);
	}
}

void HeapSort(int* arr, int n)
{
	向上调整建堆
	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(arr, i);
	//}

	//向下调整建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}

	int end = n - 1;
	for (int i = 0; i < n; i++)
	{
		//end为数组最后一个元素的下标
		Swap(&arr[0], &arr[end]);
		//end也为该元素之前元素的总个数
		AdjustDown(arr, end, 0);
		end--;
	}
}

int main()
{
	int arr[] = { 3,2,9,7,6,1,8,5,4,0 };
	HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr) / sizeof(arr[0]));
	return 0;
}

2.3.2 TopK的实现

TopK问题有在我已知情况下有两种解决情况:(假设有N个值并且要取k个最值)
当数据量过大的时候,数据只能存储在内存中,而内存中不能建堆,只能使用第二种方法

  1. 建立堆,取堆顶元素,与堆中最后一个元素交换,堆元素个数减一,向下调整堆,再重复前面的内容k-1次

值得注意的是在第一种情况下,若N与k相等,那么TopK就是堆排序。
【数据结构】非线性结构之树结构(含堆)_第18张图片

#include 
#include 
#include 
#include 
#include 

typedef int HPDataType;

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//从从后往前的第一个非叶子节点开始向下调整
void AdjustDown(int* arr, int N, int parent)
{
	int child = parent * 2 + 1;
	//这里假设左孩子为小的/大的一个
	while (child < N)
	{
		//假设这里建小堆
		//假设有右孩子并且如果右孩子比左孩子小的
		if (child + 1 < N && arr[child + 1] < arr[child])
		{
			child++;
		}
		//如果孩子比双亲小,则将双亲和孩子交换
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //当孩子比双亲大时,则不需要调整了
		{
			break;
		}
	}
}

//(1)

void TopK(int* arr,int n , int k)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}

	//定义一个变量end记录堆最后一个元素
	int end = n - 1;
	//取前k个最值
	for (int i = 0; i < k; i++)
	{
		//将堆顶的元素输出
		printf("%d ", arr[0]);
		//将堆顶与堆中最后一个元素交换,并减少堆元素个数
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		end--;
	}
}

int main()
{
	int n = 10000;
	srand((unsigned int)time(0));
	int* arr = (int*)malloc(sizeof(int)*n);
	if (arr == NULL)
	{
		perror("malloc fail");
		return;
	}
	for (int i = 0; i < n; i++)
	{
		arr[i] = rand() % 1000000 + 5;
	}

	//最大
	/*arr[100] = 1000001;
	arr[500] = 1000002;
	arr[1000] = 1000003;
	arr[5555] = 1000004;
	arr[9999] = 1000005;*/

	//最小
	arr[100] = 1;
	arr[500] = 2;
	arr[1000] = 3;
	arr[5555] = 4;
	arr[9999] = 5;

	TopK(arr, n , 5);
	return 0;
}

  1. 建立一个 k 个元素个数的堆,(取最大k个数)建小堆 /(取最小k个数)建大堆 ,
    将剩余的N - k个值依此与栈顶的数对比,若符号条件,则取代栈顶的元素
    并向下调整让其保持堆的性质,重复以下内容,直至数组遍历完

【数据结构】非线性结构之树结构(含堆)_第19张图片

#include 
#include 
#include 
#include 
#include 

typedef int HPDataType;

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//从从后往前的第一个非叶子节点开始向下调整
void AdjustDown(int* arr, int N, int parent)
{
	int child = parent * 2 + 1;
	//这里假设左孩子为小的/大的一个
	while (child < N)
	{
		//假设这里建小堆
		//假设有右孩子并且如果右孩子比左孩子小的
		if (child + 1 < N && arr[child + 1] < arr[child])
		{
			child++;
		}
		//如果孩子比双亲小,则将双亲和孩子交换
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //当孩子比双亲大时,则不需要调整了
		{
			break;
		}
	}
}


//当数据量过大的时候,数据只能存储在内存中,而内存中不能建堆,只能使用第二种方法

void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand((unsigned int)time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

Print(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
}

void PrintTopK(int k)
{
	//先打开文件
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
	//建立一个元素个数为k的堆
	int* arr = (int*)malloc(sizeof(int) * k);
	if (arr == NULL)
	{
		perror("malloc error");
		return;
	}
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &arr[i]);
	}

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

	//遍历剩余的元素比较并进行
	int val = 0;
	while (!feof(fout))
	{
		fscanf(fout, "%d", &val);
		if (val > arr[0])
		{
			Swap(&val, &arr[0]);
			AdjustDown(arr,k, 0);
		}
	}

	Print(arr , k);
}

int main()
{
	//CreateNDate();
	PrintTopK(10);
	return 0;
}

3. 二叉树的链式存储

二叉树中每一个节点包括:

  1. 需要存储的数据、
  2. 左孩子节点的地址
  3. 右孩子节点的地址
  • 关于二叉树的接口:Btree.h
#pragma once

#include 
#include 
#include 
#include 

typedef char BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);
  • 关于二叉树接口函数的实现:Btree.c

3.1 二叉树的构建

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BuyTreeNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->left = NULL;
	newnode->right = NULL;
	newnode->data = x;

	return newnode;
}

BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	//当a == '#'时说明当前节点为NULL
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}

	BTNode* root = BuyTreeNode(a[*pi]);
	(*pi)++;
	root->left = BinaryTreeCreate(a, n , pi);
	root->right = BinaryTreeCreate(a, n, pi);

	return root;
}

3.2 二叉树的前序遍历

// 二叉树前序遍历 : 根   左子树   右子树
void BinaryTreePrevOrder(BTNode* root)
{
	//当该节点为NULL时,打印N
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	printf("%c ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

3.3 二叉树的中序遍历

// 二叉树中序遍历: 左子树   根   右子树
void BinaryTreeInOrder(BTNode* root)
{
	//当该节点为NULL时,打印N
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	BinaryTreeInOrder(root->left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->right);
}

3.4 二叉树的后序遍历

// 二叉树后序遍历: 左子树   右子树    根
void BinaryTreePostOrder(BTNode* root)
{
	//当该节点为NULL时,打印N
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%c ", root->data);
}

3.5 二叉树的节点个数

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	//如果该root为NULL,则root不是节点
	if (root == NULL)
		return 0;
	//如果root不为空,则返回他的左子树节点+右子树的节点+1 (1为他本身)
	return BinaryTreeSize(root->left) 
	+ BinaryTreeSize(root->right) + 1;
}

3.6 二叉树的叶子节点个数

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	//如果该root为NULL,则root不是节点
	if (root == NULL)
		return 0;
	//当root的左子树和右子树都为NULL时,则说明root是叶子节点
	if (root->left == NULL && root->right == NULL)
		return 1;
	//返回root左子树和右子树上的叶子节点个数
	return BinaryTreeLeafSize(root->left) 
		+ BinaryTreeLeafSize(root->right);
}

3.7 二叉树的第k层节点个数

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	//k不能为0
	assert(k > 0);

	//如果该root为NULL,则root不是节点
	if (root == NULL)
		return 0;
	//当k == 1是,说明到了相对于根的第k层
	if (k == 1)
		return 1;

	return BinaryTreeLevelKSize(root->left, k - 1) 
		+ BinaryTreeLevelKSize(root->right, k - 1);
}

3.8 二叉树中寻找值为x的节点

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	//如果该root为NULL,则root不是节点
	if (root == NULL)
		return NULL;

	//当找到x后返回root
	if (root->data == x)
		return root;

	BTNode* ret1 = BinaryTreeFind(root->left, x);
	if (ret1)  //当左子树返回的地址不为空,说明已经找到了x,返回ret1
	{
		return ret1;
	}
	BTNode* ret2 = BinaryTreeFind(root->right, x);
	if (ret2)  //当右子树返回的地址不为空,说明已经找到了x,返回ret1
	{
		return ret2;
	}

	//当前面左子树和右子树都没返回,说明没有找到,返回NULL
	return NULL;
}

3.9 二叉树的层序遍历

  • 队列的接口
#pragma once

#include 
#include 
#include 

typedef struct BinaryTreeNode* QDataType;
// 链式结构:表示队列 

typedef struct QListNode
{
	struct QListNode* next;
	QDataType data;
}QNode;

// 队列的结构 
typedef struct Queue
{
	QNode* front;
	QNode* rear;  //指向队列最后一个元素的后面
	int size;
}Queue;

// 初始化队列 
void QueueInit(Queue* q);
// 队尾入队列 
void QueuePush(Queue* q, QDataType data);
// 队头出队列 
void QueuePop(Queue* q);
// 获取队列头部元素 
QDataType QueueFront(Queue* q);
// 获取队列队尾元素 
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q);
// 销毁队列 
void QueueDestroy(Queue* q);
  • 关于队列接口函数的实现:Queue.c
#include "Queue.h"

// 初始化队列 
void QueueInit(Queue* q)
{
	q->front = NULL;
	q->rear = NULL;
	q->size = 0;
}

// 队尾入队列 
void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->next = NULL;
	newnode->data = data;

	if (q->front == NULL)      //分队列是否有元素两种情况
	{
		assert(q->rear == NULL);
		q->front = newnode;
		q->rear = newnode;
	}
	else
	{
		q->rear->next = newnode;
		q->rear = newnode;
	}

	q->size++;//入队列,队列长度加一
}

// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q)
{
	assert(q);
	return q->front == NULL && q->rear == NULL;
}

// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	//出队列时,队列不能为空
	assert(!QueueEmpty(q));

	//当队列中只有一个元素的时候,不仅仅头指针需要改变,尾指针也需要改变
	//因为当删除最后一个元素时,首指针释放当前节点,并向后移动,而尾指针并没有移动
	//当释放后若在插入元素时,尾指针会造成野指针的情况
	if (q->front->next == NULL)
	{
		QNode* del = q->front;
		q->front = NULL;
		q->rear = NULL;
		free(del);
	}
	else
	{
		QNode* del = q->front;
		q->front = q->front->next;
		free(del);
	}
	q->size--;
}


// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	//获取队列头部元素时,队列不能为空
	assert(!QueueEmpty(q));

	return q->front->data;
}


// 获取队列队尾元素 
QDataType QueueBack(Queue* q)
{
	assert(q);
	//获取队列头部元素时,队列不能为空
	assert(!QueueEmpty(q));

	return q->rear->data;
}


// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);

	//结构体中定义了一个size
	//而这里遍历链表得到个数,效率低
	/*int size = 0;
	QNode* cur = q->front;
	while (cur != q->rear)
	{
		size++;
		q->front = q->front->next;
	}*/
	return q->size;
}

// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);
	QNode* cur = q->front;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue qu;
	QueueInit(&qu);
	QueuePush(&qu, root);
	while (!QueueEmpty(&qu))
	{
		BTNode* front = QueueFront(&qu);
		printf("%c ", front->data);
		QueuePop(&qu);
		if(front -> left != NULL)
			QueuePush(&qu, front->left);
		if (front->right != NULL)
			QueuePush(&qu, front->right);
	}
}

3.10 判断是否为完全二叉树

// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
	if (root == NULL)
		return true;
	//当一个节点的左子树为NULL,右子树不为NULL,那么这棵树不是完全二叉树
	if (root->left == NULL && root->right != NULL)
		return false;

	return BinaryTreeComplete(root->left)
		&& BinaryTreeComplete(root->right);
}

结尾

二叉树作为一个新知识相比较于前面的知识点会有点点陌生,里面涉及的递归也会是新手相对于薄弱的地方,这边建议对于二叉树递归知识点反复观看,二叉树递归题目先自己思考,实在想不出来的可以参考一下解决思路,这也是学习的过程,对于二叉树中递归问题最终方法就是画递归展开图。

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,请大家给一个三连支持一下!!
在这里插入图片描述

你可能感兴趣的:(数据结构,链表,算法,c语言)