问题1

扫雷

  1. 起始位置有问题,应该是从i=1开始
  2. 输入问题scanf("%c", &board_mine[i][j]) 会读取到换行符(\n),导致实际字符读取出错;
int row, col;
    scanf("%d %d", &row, &col);
    char board_mine[row + 2][col + 2];
    // 布置棋盘
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            scanf("%c", &board_mine[i][j]);
        }
    }

方法1:用getchar吸收(这里之前有一篇说过,还是忘)

int row, col;
    // 读取行列数
    scanf("%d %d", &row, &col);
    // 1. 扩大数组范围(上下左右各留1行/列),避免越界
    char board_mine[row + 2][col + 2] = {0};
    // 2. 修正输入:先吸收换行符,再读取每行字符
    getchar(); // 吸收scanf("%d %d")后的换行符
    for (int i = 1; i <= row; i++) // 从1开始,留0行/列作为边界
    {
        for (int j = 1; j <= col; j++)
        {
            scanf("%c", &board_mine[i][j]);
        }
        getchar(); // 吸收每行输入后的换行符
    }

方法2:为了读取输入中描述雷区布局的每一行字符串,而创建str数组,用来临时存储这一行的输入内容

//读取
    for(int i = 1; i <= n; i++){
        char str[m];
        scanf("%s", str);
        for(int j = 0; j < m; j++){
            data[i][j+1] = str[j];
        }
    }

%s不会读取\n,空格等空白字符

具体解释:

  1. 读取的内容:输入中,在第一行的nm之后,会有n行、每行m个字符的输入(比如示例中的*???????*),这些字符代表雷区的布局(*是地雷,?是非地雷)。这段代码就是把这n行的字符,逐行读入并存储到data数组中。

  2. 为什么创建str数组scanf("%s", str)是用来读取一行字符串的(因为输入中每行的m个字符是连续的,没有分隔符)。所以需要先创建一个str数组作为 “临时容器”,把这一行的字符串先存到str里,再逐个字符转移到data数组的对应位置(data[i][j+1] = str[j])。

举个例子(对应示例输入):

  • i=1时,读取第一行输入*??,存到str数组(str[0]='*'str[1]='?'str[2]='?'),然后把这些字符赋值给data[1][1]data[1][2]data[1][3]
  • 后续i=2i=3同理,最终把所有雷区布局存到data数组中。

问题2

26. 删除有序数组中的重复项 - 力扣(LeetCode)

1.memmove函数

memmove(nums+j,nums+j+1,numsSize-j-1);
memmove 函数原型:void *memmove(void *dest, const void *src, size_t n);

注意最后的参数单位是字节!

所以应该是

memmove(nums+j,nums+j+1,(numsSize-j-1)*sizeof(int));

2.memmove移动数组的元素个数

这个错误是数组越界访问导致的,问题出在 memmove 的参数上。

错误原因

memmove(nums+j, nums+j+1, (numsSize-j)*sizeof(int)) 中,numsSize-j 是待移动的元素个数,但实际需要移动的元素个数应该是 (numsSize - j - 1)(因为 j+1 到 numsSize-1 共 numsSize-1 - (j+1) + 1 = numsSize - j - 1 个元素)。

当 j = numsSize-1 时,numsSize-j = 1,会尝试访问 nums[j+1] = nums[numsSize],这超出了数组的合法范围(数组下标最大为 numsSize-1),从而触发堆缓冲区溢出

3.方法优化-->用双指针

双指针法的核心是用两个指针在一次遍历中完成原本需要多次遍历的操作,通常用于 ** 有序结构、线性数据结构(数组 / 链表)** 的场景,常见适用情况有这些:

1. 有序数组 / 链表的去重、查找

比如 “删除有序数组中的重复项”“两数之和(有序数组)”,利用有序的特性,通过快慢指针 / 左右指针缩小范围。

  • 例子:有序数组两数之和 → 左指针在开头,右指针在末尾,根据和的大小移动指针。

2. 数组 / 链表的遍历优化

比如 “链表的中间节点”“链表的倒数第 k 个节点”,用快慢指针(快指针走 2 步,慢指针走 1 步)一次遍历完成。

  • 例子:找链表中间节点 → 快指针到尾时,慢指针刚好在中间。

3. 原地修改数组(避免额外空间)

比如 “移除元素”“移动零”,用快慢指针区分 “已处理区域” 和 “待处理区域”,原地覆盖重复 / 目标元素。

  • 例子:移动零 → 慢指针指向非零元素的下一个位置,快指针遍历数组,遇到非零就交换到慢指针位置。

4. 滑动窗口问题

比如 “长度最小的子数组”“无重复字符的最长子串”,用左右指针维护一个动态窗口,通过移动指针调整窗口范围。

  • 例子:无重复字符的最长子串 → 右指针扩展窗口,左指针收缩窗口以排除重复字符。

简单说:当需要减少遍历次数、原地操作、利用线性结构的有序 / 连续特性时,双指针是很高效的选择。

我的双指针代码
//用双指针
int removeDuplicates2(int* nums, int sz)
{
    if (sz == 0)//考虑特殊情况-->为空时
    {
        return 0;
    }
    int* fast = nums + 1;//找重复的元素
    int* slow = nums + 1;
    while (*fast!='\n')
    {
        if (*fast != *fast-1)//覆盖
        {
            *slow = *fast;
            slow++;
            sz--;
        }
        fast++;
    }
    return sz;
}
  1. 循环终止条件错误while (*fast!='\n') 完全不合理。nums 是整型数组,不是字符串,不存在 '\n' 结束符,这会导致循环要么提前终止,要么无限循环(访问非法内存)。正确的终止条件应该是判断 fast 指针是否遍历完数组(fast < nums + sz)。
  •   逻辑上:只有找到重复元素时,数组有效长度才会减少;
  • 你实际做的:找到不重复元素时反而减长度 → 相当于 “每保留一个有效元素,就少算一个长度”,结果必然完全错误。
  1. 重复判断逻辑错误if (*fast != *fast-1) 是语法错误(*fast-1 是数值运算,不是指针访问),正确的逻辑应该是判断当前快指针指向的值,是否和慢指针前一个位置的值重复(*fast != *(slow - 1))。
  2. 数组长度更新错误sz-- 放在了错误的位置 —— 只有找到重复元素时才需要减少长度,但你在 “找到不重复元素” 时执行 sz--,会导致长度计算完全错误(实际应该用慢指针的位置计算新长度,而非修改原 sz)。
  3. 指针初始化逻辑偏差:慢指针 slow 初始化为 nums + 1 是对的,但核心逻辑是 “慢指针指向待覆盖的位置”,需要基于慢指针的最终位置返回新长度,而非直接修改传入的 sz
//用双指针
int removeDuplicates2(int* nums, int sz)
{
    if (sz == 0)//考虑特殊情况-->为空时
    {
        return 0;
    }
    int* fast = nums + 1;//找重复的元素
    int* slow = nums + 1;
   
    while (fast<sz)//是数组,字符串才能用*fast!='\n'
    {
        if (*fast != *(fast-1))//找到不重复的覆盖,注意优先级*fast-1是先解引用再计算
        {
            *slow = *fast;
            slow++;
        }
        fast++;
       // sz--;会改变循环条件
    }
    return slow-nums;
}
 fast是int*类型的,不能直接和sz比较
while (fast<sz)
改为while (fast<nums+sz)
为什么新数组长度 = 慢指针位置 - 数组起始地址
    // 新数组长度 = 慢指针位置 - 数组起始地址
    return slow - nums;

双指针法中:

  • slow 始终指向「去重后数组的下一个待填充位置」;
  • 遍历结束后,slow 之前的所有元素都是去重后的有效元素;
  • 从 nums(第 0 位)到 slow(第 n 位),中间的元素个数就是 slow - nums

问题3

27. 移除元素 - 力扣(LeetCode)

我的覆盖法
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int removeElement(int* nums, int numsSize, int val) {
    int count = 0; // 记录有效元素(非val)的数量
    int i = 0;     // 遍历指针
    
    while (i < numsSize) {
        // 情况1:当前元素不是val,直接保留,指针后移,计数+1
        if (nums[i] != val) {
            count++;
            i++;
        }
        // 情况2:当前元素是val,找后面第一个非val的元素来覆盖
        else {
            int j = i + 1; // 从当前元素的下一个位置开始找
            // 找到第一个不等于val的元素,或者遍历到数组末尾
            while (j < numsSize && nums[j] == val) {
                j++;
            }
            
            // 如果找到了非val元素,就覆盖当前val的位置
            if (j < numsSize) {
                nums[i] = nums[j];
                nums[j] = val; // ❗️把找到的元素位置标记为val(避免重复使用)
                count++;
                i++;//❗️
            }
            // 如果没找到(后面全是val),直接退出循环
            else {
                break;
            }
        }
    }
    return count;
}  return 0;
}
我的错误:

1.情况2时,没有让nums[j]=val,后面会重复使用

2.没有让i++,注意覆盖后i要的位置要偏移

问题3

#include<stdio.h>
int main()
{
	int m, n;
	int i, j;
	int* p;
	int arr[5][5];
	int count;
	int case_num = 1; 
	while (scanf("%d %d", &m, &n) != EOF)
	{
		for (i = 0; i < m; i++)
		{
			for (j = 0; j < n; j++)
			{
				scanf("%d", &arr[i][j]);
			}
		}
		count = 0;
		p = &arr[0][0];
		for (i = 0; i < m * n; i++)
		{
			if (*(p + i) % 6 == 0)
			{
				count++;
			}
		}
		case_num++;
	}
	return 0;
}

这里先输入3 3,原代码打印的结果是错误的,而如果把int arr[50][50]换为int arr[3][3]就正确了,为什么呢?

原因

这是数组越界后 “内存覆盖” 的典型现象—— 数组越大,反而更容易覆盖到count变量的内存,导致统计结果错误。

具体分析(结合你的输入)

你输入的是m=3, n=3(共 9 个元素):

  • arr[3][3]时:数组刚好能容纳 9 个元素(索引0~2行、0~2列),没有越界,所以程序正常统计出 “18” 和 “6” 这 2 个能被 6 整除的数,结果正确;

  • arr[50][50]时:数组虽然容量大,但变量在内存中的排布顺序导致问题:C 语言中局部变量(m、n、i、j、p、arr、count等)是连续存放在栈内存的,顺序通常是 “先定义的变量地址高,后定义的地址低”

  •  “入栈”(push):把数据放到esp指向的位置,然后esp减 4(int 占 4 字节),地址变低;
  • “出栈”(pop):esp加 4,然后取数据,地址变高。
  • 你的代码中arr定义在count前面,所以内存排布大概是:

    高地址 → m → n → i → j → p → arr[5][5](25个元素)→ count → 低地址
    

    arr[5][5]时,它占 25 个int的空间,而你只输入了 9 个元素(3×3),但arr后面的内存紧接着就是count变量。此时数组越界的风险看似小,但实际是 “arr的内存范围覆盖了count的初始值”

    • count初始化为 0,但它的内存地址刚好在arr的后面;
    • 程序统计时,*(p+i)遍历arr的元素,但如果arr的内存 “挤占” 了count的地址,就会把count的值当成arr的元素来判断,最终导致统计错误(比如把count的 0 当成能被 6 整除的数,或者覆盖了count的统计结果)。

本质原因

原代码的核心问题是:局部变量在栈上的内存是连续的,固定大小的数组会和其他变量(比如count)“抢内存”,无论数组是[3][3]还是[5][5],只要输入的m、n和数组大小不匹配,就会触发 “内存覆盖”,只是覆盖的位置和结果随机(有时对、有时错)

解决方法1

 // 第一步:动态分配m行的指针数组(每行对应一个int*)
        arr = (int**)malloc(m * sizeof(int*));
        if (arr == NULL) // 检查内存分配是否失败(防御性编程)
        {
            printf("内存分配失败!\n");
            return 1; // 异常退出
        }

        // 第二步:为每一行分配n个int类型的空间
        for (i = 0; i < m; i++)
        {
            arr[i] = (int*)malloc(n * sizeof(int));
            if (arr[i] == NULL)
            {
                printf("内存分配失败!\n");
                // 分配失败时,先释放已分配的内存,避免内存泄漏
                for (int k = 0; k < i; k++)
                {
                    free(arr[k]);
                }
                free(arr);
                return 1;
            }
        }

方法2

数组还是原来的方法定义,但不用指针偏移定义

// 替换原来的指针遍历逻辑
count = 0;
// 直接遍历二维数组,不越界
for (i = 0; i < m; i++) {
    for (j = 0; j < n; j++) {
        if (arr[i][j] % 6 == 0) {
            count++;
        }
    }
}
问:如果是先定义count,那么他的内存是不是就不会被覆盖呢

答:不是

 哪怕把count先定义,只要用的是固定大小的栈数组,且数组大小大于输入的m*n,依然可能出错。这不是 “谁先定义” 的问题,而是栈内存的连续性 + 指针偏移遍历的 “盲目性”+ 编译器内存优化共同导致的,我帮你一步步拆明白:

1. 先看:把count先定义,栈内存排布会变成什么样?

假设你修改代码,把count放在最前面定义:

int main()
{
    int count = 0;    // 先定义count
    int m, n;
    int i, j;
    int* p;
    int arr[5][5];    // 后定义的大数组(25个int)
    // ...
}

结合栈 “向下生长” 的规律,此时内存排布变成:

栈底(高地址) → count → m → n → i → j → p → arr[5][5] → 栈顶(低地址)

看起来count在高地址,arr在低地址,两者隔了一段内存,似乎不会互相影响?但关键问题出在指针偏移遍历的逻辑栈内存的 “内存对齐 / 空隙”

2. 核心原因 1:指针偏移遍历是 “盲目” 的,不区分数组边界

我的遍历逻辑是:

p = &arr[0][0];          // p指向arr第一个元素(低地址)
for (i = 0; i < m * n; i++) {
    if (*(p + i) % 6 == 0) {  // 关键:p+i是“无脑”偏移
        count++;
    }
}

这里的p + i纯地址计算,编译器不会检查这个地址是不是真的属于arr

  • 哪怕arr[5][5]能装 25 个元素,而你只输入m=3,n=3(9 个元素),遍历i=0~8时,p+i确实在arr范围内;
  • 但问题是:栈内存中arr后面(更低地址) 是栈顶的 “未使用内存”,而arr前面(更高地址) 是p、j、i、n、m、count这些变量 —— 编译器为了 “内存对齐”(比如让变量地址是 4/8 的倍数),会在变量之间留 “空隙”,这些空隙的内存值是随机的垃圾值

当你用p = &arr[0][0]时,p的地址是arr的起始位置(低地址),但如果编译器在arr和前面的变量(比如p)之间留了空隙,arr的内存会 “挤占” 这些空隙,而这些空隙的地址逻辑上属于 arr,但物理上是其他变量的内存区域—— 遍历p+i时,可能会读到这些空隙里的垃圾值,甚至读到m/n/i/j/p的数值,导致统计错误。

3. 核心原因 2:栈数组的 “大小冗余” 会放大内存布局的不确定性

你说 “定义的数组大于输入的”(比如arr[5][5]>9 个元素),但栈数组的 “冗余空间”(25-9=16 个空元素)并不是 “安全的空白”:

  • 这些冗余空间在栈上,紧邻着其他变量的内存(或内存空隙);
  • 栈内存的 “空白区域” 不会自动清零,里面是程序运行时留下的随机垃圾值(比如之前函数调用的残留、其他变量的临时值);
  • 哪怕你只遍历 9 个元素,arr的冗余空间里的垃圾值可能会被编译器 “优化” 到count的内存缓存区,或者因为栈帧的 “写回机制”,间接修改count的数值 —— 这就是栈内存的 “隐性污染”。

举个极端但真实的例子:arr[5][5]的冗余空间里有一个垃圾值6,编译器为了提高效率,把count的内存和这个冗余空间做了 “缓存共享”,哪怕你没遍历到这个位置,count的数值也可能被这个6影响,导致统计时多算一次。

4. 为什么动态内存分配就能解决?

动态内存(malloc分配的)是在堆内存上,和栈内存(count、m、n等)是完全隔离的两块区域:

内存布局:
栈内存(高地址→低地址):count → m → n → i → j → p → 栈顶
堆内存(低地址→高地址):arr[3][3](动态分配,仅占9个int)→ 堆尾

堆和栈之间有明确的内存边界,arr的内存无论怎么偏移,都碰不到栈上的count,自然不会有 “内存污染” 的问题。

总结

  1. 栈内存的核心问题是连续性 + 无边界保护:无论count先定义还是后定义,固定大小的栈数组都会和其他变量共享连续的栈空间,哪怕数组更大、输入更小,也会因为内存对齐、缓存优化、垃圾值等问题,导致统计错误;
  2. 指针偏移遍历是 “雪上加霜”:它不检查地址是否属于数组,只做纯数值计算,容易读到栈上的其他变量 / 垃圾值;
  3. 动态内存分配的本质是脱离栈空间:堆内存和栈内存隔离,数组的大小完全匹配输入,既没有冗余空间,也不会污染其他变量,从根本上解决问题。

简单说:栈数组就像 “挤在一个小房间里的所有人”,哪怕你只占用自己的位置,也可能被旁边的人(其他变量)影响;而动态内存是 “单独的房间”,完全不受干扰。

问:为什么用二维数组遍历也可以解决问题

二维数组原生遍历(arr[i][j])是 “有边界的”,它会严格按照数组的 “行 - 列” 结构访问元素,不会像指针偏移那样 “盲目越界”,从而避免了栈内存的污染问题。

核心原因:arr[i][j]的访问逻辑是 “合法的数组索引”

C 语言中,二维数组arr[行][列]的访问本质是先定位行,再定位列,它的地址计算是:

arr[i][j] 的地址 = arr的起始地址 + i * 列数 * sizeof(int) + j * sizeof(int)

这个计算是 ** 严格基于数组的 “行长度(列数)”** 的,而不是 “无限制的指针偏移”

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐