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 种风格迥异的实现方式。这正是编程思维的魅力所在------将复杂的问题分解、模式化,然后用最适合当前场景的工具和思想,优雅地解决它。

相关推荐
恋猫de小郭6 小时前
Flutter 真 3D 游戏引擎来了,flame_3d 了解一下
android·前端·flutter
陶甜也6 小时前
无需服务器,免费、快捷的一键部署前端 vue React代码--PinMe
服务器·前端·vue.js
烛阴6 小时前
TypeScript 进阶必修课:解锁强大的内置工具类型(二)
前端·javascript·typescript
竹苓6 小时前
前端性能优化:用虚拟列表轻松渲染 100000 条数据
前端·性能优化
用户47949283569156 小时前
🚀 面试官:什么是强缓存与协商缓存
前端·网络协议·面试
你单排吧6 小时前
Uniapp之ios真机调试篇
前端·mac
用户22152044278006 小时前
JavaScript事件循环
前端
JarvanMo6 小时前
5 个连 Remi 都不会告诉你的实用 Flutter Riverpod 技巧
前端
万少6 小时前
可可图片编辑 HarmonyOS(4)图片裁剪-canvas
前端·harmonyos