初识二叉树以及堆的简单实现

Posted 派小星233

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识二叉树以及堆的简单实现相关的知识,希望对你有一定的参考价值。

目录

一:什么是树

【1】树的概念

【2】树的另外几个重要概念

【3】树的几种表示方法

二:什么是二叉树

【1】概念以及特点

【2】两种特殊的二叉树

【3】二叉树的性质

【4】二叉树的两种存储方式

三:堆的实现


一:什么是树

【1】树的概念

我们前面所学的顺序表,链表都属于线性结构,而树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的

图解:

(1) 树有一个特殊的节点,叫根节点,在图中为结点A

(2)除根节点外,其余结点被分成M(M>0)互不相交的集合T1T2……Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以 有0个或多个后继。

(3)父子结点:例如A是B,C,D的父节点,而E,F是B的子节点

(4)叶结点:没有子节点的结点,例如E,F,C,G,H

注意:树的子树是不相交的,除了根结点以外别的结点有且只有一个父节点。

【2】树的另外几个重要概念

图解:

(1)节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6 (2)非终端节点或分支节点:度不为0的节点; 如上图:DEFG...等节点为分支节点 (3)兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:BC是兄弟节点 (4)树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6 (5)节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推; (6)树的高度或深度:树中节点的最大层次; 如上图:树的高度为4 (7)节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先 (8)子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙 (9)森林:由m(m>0)互不相交的多颗树的集合称为森林。

【3】树的几种表示方法

(1)利用顺序表存子节点的地址(结构复杂)

(2) 已经说明了树的度(最大结点的度),设置一个指针数组来存储子节点的地址

(3)结构体数组存储 

 (4)左孩子右兄弟表示法(比较常用,结构也比较简单,逻辑相对清晰)

二:什么是二叉树

【1】概念以及特点

概念:

一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成

特点:

1. 每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
2. 二叉树的子树有左右之分,其子树的次序不能颠倒。

【2】两种特殊的二叉树

1. 满二叉树:

所有叶子节点都在最后一层

所有分支节点都有两个孩子

2. 完全二叉树:

假设这个二叉树有N层,前N-1层必须满

最后一层可以不满,但是必须左向右连续

【3】二叉树的性质

1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0, 度为2的分支结点个数为 n2,则有n0=n2+1。
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=LogN。

【4】二叉树的两种存储方式

(1)顺序结构存储(数组)

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

(2)链式结构存储

链表来表示一棵二叉树,即用链来指示元素的逻辑关系。

通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。

链式结构又分为二叉链和三叉链,目前一般都是二叉链,红黑树等高阶数据结构会用到三叉链。

三:堆的实现

堆是用数组实现的二叉树,并且一般用来实现完全二叉树。

堆分为大堆和小堆

大堆:父亲>=孩子

小堆:孩子>=父亲

本文实现的是大堆

堆的实现和顺序表类似,这里就不展开细讲,只讲主要的接口实现。

附上顺序表链接:https://blog.csdn.net/2301_76269963/article/details/129352041?spm=1001.2014.3001.5501

插入数据

(1)判断扩容,和顺序表一致。

(2)存储数据,和顺序表一致。

(3)在进行数据插入后我们要保证插入以后还是大堆,就必须进行父子关系的调整。

在考虑调整之前,我们观察一下父亲下标和孩子下标的关系 

我们可以发现这样一个规律

父亲下标=(孩子下标-1)/2。 

我们根据这个规律来设计调整函数,如果要插入的数据大于父亲,就调换两者一直到小于父亲或者成为了根部节点

代码:

 

全部代码:

Heap.h(必要头文件的包含,函数和结构体声明)

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int HPDataType;
typedef struct Heap

	//存储数据
	HPDataType* a;
	//有效数据个数
	int size;
	//容量
	int capacity;
HP;
//初始化
void HeapInit(HP* hp);
//调整函数
void AdjustUp(HPDataType* a,int child);
//插入数据
void HeapPush(HP* hp, HPDataType x);
//打印数据
void HeapPrint(HP* hp);
//删除数据
void HeapPop(HP* hp);

Heap.c(接口实现)

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//初始化
void HeapInit(HP* hp)

	//断言,不能传空的结构体指针
	assert(hp);
	hp->a = NULL;
	//初始化size和容量都为0
	hp->size = hp->capacity = 0;


//调整函数
void AdjustUp(HPDataType* a, int child)

	//断言,不能传空指针
	assert(a);
	//找到父结点的下标
	int parent = (child - 1) / 2;
	//循环,以child到树根为结束条件
	while (child > 0)
	
		//如果父结点比child下,交换并更新
		if (a[child] > a[parent])
		
			HPDataType tmp = a[child];
			a[child] = a[parent];
			a[parent] = tmp;
			child = parent;
			parent = (child - 1) / 2;
		
		//如果父结点比child大,跳出循环
		else
		
			break;
		
	


//插入数据
void HeapPush(HP* hp, HPDataType x)

	if (hp->size == hp->capacity)
	
		//判断扩容多少
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		//扩容
		HPDataType* tmp =
			(HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
		//更新
		hp->capacity = newcapacity;
		hp->a = tmp;
	
	//存储数据
	hp->a[hp->size] = x;
	hp->size++;
	//进行调整
	AdjustUp(hp->a, hp->size-1);


//打印数据
void HeapPrint(HP* hp)

	//断言,不能传空的结构体指针
	assert(hp);
	int i = 0;
	for (i = 0; i < hp->size; i++)
	
		printf("%d ", hp->a[i]);
	
	printf("\\n");


//删除数据
void HeapPop(HP* hp)

	//断言,不能传空的结构体指针
	assert(hp);
	//如果为空,不能删除,避免数组越界
	if (hp->size == 0)
		return ;
	//不为空,直接将size-1就行
	hp->size--;

text.c(测试)

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"

void text1()

	HP hp;
	HeapInit(&hp);
	HPDataType a[] =  70,30,56,25,15,10,75,100 ;
	int i = 0;
	for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	
		HeapPush(&hp, a[i]);
	
	HeapPrint(&hp);


int main()

	text1();

数据结构C语言 《四》二叉树,堆的基本概念以及堆的相关操作实现(上)


在这里插入图片描述

1.树

1.1树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树也可以这样定义:树是由根节点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的节点,所定义的关系称为父子关系。父子关系在树的节点之间建立了一个层次结构。在这种层次结构中有一个节点具有特殊的地位,这个节点称为该树的根节点,或称为树根。

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

空集合也是树,称为空树。空树中没有节点;
孩子节点或子节点:一个节点含有的子树的根节点
节点的度:一个节点含有的子节点的个数
叶节点或终端节点:度为0的节点
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:最大的节点的度
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙;
森林:由m(m>0)棵互不相交的树的集合称为森林;

1.2树的表示

  1. 孩子兄弟表示法
  2. 双亲表示法
  3. 孩子表示法
  4. 孩子兄弟表示法
    在这里插入图片描述

2.二叉树

2.1二叉树的概念

二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。
二叉树的特点

  1. 每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
  2. 二叉树的子树有左右之分,其子树的次序不能颠倒。

特殊的二叉树:

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

2.2二叉树的性质

  • 性质1:二叉树的第i层上至多有2i-1(i≥1)个节点 。
  • 性质2:深度为h的二叉树中至多含有2h-1个节点 。
  • 性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1 。
  • 性质4:具有n个节点的完全二叉树深为log2x+1(其中x表示不大于n的最大整数。
  • 性质5:若对一棵有n个节点的完全二叉树进行顺序编号(1≤i≤n),那么,对于编号为i(i≥1)的节点:

当i=1时,该节点为根,它无双亲节点 。
当i>1时,该节点的双亲节点的编号为i/2 。
若2i≤n,则有编号为2i的左节点,否则没有左节点 。
若2i+1≤n,则有编号为2i+1的右节点,否则没有右节点

2.3二叉树的存储方式

  • 顺序存储:顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
  • 链式存储:二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。
    在这里插入图片描述

2.4二叉树的遍历

前序/中序/后序的递归结构遍历:是根据访问结点操作发生位置命名

  1. NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
  2. LNR:中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
  3. LRN:后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

简而言之

方式顺序
前序遍历根、左、右
中序遍历左、根、右
后序遍历左、右、根

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

3.堆

3.1堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
在这里插入图片描述

3.2堆的实现

Heap.h

#pragma once

typedef int DataType;

//定义一个函数指针类型,用来选择大小堆实现方式
typedef int(*PCompare)(DataType left, DataType right);

typedef struct Heap
{
	DataType *arr;
	int capacity;
	int size;
	PCompare PCOM;	//函数指针变量,来指向所有比较函数
}Heap;

//小堆方式
int Less(DataType child, DataType parent);
//大堆方式
int Greater(DataType child, DataType parent);

//堆的初始化
void HeapInit(Heap *hp, DataType *arr, int size,PCompare PCOM);

//堆的插入
void HeapInsert(Heap *hp, DataType x);

//堆的删除
void HeapDelete(Heap *hp);

//获取堆顶元素
DataType HeapTop(Heap *hp);

//获取堆中有效元素个数
int HeapSize(Heap *hp);

//判空
int HeapEmpty(Heap *hp);

//堆的销毁
void HeapDestroy(Heap * hp);

//堆排序
void HeapSort(int arr[],int size);

//TOP-K问题
void PrintTopK(int* a, int n, int k);

void TestTopk();

void TestHeap();

Heap.c

#include"Heap.h"
#include<stdio.h>
#include<assert.h>
#include<malloc.h>

//小堆方式
int Less(DataType child, DataType parent)
{
	return child < parent;
}

//大堆方式
int Greater(DataType child, DataType parent)
{
	return child > parent;
}


//交换
void Swap(DataType *left, DataType *right)
{
	DataType tmp = *left;
	*left = *right;
	*right = tmp;
}

//向下调整(大堆)
void AdjustDown(Heap *hp, int parent)
{
	DataType *arr = hp->arr;
	int size = hp->size;
	// 默认child标记左孩子,parent的右孩子可能不存在
	int child = parent * 2 + 1;
	while (child<size)
	{
		//在右孩子存在的情况下,找到左右孩子中最小的
		if (child + 1 < size && hp->PCOM(arr[child + 1] , arr[child]))
		{
			child += 1;
		}
		//检测双亲此时是否满足堆的特性
		if (hp->PCOM(arr[child] , arr[parent]))
		{
			Swap(&arr[child], &arr[parent]);

			//较大的双亲向下移动,可能会导致其子树不满足堆的特性,则还需要继续向下调整
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			return;
		}
	}
}

//向上调整(小堆)
void AdjustUp(Heap *hp)
{
	DataType *arr = hp->arr;
	int child = hp->size - 1;
	int parent = (child - 1) / 2;
	while (child)
	{
		if (hp->PCOM(arr[child] , arr[parent]))
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			return;
		}
	}
}

//堆的初始化
void HeapInit(Heap *hp, DataType *arr, int size, PCompare PCOM)
{
	assert(hp);
	hp->arr = (DataType *)malloc(sizeof(DataType)*size);
	if (hp->arr == NULL)
	{
		assert(0);
		return;
	}
	hp->capacity = size;
	//将元素逐个拷进来
	for (int i = 0; i < size; i++)
	{
		hp->arr[i] = arr[i];
	}
	hp->size = size;
	hp->PCOM = PCOM;
	//1.找倒数第一个非叶子结点
	int LastNotLeafNode = ((size - 1) - 1) / 2;
	//2.从该节点开始一直到根结点,逐个往前对每个节点进行向下调整
	for (int root = LastNotLeafNode; root >= 0; root--)
	{
		AdjustDown(hp,root);
	}
}

//扩容
void CheckCapacity(Heap *hp)
{
	if (hp->size == hp->capacity)
	{
		hp->arr = (DataType *)realloc(hp->arr, sizeof(DataType)*hp->size * 2);
		if (hp->arr == NULL)
		{
			assert(0);
			return ;
		}
		hp->capacity *= 2;
	}
}
 
//堆的插入
void HeapInsert(Heap *hp, DataType x)
{
	CheckCapacity(hp);
	//1.将元素先插入有效元素之后
	hp->arr[hp->size++] = x;
	//2.新元素插入后可能会破坏堆的特性,则还需要对堆进行调整
	AdjustUp(hp);

}

//堆的删除
void HeapDelete(Heap *hp)
{
	if(HeapEmpty(hp))
		return;
	//将堆顶元素与堆中最后一个元素交换
	Swap(&hp->arr[0], &hp->arr[hp->size - 1]);
	//将堆中有效元素个数减一
	hp->size -= 1;
	//将堆顶元素向下调整
	AdjustDown(hp, 0);
}

//获取堆顶元素
DataType HeapTop(Heap *hp)
{
	assert(!HeapEmpty(hp));
	return hp->arr[0];
}

//获取堆中有效元素个数
int HeapSize(Heap *hp)
{
	assert(hp); 
	return hp->size;
}

//判空
int HeapEmpty(Heap *hp)
{
	assert(hp);
	return hp->size == 0;
}

//堆的销毁
void HeapDestroy(Heap * hp)
{
	assert(hp);
	if (hp->arr)
	{
		free(hp->arr);
		hp->arr = NULL;
		hp->capacity = 0;
		hp->size = 0;
	}
}

//堆排序的向下调整
void HeapAdjust(int arr[], int size, int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size &&arr[child + 1] > arr[child])
		{
			child += 1;
		}
		if (arr[child]>arr[parent])
		{
			int tmp = arr[child];
			arr[child] = arr[parent];
			arr[parent] = tmp;
			
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			return;
		}
	}
}

//堆排序
void HeapSort(int arr[],int size)
{
	//1.建堆  大堆升序,小堆降序
	int end = size - 1;
	for (int root = (size-2) / 2; root >= 0; root--)
	{
		HeapAdjust(arr, size, root);
	}
	//2.利用堆删除的思想进行排序
	while (end>0)
	{
		Swap(&arr[end], &arr[0]);
		HeapAdjust(arr, end, 0);
		end--;
	}
}

//TOP-K问题,利用堆来处理,时间复杂度O(N)
//找最大的元素就建小堆,使用前k个元素来建堆,剩下N-k个元素依次与堆顶元素比较
//如果该元素比堆顶元素大,则用该元素替换掉堆顶元素
//1.找最大的K个元素,同理也可以找最小的k个元素,这里就不一一演示了
void PrintTopK(int* a, int n, int k)
{
	Heap hp;
	HeapInit(&hp, a, k,Less);
	for (int i = k; i < n; i++)
	{
		//每次和堆顶元素比较,大于堆顶元素,则删除堆顶元素,插入新的元素
		if (a[i]>HeapTop(&hp))
		{
			HeapDelete(&hp);
			HeapInsert(&hp, a[i]);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", HeapTop(&hp));
		HeapDelete(&hp);
	}
	printf("\\n");
	HeapDestroy(&hp);
}

void TestHeap()
{
	int arr[] = { 5, 6, 2, 4, 1, 7, 3, 9, 8 };
	Heap hp;
	HeapInit(&hp, arr, sizeof(arr) / sizeof(arr[0]),Greater);

	printf("top= %d\\n",HeapTop(&hp));
	printf("size= %d\\n", HeapSize(&hp));

	HeapInsert(&hp, 10);
	printf("top= %d\\n", HeapTop(&hp));
	printf("size= %d\\n", HeapSize(&hp));

	HeapDelete(&hp);
	printf("top= %d\\n", HeapTop(&hp));
	printf("size= %d\\n", HeapSize(&hp));
	HeapDestroy(&hp);
}

#include"Heap.h"

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

TOP-k问题,自主实现

在这里插入图片描述

void SwapTopk(int *left, int *right)
{
	int tmp = *left;
	*left = *right;
	*right = tmp;
}
//向下调整
void TOPk(int arr[], int size,int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size &&arr[child + 1] < arr[child])
		{
			child += 1;
		}
		if (arr[child]<arr[parent])
		{
			SwapTopk(&arr[child], &arr[parent]);

			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			return;
		}
	}
}
void PrintTopK(int* a, int n, int k)
{
	TOPk(a, k,0);
	for (int i = k; i < n; i++)
	{
		if (a[i] > a[0])
		{
			SwapTopk(&a[i], &a[0]);
		}
		TOPk(a, k,0);
	}	
	for (int i = 0; i < k; i++)
	{
		for (int j = 0; j < k-i; j++)
		以上是关于初识二叉树以及堆的简单实现的主要内容,如果未能解决你的问题,请参考以下文章

数据结构C语言 《四》二叉树,堆的基本概念以及堆的相关操作实现(上)

数据结构C语言版 —— 二叉树的顺序存储堆的实现

数据结构C语言版 —— 二叉树的顺序存储堆的实现

初阶数据结构——初识二叉树及其应用——堆——及其向下向上调整算法

C语言 二叉树与堆

C语言 二叉树与堆