JavaScript 算法全解:8 种方法实现"数字回文金字塔"(从入门到高阶)
在编程学习的旅程中,我们总会遇到一些看似简单却暗藏玄机的图形打印问题。它们是绝佳的逻辑思维训练场。今天,让我们来挑战一个非常有趣的数字金字塔,它不仅对称,而且每一行都像一个"回文"数字。
简单题目
markdown
行数(i) | 输出
---------------------
1 | 1
2 | 212
3 | 32123
4 | 4321234
5 | 543212345
仔细看第 4 行 4321234,我们可以把它拆解成三个部分:
- 降序部分:从当前行数 4 开始,递减到 2。即 432。
- 中心部分:永远是 1。
- 升序部分:从 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
------作为分析样本。我们可以清晰地把它拆解成三个部分:
- 降序部分 :从当前行数
5
开始,一路递减到2
。即5432
。 - 中心部分 :永远是固定的数字
1
。 - 升序部分 :从
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 种风格迥异的实现方式。这正是编程思维的魅力所在------将复杂的问题分解、模式化,然后用最适合当前场景的工具和思想,优雅地解决它。