目录
[1. 树](#1. 树)
[1.1 概念](#1.1 概念)
[1.2 基本术语](#1.2 基本术语)
[1.2.1 结点间的关系描述](#1.2.1 结点间的关系描述)
[1.2.2 结点和树之间的属性描述](#1.2.2 结点和树之间的属性描述)
[1.3 树的基本性质](#1.3 树的基本性质)
[2. 二叉树](#2. 二叉树)
[2.1 概念](#2.1 概念)
[2.2 特殊的二叉树](#2.2 特殊的二叉树)
[2.2.1 满二叉树](#2.2.1 满二叉树)
[2.2.2 完全二叉树](#2.2.2 完全二叉树)
[2.2.3 搜索二叉树](#2.2.3 搜索二叉树)
[2.3 二叉树性质](#2.3 二叉树性质)
[2.4 二叉树的存储](#2.4 二叉树的存储)
[2.4.1 顺序存储 - 堆](#2.4.1 顺序存储 - 堆)
[2.4.1.1 概念](#2.4.1.1 概念)
[2.4.1.2 堆代码](#2.4.1.2 堆代码)
[2.4.1.3 建堆](#2.4.1.3 建堆)
[2.4.2 堆经典题目](#2.4.2 堆经典题目)
1. 树
1.1 概念
定义:树是n (n >= 0) 个结点的有限集。当 n = 0 时,称为空树。当 n > 0 时,称为非空树。
任意一颗非空树应该满足以下要求:
(1) 有且仅有一个叫做"根"的特定结点。
(2) 当 n > 1 时,其余结点可分为m(m>0)个互不相交的有限集
,其中每个集合本身又是一棵树,称为"根的子树"。
注意:**(1)**树的根节点没有前驱,除根结点以外的结点有且只有一个前驱结点,树中所有结点可以有多个后继结点。
**(2)**m叉树是指每个结点的度最大为 m,最少的度可以是 0,m叉树中不一定存在有m度的结点。
简图:

1.2 基本术语
1.2.1 结点间的关系描述
**(1)**祖先:从根一路走到这个结点,路上经过的所有结点,都是它的祖先。例子:根 A → C → G,A、C 都是 G 的祖先。
**(2)**子孙:一个结点下面,它的子树里面所有结点,全是它的子孙。例子:A 下面所有点都是 A 的子孙;C 下面只有 G,所以 C 的子孙只有 G。
**(3)**双亲:直接连着它、在它上面一层的那个结点。例子:B 往下直接连 E、F,B 是 E、F 的双亲。
**(4)**孩子:它直接指向下一层的结点。例子:E、F 是 B 的孩子。
**(5)**兄弟:同一个双亲生出来的结点,互相叫兄弟。例子:H 和 I 的双亲是同一个点,H、I 互为兄弟。
**(6)**堂兄弟:各自双亲在同一层,但是双亲不是同一个,这两个结点就是堂兄弟。例子:G 的双亲 C,H 的双亲 B;B 和 C 在同一层,所以 G 和 H 是堂兄弟。

1.2.2 结点和树之间的属性描述
**(1)**度:树中结点的孩子个数称为该结点的"度",树中结点的最大度数称为"树的度"。如:下图中,A结点的度是3,B结点的度是2,树的度是3。
**(2)**叶子结点/终端结点:度为0的结点称为"叶子结点"或"终端结点"。如:下图中的绿色结点。
**(3)**分支结点/非终端结点:度不为0的结点称为"分支结点"或"非终端结点"。
**(4)**结点的层次:结点的层次从树根结点开始定义,根结点A为第一层,分支结点B、C、D为第二层,以此类推。
**(5)**树的高度/深度:树种结点的最大层数称为"树的高度/深度"。如:下图中树的高度为4。(有的地方根从第 0 层开始算,做题留意)
**(6)**有序树/无序树:有序树中结点的子树有左右顺序,不能随便交换位置;最左边叫"第一个孩子",最右边叫"最后一个孩子"。
**(7)**路径:两个结点之间,沿着上下连线经过的一串结点。
路径长度:这条路径上结点的个数。(有的地方定义为边是数量,做题留意)
**(8)**边:双亲结点和它孩子结点之间的连线,叫做"边"。

1.3 树的基本性质
性质一:树中的结点数 = 所有结点的度数之和 + 1
理解:树中"所有结点度数之和"为除根节点外的所有结点数量之和;"+1"加的就是根结点。
性质二:m叉树的第 i 层上至多有 个结点。(i >= 0)
理解:要让第 i 层结点数量达到最大值上一层每一个结点都生出全部 m 个孩子。
性质三:高度为h的m叉树至多有个结点。
理解:每一个分支都长满m个孩子,每层全部填满,所有叶子都在第h层。由等比数列求和算出。
2. 二叉树
2.1 概念
定义:每个结点最多 2 棵子树(结点的度 ≤ 2,不存在度大于 2 的结点;树的度不一定等于2),是一个递归定义的数据结构。
👉重点:是有序树!子树分左、右,不能随便互换,分别叫左子树、右子树。

2.2 特殊的二叉树
2.2.1 满二叉树
定义:高度h,每一层结点全部填满、达到最大值,就是满二叉树。
特点:
1、叶子结点全部集中在最后一层。
**2、**所有分支结点的度一定等于 2(每个非叶子结点都同时有左、右孩子)。
**3、**同高度下,满二叉树是结点数量最多的二叉树。
**4、**结点总数:

2.2.2 完全二叉树
定义:高度h:前 h - 1 层全部填满;第 h 层可以不满,但结点必须从左往右连续排列,不能中间空缺。
关系:满二叉树一定是完全二叉树;完全二叉树不一定是满二叉树。
特点:
1、叶子结点只可能在最后两层。
2、度为 1 的结点最多只能有 1 个。
**3、**高度h的完全二叉树,结点数量范围:
。

2.2.3 搜索二叉树
定义:二叉搜索树也叫二叉排序树,它要么是一棵空树,要么是满足特定规则的二叉树;最典型的特征是:对其做中序遍历,得到的结果是有序序列。
性质:
1、左子树约束 :若左子树不为空,左子树上所有结点的值都小于 根结点的值。(实践中存在变形规则:左子树所有结点的值可小于等于根结点的值)
2、右子树约束 :若右子树不为空,右子树上所有结点的值都大于 根结点的值。(实践中存在变形规则:右子树所有结点的值可大于等于根结点的值。)
3、递归性质:它的左子树、右子树本身,也分别是一棵二叉搜索树。

二叉搜索树是二叉树最核心的应用方向之一,是 "查找" 相关知识模块的重点数据结构,会在查找章节做系统的深入讲解。
2.3 二叉树性质
性质一:二叉树的第 i 层上至多有个结点(i >= 1)
性质二:高度为h的二叉树至多有个结点。
性质三:对任意二叉树,若度为 0 的叶子结点数为,度为 2 的结点数为
,
则恒满足:
理解:设总结点数为m,度为1的结点数为,则
,又因 总结点数=所有结点度之和+1,则
,联立求得
性质四:具有 n 个结点的完全二叉树,高度为:
理解:高度为 h 的完全二叉树,结点数满足:,可以缩放为:
;对不等式取以 2 为底的对数,得
;由于高度 h 是整数,因此
,整理得
。
2.4 二叉树的存储
2.4.1 顺序存储 - 堆
2.4.1.1 概念
定义:堆是顺序存储结构,逻辑上是一颗完全二叉树。
堆又分为:小根堆、大根堆。
小根堆:小堆的根是当前树的最小值,任意子树的根也是当前子树的最小值。
如图:

大根堆:大堆的根是当前树的最大值,任意子树的根也是当前子树的最大值。
如图:

2.4.1.2 堆代码
堆的结构
cpp
typedef int HDataType;
typedef struct heap {
HDataType* arr; // 存储数据
int size; // 有效元素个数
int capacity; // 最大容量
}Heap;
堆的初始化
cpp
void HeapInit(Heap* L) {
assert(L);
HDataType* cur = (HDataType*)malloc(sizeof(HDataType) * 4);
if (cur == NULL) {
printf("堆开辟失败!!!\n");
exit(-1);
}
L->arr = cur;
L->size = 0;
L->capacity = 4; // 初始化就给4块空间
}
堆的判空
cpp
int HeapEmpty(Heap* L) {
assert(L);
return L->size == 0;
}
堆的销毁
cpp
// 堆的销毁
void HeapDestory(Heap** L) {
assert(*L && (*L)->arr);
free((*L)->arr);
free(*L);
*L = NULL;
}
向上调整建堆
小堆进行向上调整算法要求:调整之前的数据是小堆
大堆进行向上调整算法要求:调整之前的数据是大堆
cpp
void Swap(HDataType* a, HDataType* b) {
assert(a && b);
HDataType tmp = *a;
*a = *b;
*b = tmp;
}
// 向上调整建堆 这里建小堆
void AdjustDown(HDataType* arr, int child) {
assert(arr);
//双亲下标
int parent = (child - 1) / 2;
// 调整双亲和孩子的位置
while (child > 0) {
// if中 "<" 换成 ">"就是调整大堆
if (arr[child] < arr[parent]) {
Swap(&arr[child] , &arr[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else {
return;
}
}
}
图形演示:


堆的插入
cpp
void HeapInsert(Heap* L , HDataType x) {
assert(L);
// 判断容量
if (L->size == L->capacity) {
HDataType* tmp = (HDataType*)realloc(L->arr, sizeof(HDataType) * L->capacity * 2);
if (tmp == NULL) {
printf("扩容失败,取消插入!\n");
return;
}
L->arr = tmp;
L->capacity *= 2;
}
L->arr[L->size] = x;
L->size++;
// 进行向上调整,维持堆结构
AdjustUp(L->arr, L->size - 1);
}
向下调整建堆
小堆进行向下调整算法要求:根的左子树和右子树是小堆
大堆进行向下调整算法要求:根的左子树和右子树是大堆
cpp
// 向下调整建堆 这里建小堆
void AdjustDown(HDataType* arr, int length, int parent) {
int child = parent * 2 + 1; // 左孩子下标,右孩子下标则+1
// 调整双亲和孩子
while (child < length) {
// 找出两个孩子中的最小值
if (child + 1 < length && arr[child] > arr[child + 1]) {
++child;
}
// 建大堆">" 换成 "<"
if (arr[parent] > arr[child]) {
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else {
return;
}
}
}
图形演示:



堆的删除
堆的删除一般删除堆顶数据,删除堆尾数据无意义
cpp
void HeapDelete(Heap* L){
assert(L && !HeapEmpty(L));
Swap(&L->arr[0] , &L->arr[L->size - 1]);
L->size--;
// 向下调整 维持堆结构
AdjustDown(L->arr, L->size, 0);
}
2.4.1.3 建堆
建堆,就是把一堆无序数据构建成堆的结构,有时我们需要把一个数组世界构建成一个小堆或者大堆,建堆的方法有两种。
方法一:向上调整算法
思路:先把第0个数看做堆,然后依次把1至n-1位置的数据使用向上调整算法插入堆。


以此类推,直到最后一个元素完成调整。
代码实现:
cpp
void HeapInitArray(Heap* L, int* a, int length) {// 堆 给定数组 数组长度
assert(L && a);
assert(length > 0);
HDataType* cur = (HDataType*)malloc(sizeof(HDataType) * length);
if (cur == NULL) {
printf("开辟内存失败!!!\n");
exit(-1);
}
L->arr = cur;
L->size = L->capacity = length;
memcpy(L->arr, a, sizeof(HDataType) * length);
for (int i = 1; i < length; i++) {
AdjustUp(L->arr, i);
}
}
方法二:向下调整算法
思路:把所有的叶子结点看做是一个个堆,从倒数第二层的最后一个非叶子结点,开始进行向下调整算法建堆,直到调整到根。



代码实现:
cpp
void HeapInitArray2(Heap* L, HDataType* a, int length) {
assert(L && a);
assert(length > 0);
HDataType* cur = (HDataType*)malloc(sizeof(HDataType) * length);
if (cur == NULL) {
printf("内存开辟失败!!!\n");
exit(-1);
}
L->arr = cur;
L->size = L->capacity = length;
memcpy(L->arr, a, sizeof(HDataType) * length);
for (int i = (L->size - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(L->arr, L->size, i);
}
}
时间复杂度对比
向上调整算法时间复杂度:

向下调整算法时间复杂度:

向下调整时间 < 向上调整时间,故一般选择向下调整算法。
测试代码
测试代码是vibcoding出来的,需要测试上述函数的可以用。
cpp
/* ==================== Heap test code (appended) ==================== */
/*
* Test-only code. The heap implementation above is unchanged.
* Build with MSVC:
* cl /nologo /TC /std:c17 /W4 /Zi /Fe:HeapTest.exe Heap.c
*/
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static Heap* NewHeap(void)
{
Heap* h = (Heap*)malloc(sizeof(Heap));
if (h == NULL) {
printf("FAIL: unable to allocate Heap\n");
exit(1);
}
h->arr = NULL;
h->size = 0;
h->capacity = 0;
return h;
}
static int IsMinHeap(const Heap* h)
{
int i;
if (h == NULL || h->arr == NULL) {
return 0;
}
if (h->size < 0 || h->capacity < h->size) {
return 0;
}
for (i = 1; i < h->size; ++i) {
if (h->arr[(i - 1) / 2] > h->arr[i]) {
return 0;
}
}
return 1;
}
static void CheckPopOrder(Heap* h, const HDataType* expected, int count)
{
int i;
CHECK(h->size == count);
if (h->size != count) {
return;
}
for (i = 0; i < count; ++i) {
CHECK(h->arr[0] == expected[i]);
HeapDelete(h);
CHECK(IsMinHeap(h));
}
CHECK(h->size == 0);
CHECK(HeapEmpty(h) != 0);
}
static void CheckPopSorted(Heap* h)
{
int have_prev = 0;
HDataType prev = 0;
while (h->size > 0) {
HDataType current = h->arr[0];
if (have_prev) {
CHECK(prev <= current);
}
prev = current;
have_prev = 1;
HeapDelete(h);
CHECK(IsMinHeap(h));
}
CHECK(h->size == 0);
CHECK(HeapEmpty(h) != 0);
}
static void DestroyHeap(Heap** h)
{
HeapDestory(h);
CHECK(*h == NULL);
}
static void TestInitAndDestroy(void)
{
Heap* h = NewHeap();
HeapInit(h);
CHECK(h->arr != NULL);
CHECK(h->size == 0);
CHECK(h->capacity == 4);
CHECK(HeapEmpty(h) != 0);
CHECK(IsMinHeap(h));
DestroyHeap(&h);
}
static void TestInsertDelete(void)
{
const HDataType values[] = { 5, 3, 8, 1, 9, 2 };
const HDataType expected[] = { 1, 2, 3, 5, 8, 9 };
int i;
Heap* h = NewHeap();
HeapInit(h);
for (i = 0; i < (int)(sizeof(values) / sizeof(values[0])); ++i) {
HeapInsert(h, values[i]);
CHECK(h->size == i + 1);
CHECK(IsMinHeap(h));
}
CHECK(h->capacity == 8);
CheckPopOrder(h, expected, (int)(sizeof(expected) / sizeof(expected[0])));
DestroyHeap(&h);
}
static void TestCapacityGrowth(void)
{
const HDataType expected[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i;
Heap* h = NewHeap();
HeapInit(h);
for (i = 0; i < 4; ++i) {
HeapInsert(h, (HDataType)(i + 1));
}
CHECK(h->size == 4);
CHECK(h->capacity == 4);
HeapInsert(h, 5);
CHECK(h->size == 5);
CHECK(h->capacity == 8);
for (i = 6; i <= 8; ++i) {
HeapInsert(h, (HDataType)i);
}
CHECK(h->size == 8);
CHECK(h->capacity == 8);
HeapInsert(h, 9);
CHECK(h->size == 9);
CHECK(h->capacity == 16);
CHECK(IsMinHeap(h));
CheckPopOrder(h, expected, (int)(sizeof(expected) / sizeof(expected[0])));
DestroyHeap(&h);
}
static void TestInitArray1(void)
{
HDataType input[] = { 9, 4, 7, 1, 8, 2, 6, 3, 5 };
const HDataType expected[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int n = (int)(sizeof(input) / sizeof(input[0]));
Heap* h = NewHeap();
HeapInitArray1(h, input, n);
CHECK(h->arr != NULL);
CHECK(h->size == n);
CHECK(h->capacity == n);
CHECK(IsMinHeap(h));
CheckPopOrder(h, expected, n);
DestroyHeap(&h);
}
static void TestInitArray2(void)
{
HDataType input[] = { 9, 4, 7, 1, 8, 2, 6, 3, 5 };
const HDataType expected[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int n = (int)(sizeof(input) / sizeof(input[0]));
Heap* h = NewHeap();
HeapInitArray2(h, input, n);
CHECK(h->arr != NULL);
CHECK(h->size == n);
CHECK(h->capacity == n);
CHECK(IsMinHeap(h));
CheckPopOrder(h, expected, n);
DestroyHeap(&h);
}
static void TestDuplicatesNegative(void)
{
HDataType input[] = { -3, 0, -3, 5, 0, -10 };
const HDataType expected[] = { -10, -3, -3, 0, 0, 5 };
int n = (int)(sizeof(input) / sizeof(input[0]));
Heap* h = NewHeap();
HeapInitArray2(h, input, n);
CHECK(IsMinHeap(h));
CheckPopOrder(h, expected, n);
DestroyHeap(&h);
}
static void TestSingleElement(void)
{
HDataType input[] = { 42 };
const HDataType expected[] = { 42 };
const HDataType expected_after_insert[] = { 7, 42 };
Heap* h = NewHeap();
HeapInitArray1(h, input, 1);
CHECK(IsMinHeap(h));
CheckPopOrder(h, expected, 1);
DestroyHeap(&h);
h = NewHeap();
HeapInitArray2(h, input, 1);
HeapInsert(h, 7);
CHECK(h->size == 2);
CHECK(h->capacity == 2);
CHECK(IsMinHeap(h));
CheckPopOrder(h, expected_after_insert, 2);
DestroyHeap(&h);
}
static void TestInsertAfterInitArray(void)
{
HDataType input[] = { 4, 1, 3 };
const HDataType expected[] = { 0, 1, 3, 4 };
Heap* h = NewHeap();
HeapInitArray2(h, input, 3);
CHECK(h->capacity == 3);
HeapInsert(h, 0);
CHECK(h->size == 4);
CHECK(h->capacity == 6);
CHECK(IsMinHeap(h));
CheckPopOrder(h, expected, 4);
DestroyHeap(&h);
}
static void TestLarge(void)
{
enum { LARGE_N = 1000 };
unsigned int seed = 123456789u;
int i;
Heap* h = NewHeap();
HeapInit(h);
for (i = 0; i < LARGE_N; ++i) {
seed = seed * 1103515245u + 12345u;
HeapInsert(h, (HDataType)((int)(seed % 2001u) - 1000));
if ((i + 1) % 97 == 0) {
CHECK(IsMinHeap(h));
}
}
CHECK(h->size == LARGE_N);
CHECK(h->capacity >= LARGE_N);
CHECK(IsMinHeap(h));
CheckPopSorted(h);
DestroyHeap(&h);
}
int main(void)
{
printf("Running heap tests...\n");
TestInitAndDestroy();
TestInsertDelete();
TestCapacityGrowth();
TestInitArray1();
TestInitArray2();
TestDuplicatesNegative();
TestSingleElement();
TestInsertAfterInitArray();
TestLarge();
if (g_failures == 0) {
printf("ALL TESTS PASSED\n");
return 0;
}
printf("%d TEST CHECK(S) FAILED\n", g_failures);
return 1;
}
2.4.2 堆经典题目
堆排序
思路:无需单独实现完整的堆数据结构(实现繁琐且空间效率低),可以直接将原数组视作完全二叉树,在数组原地完成建堆、选数的全部操作。
规则:对 n 个数做升序排序时,不能构建小堆。原因是小堆仅能确定首位最小值,后续选取次小值时会打乱剩余数据的堆结构,需要全程重建堆,时间代价极高。(降序排序相反)
时间复杂度为,与C++中的 qsort 时间相当。
代码实现:
cpp
// 堆排序
void HeapSort(HDataType* a, int length) {
assert(a);
if (length <= 1) {
return;
}
//升序排序,建大堆
for (int i = (length - 1 - 1) / 2; i >= 0;i--) {
AdjustDown(a, length, i);
}
int j = length - 1;
while (j > 0) {
Swap(&a[0] , &a[j]);
AdjustDown(a, j, 0);
--j;
}
}
总结:
1、升序排序 → 必须建大堆 :堆顶是全局最大值,方便每次把最大值放到数组末尾。
2、降序排序 → 必须建小堆 :堆顶是全局最小值,方便每次把最小值放到数组末尾。
top-k问题
题目介绍:top-k 是经典算法问题,指从海量数据(数据规模可大到无法一次性全部装入内存)中,快速高效地筛选出最大 / 最小的前 k 个元素,或是出现频率最高的前 k 个元素。
思路:取数组前 k 个元素构建小顶堆;将剩余的 n−k 个元素依次与堆顶比较,若元素大于堆顶则替换堆顶,并向下调整维护堆结构;遍历完成后,堆中留存的就是前 k 大的数据,堆顶元素即为第 k 大的值。
图形演示:

代码实现:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 小顶堆向下调整函数声明
// a:堆数组,n:堆元素总数,root:调整起始根节点下标
void AdjustDown(int* a, int n, int root);
// 生成10万条随机测试数据,写入data.txt文件
void CreateNData() {
int n = 100000;
srand(time(0)); // 初始化随机数种子
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL) {
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i) {
int x = (rand() + i) % 1000000; // 加i分散数据分布
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
// 小顶堆法求解top-k:找出数据中前k大的元素
// 特点:仅占用O(k)内存,支持无法全量加载的海量数据
void topk() {
int k = 0;
printf("请输入k: >");
scanf("%d", &k);
// 输入合法性校验
if (k <= 0) {
printf("错误:k必须为正整数!\n");
return;
}
const char* file = "data.txt";
FILE* fin = fopen(file, "r");
if (fin == NULL) {
perror("fopen error");
return;
}
// 申请大小为k的堆空间
int* minheap = (int*)malloc(sizeof(int) * k);
if (minheap == NULL) {
perror("malloc error");
fclose(fin); // 退出前关闭文件,避免资源泄漏
return;
}
// 读取前k个元素作为堆的初始数据
int readCnt = 0;
for (int i = 0; i < k; ++i) {
if (fscanf(fin, "%d", &minheap[i]) != EOF) {
readCnt++;
} else {
break;
}
}
// 边界处理:文件总数据量不足k
if (readCnt < k) {
printf("文件共%d条数据,不足%d个:\n", readCnt, k);
for (int i = 0; i < readCnt; ++i) {
printf("%d ", minheap[i]);
}
printf("\n");
free(minheap);
fclose(fin);
return;
}
// 构建小顶堆,时间复杂度O(k)
for (int i = (k - 2) / 2; i >= 0; --i) {
AdjustDown(minheap, k, i);
}
// 遍历剩余所有数据,筛选前k大
int x = 0;
while (fscanf(fin, "%d", &x) != EOF) {
// 当前元素比堆顶(第k大)大,则替换并维护堆
if (x > minheap[0]) {
minheap[0] = x;
AdjustDown(minheap, k, 0);
}
}
// 输出结果:堆内为前k大元素,堆顶是第k大的值
printf("前%d大的元素:\n", k);
for (int i = 0; i < k; ++i) {
printf("%d ", minheap[i]);
}
printf("\n");
// 释放内存、关闭文件
free(minheap);
fclose(fin);
}
其他思路不可行的原因:
**1、**全排序法:对全部数据做降序排序,直接取前 k 个。最优排序时间复杂度为
;缺点是必须将所有数据一次性装入内存,无法处理超大规模海量数据。
**2、**全量建大堆法:将全部数据建成大顶堆,连续取 k 次堆顶得到前 k 大元素。时间复杂度
远小于 n 时近似
,k 接近 n 时退化为
;同样依赖全量数据加载到内存,无法处理海量外存数据。
最后,感谢你的阅读,欢迎交流与指正。