既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
- 比较
N
与孩子结点的大小关系。如果N
大于等于两个孩子结点,调整结束;不然就让N
与较大的孩子交换。 - 重复2过程,直到
N
调整结束或者被调整到叶子结点。
小堆类比大堆思想就可以:
- 保证要调整结点
N
的左右子树都是小堆。 - 比较
N
与孩子结点的大小关系。如果N
小于等于两个孩子结点,调整结束;不然就让N
与较小的孩子交换。 - 重复2过程,直到
N
调整结束或者被调整到叶子结点。
上述过程N
结点始终不变,向下调整的意思就是,向下调整N
结点到应在的位置
根据以上思想,我们可以实现向下调整:
// 大堆向下调整
// 向下调整结束条件
// 1. 被调整结点 >= 孩子
// 2. 调整到叶子节点
void AdjustDown\_big(HPDataType\* a, int n, int parent)
{
assert(a);
int child = parent \* 2 + 1;
//孩子必须在堆的范围内
while (child < n)
{
//选出两个孩子中最大的进行交换,并且要保证右孩子存在
if (a[child + 1] > a[child] && child + 1 < n)
{
child++;
}
//孩子大于父亲,则交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
parent = child;
child = parent \* 2 + 1;
}
}
// 小堆向下调整
// 向下调整结束条件
// 1. 被调整结点 <= 孩子
// 2. 调整到叶子节点
void AdjustDown\_little(HPDataType\* a, int n, int parent)
{
assert(a);
int child = parent \* 2 + 1;
//孩子必须在堆的范围内
while (child < n)
{
//选出两个孩子中最小的进行交换,并且要保证右孩子存在
if (a[child + 1] < a[child] && child + 1 < n)
{
child++;
}
//孩子小于父亲,则交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
parent = child;
child = parent \* 2 + 1;
}
}
这里我们假设我们堆就是满二叉树,向下调整一次最坏情况的**时间复杂度为
O
(
l
o
g
2
n
)
O(log_2~n)
O(log2 n)**
我们现在回到最开始提出的问题,一个完全二叉树怎么才能调整为一个大堆?
因为向下调整要保证左右子树都是大堆,所以我们不可能从根结点开始调整,但是我们可以倒着调整。
- 我们可以从倒数第一个非叶子结点开始向下调整,如上图
3
结点
- 再向下调整倒数第二个非叶子结点
5
3.这时根结点的左右子树就是大堆了,我们现在来调整它
调整完毕,这时我们就得到了一个大堆。
具体调整过程如下:
现在我们就可以抽象出一种向下调整建堆的通法:
- 从最后一个非叶子结点开始向下调整,一直调整到根结点
最后,再补充一点向下建堆的时间复杂度(了解即可)
向上调整算法
来看这样一种情况,现在在一个大堆后面插入一个数9
,由于9>6,所以现在构不成大堆了,要怎么调整才能将其再变为大堆呢?
- 由于9>6,所以我们交换
6
和9
结点。
- 比较
8
和9
,8 < 9,所以根据大堆规则,交换8
和9
。
9
已经被调整到根结点,调整结束。
同样我们可以上述过程中得到一些经验:
- 被调整的结点以前的结点必须构成一个大堆
- 该调整也只会影响从被调整结点到最后调整到的位置的结点这一条路径,完全二叉树其余结点都不受影响
从以上我们可以抽象出向上调整的过程(大堆):
- 保证被调整的结点
N
以前的结点构成一个大堆。 - 被调整的结点
N
与其父节点比较大小,如果N
大于父节点,则交换这两个结点;如果N
小于等于父节点,则结束调整。 - 重复2过程,直到
N
被调整调整完毕或者调整到根结点。
小堆的向上调整类比大堆可得:
- 保证被调整的结点
N
以前的结点构成一个小堆。 - 被调整的结点
N
与其父节点比较大小,如果N
小于父节点,则交换这两个结点;如果N
大于等于父节点,则结束调整。 - 重复2过程,直到
N
被调整调整完毕或者调整到根结点。
// 大堆向上调整
// 调整结束条件:
// 1.父节点 >= 被调整结点
// 2.被调整结点已经调整到根结点
void AdjustUp\_big(HPDataType\* a, int child)
{
assert(a);
while (child > 0)
{
int parent = (child - 1) / 2;
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
child = parent;
}
}
// 小堆向上调整
// 调整结束条件:
// 1.父节点 <= 被调整结点
// 2.被调整结点已经调整到根结点
void AdjustUp\_little(HPDataType\* a, int child)
{
assert(a);
//当child为0时,结束循环
while (child > 0)
{
int parent = (child - 1) / 2;
//孩子小于父亲时交换,否则退出
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
child = parent;
}
}
那么向上调整可不可以将一个完全二叉树调整成堆呢?
答案是可以的,但是向上调整前提是被调整的结点以前的结点必须构成一个堆,所以必须要从上到下向上调整,具体要从第二个结点开始调整,保证前面的都是大堆,一直要调整到最后一个结点。
所以向上调整建堆所花费的时间要比向下调整建堆花费的时间长,一般调整一棵完全二叉树成堆我们使用向下调整法。
向上调整的真正用处在于在堆后面插入数据,我们就要用向上调整将这个新插入的数据调整到正确的位置上,使这棵完全二叉树再建成堆。这个用处我们后面还会提到。
堆的插入
上文提到了向上调整一般用来插入数据,现在我们就来具体探讨一下堆的插入。
- 当堆中插入数据时,首先要判断堆中是否已经满了,如果满了的话,就要扩容;
- 然后将数据插入到堆的最后;
- 最后将插入的数据向上调整。
具体代码实现:
// 大堆数据的插入
void HeapPush\_big(Heap\* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newCapacity = hp->capacity == 0 ? 4 : hp->capacity \* 2;
HPDataType\* tmp = (HPDataType\*)realloc(hp->a, newCapacity\*sizeof(HPDataType));
if (tmp == NULL)
{
printf("relloc fail");
exit(-1);
}
hp->capacity = newCapacity;
hp->a = tmp;
}
hp->a[hp->size] = x;
AdjustUp\_big(hp->a, hp->size);
hp->size++;
}
// 小堆数据的插入
void HeapPush\_little(Heap\* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newCapacity = hp->capacity == 0 ? 4 : hp->capacity \* 2;
HPDataType\* tmp = realloc(hp->a, newCapacity \* sizeof(HPDataType));
if (tmp == NULL)
{
printf("relloc fail");
exit(-1);
}
hp->capacity = newCapacity;
hp->a = tmp;
}
hp->a[hp->size] = x;
AdjustUp\_little(hp->a, hp->size);
hp->size++;
}
代码逻辑运行过程:
堆的删除
堆的删除是指删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
堆的删除指的是删除堆顶的元素,也就是说删除顺序表数组中下标为0的元素。
直接删除堆顶元素肯定行不通,因为直接删除这个元素会,将数据整体向前移动一位,这样大概率会破坏堆的结构,所以我们要利用堆的特性删除栈顶元素。
我们以大堆为例,要删除栈顶元素(也就是整个栈中最大的元素)
- 我们可以将栈顶元素与栈底元素交换,然后删除数组最后一个数据,这时候栈顶元素已经被删除了。
- 现在根结点左右子树都是大堆,所以我们可以使用向下调整,调整根结点的位置,重新构建成堆。
小堆
具体实例如下:
具体代码实现如下:
// 大堆数据的删除
void HeapPop\_big(Heap\* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_big(hp->a, hp->size, 0);
}
// 小堆数据的删除
void HeapPop\_little(Heap\* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_little(hp->a, hp->size, 0);
}
代码逻辑运行过程:
进阶
:
我们可以思考一个问题,堆如何实现任意删除?
任意删除其实思想仍是上述思想,我们知道移除一个元素会破坏大堆或者小堆属性,所以我们需要将要删除的元素和最后一个元素交换,再向下调整原最后一个元素。
具体思路:
- 上文已经讲述了堆的查找,所以我们要删除指定的元素的话,先可以使用堆的查找函数把要删除的元素下标找到
- 然后必须保证下标大于等于0,小于堆的大小
- 接着交换要删除的元素和最后一个元素,删除最后一个数组数据
- 向下调整原最后一个元素
具体代码实现如下:
// 大堆任意删除元素
void HeapDelect\_big(Heap\* hp,int x)
{
assert(hp);
assert(!HeapEmpty(hp));
assert(x >= 0 && x < hp->size);
Swap(&hp->a[x], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_big(hp->a, hp->size, x);
}
// 小堆任意删除元素
void HeapDelect\_little(Heap\* hp,int x)
{
assert(hp);
assert(!HeapEmpty(hp));
assert(x >= 0 && x < hp->size);
Swap(&hp->a[x], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_little(hp->a, hp->size, x);
}
其实上面的任意删除函数仍然有优化的空间,因为下标是随着堆的结构变化不停变动的,所以使用一次删除前就必须查找一次。
但是我们如果可以改进查找函数,使其返回对应位置的指针,在删除时,传递指针,找到此数据进行删除。此功能较好实现,大家可以自己试一试将其实现。
最后,堆的删除大部分都是删除堆顶元素,所以任意删除不是必须要掌握,大家做一了解即可。
🍋堆的应用
我们已经了解了堆的结构、特性和常用的接口函数,现在就可以将其应用到日常的问题中。堆最常用的应用一般有两个——Topk问题和堆排序。
Topk问题
Topk问题指的是选出一株数据中最大或者最小的几个。
如,王者荣耀中英雄的国服最强,考试成绩的全校前十等
如果给我们一组数据,如果要让我们选出最大的10个数,那么我们可能有以下思路:
方式2到方式1的时间复杂度小了很多,但是这两个方法都有一个致命的缺陷——**空间复杂度为
O
(
N
)
O(N)
O(N)**。
我们来假设一种场景,如果我们有10亿个数据,内存中无法存储这么多数据,那应该怎么办呢?
有机智的小伙伴可能就会说用外部排序不就可以给磁盘中的数据排序了吗?这是一种可行的方法,但是如果为了找10个数据就要排序全部的数据,时间成本和消耗都太大,得不偿失。
所以,现在就是体现我们堆结构优越性的地方了,我们可以得到第三种方式:
- 首先解释为什么要建小堆,如果要排最大几个数,一般思路应该是建大堆,但是大堆有个致命的缺点,大堆堆顶元素是最大的元素,如果刚好最大的元素在前k个数中,那么直接就会堵死堆,应该用什么方法判断一个数据是否是最大的几个数(是否能进堆)呢?
- 当然不能与堆中的全部元素比较,这样时间复杂度太高。所以我们可以先建一个大小为k的小堆,这样堆顶数据就是堆中数据中最小的数据,其余的数据只要大于栈顶的数据就可以进堆,然后再向下调整,使堆中最小的数据再次回到堆顶。
- 这里我们可以感性认识一下,小堆可以让小的数据占据堆顶,而让大的数据向下滑,所以不会堵死堆。
- 最后比较完全部的数,堆里的数就是最大的k个数。
选出最小的数也是一个道理:
- 用前k个数建一个大堆(最大的数在堆顶)
- 用剩下的数与栈顶数据比较,如果小于这个数就替换堆顶的数,再向下调整,使堆中最大的数始终在堆顶
- 比较结束后,堆中的k个数就是最小的k个数
特别提醒
:这种方法只是选出最大或者最小的前k个数,并不会给这k个数排序。
具体代码实现如下:
// 找出最大的前k个数
void PrintTopK(int\* a, int n, int k)
{
assert(a);
// 建小堆
Heap hp;
HeapInit(&hp);
for (int i = 0; i < k; i++)
{
HeapPush\_little(&hp, a[i]);
}
// 堆顶元素与其余元素比较
for (int i = k; i < n; i++)
{
if (a[i] > HeapTop(&hp))
{
hp.a[0] = a[i];
AdjustDown\_little(hp.a, hp.size, 0);
}
}
HeapPrint(&hp);
HeapDestory(&hp);
}
// 找出最小的前k个数
void PrintTopK(int\* a, int n, int k)
{
assert(a);
// 建大堆
Heap hp;
HeapInit(&hp);
for (int i = 0; i < k; i++)
{
HeapPush\_big(&hp, a[i]);
}
// 堆顶元素与其余元素比较
for (int i = k; i < n; i++)
{
if (a[i] > HeapTop(&hp))
{
hp.a[0] = a[i];
AdjustDown\_big(hp.a, hp.size, 0);
}
}
HeapPrint(&hp);
HeapDestory(&hp);
}
堆排序
堆排序
(Heapsort) 是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
所以说堆排序就是利用堆的性质完成排序的过程,堆排序主要妙就妙在它的思想上,这里我们直接来看堆排序的思想:
总共分为两个步骤:
- 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
具体解释(升序):
- 升序就是从小到大排列,正常思维应该是建小堆,可是建小堆无法保证除了堆顶元素其余元素的顺序。所以我们要反其道而行之,我们先建大堆,堆顶元素就是最大的元素。
- 然后利用堆删除的思想,交换堆顶和堆底的元素,然后将堆的大小减小1个数据,这时候最大的元素就被换到了最后,再向下调整现在的栈顶元素。
- 重复过程2,直到剩堆中只剩一个元素,就完成了堆排序。
降序的思想也与升序相似:
- 降序就是从大到小排列,正常思维应该是建大堆,可是建大堆无法保证除了堆顶元素其余元素的顺序。所以我们要反其道而行之,我们先建小堆,堆顶元素就是最小的元素。
- 然后利用堆删除的思想,交换堆顶和堆底的元素,然后将堆的大小减小1个数据,这时候最小的元素就被换到了最后,再向下调整现在的栈顶元素。
- 重复过程2,直到剩堆中只剩一个元素,就完成了堆排序。
具体实例图解如下:
堆排序动态的逻辑过程:
具体代码实现:
// 堆排序升序
void HeapSortUp(HPDataType\* a, int n)
{
assert(a);
// 向下调整建大堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown\_big(a, n, i);
}
// 将最后一个元素与堆顶元素交换,选出最大的元素,堆大小减一,然后向下调整,选出此大的元素,以此类推
for (int i = n - 1; i > 0; i--)
{
Swap(&a[0], &a[i]);
AdjustDown\_big(a, i, 0);
}
}
// 堆排序降序
void HeapSortDown(HPDataType\* a, int n)
{
assert(a);
// 向下调整建小堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown\_little(a, n, i);
}
// 将最后一个元素与堆顶元素交换,选出最小的元素,堆大小减一,然后向下调整,选出此大的元素,以此类推
for (int i = n - 1; i > 0; i--)
{
Swap(&a[0], &a[i]);
AdjustDown\_little(a, i, 0);
}
}
代码逻辑动态展示:
🍍堆的全局代码
typedef int HPDataType;
typedef struct Heap
{
HPDataType\* a;
int size;
int capacity;
}Heap;
// 堆的构建
void HeapInit(Heap\* hp);
// 堆的销毁
void HeapDestory(Heap\* hp);
// 向上调整
void AdjustUp\_big(HPDataType\* a, int child);
void AdjustUp\_little(HPDataType\* a, int child);
// 堆的插入
void HeapPush\_big(Heap\* hp, HPDataType x);
void HeapPush\_little(Heap\* hp, HPDataType x);
// 堆的打印
void HeapPrint(Heap\* hp);
// 堆的删除
void HeapPop\_big(Heap\* hp);
void HeapPop\_little(Heap\* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap\* hp);
// 堆的数据个数
int HeapSize(Heap\* hp);
// 堆的判空
bool HeapEmpty(Heap\* hp);
// 堆的查找
int HeapSearch(Heap\* hp, HPDataType x);
// 堆的任意删除
void HeapDelect\_big(Heap\* hp, int x);
void HeapDelect\_little(Heap\* hp, int x);
// TopK问题:找出N个数里面最大/最小的前K个问题。
// 需要注意:
// 找最大的前K个,建立K个数的小堆
// 找最小的前K个,建立K个数的大堆
void PrintTopK(int\* a, int n, int k);
// 对数组进行堆排序
void HeapSortUp(HPDataType\* a, int n);
void HeapSortDown(HPDataType\* a, int n);
void HeapInit(Heap\* hp)
{
assert(hp);
hp->a = NULL;
hp->size = 0;
hp->capacity = 0;
}
void HeapDestory(Heap\* hp)
{
assert(hp);
free(hp->a);
hp->capacity = 0;
hp->size = 0;
}
bool HeapEmpty(Heap\* hp)
{
assert(hp);
return hp->size == 0;
}
HPDataType HeapTop(Heap\* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
return hp->a[0];
}
int HeapSize(Heap\* hp)
{
assert(hp);
return hp->size;
}
void HeapPrint(Heap\* hp)
{
assert(hp);
int i = 0;
for (i = 0; i < HeapSize(hp); i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
void Swap(HPDataType\* x, HPDataType\* y)
{
HPDataType tmp = \*x;
\*x = \*y;
\*y = tmp;
}
void AdjustUp\_big(HPDataType\* a, int child)
{
assert(a);
while (child > 0)
{
int parent = (child - 1) / 2;
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
child = parent;
}
}
void AdjustUp\_little(HPDataType\* a, int child)
{
assert(a);
//当child为0时,结束循环
while (child > 0)
{
int parent = (child - 1) / 2;
//孩子小于父亲时交换,否则退出
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
child = parent;
}
}
void HeapPush\_big(Heap\* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newCapacity = hp->capacity == 0 ? 4 : hp->capacity \* 2;
HPDataType\* tmp = (HPDataType\*)realloc(hp->a, newCapacity\*sizeof(HPDataType));
if (tmp == NULL)
{
printf("relloc fail");
exit(-1);
}
hp->capacity = newCapacity;
hp->a = tmp;
}
hp->a[hp->size] = x;
AdjustUp\_big(hp->a, hp->size);
hp->size++;
}
void HeapPush\_little(Heap\* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newCapacity = hp->capacity == 0 ? 4 : hp->capacity \* 2;
HPDataType\* tmp = realloc(hp->a, newCapacity \* sizeof(HPDataType));
if (tmp == NULL)
{
printf("relloc fail");
exit(-1);
}
hp->capacity = newCapacity;
hp->a = tmp;
}
hp->a[hp->size] = x;
AdjustUp\_little(hp->a, hp->size);
hp->size++;
}
void AdjustDown\_big(HPDataType\* a, int n, int parent)
{
assert(a);
int child = parent \* 2 + 1;
//孩子必须在堆的范围内
while (child < n)
{
//选出两个孩子中最大的进行交换,并且要保证右孩子存在
if (a[child + 1] > a[child] && child + 1 < n)
{
child++;
}
//孩子大于父亲,则交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
parent = child;
child = parent \* 2 + 1;
}
}
void AdjustDown\_little(HPDataType\* a, int n, int parent)
{
assert(a);
int child = parent \* 2 + 1;
//孩子必须在堆的范围内
while (child < n)
{
//选出两个孩子中最小的进行交换,并且要保证右孩子存在
if (a[child + 1] < a[child] && child + 1 < n)
{
child++;
}
//孩子小于父亲,则交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
parent = child;
child = parent \* 2 + 1;
}
}
void HeapPop\_big(Heap\* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_big(hp->a, hp->size, 0);
}
void HeapPop\_little(Heap\* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_little(hp->a, hp->size, 0);
}
int HeapSearch(Heap\* hp, HPDataType x)
{
assert(hp);
for (int i = 0; i < hp->size; i++)
{
if (hp->a[i] == x)
{
return i;
}
}
return -1;
}
void PrintTopK(int\* a, int n, int k)
{
assert(a);
Heap hp;
HeapInit(&hp);
for (int i = 0; i < k; i++)
{
HeapPush\_little(&hp, a[i]);
}
for (int i = k; i < n; i++)
{
if (a[i] > HeapTop(&hp))
{
hp.a[0] = a[i];
AdjustDown\_little(hp.a, hp.size, 0);
}
}
HeapPrint(&hp);
HeapDestory(&hp);
}
void HeapSortUp(HPDataType\* a, int n)
{
![img](https://img-blog.csdnimg.cn/img_convert/2babfad69f9c14e8272571af9898f1f5.png)
![img](https://img-blog.csdnimg.cn/img_convert/4c77777669d22b851cbc72f20a09cac7.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**
ssert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_big(hp->a, hp->size, 0);
}
void HeapPop\_little(Heap\* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown\_little(hp->a, hp->size, 0);
}
int HeapSearch(Heap\* hp, HPDataType x)
{
assert(hp);
for (int i = 0; i < hp->size; i++)
{
if (hp->a[i] == x)
{
return i;
}
}
return -1;
}
void PrintTopK(int\* a, int n, int k)
{
assert(a);
Heap hp;
HeapInit(&hp);
for (int i = 0; i < k; i++)
{
HeapPush\_little(&hp, a[i]);
}
for (int i = k; i < n; i++)
{
if (a[i] > HeapTop(&hp))
{
hp.a[0] = a[i];
AdjustDown\_little(hp.a, hp.size, 0);
}
}
HeapPrint(&hp);
HeapDestory(&hp);
}
void HeapSortUp(HPDataType\* a, int n)
{
[外链图片转存中...(img-CLWeFf3O-1715726209317)]
[外链图片转存中...(img-1xxORpbe-1715726209317)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**