往期数据结构文章可点击下列链接
【数据结构】时间复杂度
【数据结构】顺序表
【数据结构】链表——增、删、查、改
【数据结构】双向循环链表
【数据结构】栈和队列详解
有兴趣的同学可以点击前往支持一下
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因
为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
在linux操作系统下的目录系统就是用树结构表示的:
我们可以下载一个tree软件查看Linux下的目录:
sudo apt install tree
树的每一个结点的度都不大于2的树即为二叉树
注意:
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结
构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统
虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段
堆的性质:
即如图所示10小于15和56, 15小于25和30, 56小于70每个结点都小于等于其子结点。且该树为完全二叉树, 所以第一个二叉树为小堆。
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*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");
}
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-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);
}