目录
1. 树的概念
1. 基本概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根节点没有前驱结点除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。因此,树是递归定义的。
需要注意的是,树形结构中,字数之间不能有交集,否则就不是树形结构。
例如下列的结构。
2.树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林
3. 树的表示
树的结构可以有顺序结构和链式结构,相对于前面所学的顺序表链表的结构,它的结构在物理结构上没有太大的区别,但是在逻辑结构上得到区别就有很大的差别。既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。
这里所提到的孩子兄弟表示法,就是定义一个结构体,结构体内有两个结构体指针,一个指向自己的最左边的孩子节点,另一个指向自己的有兄弟节点,这样即使有有些地方不能够一次性表现出来,但是可以将其全部表示出来。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
下图就是逻辑结构的图示。
2. 二叉树概念和结构
2.1 概念
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
从上图可以看出,
1. 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
2.2 特殊的二叉树
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.3 二叉树的性质
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 .
3. 对任何一棵二叉树, 如果度为0其叶结点个数为 , 度为2的分支结点个数为 ,则有 = +1
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= . (ps: 是log以为底,n+1为对数)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
2.4 二叉树的顺序存储。
1.顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式结构
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
3. 二叉树顺序结构的实现
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
给出下图这样一个二叉树。
这样一个二叉树在顺序结构中的存储如下图,其中空的位置在数组中也是要空下来的。
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:1. 堆的某个节点的值总是不大于或者不小于其父亲的值。
2. 堆是一个完全二叉树
4. 堆的实现
1. 结构体定义
堆的结构体定义和顺序表的是一样的,因为顺序二叉树的底层逻辑就是顺序表。
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
2. 向上向下调整
堆的向上向下调整是堆这个数据结构十分重要的一个操作,后面的排序,增删查改,建堆都会离不开它。所谓的向上向下调整就是说将堆中的某个元素向下或者向上调整来实现大根堆或者小根堆,方便后续的操作。
2.1 向上调整(大堆为例)
首先看到一个堆
很明显看出这并不是一个大根堆,需要将左下角的34往上调整。所以就是将34这个节点和它的父亲去交换。
换完之后如下图,发现还不是大根堆还需要向上调整。
调整完就是下图,发现是一个大根堆,也就是每一个节点的孩子都要小于等于这个节点。
那么如何用代码实现这样一个操作呢,图上所展示的是一个逻辑结构,到了计算机存储的结构就变成了一个顺序表,也就是这样一个结构。
到了数组之中也就是将数组中的两个值进行互换。
最后调整完成就是下面的数组,在物理结构上符合大根堆。
以上的内容是通过图形的思维来实现,那如何用代码实现呢?
下面的代码中已给除,所需要的解释注释也已经讲解到位。
//函数参数是实现堆的数组和需要调整的节点的下标
void AdjustUp(HPDataType* a, int child)
{
//首先要将这个节点的父亲节点找到,这里无论是左孩子还是右孩子用child减一再除2
//得到的父亲节点都是同一个值。
int parent = (child - 1) / 2;
while (child > 0)
//这里的循环条件是child要大于0,也就是直到调整到顺序表中的第一个值,也就是第一层的
//根节点才停止。
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);//Swap函数就是一个很简单的一个交换两个值。
//下面两行的代码需要将child和parent的值重置,也就是调整完第一次再看看需不需要
//再继续往上调整。
child = parent;
parent = (child - 1) / 2;
}
else
{
break; //如果不需要调整那么久退出此次循环,然后这个节点就调整完了。
}
}
}
2.2 向下调整 (小根堆为例)
有了向上调整的经验,这里的向下调整也就得心应手了。
这里需要7和19互换
然后需要7和32互换
最后还需要19和32互换
这个向下调整得到过程也是比较好实现的。
【代码实现】
//函数的参数是存储堆的数组a,数组的长度n,需要调整的节点
void AdjustDown(HPDataType* a, int n, int parent)
{
//首先要找到它的孩子,这里找的是左孩子
int child = parent * 2 + 1;
//和前面的循环条件一样,也是一直到数组的首元素判断
while (child < n)
{
//这里需要找到左右孩子中较小的一个,因为是以小根堆为例,所以向下调整时就需要
//找到那个较小的孩子,如果左孩子是较小的孩子,那么就不需要做什么,如果说是右
//孩子较小并且存在右孩子,那么child就需要加一。
if (a[child + 1] < a[child] && child + 1 < n)
{
child = child + 1;
}
//如果说孩子比父亲小,那就可以交换,将较大的父亲往下“沉”。
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child; //这里也需要将父亲节点和孩子节点的下标进行重置
child = parent * 2 + 1;
}
else
{
break;//结束跳出循环,向下调整的操作也随之了结。
}
}
}
3. 增删等
增删查改可以说是基本上所有数据结构中所都必须包含的。
3.1 添加数据
添加数据就是再数组的末尾添加一个数据,然后再向上调整。
void HeapPush(HP* php, HPDataType x)
{
//先判断需不需要扩容
assert(php);
if (php->capacity == php->size)
{
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
//将数据向上调整到合适的位置
AdjustUp(php->a, php->size - 1);
}
3.2 删除数据
通常我们所提到的删除数据,默认的就是删除堆顶的元素。
代码的实现思想就是将堆顶的元素和最后一个元素先交换位置,然后将最后已经换到数组尾部的原来的堆顶元素去删除,最后需要将新换上的原来的数组尾部的元素的堆顶向下调整。
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));//判断是否为空
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
3.3 堆顶元素 判空 堆长度
这三个函数的实现非常简单,直接返回一个值就行。
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->a[0] == 0;
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
3.4 初始化
void HeapInit(HP* php)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = 0;
php->capacity = 4;
}
3.5 创建堆
创建堆有向上建堆和向下建堆两个方法实现。
但是通常用向下建堆,它的时间复杂度是O(N),向上建堆的时间复杂度是O(N*LogN).
void HeapInitArray(HP* php, int* a, int n)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = n;
php->capacity = n;
//建堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(php->a, php->size, i);
}
}
这个代码需要说明的就是建堆的过程。核心的就是for循环那一块的代码。
举例说明,先来一个数组。
int a[] = {30,45,4,28,56,3,18};
在电脑中的逻辑结构就是下图这样。
首先i的初始值是(n-2)/2,更好的应该是理解成n-1-1,n-1是找到最后一个元素的下标,再减去1是找到他前面的一个元素。然后再除二就可以找到它的父亲节点,这里再i的初始化时多减去一个1就是为了无论最后的一个元素值在什么位置都能找到它的父亲节点。这里可以先走一波,将第一个i的值带入到向下调整的函数中去。
先找到了parent和child所指向的节点。
将其调整完且par重置。
此次调整完,i--,所指向的就是45,child就是56
这里需要调整两者位置,再重置par。
这里i再--,找到par时30,child是56.
再将其执行向下调整操作。先是第一波调整。
再进行第二次调整。
到这里也就建堆完了,会发现,这就是一个标准的大堆。
通过这样一个流程示意图,可以很明显的看到建堆的物理结构的变化。
4. 堆排序
堆排序的思想就是通过建立大小堆,然后取出堆顶的最值,然后进行排序。
例如,现在需要排升序,乍一看会觉得排升序直接建一个小堆,然后每次把堆顶的元素取出来不就可以了么,后面树再去调整到堆顶的数值最小。那么这个思想就是大错特错了。当你取出堆顶的那一个元素之后,剩下的n-1个元素已经不再是一个堆了,就相当于删除数组中的元素,删除第一个元素很麻烦,但是删除最后一个元素直接将数组的长度减一就行了。所以说,排升序,需要建大堆。那么思想是什么呢?这里就是因为一个堆删除堆顶元素会有很大的变化,删除最后一个元素却只有稍微的一个小变化。所以就是可以建大堆,然后将堆顶的元素和最后一个元素互换,删除这个最后位置的元素,然后再将堆顶的那个元素向下调整,这样既可以找到最大值,也能很好的保持住堆的结构。
上面所提到的删除并非是真正的删除,而是不将这个元素再看作是堆中的一个节点,但是最后输出结果的时候还是会在,每次将最大值放在外面,一次次循环就可以排序了。
【代码实现】
//函数的参数是数组a和数组长度
void HeapSort(int* a, int n)
{
//建堆的操作思想在上面已经有了阐述
//建堆 向下建堆 O(N)
for (int i = (n - 2) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//这里就是找到最后一个元素值,将其作为end
int end = n - 1;
while (end > 0)
{
Swap(&a[end], &a[0]); //交换堆顶元素
AdjustDown(a, end, 0);//堆顶的元素向下调整
--end;
}
}
5. TopK问题
TopK问题就是在一大堆数组中,找到最大的或者最小的前k个值。
首先还是一样的建堆,这里的关键是需要建什么样的堆。这里以找到最大的个值为例。首先需要创建一个大小为k个数的小堆,然后将其余的n-k个数和堆顶的数据进行比较,如果说比堆顶元素大,那么就替换他,然后再向下调整,始终保持着堆顶的元素是最小的。用通俗的话讲,这个堆中的元素最小在最顶上,如果新来的数比他的那就是可以把他踢走,被踢走的数就比这个堆中的所有数都要小,那也就代表着这个数不可能是前k个元素中的一个。那么这样不断地进行就比较入堆操作,将所有的都比较玩,这个堆中的节点就是topK。这里的思想巧妙在建小堆,总能在k个元素中找到最小的并且放在第一的位置。
【代码实现】
//函数参数是数组a,数组长度n,前k个元素
//这里是以找到最大的TopK为例
void TopK(int* a, int n, int k)
{
HP hp;
HeapInit(&hp);
//创建一个k个数的小根堆
for (int i = 0; i < k; i++)
{
HeapPush(&hp, a[i]);
}
//剩下的n-k个数据和堆顶的数据进行比较,比他大,就替换进去在向下调整
for (int i = k; i < n; i++)
{
if (a[i] > HeapTop(&hp))
{
HeapPop(&hp);
HeadPush(&hp, a[i]); //HeadPush这个函数中已经有了向下调整的操作。
//或者
//hp.a[0] = a[i];
//AdjustDown(hp.a,hp->size,0);
}
}
HeapDestroy(&hp);
}
6. 所有的代码实现
6.1 heap.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapInitArray(HP* php, int* a, int n);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
void Swap(HPDataType* p1, HPDataType* p2);
6.2 heap.c
#include "Heap.h"
void HeapInit(HP* php)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = 0;
php->capacity = 4;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
void HeapInitArray(HP* php, int* a, int n)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = n;
php->capacity = n;
//建堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(php->a, php->size, i);
}
}
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->capacity == php->size)
{
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->a[0] == 0;
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
//大根堆
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//大根堆
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (a[child + 1] > a[child] && child + 1 < n)
{
child = child + 1;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
6.3 test.c
#include "Heap.h"
void TopK(int* a, int n, int k)
{
HP hp;
HeapInit(&hp);
//创建一个k个数的小根堆
for (int i = 0; i < k; i++)
{
HeapPush(&hp, a[i]);
}
//剩下的n-k个数据和堆顶的数据进行比较,比他大,就替换进去在向下调整
for (int i = k; i < n; i++)
{
if (a[i] > HeapTop(&hp))
{
HeapPop(&hp);
HeadPush(&hp, a[i]);
//或者
//hp.a[0] = a[i];
//AdjustDown(hp.a,hp->size,0);
}
}
HeapDestroy(&hp);
}
void HeapSort(int* a, int n)
{
// 建堆 向上建堆 O(N*logN)
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
//建堆 向下建堆 O(N)
for (int i = (n - 2) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//自己先实现
int end = n - 1;
while (end > 0)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
--end;
}
}
void test1()
{
HP hp;
HeapInit(&hp);
HeapPush(&hp, 18);
HeapPush(&hp, 23);
HeapPush(&hp, 6);
HeapPush(&hp, 45);
HeapPush(&hp, 3);
HeapPush(&hp, 34);
HeapPush(&hp, 9);
HeapPush(&hp, 35);
}
int main()
{
test1();
int arr[] = { 32,34,76,23,7,6,243,56,75,35,7,54,34,45,546,2345,45,2756,425,6345,654,62 };
HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}
以上内容仅仅只是数的一小部分内容,后续持续更新中!!!