JavaScript 打印星号三角形

2,123 阅读7分钟

打印由星号组成的x层三角形,如下图:

看规律, 设三角层数为x,每层星星数为y,每层开头空格数为z

那么,对于第i层:

  1. y 为1,3,5,7,9...奇数。光知道奇数你并不知道印象中的公式是2n-1还是2n+1更哪个更适合这里, 那我们一步步来, 这里隔离变与不变, 1是最开始的基准, 3和1的关系是+2, 5和3的关系是+2... 因此每次加2,即1, 1+2, 1+2+2, 1+2+2+2...注意这里隔离变与不变, 数量不变的1, 和数量变化的2, 那么就看这个变化的2的数量和i的关系, 即0, 1, 2, 3...和1, 2, 3...的关系,这样才由题意得y = 1+(i-1)*2 = 2i-1;
  2. z 在最后一层为0,朝上依次增加1,共有x层,故最上层为x-1,第二层x-2, 第三层x-3, 因此z = x-i;

虽然很多时候这两个简单的变量之间的数量关系可以一眼看出, 但通常都是碰运气的不断尝试拼凑, 本着九年义务教育提供的数学素养, 我们应该可以从头推出这些式子, 并且有理有据, 而不是背下来.

个人最讨厌背了,除了3.1415926, 九九乘法表之类的, 爸妈的手机号... 其他时候是能不背就不背.

我们如果能掌握如何产生公式, 是不是就比直接背公式更容易记忆呢?短期看背的收益是高回报的, 无需思考, 提取就可以. 而理解就像计算机中的函数, 他们多了之后可以互相利用, 组合成更复杂的功能. 而背除了诗词能假以时日身临其境,有所感慨可以陶冶情操外, 很多时候他们真的除了提取数据, 毫无发展和其他有趣的事情联系起来从而有更大的用处.

当然理解是要付出代价的, 即理解的时间, 和从原数据推演计算的时间, 因此重要的或者有趣的才要去理解, 如果是一次性使用的信息, 比如学校旁的小面馆说13号担担面好了~13号在哪~, 这样的信息还是直接背下来比较好, 当然你也可以想象背叛上帝的犹大, 借此把13记下来

而手机验证码的六位, 原先还要不断重复口念450893, 生怕有人跟我说话我又要切换应用重新记, 哈利路亚, 这种纯粹没有逻辑的死板的信息, 现在的输入法自带的信息提示, 不用切换到短信, 直接点一下就Ok了.

看, 越是死的东西, 越是会被机器+软件替换掉.

简易代码实现

const printTriangle = x => {
    for(let i=1; i<=x; i++) {
        console.log(" ".repeat(x-i)+ "*".repeat(2*i-1))
    }
}
printTriangle(7)

效果:

VM189:3       *
VM189:3      ***
VM189:3     *****
VM189:3    *******
VM189:3   *********
VM189:3  ***********
VM189:3 *************

遐想

代码逻辑很简单,这里有一点引起我的遐想。

*.repeat()为例,如果

  1. 需要1个*,是我们程序员手动输入的,无法精简
  2. 需要2个*,可能我们更愿意手动输入,也无需精简
  3. 需要3个*,可能我们会想相关重复字符的API是不是比三个星星简单,于是想到str.repeat(), 但很明显,还是手动输入***更简单
  4. 需要4,5,6,7个*呢?很明显,这时候我们理性一点,在这种事上不能够选择相信我们的眼睛真的每次都能数对数目,尽管小学算术经常拿满分 :)
  5. 而如果我们不是一次只需要几个*,而是同时需要1个,2个,3个,4个*呢?这时候我们会选择自动生成。即有str.repeat()那就用它,没有的话呢?

手动实现str.repeat()

第一步,先保证基本能用最简单的

const repeat = (str,n) => {
    let result = "";
    for(let i=0; i<n; i++) {
        result = result + str
    }
    return result
}

复杂度:O(n)

第二步: 优化

上面的实现间接生成了任意长度的*字符串

  1. *, 手动输入
  2. **, 由* + *
  3. ***, 由** + *
  4. ****, 由*** + *
  5. *****, 由**** + *
  6. ******, 由***** + *
  7. ...

而对于本题目需要奇数个长度的*, 这里生成的一半都是不需要的, 造成了浪费

那么怎么才能不浪费呢.

*, **, ****, ********, 每次以之前的字符为基本, 自我复制,达到2倍法来逼近指定长度, 达到log2(n)的复杂度(想一想二分法...)

但是对于本题而言, *, **, ****, ********, ****************是相对不够的, 所以

  1. 对于每行的*, 从第一层的*开始, 不断的在上一层字符串 + **,
  2. 而对于每行的空格" ", 一开始就有x-1个, 所以要最大速度的增加到x-1个字符串, 用2倍法, O(log2(n))复杂度, 2^40 = 10995,1162,7776, 即长度为一万亿以下的字符串的重复, 不超过40次计算即可拼接成.
const printTriangle = x => {
    let blanks_max_length = x-1
    let blanks_max = ""  // empty str
    let blank = " "  // blank str
    for(;blanks_max_length!=0;){ // only stop while length became 0, 
                                // there is no (let i i...i++), because it's all about length! 
        if(blanks_max_length%2===1) {    // remember decimal 5 trans into binary 101?
                                        // 5/2 = 2...1, 2/2 = 1...0, 1/2 = 0...1, 
                                        // the first remainder 1 indicates 1 while the  last 1 indicate 2^2 which is 4
            blanks_max = blanks_max + blank  // so here if there remains 1,  blanks_max should add 1 blank
        }
        blanks_max_length = Math.floor(blanks_max_length/2)  // here is the result of /2
        blank = blank + blank   // here is where log2(n) come from, cause we double the string rather than linear increase

    }
    let star = "*"
    const addon = "**"
    // first line
    console.log(blanks_max + star)
    // other lines
    for(let i=2; i<x; i++) {
        star = star + addon
        console.log(blanks_max.slice(0, x-i) + star)
    }
    // last line
    console.log(star+addon)
}

  1. 对比结果时, 我发现第一行和其他行的格式缩进不一致, 找了半天代码, 我默认其他是对的, 那么一对比结果可能正确的那个就表现的错误, 而我怎么也找不到这错在哪, 这个时候要联系上下文, 我发现其实这只是因为两个逻辑, 第一个逻辑产生了1个实例即第一行, 第二个逻辑产生了更多的实例即其他行, 当错误的那个逻辑应用在很多行, 导致其他行一致性的表现规律, 以至于你认为其他行都格式正确了, 第一行是错误的, 其实这不是多对一, 本质上这是一对一, 如果不是这个逻辑的这一行错了, 就是那个逻辑的很多行错了!
  2. 为什么会这么用除2判断余数来标识32,16,8421码的位是否存在的方式来组合总空格的长度以及用空格double的方式实现复杂度O(log2(n)), 首先考虑的是先生成第一层最长的空格, 然后依次递减. 其实这是个str.repeat()从O(n)到O(log2(n))的方法,被强行套在这里.
  3. 关于本题实际上更适合生成空字符串逐个加1长度, 存入栈, 然后打印的时候出栈, 满足倒序打印的特点

代码如下

const printTriangle = x => {
    let blanks_max_length = x-1
    let blanks_arr = []
    let blank_char = " "
    let tmp =""
    for (let i=0; i<blanks_max_length; i++) {
        tmp = tmp + blank_char
        blanks_arr.push(tmp)
    }
    let star = "*"
    const addon = "**"
    // first line
    console.log(blanks_arr.pop() + star)
    // other lines
    for(let i=2; i<x; i++) {
        star = star + addon
        console.log(blanks_arr.pop() + star)
    }
    // last line
    console.log(star+addon)
}

噢, 丑陋的代码, 就是为了不产生浪费的字符串.

好了, 上面给出了一些个人答案,这些答案分别面向节省程序员时间(str.repeat)、面向自己实现str.repeat(),面向不浪费生成的对象等角度。

最后还是好爱第一个答案,不禁让我想到一句话

JavaScript是世界上最好的语言~