目录:
一,树的基本知识
二,树的类型
三,树的存储
四,树的基本运算
五,二叉树堆的基本运用
一,树的基本知识
树是一种非线性的数据结构,它是由n个有限结点组合而成为一个具有层次关系的集合,把它叫做树因为它看起来像一颗倒挂的树,也就是说它的根是朝上,叶朝下的,如下图:
在以上的所有结点种,其中,A结点称为根结点,而根节点没有前驱结点,其它结点都是通过层次非线性进行连接,它们彼此间不相交(注意此特点,要是相交的话就不是树)。
树结构中有以下常用的关健名:
结点的度:一个结点含有的子树个数称为该结点的度,如上图:A有子树B,C,D,度为3。
叶子结点或终端结点:度为0的结点称为叶结点,如上图中的J,F,K,L,H,I。
双亲结点:若一个结点含有子结点,则这个结点称为其子结点的双亲结点,如上图中A是B的双亲结点。
孩子结点:一个结点含有的子树的根结点称为该结点的子结点,如上图:B是A的孩子结点。
兄弟结点:具有相同父结点的结点互为兄弟结点,如上图:B,C,D是兄弟结点。
树的度:一棵树中,最大的结点的度称为树的度,如上图:树的度为3。
结点的层次:从根开始定义起,根为第一层,根的结点为第二层,其余的以此类推。
树的高度或深度:树中结点的最大层次,如上图:树的高度为4。
树的祖先:一颗树的根节点称为所有子结点的祖先,如上图:A为祖先。
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙。
森林:由多棵不相交的树的集合称为森林。
二,树的类型
树的类型根据结构来讲有多种,我们最常用的类型就是二叉树,在初步学习中先深入学习二叉树即可。
1,二叉树的概念
二叉树具有以下特征:
二叉树的结构中不存在度大于2的结点,即在二叉树中,最多由两个结点。
二叉树的子树有左右之分,次序不可颠倒,因为二叉树是有序结构。
2,特殊的二叉树
满二叉树:假设一颗二叉树的高度为h,每一层的结点都是满的,即每个结点的度都为2。
完全二叉树:假设一颗二叉树的高度是h,前h-1层都是满的,最后一层不一定满,且从左到右都连续。
三,树的存储
首先,我们要明白一点,在二叉树中,最多就有两个结点,并且这两个结点还都是有序的,在设计中,可能用到增添查找,用数组的结构存储在运用中比较麻烦,因此,二叉树最好用链表的形式来存储,而普通的树结构中结点并不确定,要用链式结构很是麻烦,普通的树结构最好用数组的形式进行存储。具体定义如下:
1,二叉树的形式
struct TreeNode
{
//在这里树中存储整型数据,也可换成其它类型
int val;
//左子树,不存在通常置为NULL
struct TreeNode* left;
//右子树,不存在通常置为NULL
struct TreeNode* right;
};
2,普通树的形式
//说明树的度
#define Max = 10;
struct TreeNode
{
//存放整型数据
int val;
//顺序表存放孩子指针
struct TreeNode* child;
};
树的设计方式其实还有很多种,但以上两种方式是我们目前最为基础的使用,在后面深入学习中还会设计更为精妙的存储方式,在我们计算机上的文件系统基本就是通过精妙建立的树状结构来存储,只是这种结构比较复杂,在后面的文章会给大家进行介绍。
四,树的基本运算
在了解以上的基本知识点后我们必须学会对树进行基本的运算,而笔者对树的研究总结了以下几点重要公式和定理,在树的计算中可很方便的运用。
1,若规定根结点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个结点。
2,若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2^(h) - 1。
3,对任何一颗二叉树,如果度为0,其叶结点(即度为0)个数为n0,度为2的分支结点个数为n2,则有n0 = n2 + 1。(此公式为重点公式)
4,若规定根结点的层数为1,具有n个结点的满二叉树的深度h = log2(n + 1)。
5,一颗完全二叉树度为1的结点数要么为1,要么为0,因为其有序结构是从左向右排布的。
在以上公式中,第三个和第五个为重点记忆,运用较多,第一个和第二个较为简单,可自行推理,第四个运用的比较少。
明白以上的逻辑后我们需注意在树计算中的几点隐含条件,接下来我用例题的形式跟大家进行解说。
在树的运算中要注意的是n1的情况(以上的情况只限制与在完全二叉树中),其它的运算掌握以上的公式和定理,并且自行灵活运用都比较简单。
五,二叉树堆的基本运用
堆的概念:
首先我们先了解下堆的概念,对于这个名词我们很是熟悉,在计算机的存储方式中就有堆的存储,但这里的堆并不是存储方式的那个堆,而是一种非线性结构中的完全二叉树,通常我们会将二叉树改制为堆结构或直接将数据改成堆结构,而在改制之前,我们先了解以下知识。
堆的分类:
1,小堆:树中任意一个父亲都小于或等于孩子。
2,大堆:树中任意一个父亲都大于或等于孩子。
完全二叉树父子结点的关系:
如若我们用顺序表的形式进行存储二叉结构(下标从0开始),在完全二叉树中我们观察不同两层直接双亲结点和孩子结点之间的数学联系,不难发现,其中有:如图中关系:
堆结构的最大问题就在于排序的时候其中的逻辑连接,明白了以上知识后,我们先要思考,如何用孩子结点找双亲结点之间的关系来进行算法的设置,总的来说就是通过双亲与孩子的比较,进而进行大堆或小堆的建立,其中,最为主要的算法为向下调整或向下调整,代码如下:
//数据从下往上调整
void AdjustUp(int* a, int child)//child是孩子,a是顺序表
{
int parent = (child - 1) / 2;
while (child > 0)
{
//大堆树状规则排序法
if (a[child] > a[parent])
{
//交换
int t = a[child];
a[child] = a[parent];
a[parent] = t;
//继续下一次往上遍历
child = parent;
parent = (parent - 1) / 2;
}
//因为一开始就是不断向上面排序,树状序列都已经排序好了,当满足排序要求时说明都已经满足了
else
{
break;
}
}
}
//数据从上往下调整
void AdjustDown(int* a, int n, int parent)//a是顺序表,n是大小,parent是双亲
{
int child = parent * 2 + 1;
while (child < n)
{
//大堆树状规则排序法
// 找出小的那个孩子,在这里要注意的是控制下一个孩子结点的不能超出范围
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
//交换
int t = a[child];
a[child] = a[parent];
a[parent] = t;
// 继续往下调整
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
因为堆是完全二叉树,在排布的时候我们只能通过双亲与孩子的关系来进行排序分布,即上下结构调整,也就是以上的算法排布。
在队中还可自行运用很多种算法设置,但基本算法都要靠向上调整算法或向下调整算法。在这里我先用一种方法演示:
首先:我们设置一个自己的头文件Heap.h:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<stdbool.h>typedef struct Heap
{
//以顺序表的形式存储堆
int* a;
int size;
int capacity;
}HP;void AdjustUp(int* a, int child);
void AdjustDown(int* a, int n, int parent);
void HeapPrint(HP* php);
void HeapInit(HP* php);
void HeapInitArray(HP* php, int* a, int n);
void HeapDestory(HP* php);
具体代码如下:
#include "Heap.h"
//数据从下往上调整
void AdjustUp(int* a, int child)//child是孩子,a是顺序表
{
int parent = (child - 1) / 2;
while (child > 0)
{
//大堆树状规则排序法
if (a[child] > a[parent])
{
//交换
int t = a[child];
a[child] = a[parent];
a[parent] = t;
//继续下一次往上遍历
child = parent;
parent = (parent - 1) / 2;
}
//因为一开始就是不断向上面排序,树状序列都已经排序好了,当满足排序要求时说明都已经满足了
else
{
break;
}
}
}
//数据从上往下调整
void AdjustDown(int* a, int n, int parent)//a是顺序表,n是大小,parent是双亲
{
int child = parent * 2 + 1;
while (child < n)
{
//大堆树状规则排序法
// 找出小的那个孩子,在这里要注意的是控制下一个孩子结点的不能超出范围
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
//交换
int t = a[child];
a[child] = a[parent];
a[parent] = t;
// 继续往下调整
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//输出堆
void HeapPrint(HP* php)
{
assert(php);
fprintf(stdout, "堆的数据: ");
for (int i = 0; i < php->size; i++) {
fprintf(stdout, "%d ", php->a[i]);
}
puts("");
}
//堆的初始化
void HeapInit(HP* php)
{
assert(php);
php->a = 0;
php->size = php->capacity = 0;
}
//堆的构建
void HeapInitArray(HP* php, int* a, int n)
{
assert(php && a);
php->a = (int*)malloc(sizeof(int) * n);
if (!php->a)
{
perror("php->a malloc");
exit(-1);
}
php->capacity = n;
memcpy(php->a, a, sizeof(int) * n);
php->size = n;
//建立堆,即进行排序,在这里以向上调整算法进行树状排布
for (int i = 1; i < php->size; i++)
{
AdjustUp(php->a, i);
}
}
//删除,因为是动态管理,要自己删除
void HeapDestory(HP* php)
{
assert(php);
free(php);
php = 0;
}
//大堆的构造
int main()
{
int a[] = { 2,3,5,7,4,6,8 };
HP* p = (HP*)malloc(sizeof(HP));//建立堆结构p,在这里以顺序表结构进行存储
HeapInit(p);//初始化
HeapInitArray(p, a, sizeof(a) / sizeof(int));//进行堆的构造
HeapPrint(p);//输出堆
HeapDestory(p);
return 0;
}
总:从树之后相关的结构和算法就要开始上点难度了,对于要想学好这一块,自己必须要多多试试上机练习并自己学会改动其中的算法设置,以上的设置并不算完整,算法还有大有改进的空间,我们自己要学会自行设置并且找到其中算法关键效应,然后自己设置更加高效的算法,这样才能达到举一反三的效果。