树简介
树是⼀种⾮线性的数据结构,它是由n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。
- 有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
- 除根结点外,其余结点被分成M(M>0) 个互不相交的集合T1、T2、......、Tm ,其中每⼀个集合Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有0个或多个后继。因此,树是递归定义的。

树形结构中,⼦树之间不能有交集,否则就不是树形结构,而是图结构了

树形结构:
- ⼦树是不相交的
- 除了根结点外,每个结点有且仅有⼀个⽗结点
- ⼀棵N个结点的树有N-1条边
树的相关术语

- ⽗结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点;如上图:A是B的⽗结点
- ⼦结点/孩⼦结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点;如上图:B是A的孩⼦结点
- 结点的度:⼀个结点有⼏个孩⼦,他的度就是多少;⽐如A的度为6,F的度为2,K的度为0
- 树的度:⼀棵树中,最⼤的结点的度称为树的度;如上图:树的度为6
- 叶⼦结点/终端结点:度为0的结点称为叶结点;如上图:B、C、H、I... 等结点为叶结点
- 分⽀结点/⾮终端结点:度不为0的结点;如上图:D、E、F、G... 等结点为分⽀结点
- 兄弟结点:具有相同⽗结点的结点互称为兄弟结点(亲兄弟);如上图:B、C 是兄弟结点
- 结点的层次:从根开始定义起,根为第1 层,根的⼦结点为第2层,以此类推;
- 树的⾼度或深度:树中结点的最⼤层次;如上图:树的⾼度为4
- 结点的祖先:从根到该结点所经分⽀上的所有结点;如上图:A是所有结点的祖先
- 路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列;⽐如A到Q的路径为:A-E-J-Q;H到Q的路径H-D-A-E-J-Q
- ⼦孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是A的⼦孙
- 森林:由m(m>0) 棵互不相交的树的集合称为森林;
二叉树
在树形结构中,我们最常⽤的就是⼆叉树,⼀棵⼆叉树是结点的⼀个有限集合,该集合由⼀个根结点加上两棵别称为左⼦树和右⼦树的⼆叉树组成或者为空。
- ⼆叉树不存在度⼤于2的结点
- ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树
- 对于任意的⼆叉树都是由以下⼏种情况复合⽽成的

满二叉树
⼀个⼆叉树,如果每⼀个层的结点数都达到最⼤值,则这个⼆叉树就是满⼆叉树。也就是说,如果⼀个⼆叉树的层数为K,且结点总数是2k−12^k-12k−1,则它就是满⼆叉树。

完全二叉树
完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为K的,有n个结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从1⾄n的结点⼀⼀对应时称之为完全⼆叉树。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。

也就是说完全二叉树,需要满足节点每层依次从左往右进行排列
总结:
- 若规定根结点的层数为1,则⼀棵⾮空⼆叉树的第i层上最多有2i−12^{i-1}2i−1个结点
- 若规定根结点的层数为1,则深度为h的⼆叉树的最⼤结点数是2h−12^h-12h−1
- 若规定根结点的层数为1,具有n个结点的满⼆叉树的深度h=log2(n+1)h=log_2(n+1)h=log2(n+1)
⼆叉树存储结构
⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构。
顺序结构
顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会有空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。


- 对于具有n个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从0 开始编号,则对于序号为i的结点有:
- 若i>0,i位置结点的双亲序号:(i-1)/2;i=0则i为根节点编号,无双亲节点
- 若2i+1<n,则有左孩子节点,编号为2i+1,否则无左,右孩子
- 若2i++<n,则有右孩子节点,编号为2i+2,否则无右孩子
现实中我们通常把堆(⼀种⼆叉树)使⽤顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段。
链式结构
⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址。链式结构⼜分为⼆叉链和三叉链

堆
-
堆是完全二叉树结构,使⽤顺序结构的数组来存储
-
大堆:树的任何一个父亲都大于或等于其孩子

-
小堆:树的任何一个父亲都小于或等于其孩子

c
//heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int datatype;
typedef struct Heap
{
int* HeapArr;
int n;
int capacity;
}Heap;
void HeapSort_2(int* , int );
void HeapSort_1(int* ,int);
void AdjustDown(int, int*, int);
void AdjustUp(int*, int);
void swap(int*, int*);
void HeapPop(Heap*);
void Insert(Heap*, datatype);
void Initial(Heap* );
c
//heap.c
#include"heap.h"
void Initial(Heap* p)
{
p->HeapArr = NULL;
p->n = 0;
p->capacity = 0;
}
void Insert(Heap* p, datatype x)
{
if (p->capacity == p->n)
{
int newcapacity = p->capacity == 0 ? 4 : 2 * p->capacity;
int* a = (int*)realloc(p->HeapArr,newcapacity * sizeof(int));
if (a == NULL)
{
perror("realloc fail");
return;
}
p->HeapArr = a;
p->capacity = newcapacity;
}
p->HeapArr[p->n] = x;
p->n++;
AdjustUp(p->HeapArr, p->n);
}
void HeapPop(Heap* p)
{
assert(p->n);
swap(&p->HeapArr[0], &p->HeapArr[p->n - 1]);
p->n--;
AdjustDown(0, p->HeapArr, p->n);
}
void swap(int* a, int* b)
{
int tmp = *a;
* a = *b;
*b = tmp;
}
void AdjustUp(int* a, int child) /*大堆调整*/
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(int parent, int* a,int n) /* 大堆调整*/
{
int child = parent * 2 + 1;
while (child<n)
{
if (child + 1 < n && a[child + 1] > a[child])
child++;
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 +1;
}
else
{
break;
}
}
}
void HeapSort_1(int* a, int n) /*大堆调整是升序*/
{
for (int i = 0; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end>0)
{
swap(&a[0], &a[end]);
AdjustDown(0, a, end);
end--;
}
}
void HeapSort_2(int* a, int n) /*大堆调整是升序*/
{
for (int lastparent = (n - 1 - 1) / 2; lastparent >= 0; lastparent--)
{
AdjustDown(lastparent, a, n);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
AdjustDown(0, a, end);
end--;
}
}
c
//main.c
#include"heap.h"
int main()
{
int arr[10] = { 0,4,7,2,9,6,3,8,1,5 };
HeapSort_1(arr, 10);
for (int i = 0; i < 10; i++)
printf("%d ", arr[i]);
return 0;
}
解释下面是相关解释:
向上调整的作用
向上调整建堆,贴近 "逐个插入建堆" 的思路
- 从第二个元素(索引 1)开始,向后遍历所有节点;
- 对每个节点执行 "向上调整":将当前节点与父节点比较,若不满足堆性质则交换,然后继续对交换后的父节点重复此过程,直到节点落位(相当于把当前节点 "插入" 到前面已构建的堆中)。
- 向上调整算法建堆时间复杂度为:O(n∗log2n)O(n*log_2n)O(n∗log2n)
c
void AdjustUp(int* a, int child) /*大堆调整*/
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
向下调整的作用
- 向下调整建堆
- 从最后一个非叶子节点开始,向前遍历所有非叶子节点;
- 对每个节点执行 "向下调整":将当前节点与左右孩子中更大(大顶堆)/ 更小(小顶堆)的节点比较,若不满足堆性质则交换,然后继续对交换后的子节点重复此过程,直到节点落位。
- 向下调整算法建堆时间复杂度为:O(n)O(n)O(n),比向上调整建堆效率高
- 移除堆顶元素后进行向下调整
- 替换堆顶:将堆的最后一个元素放到堆顶位置
- 缩小堆规模:堆的有效长度减 1(原最后一个元素已被移走,不再参与堆的调整);
- 向下调整新堆顶(设为大顶堆):新堆顶大概率不满足 "父节点≥子节点" 的大顶堆性质,因此从堆顶开始,将其与左右孩子中更大的节点交换,直到该节点落位到正确位置,恢复堆的有序性。
c
void AdjustDown(int parent, int* a,int n) /* 大堆调整*/
{
int child = parent * 2 + 1;
while (child<n)
{
if (child + 1 < n && a[child + 1] > a[child])
child++;
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 +1;
}
else
{
break;
}
}
}
堆排序
这里以升序为例,使用大堆(降序则用小堆)
- 将待排序数组转化为大顶堆,此时堆顶是整个数组的最大值。
- 把堆顶(最大值)和当前未排序区域的最后一个元素交换,这样最大值就 "归位" 到数组末尾(成为有序区的第一个元素)。
- 将堆的有效规模减 1(排除已归位的最大值),然后对新的堆顶执行向下调整,恢复剩余元素的大顶堆性质。
- 重复步骤 2-3,直到堆的有效规模为 1,整个数组就升序排列完成。
版本一:
在步骤一中,使用向上调整构建堆
c
void HeapSort_1(int* a, int n) /*大堆调整是升序*/
{
//使用向上调整建堆
for (int i = 0; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end>0)
{
swap(&a[0], &a[end]);
AdjustDown(0, a, end);
end--;
}
}
版本2:
在步骤一中,使用向下调整构建堆
c
void HeapSort_2(int* a, int n) /*大堆调整是升序*/
{
for (int lastparent = (n - 1 - 1) / 2; lastparent >= 0; lastparent--)
{
AdjustDown(lastparent, a, n);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
AdjustDown(0, a, end);
end--;
}
}
总结:堆排序时间复杂度为:O(n∗logn)O(n*logn)O(n∗logn)
topK问题
TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。
对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,这里以找前K个最⼤的元素为例(前K个最⼤的元素,建小堆。前k个最⼩的元素,则建⼤堆):
- 先用前 k 个元素构建小顶堆(堆顶是这 k 个元素的最小值);
- 遍历剩余元素,若当前元素 > 堆顶,说明它比堆中最小元素大,应加入 "候选集":替换堆顶,再对新堆顶执行向下调整,保持小顶堆性质;如此一来,堆中的小值元素都会被替换出去
- 遍历结束后,堆中所有元素就是整个数组中最大的 k 个元素(堆顶是这 k 个元素的最小值)。
二叉树遍历
- 前序遍历(PreorderTraversal亦称先序遍历):访问根结点的操作发⽣在遍历其左右⼦树之前
- 访问顺序为:根结点、左⼦树、右⼦树
- 中序遍历(InorderTraversal):访问根结点的操作发⽣在遍历其左右⼦树之中(间)
- 访问顺序为:左⼦树、根结点、右⼦树
- 后序遍历(PostorderTraversal):访问根结点的操作发⽣在遍历其左右⼦树之后
- 访问顺序为:左⼦树、右⼦树、根结点
- 二叉树的层序遍历(也叫广度优先遍历 / BFS)是按从上到下、从左到右 的顺序访问每一层的所有节点,核心依赖队列(先进先出 FIFO) 实现
- 初始化:将二叉树的根节点入队
- 循环处理队列:只要队列不为空,重复步骤:队列出一个节点,遍历该节点,然后将该节点的左右节点入队列
- 结束:队列空时,所有层遍历完成。