🎬 HoRain 云小助手:个人主页
⛺️生活的理想,就是为了理想的生活!
⛳️ 推荐
前些天发现了一个超棒的服务器购买网站,性价比超高,大内存超划算!忍不住分享一下给大家。点击跳转到网站。
目录
⛳️ 推荐
R 循环——从语法到反模式(完整实战指南)
一、R 的三种基本循环
1️⃣ for——知道(或能确定)迭代次数时用
2️⃣ while——不知道要迭代多少次,靠条件控制
3️⃣ repeat——无限循环,必须手动 break
二、循环控制:break与 next
三、嵌套循环
四、最重要的一节:R 循环的致命反模式
❌ 最经典的错误——在循环内"生长"结果向量
✅ 修正:预分配(Pre-allocate)
🏆 终极方案:向量化——直接消灭循环
五、R 的核心哲学:先问"一定要用循环吗?"
决策树
对照示例
六、apply家族——循环的结构化替代品
七、tidyverse 流派:purrr::map()系列
八、什么时候必须用循环(无法向量化)
九、速查小结
R 循环——从语法到反模式(完整实战指南)
一、R 的三种基本循环
1️⃣for——知道(或能确定)迭代次数时用
# 最常用形式:遍历一个序列 for (i in 1:5) { print(i) } # 遍历向量元素(不是下标) fruits <- c("apple", "banana", "cherry") for (fruit in fruits) { print(paste("我喜欢吃", fruit)) }R 的
for (var in sequence)本质是"遍历序列中的每个元素",不是传统 C 语言那种for(i=0; i<n; i++)下标思维——虽然你也可以这样写:
x <- c(10, 20, 30, 40) for (i in seq_along(x)) { # seq_along 比 1:length(x) 更安全 cat(sprintf("x[%d] = %d\n", i, x[i])) }⚠️ 坑 |
| ✅ 用 |
|---|
2️⃣while——不知道要迭代多少次,靠条件控制
# 例:不断除以2直到小于10 n <- 1000 while (n >= 10) { n <- n / 2 cat(sprintf("half: %.4f\n", n)) } # 最终 n = 7.8125 # 例:猜数小游戏 secret <- sample(1:100, 1) guess <- 0 while (guess != secret) { guess <- as.numeric(readline(prompt = "猜 1~100 的数: ")) if (guess < secret) cat("太小了!\n") if (guess > secret) cat("太大了!\n") } cat("对了!答案是", secret, "\n")死循环风险:
while的条件如果永远不为FALSE,R 会话会卡死。写while时心里必须确认:"条件一定会在某次迭代后变 FALSE 吗?"
3️⃣repeat——无限循环,必须手动break
counter <- 1 repeat { cat(sprintf("counter = %d\n", counter)) counter <- counter + 1 if (counter > 5) break # ← 没有这个就是死循环 } # 例:掷骰子直到出现6 repeat { roll <- sample(1:6, 1) cat(sprintf("掷出了 %d\n", roll)) if (roll == 6) { cat("🎉 出现了6,停止!\n") break } }
repeat { ... }等价于while (TRUE) { ... },区别只是语法——用哪个都行,关键是一定写break。
二、循环控制:break与next
关键字 | 作用 | 类比其他语言 |
|---|---|---|
| 立刻跳出整个循环 |
|
| 跳过本次,进入下一轮 |
|
for (i in 1:10) { if (i == 5) break # 遇到5就停 if (i %% 2 == 0) next # 偶数跳过 print(i) # 输出:1 3 } # 打印1~100中第一个能被7整除且大于50的数 for (i in 1:100) { if (i <= 50 || i %% 7 != 0) next print(i) # 56 break }
break/next只对最内层循环生效。嵌套循环里想跳多层需要额外标志变量或用函数return。
三、嵌套循环
# 九九乘法表 for (i in 1:9) { for (j in 1:i) { cat(sprintf("%d×%d=%d\t", j, i, i*j)) } cat("\n") }⚠️ 嵌套越深 → 迭代次数乘积增长 → 性能雪上加霜。如果内层循环体不依赖外层索引,考虑能不能向量化或合并。
四、最重要的一节:R 循环的致命反模式
❌ 最经典的错误——在循环内"生长"结果向量
# 💀 千万别这样写(慢到指数级) result <- c() # 空向量 for (i in 1:100000) { result <- c(result, i^2) # 每次都要拷贝整个向量! }为什么会慢? 因为 R 的c()每次都在创建一个更大的新对象,把旧数据拷过去再追加——时间复杂度从 O(n) 退化到 O(n²)。
✅ 修正:预分配(Pre-allocate)
n <- 100000 result <- numeric(n) # ← 先开好空间 for (i in 1:n) { result[i] <- i^2 }🏆 终极方案:向量化——直接消灭循环
result <- (1:100000)^2 # 一步到位,C级别执行基准实测差异: 向量化版本可比 naive 循环快数千倍。这不是夸张——是 R 设计的根本特征:向量化运算委托到底层 C/Fortran 代码执行,而for循环在 R 解释器层面一步步跑。
五、R 的核心哲学:先问"一定要用循环吗?"
决策树
要做的事对每个元素都一样? ├── 是 → 能用向量化运算吗? │ ├── 能 → 直接用向量化(最快,最 R-style) │ │ x * 2 / sqrt(x) / ifelse(test, a, b) │ └── 不能(非同质操作)→ lapply / purrr::map └── 否(迭代间有依赖 / 副作用 / 未知终止条件) └── 用 for / while(无法避免才用)对照示例
# ❌ 没必要写循环 vec <- c(3, 7, 2, 9) squared <- c() for (i in seq_along(vec)) { squared[i] <- vec[i]^2 } # ✅ 向量化——R 的本来面目 squared <- vec^2 # ❌ 没必要写循环 for (i in seq_along(vec)) { if (vec[i] > 5) cat(vec[i], "大\n") else cat(vec[i], "小\n") } # ✅ 向量化条件 ifelse(vec > 5, paste(vec, "大"), paste(vec, "小")) # ❌ 没必要写循环算均值 means <- c() for (col in c("mpg", "hp", "wt")) { means[col] <- mean(mtcars[[col]]) } # ✅ 一行搞定 sapply(mtcars[c("mpg", "hp", "wt")], mean)六、apply家族——循环的结构化替代品
函数 | 输入 | 输出 | 典型场景 |
|---|---|---|---|
| 列表/向量 | list | 对列表中每项做变换 |
| 列表/向量 | 简化(向量/矩阵) | 同上,但结果尽量压扁 |
| 列表/向量 | 指定类型的向量 | 生产代码首选(类型安全) |
| 矩阵 | 按行(M=1)/列(M=2) | 行列汇总 |
| 向量 | 按分组 |
|
| 多向量 | 并行迭代 |
|
# lapply → 总是返回 list nums <- list(a = 1:3, b = 4:6, c = 7:9) lapply(nums, sum) # $a [1] 6 $b [1] 15 $c [1] 24 # sapply → 尝试简化 sapply(nums, sum) # 变成具名向量: a=6, b=15, c=24 # vapply → 最安全(声明返回值类型) vapply(nums, sum, numeric(1)) # 一样的结果,但如果 sum 返回非数值会报错警告 # apply → 矩阵行列 mat <- matrix(1:12, nrow = 3) apply(mat, 1, sum) # 行和: [1] 22 26 30 apply(mat, 2, mean) # 列均: [1] 2 5 8 11⚠️
apply会把 data.frame 先转成 matrix(丢失列类型),对纯数值矩阵没问题,但对混杂类型 data.frame 要小心。
七、tidyverse 流派:purrr::map()系列
如果你用 tidyverse,推荐purrr::map_*()替代 base R 的lapply/sapply——更一致、类型更安全、错误信息更友好:
library(purrr) nums <- 1:5 map(nums, ~ .x^2) # → list map_dbl(nums, ~ .x^2) # → numeric vector map_chr(nums, ~ paste0("ID_", .x)) # → character vector # 安全包裹:不中断整个循环 safe_log <- safely(log) map(list(10, 0, -5, 100), safe_log)八、什么时候必须用循环(无法向量化)
场景 | 为什么不能向量化 |
|---|---|
随机游走 / 递推: | 前向依赖 |
迭代收敛: | 终止条件未知 |
副作用序列:逐文件读写、逐图保存、API 分页拉取 | 每一步有 I/O |
提前退出逻辑太复杂, | 语义驱动 |
# 例:牛顿迭代求根(依赖前一步,不能用向量化) newton <- function(f, df, x0, tol = 1e-8, max_iter = 100) { x <- x0 for (i in 1:max_iter) { x_new <- x - f(x) / df(x) if (abs(x_new - x) < tol) { cat(sprintf("收敛于第 %d 步\n", i)) return(x_new) } x <- x_new } warning("未收敛"); x }这种情况下写循环不仅合理,而且是最清晰的写法。
九、速查小结
你想做的事 | 推荐写法 |
|---|---|
对每个元素做同样运算 | 向量化: |
对 list 逐项变换 |
|
矩阵行列汇总 |
|
按分组聚合 |
|
递推 / 条件未知 / 副作用 |
|
一定要写 | 先 |
一句话记住:在 R 里,循环不是"低级"也不是"高级"——它是最后手段。先向量化,再
apply/map,实在不行再写for(但记得预分配)。
如果你告诉我你当前的数据形态(比如一个 data.frame 要逐行算什么 / 读一堆 CSV / 做模拟抽样),我可以帮你把具体代码写成最优版本,并指出该不该用循环。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄
💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍
🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙