C语言学习笔记20260701-环形数组移动与随机数生成

C语言学习笔记20260701-环形数组移动与随机数生成

1. 项目背景

本题是一个经典的模拟算法 问题。我们需要模拟一个角色(旺仔哥哥)在一个由 NNN 个小朋友组成的圆圈中移动的过程。

  • 输入 :一组代表步数的数字序列、移动次数 MMM、移动方向(0为逆时针,1为顺时针)
  • 输出:最终停留位置的小朋友编号(1~N)
  • 难点:处理圆环的边界溢出(取模运算)以及负数索引的处理

2. 核心知识点解析

2.1 环形移动的数学模型

在计算机中,数组是线性的(0 到 N-1),而题目是环形的。我们需要利用**取模运算(%)**来实现"转圈"的效果。

假设当前下标为 cur,步数为 step,数组长度为 sz

顺时针移动(下标增加)

直接相加并取模:

c 复制代码
cur = (cur + step) % sz;
逆时针移动(下标减小)

C语言中负数取模可能得到负数(例如 -1 % 5 结果为 -1),这会导致数组越界。必须加上一个周期 sz 再取模,将其转化为正数索引:

c 复制代码
// 标准公式:(当前 - 步数 + 总长) % 总长
cur = (cur - step % sz + sz) % sz;

2.2 常见陷阱与避坑指南

  1. **sizeof**** 的误区**
    • sizeof(arr) 返回的是字节数 。对于 int arr[10],结果是 40(假设 int 占 4 字节)
    • 正确获取元素个数int sz = sizeof(arr) / sizeof(arr[0]);
  2. 状态保持
    • for 循环模拟移动时,千万不要在循环内部重置当前位置
    • cur 必须在循环外初始化,并在循环内不断更新,代表"上一步结束时的位置"
  3. 随机数种子
    • 使用 srand((unsigned int)time(NULL)) 确保每次运行程序生成的随机序列不同

3. 完整修正代码

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h> // rand, srand
#include <time.h>   // time

/**
 * @brief 计算旺仔哥哥最终停留的位置
 * @param arr 小朋友衣服上的数字数组
 * @param sz  数组长度(小朋友人数)
 * @param m   移动次数
 * @param dir 移动方向 (0: 逆时针, 1: 顺时针)
 * @return 最终停留的小朋友编号 (1 ~ sz)
 */
int stopAtWho(int arr[], int sz, int m, int dir) {
    // 初始站在第1个小朋友旁边,对应数组下标 0
    int cur = 0; 

    for (int i = 0; i < m; i++) 
	{
        // 1. 获取当前站立位置的数字作为本次移动的步数
        int step = arr[cur];

        // 2. 根据方向计算新位置
        if (dir == 1) 
		{
            // 顺时针:下标增加
            // 注意:step 可能很大,先 % sz 优化性能(可选)
            cur = (cur + step) % sz;
        } else
		{
            // 逆时针:下标减小
            // 核心技巧:+ sz 防止负数取模出错
            cur = (cur - (step % sz) + sz) % sz;
        }
    }

    // 题目要求返回编号(1开始),数组下标是0开始,所以 +1
    return cur + 1;
}

int main() 
{
    int arr[10] = { 0 };
    
    // --- 1. 随机生成数据 ---
    srand((unsigned int)time(NULL));
    printf("生成的随机数字序列: ");
    for (int i = 0; i < 10; i++) 
	{
        // 生成 1~100 之间的随机数
        arr[i] = rand() % 100 + 1; 
        printf("%d ", arr[i]);
    }
    printf("\n");

    // --- 2. 获取用户输入 ---
    int m = 0, n = 0;
    // 计算数组实际元素个数,而不是字节数
    int sz = sizeof(arr) / sizeof(arr[0]); 

    printf("请输入移动次数 m 和方向 n (0逆/1顺): ");
    if (scanf("%d %d", &m, &n) != 2) 
	{
        printf("输入格式错误\n");
        return 1;
    }

    // --- 3. 调用函数并输出结果 ---
    // 将方向参数 n 传入函数
    int result = stopAtWho(arr, sz, m, n);
    
    printf("旺仔哥哥最后停在第 %d 号小朋友旁边\n", result);

    return 0;
}

4. 学习总结

通过这个练习,我们掌握了三个重要的编程技能:

  1. 环形缓冲区思想:利用取模运算将线性数组首尾相接
  2. 防御性编程 :在处理减法取模时,习惯性加上模数(+ sz)以防止负数溢出
  3. 调试思维 :学会区分"循环计数器"(i)和"业务状态变量"(cur),这是新手最容易犯的逻辑错误之一

5. 常见问题解答

Q1: 为什么需要 (step % sz) 而不是直接用 step

A : 当步数 step 大于数组长度 sz 时,step % sz 可以避免不必要的完整循环,提高效率。例如在长度为5的数组中走7步,等价于走2步。

Q2: 为什么逆时针移动要加 sz

A : C语言中负数取模的结果可能是负数(如 -2 % 5 = -2),而数组下标不能为负。通过 + sz 将结果调整到 [0, sz-1] 范围内。

Q3: 如何验证代码正确性?

A: 可以用简单的测试用例手动验证:

  • 数组 [2,1,4,5,2,3],m=3,顺时针
  • 初始位置:1号(下标0)
  • 第1步:走2步 → 3号(下标2)
  • 第2步:走4步 → 1号(下标0)
  • 第3步:走2步 → 3号(下标2)
  • 结果:3号小朋友