数据结构 二叉树

Posted Suk_god

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构 二叉树相关的知识,希望对你有一定的参考价值。

概念及结构

二叉树基本概念

二叉树是结点的一个有限集合,该集合:
1.为空
2.由一个根节点和它的左子树与右子树构成

由上图可以得知:
1.二叉树不存在度大于2的结点
2.二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

特殊的二叉树

满二叉树

1.是一棵空树
2.非空,则该二叉树的每一层节点数达到该层若能容纳结点的最大值
也即:层数为k时,总结点数为2^k -1 个结点

完全二叉树

完全二叉树是由满二叉树引出的~~

1.是一棵空树
2.若不空,对于层数为k的二叉树来说:
k-1 层是一个 满二叉树
k从左到右依次按序排列剩余节点数

二叉树的性质

1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点
2.若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h -1 个
3.对任何一棵二叉树,若 度为 0 的结点个数 为 n0, 度为 2 的结点个数 为 n2, 则有关系 n0 = n2+1成立

对于3的证明如下:

4.若规定根节点的层数为1,具有n个结点的满二叉树的深度为h = log(n+1) (表示log以2为底n+1的对数)
5.对于具有n个结点的完全二叉树,如果按照从上至下,从左至右的数组顺序对所有结点从0开始编号,则对于序号为 i 的结点有:

1.若 i > 0, i 位置结点的双亲序号:(i-1)/ 2; i = 0, i 为根节点编号,无双亲结点
2.若2i+1 < n,左孩子序号:2i+1 否则无左孩子
3.若2i+2 < n,右孩子序号:2i+2 否则无右孩子

对于5的简单说明

二叉树的存储结构

对于二叉树来说,一般有两种存储结构

顺序存储

数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中只有堆才会使用数组来存储。
二叉树顺序存储在物理上是一个数组,在逻辑上是一棵二叉树

完全二叉树的顺序存储

非完全二叉树的顺序存储

链式存储

用链表来表示一棵二叉树
链表中的每个结点由3部分组成,数据域和左右指针域,其中左右指针域分别指向该结点的左右孩子所在结点,目前采用二叉链来存储。

对应的数据结构类型为

typedef char BTDataType;

typedef struct BinaryTree

	BTDataType data;//数据域
	struct BinaryTree* left;//指向当前结点的左孩子
	struct BinaryTree* right;//指向当前结点的右孩子
BTNode;

二叉树的顺序结构及实现

前面提到,顺序存储对于完全二叉树比较实用,顺序存储主要是用来对堆进行存储。下面介绍堆的相关概念

堆的概念及结构

概念:有一组数据将其按照完全二叉树的顺序存储方式存储在数组中。这一组数据需要满足以下条件:
该二叉树的父节点始终大于它的孩子结点,称它为大堆

该二叉树的父节点始终小于于它的孩子结点,称它为小堆

堆的性质

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

堆的实现

问题引入(以小堆为例)


如上图所示:红色部分都已经符合小堆的特性,那么我们应该如何调整绿色部分的结点,使整个二叉树是一个小堆呢?

我们只需要进行以下步骤:
1.找到该结点左右孩子中较小的结点
2.交换这两个结点
3.对已交换的部分进行同样的操作(1.2)
4.直到该结点没有孩子结点时停止

那么对于上述情况的操作步骤如下图所示:

调整算法

针对于上述情况的执行过程,大佬们总结出一个算法来实现这样的功能,那就是向下调整算法

前提:左右子树必须是一个堆时,才能调整

所以执行步骤为:
1.找到倒数第一个非叶子结点,从该结点开始调整
2.找到该结点左右孩子中较小的结点
3.交换这两个结点
4.对已交换的部分进行同样的操作(2.3)
5.直到该结点没有孩子结点时表示一轮调整完成
6.紧接着调整前一个非叶子结点,直到调整到根节点停止
7.此时构建的堆为小堆

若要构建大堆,只需要每次交换时将上述的较小换为较大即可

堆的创建

对于给定的一个任意数据的数组,起初它是不符合堆的特性的。
由于根节点的左右子树并不是堆,所以我们从倒数第一个非叶子结点开始调整,一直调整到根节点,就可以调整为堆

e.g int a[] = 1,5,3,8,7,6;
调整成一个大堆
对于该数组的调整过程如下图所示:

在每一次调换元素的时候,有可能破坏其子堆的结构,因此在每一次发生元素交换的时候,都需要递归调用重新构造堆的结构。

建堆的时间复杂度

由于堆是一个完全二叉树,而满二叉树也是一种特殊的完全二叉树,因此使用满二叉树来求解时间复杂度(时间复杂度本里就是一个近似值,多几个结点不影响最终结果)
设树的高度为h
第1层, 2^0个结点,需要向下移动h-1层
第2层, 2^1个结点,需要向下移动h-2层
第3层, 2^2个结点,需要向下移动h-3层
第4层, 2^3个结点,需要向下移动h-4层

第h-1层, 2^(h-2)个结点,需要向下移动1层

因此需要移动结点总的移动步数为:

堆的插入

将要插入的数据放在数组的末尾,再进行向上调整算法直到满足堆为止
e.g 向建堆时的例子中插入数据100
则新数组为8,7,6,5,1,3,100
对应的调整过程见下图

堆的删除

删除堆是删除堆顶的数据,将堆顶的数据与最后一个数据交换,然后删除最后一个元素,再进行向下调整算法
e.g 删除刚刚建好的大堆中的堆顶元素100

堆的代码实现

heap.h
申明了一些堆的基本操作

#pragma once

#include <stdio.h>
#include <windows.h>


typedef int HPDataType;

//定义一个返回值为int且有两个int型参数的函数指针类型
typedef int(*Com) (HPDataType, HPDataType);

typedef struct Heap

	HPDataType* arr;
	int size;
	int capacity;
	Com Compare;
Heap;

// 堆的构建
extern void HeapCreat(Heap* hp, HPDataType* a, int n,Com com);
// 堆的销毁
extern void HeapDestory(Heap* hp);

// 取堆顶的数据
extern HPDataType HeapTop(Heap* hp);
//堆的数据个数
extern int HeapsSize(Heap* hp);
// 堆的判空
extern int HeapEmpty(Heap* hp);
// 堆的插入
extern void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
extern void HeapPop(Heap* hp);

extern void HeapTest();

heap.c

#include "heap.h"
#include <malloc.h>
#include <assert.h>

static void Swap(HPDataType* xp, HPDataType* yp)

	HPDataType temp = *xp;
	*xp = *yp;
	*yp = temp;


static void AdjustDown(Heap* hp, int parent)

	assert(hp);
	//1.child标记左右孩子较小的,默认父节点的左孩子为左右孩子中较小的一个
	int child = 2 * parent + 1;
	while (child < hp->size)
	
		//2.确定实际较小的孩子结点
		if (child + 1 < hp->size && hp->Compare(hp->arr[child+1],hp->arr[child]))
		
			child += 1;
		
		//3.将较小的孩子结点与父节点比较,判断是否需要交换
		if (hp->Compare(hp->arr[child] , hp->arr[parent]))
		
			Swap(&hp->arr[child], &hp->arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		
		else
		
			return;
		
	



// 堆的构建
void HeapCreat(Heap* hp, HPDataType* a, int n,Com com)

	assert(a);
	assert(hp);

	//1.开辟空间
	hp->arr = (HPDataType*)malloc(sizeof(HPDataType)*n);
	if (NULL == hp->arr)
	
		assert(0);
		return;
	
	//2.空间开辟成功,对新空间初始化
	for (int i = 0; i < n; i++)
	
		hp->arr[i] = a[i];
	
	hp->capacity = n;
	hp->size = n;
	hp->Compare = com;
	//3.使用向下调整方法进行堆的构建
	int parent = (n - 2) / 2;
	while (parent >= 0)
	
		AdjustDown(hp, parent);
		parent--;
	



// 堆的销毁
void HeapDestory(Heap* hp)

	assert(hp);
	if (hp->arr)
	
		free(hp->arr);
		hp->arr = NULL;
		hp->capacity = hp->size = 0;
	


// 堆的判空  返回值:1  空的   0 不空
int HeapEmpty(Heap* hp)

	assert(hp);
	return hp->size == 0? 1:0;


// 取堆顶的数据
HPDataType HeapTop(Heap* hp)

	assert(hp);
	if (!HeapEmpty(hp))
	
		return hp->arr[0];
	
	else
	
		printf("堆为空!返回值为-1\\n");
	
	return -1;


//或取堆的元素个数
int HeapsSize(Heap* hp)

	assert(hp);
	return hp->size;


// 堆的删除
void HeapPop(Heap* hp)

	assert(hp);
	//1.将堆顶元素与堆的最后一个元素交换
	Swap(&hp->arr[0], &hp->arr[hp->size - 1]);
	//2.对hp的size-1,控制有效元素个数
	hp->size--;
	if (hp->size == 0)
	
		free(hp->arr);
		hp->arr = NULL;
		hp->capacity = 0;
	
	//3.对新的堆顶元素进行向下调整操作
	AdjustDown(hp,0);


//扩容函数
static void HeapExpand(Heap* hp)

	int newCapacity = hp->capacity * 2;
	//1.开辟新空间
	HPDataType *tmp = (HPDataType*)malloc(sizeof(HPDataType)*newCapacity);
	if (NULL == tmp)
	
		assert(0);
		return;
	
	//2.将旧空间的内容拷贝至新空间
	memcpy(tmp, hp->arr, sizeof(HPDataType)*hp->capacity);
	//3.释放旧空间
	free(hp->arr);
	hp->arr = tmp;
	hp->capacity = newCapacity;


//向上调整
static void AdjustUp(Heap* hp)

	int child = hp->size - 1;
	int parent = (child - 1) / 2;
	while (child > 0)
	
		if (hp->Compare(hp->arr[child] , hp->arr[parent]))
		
			Swap(&hp->arr[parent], &hp->arr[child]);
			child = parent;
			parent = (child - 1) / 2;
		
		else
		
			return;
		
	


// 堆的插入
void HeapPush(Heap* hp, HPDataType x)

	assert(hp);
	//1.判断能否进行插入
	if (hp->size == hp->capacity)
	
		//不能插入,先扩容
		HeapExpand(hp);
	
	//2.执行插入操作
	hp->arr[hp->size] = x;
	hp->size++;
	AdjustUp(hp);


/

static int Less(HPDataType left,HPDataType right)

	return left < right;


static int Greater(HPDataType left, HPDataType right)

	return left > right;



void HeapTest()

	Heap hp;
	HPDataType array[] =  16, 72, 31, 23, 94, 53 ;

	printf("初始化堆\\n");
	HeapCreat(&hp, array, sizeof(array) / sizeof(array[0]), Greater);
	printf("堆顶元素:%d\\n", HeapTop(&hp));
	printf("堆的有效元素个数:%d\\n",HeapsSize(&hp));
	

	printf("插入新元素后\\n");
	HeapPush(&hp, 1000);
	printf("堆顶元素:%d\\n", HeapTop(&hp));
	printf("堆的有效元素个数:%d\\n", HeapsSize(&hp));

	printf("删除堆顶元素后\\n");
	HeapPop(&hp);
	printf("堆顶元素:%d\\n", HeapTop(&hp));
	printf("堆的有效元素个数:%d\\n", HeapsSize(&hp));

	HeapDestory(&hp);




main.c

#include "heap.h"

int main()

	HeapTest();
	system("pause");
	return 0;



堆的相关操作见源码 堆!

堆的应用

堆排序

顾名思义:就是利用堆的思想来对数据进行排序
主要分成两个步骤:
1.建堆
升序:建大堆
降序:建小堆
2.利用堆删除的思想来进行排序
利用向下调整算法实现

e.g 对数组8,7,6,5,1,3进行升序排列
首先,升序需要建立大堆

其次:对该对结构进行删除的操作
主要过程如下

实现代码

#include "heap.h"

static void Swap(int* xp,int* yp)

	int temp = *xp;
	*xp = *yp;
	*yp = temp;

static void AdjustHeap(int* array, int parent, int size)

	//使用child来标记parent的较小孩子结点
	//由于堆是一个完全二叉树,所以对于任意一个父节点来说,
	//它一定会有左孩子,但不一定有右孩子,
	//所以先让child标记它的左孩子
	int child = parent * 2 + 1;
	//左孩子存在
	if (child < size)
	
		//右孩子存在,并且右孩子小于左孩子
		if (child + 1 < size && array[child] > array[child + 1])
		
			//child重新标记右孩子
			child += 1;
		
		if (array[parent]>array[child])
		
			Swap(&array[parent], &array[child]);
		
		else
		
			return;
		
	


void  HeapSort(int *array, int size)

	//1.找到第一个非叶节点
	int parent = (size - 1 - 1) / 2;
	//2.从该结点开始,采用向下调整的方式进行排列
	//3.直到数组的第一个元素被排完-后结束
	while (parent >= 0)
	
		AdjustHeap(array, parent, size);
		parent--;
	


void testHeap()

	int array[] =  16, 72, 31, 23, 94, 53 ;
	int size = sizeof(array) / sizeof(array[0]);
	printf("Before:\\n");
	for (int i = 0; i < size; i++)
	
		printf("%d ",array[i]);
	
	printf("\\n");

	HeapSort(array, size);

	printf("After:\\n");
	for (int i = 0; i < size; i++)
	
		printf("%d ", array[i]);
	
	printf("\\n");

源码见 堆排序

Top-K问题

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

以上是关于数据结构 二叉树的主要内容,如果未能解决你的问题,请参考以下文章

Java集合与数据结构 二叉树

Java集合与数据结构 二叉树

数据结构《四》二叉树的实现

数据结构《四》二叉树的实现

数据结构与算法-二叉树(斜二叉树满二叉树完全二叉树线索二叉树)

数据结构二叉树的基本操作~~~~