JavaScript 算法探秘:如何优雅地打印一个“回文”数字金字塔(从入门到高阶)


JavaScript 算法全解:8 种方法实现"数字回文金字塔"(从入门到高阶)

在编程学习的旅程中,我们总会遇到一些看似简单却暗藏玄机的图形打印问题。它们是绝佳的逻辑思维训练场。今天,让我们来挑战一个非常有趣的数字金字塔,它不仅对称,而且每一行都像一个"回文"数字。

简单题目

markdown 复制代码
行数(i) | 输出
---------------------
  1    | 1
  2    | 212
  3    | 32123
  4    | 4321234
  5    | 543212345

仔细看第 4 行 4321234,我们可以把它拆解成三个部分:

  1. 降序部分:从当前行数 4 开始,递减到 2。即 432。
  2. 中心部分:永远是 1。
  3. 升序部分:从 2 开始,递增到当前行数 4。即 234。

把这三部分拼接起来,就是 432 + 1 + 234 = 4321234。

我们可以用这个规律去验证其他行:

  • 第 3 行 (i=3) :降序(32) + 中心(1) + 升序(23) = 32123。 吻合!
  • 第 1 行 (i=1) :降序部分为空,中心(1),升序部分为空 = 1。 吻合!

规律找到了!现在我们就可以用代码来实现它。


写法一:经典的 for 循环拼接

这是最直观、最容易理解的实现方式。我们用一个外层循环控制行数,用内层循环来拼接字符串的各个部分。

js 复制代码
function print(n) {
  // 外层循环,控制行数 i 从 1 到 n
  for (let i = 1; i <= n; i++) {
    let leftPart = '';
    let rightPart = '';
    
    // 1. 构建降序部分 (从 i 递减到 2)
    for (let j = i; j > 1; j--) {
      leftPart += j;
    }
    
    // 2. 构建升序部分 (从 2 递增到 i)
    for (let k = 2; k <= i; k++) {
      rightPart += k;
    }
    
    // 3. 拼接三部分并打印
    // 最终形态:leftPart + '1' + rightPart
    console.log(leftPart + '1' + rightPart);
  }
}

console.log("--- 经典 for 循环 ---");
print(5);
写法二:现代的数组方法

对于更喜欢函数式或现代语法的开发者来说,我们可以用数组来构建每一行,最后再用 .join('') 合并成字符串。这种方法在处理数据和字符串分离上更清晰。

js 复制代码
function printWithArrays(n) {
  for (let i = 1; i <= n; i++) {
    const leftArr = [];
    for (let j = i; j > 1; j--) {
      leftArr.push(j);
    }
    
    const rightArr = [];
    for (let k = 2; k <= i; k++) {
      rightArr.push(k);
    }
    
    // 使用数组展开语法 (...) 合并数组
    const finalArray = [...leftArr, 1, ...rightArr];
    
    // 将数组元素连接成一个字符串并打印
    console.log(finalArray.join(''));
  }
}

console.log("\n--- 现代数组方法 ---");
printWithArrays(5);

进阶题目

markdown 复制代码
    1
   212
  32123
 4321234
543212345

看起来是不是比简单题目要复杂一些?别担心,让我们先从最基础的逻辑推导开始,然后为你展示解决这个问题的"十八般武艺"。


第一步:拨开迷雾,发现图案的规律

解决所有图形打印问题的核心在于模式识别。让我们把这个金字塔写下来,仔细观察每一行的构成:

行数 (i) 输出
1 1
2 212
3 32123
4 4321234
5 543212345

选取最复杂的一行------第 5 行 543212345------作为分析样本。我们可以清晰地把它拆解成三个部分:

  1. 降序部分 :从当前行数 5 开始,一路递减到 2。即 5432
  2. 中心部分 :永远是固定的数字 1
  3. 升序部分 :从 2 开始,一路递增回到当前行数 5。即 2345

将这三部分拼接起来,就是 5432 + 1 + 2345 = 543212345

这个规律对于第 3 行也同样适用:降序 (32) + 中心 (1) + 升序 (23) = 32123完全吻合!

同时,为了居中对齐,我们还需要在每行前面加上适量的空格。通过计算可以得出结论:在第 i 行的数字前,我们需要添加 n - i 个空格。


第二步:八仙过海 ------ 解锁 8 种实现姿势

有了这个清晰的逻辑,我们就可以开始编码了。但正如编程世界的魅力所在,通往罗马的道路不止一条。下面,我们将为你展示从经典到现代、从基础到高级的 8 种 不同实现方式,让你全方位掌握这个问题。

方法 1 --- 经典双重循环(逐字符拼接)

这是最基础的实现,将我们刚刚分析的规律直接翻译成代码。直观、安全、兼容所有环境,不依赖高级 API。

js 复制代码
function pyramid1(n) {
  for (let i = 1; i <= n; i++) {
    let line = '';
    // 1. 拼接左侧空格
    for (let s = 0; s < n - i; s++) line += ' ';
    // 2. 拼接下降部分(从 i 到 1)
    for (let x = i; x >= 1; x--) line += x;
    // 3. 拼接上升部分(从 2 到 i)
    for (let x = 2; x <= i; x++) line += x;
    console.log(line);
  }
}

// demo
pyramid1(5);
方法 2 --- Array.from + map + join(函数式风格)

这种写法更具声明性,通过数组方法生成每一部分,最后拼接,代码更紧凑。

js 复制代码
function pyramid2(n) {
  const rows = Array.from({length: n}, (_, idx) => {
    const i = idx + 1;
    const left = Array.from({length: i}, (_, k) => (i - k)).join('');
    const right = Array.from({length: i - 1}, (_, k) => (k + 2)).join('');
    const spaces = Array.from({ length: n - i }, () => ' ').join('');
    return spaces + left + right;
  });
  console.log(rows.join('\n'));
}

pyramid2(5);
方法 3 --- 利用字符串 repeat

结合 repeat 方法生成空格,配合数组生成数字序列,是可读性与现代语法的一个很好平衡。

js 复制代码
function pyramid3(n) {
  for (let i = 1; i <= n; i++) {
    const left = Array.from({length: i}, (_, k) => i - k).join('');
    const right = Array.from({length: i - 1}, (_, k) => k + 2).join('');
    console.log(' '.repeat(n - i) + left + right);
  }
}

pyramid3(5);
方法 4 --- 利用 padStart(先生成数字串再居中)

这是一个有趣的思路:先生成核心的数字串,然后利用 padStart 巧妙地将其推向居中位置。

推算公式

总宽度 = Total Width

当前行文本长度 = Current Row Length

剩余的总空格数 = Total Width - Current Row Length

因为要居中,所以左边的空格数 = (剩余的总空格数) / 2

左边空格数 = (Total Width - Current Row Length) / 2

padStart 的 targetLength 是最终字符串的总长度,它等于 "左边空格数" + "原始文本长度"。

targetLength = (Total Width - Current Row Length) / 2 + Current Row Length

targetLength = (Total Width + Current Row Length) / 2

js 复制代码
function pyramid4(n) {
  for (let i = 1; i <= n; i++) {
    const left = Array.from({length: i}, (_, k) => i - k).join('');
    const right = Array.from({length: i - 1}, (_, k) => k + 2).join('');
    const mid = left + right;
    // 总宽度是 2*n - 1,padStart 的目标长度需要计算
    // (总宽度 + 当前行长度) / 2 = ((2n - 1) + (2i - 1)) / 2 = n + i - 1
    console.log(mid.padStart(n + i - 1));
  }
}

pyramid4(5);
方法 5 --- 利用已有连续字符串切片(最简洁之一)

这是我个人最欣赏的方法之一。它非常聪明,先生成一个基础字符串 "123...n",然后通过切片、反转等操作高效地构建每一行。

js 复制代码
function pyramid5(n) {
  const base = Array.from({length: n}, (_, k) => k + 1).join(''); // "12345"
  for (let i = 1; i <= n; i++) {
    const left = base.slice(0, i).split('').reverse().join(''); // "i...(1)"
    const right = base.slice(1, i); // "2..i"
    console.log(' '.repeat(n - i) + left + right);
  }
}

pyramid5(5);
方法 6 --- 递归构造每一行

虽然循环是解决此问题的最佳选择,但用递归可以提供一个不同的视角,对理解递归调用栈很有帮助。

js 复制代码
function pyramid6(n, i = 1) {
  if (i > n) return; // 递归基准条件
  const left = (() => {
    let s = '';
    for (let k = i; k >= 1; k--) s += k;
    return s;
  })();
  const right = (() => {
    let s = '';
    for (let k = 2; k <= i; k++) s += k;
    return s;
  })();
  console.log(' '.repeat(n - i) + left + right);
  pyramid6(n, i + 1); // 递归调用
}

pyramid6(5);
方法 7 --- 生成器(Generator)逐行产出

生成器函数非常适合需要逐行处理或"懒加载"输出的场景,它允许你暂停和恢复函数的执行。

js 复制代码
function* pyramidGen(n) {
  const base = Array.from({length: n}, (_, k) => k + 1).join('');
  for (let i = 1; i <= n; i++) {
    const left = base.slice(0, i).split('').reverse().join('');
    const right = base.slice(1, i);
    yield ' '.repeat(n - i) + left + right; // 每次调用 next() 产出一行
  }
}

// 使用生成器
for (const line of pyramidGen(5)) {
  console.log(line);
}
方法 8 --- 在 DOM 中渲染(用于网页显示)

最后,如果你的目标不是控制台,而是在网页上展示,最直接的方式就是生成所有行然后更新 DOM。

html 复制代码
<!-- 在浏览器中运行此HTML片段 -->
<pre id="pyramid-container" style="font-family: monospace;"></pre>

<script>
function pyramidDom(n, elementId) {
  const container = document.getElementById(elementId);
  if (!container) return;
  
  const lines = [];
  const base = Array.from({length: n}, (_, k) => k + 1).join('');
  
  for (let i = 1; i <= n; i++) {
    const left = base.slice(0, i).split('').reverse().join('');
    const right = base.slice(1, i);
    lines.push(' '.repeat(n - i) + left + right);
  }
  
  container.textContent = lines.join('\n');
}

// 调用函数,将 n=5 的金字塔渲染到页面
pyramidDom(5, 'pyramid-container');
</script>
方法 9 固定宽度画布法

这种方法非常独特。它不拼接变长的字符串,而是为每一行都创建一个与金字塔最底层等宽的"画布"(一个填满空格的数组)。然后,像在坐标系上画点一样,精确地计算出每个数字应该放置的索引位置,直接"绘制"上去。由于所有行都在同一个宽度的画布上绘制,因此天然就实现了居中对齐。

js 复制代码
function pyramid9(n) {
  const result = new Array(n)
    .fill(0)
    .map((_, i) => {
      // 1. 创建一个固定宽度的、填满空格的画布
      const line = new Array(2 * n - 1).fill(' ');
      
      // 2. 在中心点放置 '1'
      line[n - 1] = 1;
      
      // 3. 从中心向两侧对称地放置数字 2, 3, ...
      for (let j = 2; j <= i + 1; j++) {
        line[n - j] = j;
        line[n + j - 2] = j;
      }
      
      return line.join('');
    })
    .join('\n');
    
  console.log(result);
}

// demo
pyramid9(5);

第三步:性能对比与选择建议

  • 时间复杂度 :以上大多数实现的算法时间复杂度都为 O(n²) ,因为共有 n 行,而每一行的处理(拼接、生成)都与行号 i(最大为 n)相关。
  • 空间复杂度
    • 逐行打印 (如方法 1, 3, 4, 6),额外空间复杂度为 O(n),用于存储当前最长的一行。
    • 先收集所有行再输出 (如方法 2, 8),空间复杂度为 O(n²),因为存储了整个金字塔。
  • 推荐选择
    • 面试或教学方法 1 最能清晰地展示思考过程。
    • 追求代码简洁方法 5 (切片基字符串) 非常优雅且高效。
    • 函数式编程爱好者方法 2方法 9 都是绝佳选择,方法 9 在处理对齐问题上思路尤其巧妙。
    • 在网页上动态展示方法 8 是最实用的。

总结

从一个看似复杂的图形,到一行行清晰的代码,我们不仅找到了规律,还探索了 9 种风格迥异的实现方式。这正是编程思维的魅力所在------将复杂的问题分解、模式化,然后用最适合当前场景的工具和思想,优雅地解决它。

相关推荐
醉城夜风~1 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华2 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子4 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅4 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni4 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学6 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师6 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc886 小时前
微软EDGE浏览器功能学习
前端·学习·edge
Csvn7 小时前
🎯 原生 `<dialog>` 元素:终于可以扔掉一半的自定义弹窗组件了?
前端