赞
踩
往期数据结构文章可点击下列链接
【数据结构】时间复杂度
【数据结构】顺序表
【数据结构】链表——增、删、查、改
【数据结构】双向循环链表
【数据结构】栈和队列详解
有兴趣的同学可以点击前往支持一下
树是一种非线性的数据结构,它是由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)!
这是一种时间复杂度达到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); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。