目录
一、前言
往期数据结构文章可点击下列链接
【数据结构】时间复杂度
【数据结构】顺序表
【数据结构】链表——增、删、查、改
【数据结构】双向循环链表
【数据结构】栈和队列详解
有兴趣的同学可以点击前往支持一下
二、树的概念及其结构
1.树直接的关系
2.数的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因
为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
- 因此,树是递归定义的
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
3.树的基本概念
- 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林;
4.多叉树的的表示
- 左孩子右兄弟表示法
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
5.树的应用
在linux操作系统下的目录系统就是用树结构表示的:
我们可以下载一个tree软件查看Linux下的目录:
sudo apt install tree
三、二叉树的概念及结构
1.概念
树的每一个结点的度都不大于2的树即为二叉树
一个二叉树由根节点,左子树与右子树组成:
注意:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
2.特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
四、完全二叉树(堆)的顺序结构及其实现
1.完全二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结
构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统
虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段
2.完全二叉树的数组存储关系
由图可推知结点父子关系:
- 父结点:parent = (child - 1) / 2(其中child可为左右结点下标,parent为父结点下标)
- 左子结点:child = 2 * parent + 1
- 右子结点:child = 2 * parent + 2
3.堆的结构以及概念
堆的性质:
- 堆总是一棵完全二叉树
- 堆中某个节点的值总是不大于或不小于其父节点的值
即如图所示10小于15和56, 15小于25和30, 56小于70每个结点都小于等于其子结点。且该树为完全二叉树, 所以第一个二叉树为小堆。
3.堆的实现
堆的初始化
typedef int HPDataType;
typedef struct
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
void HeapInit(Heap* hp)
{
assert(hp);
hp->_a = NULL;
hp->_capacity = hp->_size = 0;
}
堆的插入及向上调整
插入:
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->_size == hp->_capacity)
{
int newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
HPDataType* new = realloc(hp->_a, sizeof(HPDataType) * newcapacity);
if (new == NULL)
{
perror("realloc fail");
exit(-1);
}
hp->_a = new;
hp->_capacity = newcapacity;
}
hp->_a[hp->_size] = x;
++hp->_size;
AdjustUp(hp->_a, hp->_size - 1);
}
向上调整:
void AdjustUp(HPDataType* php, int child)
{
int parent = (child - 1) / 2;
while (child != 0)
{
if (php[child] > php[parent])
{
Swap(&php[child], &php[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
堆的删除以及向下调整
堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
堆的向下调整法:
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
而在删除的过程中我们采用了交换法,没有改变左右子树的结构顺序,他们仍然是一个堆,所以可以对根结点使用向下调整法。
向下调整法是利用根结点与左右子树的根较小的值进行比较,如果比较小值大则不满足堆的调节进行交换。直到交换到满足堆的条件。
删除:
void HeapPop(Heap* hp)
{
assert(hp);
assert(hp->_size > 0);
hp->_a[0] = hp->_a[hp->_size - 1];
hp->_size--;
AdjustDown(hp->_a, hp->_size, 0);
}
向下调整:
void AdjustDown(HPDataType* a, int n, int parent)
{
//左孩子
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child = child + 1;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
堆的创建
如果我们有一个数组,想让它以堆的顺序进行排列,那我们应该怎么办呢?一个无序的数组的根节点左右子树不是一个堆,所以我们不能对根使用向下调整。那么我们可以换一种思路对最后一个非叶子结点向下调整,直到调整到根节点,就可以调整成堆。
int a[] = {1,5,3,8,7,6};
这样建堆的时间复杂度达到了O(n)!
4.堆的应用
堆排序
这是一种时间复杂度达到O(n*logn)的排序算法
- 建堆
- 升序 :建大堆
- 降序 :建小堆
- 利用堆删除思想来进行排序
void HeapSort(int* a, int n)
{
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
//利用堆删除思想来进行排序
for (int i = 0; i < n; i++)
{
Swap(&a[0], &a[n - i - 1]);
AdjustDown(a, n - i - 1, 0);
}
//打印
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
topK
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
void PrintTopK(int k)
{
//data.txt里有10000个数据
const char* file = "data.txt";
FILE* fin = fopen(file, "r");
if (fin == NULL)
{
ferror("fopen fail");
exit(-1);
}
int* minheap = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; i++)
{
fscanf(fin, "%d", &minheap[i]);
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minheap, k, i);
}
int x = 0;
while (fscanf(fin, "%d", &x) != EOF)
{
if (x > minheap[0])
{
minheap[0] = x;
}
AdjustDown(minheap, k, 0);
}
for (int i = 0; i < k; i++)
{
printf("%d ", minheap[i]);
}
printf("\n");
fclose(fin);
}