学习方向: C++ / 数据结构基础
今日主题: 数组、指针、嵌套循环、递推、原地删除算法
一、今日训练概况
今天进行了第四次 C++ Coding 专项训练,主要用于检验前三次学习的知识是否能够独立应用。
今天最重要的收获是第二题中使用了递推方法,但第三题的原地删除算法暂时没有理解,需要以后继续学习。
二、第一题:数组综合统计
1. 题目要求
已知数组:
int arr[8] = {12, -5, 7, 0, 18, -3, 9, 4};
要求使用指针和循环,完成以下统计:
- 计算所有正数的总和。
- 统计偶数的个数,包括 0。
- 找出最大值及其下标。
- 计算数组的平均值,保留小数。
正确结果:
正数总和:50
偶数个数:4
最大值:18
最大值下标:4
数组平均值:5.25
2. 最初代码中的问题
问题一:累加了下标,而不是数组元素
原代码:
for (int i = 0; i < 8; i++)
{
sum += i;
value = *p;
}
i 表示当前下标,不是数组元素。
因此 sum += i 计算的是:
0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28
但数组所有元素的总和应该是 42。
正确写法:
value = *p;
sum += value;
问题二:混淆计数器和累加器
原代码:
if (value > 0)
{
positive_num++;
}
这段程序统计的是正数个数,而不是正数总和。
正确写法:
if (value > 0)
{
positive_num += value;
}
两者区别:
count++; // 统计次数,增加 1
sum += value; // 累加当前元素的值
问题三:反复重置指针
原代码中多次出现:
p++;
p = arr;
p++ 让指针移动到下一个数组元素。
但是紧接着执行 p = arr,又把指针重新指向数组首元素。
结果导致程序反复读取第一个元素,无法正常遍历整个数组。
正确方法是在每次循环中读取当前元素,完成所有判断后,只移动一次指针:
for (int i = 0; i < 8; i++)
{
int value = *p;
// 在这里完成所有统计
p++;
}
问题四:最大值更新错误
原代码:
if (Max_value < value)
{
Max_value++;
}
Max_value++ 只是让当前值增加 1,不是更新最大值。
正确方法:
if (value > Max_value)
{
Max_value = value;
}
问题五:不会记录最大值下标
后来学习到,可以同时维护最大值变量和下标变量:
int Max_value = *p;
int maxIndex = 0;
当发现更大的元素时:
if (value > Max_value)
{
Max_value = value;
maxIndex = i;
}
Max_value 保存当前最大元素的值,maxIndex 保存该元素在数组中的位置。
问题六:平均值类型与计算时机错误
原代码:
int average = 0;
而平均值可能是小数,因此应使用:
double average = 0;
此外,应该先计算完所有数组元素的总和,再计算平均值:
average = static_cast<double>(sum) / std::size(arr);
3. 修正后的完整代码
#include <iostream>
#include <iterator>
using namespace std;
int main()
{
int arr[8] = {12, -5, 7, 0, 18, -3, 9, 4};
int* p = arr;
int sum = 0;
int positiveSum = 0;
int evenCount = 0;
int maxValue = *p;
int maxIndex = 0;
for (int i = 0; i < 8; i++)
{
int value = *p;
// 所有元素的总和
sum += value;
// 正数总和
if (value > 0)
{
positiveSum += value;
}
// 偶数个数
if (value % 2 == 0)
{
evenCount++;
}
// 最大值及其下标
if (value > maxValue)
{
maxValue = value;
maxIndex = i;
}
p++;
}
// 平均值
double average =
static_cast<double>(sum) / std::size(arr);
cout << "正数总和:" << positiveSum << endl;
cout << "偶数个数:" << evenCount << endl;
cout << "最大值:" << maxValue << endl;
cout << "最大值下标:" << maxIndex << endl;
cout << "数组平均值:" << average << endl;
return 0;
}
4. 本题总结
这道题最大的困难不是单独的 for、if 或指针语法,而是同时管理多个统计任务。
需要明确不同变量的用途:
sum数组总和+= valuepositiveSum正数总和+= valueevenCount偶数个数++maxValue当前最大值= valuemaxIndex最大值下标= ip当前读取位置p++以后编写类似综合题时,需要先确定每个变量保存什么,再决定什么时候更新。
三、第二题:数字三角形综合统计
1. 题目要求
输入一个正整数 n,输出数字三角形,同时统计:
- 每行的数字总和。
- 每行偶数出现的次数。
- 所有行的数字总和。
- 偶数出现的总次数。
输入:
4
预期输出:
1 行和:1 偶数个数:0
12 行和:3 偶数个数:1
123 行和:6 偶数个数:1
1234 行和:10 偶数个数:2
总和:20
偶数总次数:4
2. 独立完成的代码
#include <iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int rowSum = 0;
int evenCount = 0;
int totalSum = 0;
int totalEvenCount = 0;
for (int i = 1; i <= n; i++)
{
rowSum += i;
for (int k = 1; k <= i; k++)
{
cout << k;
}
if (i % 2 == 0)
{
evenCount++;
}
cout << " 行和:" << rowSum
<< " 偶数个数:" << evenCount
<< endl;
totalSum += rowSum;
totalEvenCount += evenCount;
}
cout << "总和:" << totalSum << endl;
cout << "偶数总次数:" << totalEvenCount << endl;
return 0;
}
这道题独立完成,结果正确。
3. 为什么 rowSum 不清零也能得到正确结果?
之前做类似题目时,采用的是每行重新清零的方法:
rowSum = 0;
for (int k = 1; k <= i; k++)
{
rowSum += k;
}
但今天使用的是:
rowSum += i;
最初以为这种写法有问题,后来分析发现,它实际上利用了数字三角形相邻两行之间的关系。
观察:
第1行:1 行和 1
第2行:1 2 行和 3
第3行:1 2 3 行和 6
第4行:1 2 3 4 行和 10
每一行都比上一行多了一个新数字 i。
因此:
第一行:0 + 1 = 1
第二行:1 + 2 = 3
第三行:3 + 3 = 6
第四行:6 + 4 = 10
这说明可以直接利用上一行的结果,计算当前行的总和。
这种方法称为 递推(Recurrence)。
4. evenCount 也是同样的原理
今天使用:
if (i % 2 == 0)
{
evenCount++;
}
因为每一行相较于上一行只增加一个数字 i,所以只需要判断这个新增数字是不是偶数。
如果是偶数,当前行偶数个数就比上一行增加 1。
5. 递推和普通累加的区别
普通方法:
每一行重新遍历所有数字,独立计算本行结果。
递推方法:
保留上一行的结果,只根据新增加的元素更新当前行。
对于这道题,递推更简洁,也减少了重复统计工作。
不过递推依赖于相邻两行存在明确关系,不能直接套用到所有数字三角形问题。
6. 本题总结
今天第二题的进步比较明显。
不仅独立写出了正确的嵌套循环,还使用了之前没有专门学习过的递推思路。
这说明已经开始尝试利用数据本身的规律设计程序,而不只是机械地按照题目要求编写循环。
这个思路值得以后继续训练。
四、第三题:双指针原地删除数组元素
掌握状态:未掌握,需要重新学习。
1. 题目要求
已知数组:
int arr[8] = {3, 0, 7, 0, 5, 9, 0, 2};
要求删除所有值为 0 的元素,将非零元素移动到数组前面,同时保持相对顺序。
预期输出:
删除后的数组:3 7 5 9 2
有效元素数量:5
2. 最初的思路
最开始想到的是:如果发现一个 0,就将后面所有元素向左移动一个位置。
主要代码:
int arr[8] = {3, 0, 7, 0, 5, 9, 0, 2};
int* p = arr;
int value = *p;
int n = 8;
int index = 0;
for (int k = 0; k < 8; k++)
{
if (value == 0)
{
for (int i = index; i < n - 1; i++)
{
*(p + i) = *(p + i + 1);
}
n--;
for (int i = 0; i < n; i++)
{
cout << *(p + i) << " ";
}
}
}
这种思路属于数组元素的左移删除法,原理本身可行,但实际代码没有完成。
3. 遇到的问题
问题一:value 没有更新
int value = *p;
这句话写在循环外面,只读取了一次数组首元素。
进入循环后,value 一直没有变化,因此无法依次判断数组中的所有元素。
问题二:index 没有变化
int index = 0;
后续始终没有更新 index,导致即使发现需要删除的元素,也无法从正确位置开始左移。
问题三:删除后没有重新检查当前位置
删除一个元素并把后面的元素左移后,新的元素会移动到被删除的位置。
如果连续出现两个零,就不能直接跳过这个位置。
问题四:循环范围与有效长度不一致
虽然使用 n-- 减少有效元素数量,但外层循环仍按照原始长度 8 执行,没有正确处理删除后数组的有效范围。
4. 学习到的新方法:双指针覆盖法
后来学习了另一种更简单的实现方式。
使用两个指针:
int* read = arr;
int* write = arr;
其中:
read负责依次读取所有元素。write指向下一个非零元素应该保存的位置。
当 read 读取到非零元素时:
*write = *read;
write++;
每次循环,无论当前值是否为零,read 都需要移动:
read++;
这样就能把所有非零元素依次覆盖到数组前面。
5. 参考代码(尚未独立掌握)
#include <iostream>
using namespace std;
int main()
{
int arr[8] = {3, 0, 7, 0, 5, 9, 0, 2};
int* read = arr;
int* write = arr;
for (int i = 0; i < 8; i++)
{
if (*read != 0)
{
*write = *read;
write++;
}
read++;
}
int validCount = static_cast<int>(write - arr);
cout << "删除后的数组:";
for (int i = 0; i < validCount; i++)
{
cout << *(arr + i) << " ";
}
cout << "\n有效元素数量:" << validCount;
return 0;
}
6. 暂时没有完全理解的知识
第一:为什么要使用两个指针?
此前主要使用一个指针遍历数组,现在需要同时维护读取位置和写入位置。
第二:为什么 write 有时移动,有时不移动?
读取到 0 时,write 不应该移动,因为没有需要保留的元素。
读取到非零元素时,才需要将它保存到当前写入位置。
第三:为什么 write - arr 等于有效元素数量?
这是指针减法的应用。
对于同一数组中的指针,相减得到的是元素位置之差,而不是字节数量。
第四:数组原地删除究竟意味着什么?
原地删除并不意味着数组的物理内存长度变短。
这道题只是把需要保留的元素移动到前面,并通过 validCount 记录有效范围。
7. 本题总结
这道题没有独立完成,暂时标记为未掌握。
虽然已经理解了部分左移删除法和双指针覆盖法的原理,但仍需要进一步学习两个指针如何协同移动、原地修改数组与有效长度管理。
下一次可以先使用简单数组模拟读写指针的位置变化,再尝试重新实现,不必直接挑战完整删除算法。
五、今日知识点总结
for 循环边界基本掌握嵌套循环结构基本掌握数组指针访问基本掌握多变量综合统计需要巩固计数器与累加器需要巩固最大值及下标更新需要巩固递推思想初步理解并成功应用双指针交换此前已掌握基础双指针覆盖法未掌握数组原地删除未掌握指针相减新接触,需要巩固六、四次 Coding 训练进度分析
Day 1:11 月 8 日
主要学习循环、累加、数组与基础指针。
当时频繁出现循环边界错误,并且对嵌套循环和累加器理解不充分。
Day 2:11 月 10 日
开始使用指针寻找最小值,并独立完成双指针反转数组。
循环边界问题有所改善,但多个循环的执行顺序、变量初始化仍存在不足。
Day 3:11 月 11 日
开始练习多步骤统计程序。
已经能正确使用两个独立循环,完成指针重置,并在一个循环中进行多项统计。
但累加器初始化和条件判断仍需要巩固。
Day 4:11 月 12 日
开始进行独立综合测验。
第一题暴露出多个统计任务组合时的变量更新问题。
第二题独立完成,并使用了递推方法。
第三题涉及数组原地修改和双指针读写操作,暂时未能完成。
目前可以看出,简单循环与数组遍历已经相对熟悉,但涉及多个变量协同更新、数据位置修改的算法仍存在困难。
七、下一阶段学习安排
下一次 Coding 训练重点不再增加新知识,而是进行针对性的巩固。
重点一:多变量综合统计
继续区分累加器、计数器、最大值及下标变量,训练每个变量的正确更新方式。
重点二:双指针读写
先学习两个指针如何分别读取和写入元素,通过简单例子理解指针位置变化。
重点三:数组原地修改
重新练习本次第三题,但先不要求一次性完成全部功能。
重点四:独立算法设计
每道题先分析输入、输出、变量及执行步骤,再动手编写代码。
在这些基础掌握得更加稳定之后,再开始学习单链表的创建、遍历和插入。
八、今日总结
今天三道综合练习中,第一题部分完成,第二题独立正确完成,第三题没有完成。
最大的进步,是在数字三角形题目中自主利用相邻两行之间的规律进行递推。
最大的不足,是多个统计变量与指针位置之间的关系仍容易混淆,特别是涉及原地修改数组时,还不能独立设计完整算法。
接下来需要继续强化变量状态管理与双指针读写操作,而不是急于增加题目难度。
学习原则:先理解算法,再独立实现;不能完成的题目如实记录,理解后再重新练习。