2

TL;DR

这是一个 Coderwars 上的练习,等级 4kyu 。Coderwars 上的题目分为 8 级,数字越小越难。这题算是中等难度。下面是我的分析和解法,语言使用的 JavaScript ,但你也可以用任何其他语言来实现。

Kata 描述

你需要提供一个函数,它接受一个正整数为参数并返回另一个正整数。返回值必须由输入的整数的每位数字构造而成,并且是最接近原整数的更大的数字。英文原文是 the next bigger number formed by the same digits 。如果这种数字不存在,函数返回 -1 。

听起来挺绕的,看看例子吧。下面的 nextBigger 就是要写的函数:

nextBigger(12) == 21
nextBigger(513) == 531
nextBigger(2017) == 2071

nextBigger(9) == -1
nextBigger(111) == -1
nextBigger(531) == -1

拿 2017 举例子,比它更大的数有 2071, 2107, 2170, 2701 等等,但最接近 2017 的大数是 2071 ,这就是函数的返回值。再拿 531 举例子,不管怎么组合都无法形成比它更大的数,就返回 -1 。

思路

我觉得对任何 kata 题目而言,最重要的不是用什么技巧写代码,而是如何发现问题中的规律。这个问题也是如此。想知道如何解题,我们先想想 数字是怎么比大小 的。

数字比大小的规则很简单,大概描述起来如下:

  • 先比较位数,位数高的更大。

  • 如果位数相同,则从第一位数字开始比较,数字更大的取胜。如果第一位数字相等,则比较第二位数字,以此类推直到末位数。

对这个题目而言,构造出来的新数字位数跟原数字是一样的,所以只用考虑上面的第二条规则。加上题目的描述,我们就可以分析出 下一个更大数字 到底是什么意思:尽量只调整末位 x 位数获得满意的结果,并且 x 尽可能小。换句话说,能动最后两位数字的就别动最后三位。

那么怎么知道最少动最后几位数字能满足要求呢?这就得进一步分析下规律了。让我们回顾两个例子:

nextBigger(513) == 531
nextBigger(531) == -1
nextBigger(2531) == 3125

第一个例子里,我们把 13 换成了 31 ,5 根本没必要动。第二个例子里完全没有可换的。第三个例子最有趣,我们把首位换成了 3 ,然后把其次三位数全部重排了,重排规律是从小到大,这样才能保证新数字是 "下一个更大" 的 。

规律得自己琢磨。我就说说结论。对于 xyz 这种数字,先分析一下最后两位 yz ,如果 y < z ,就只用换最后两位。如果 y >= z ,说明换两位不可行,所以只能考虑最后三位 xyz 。这时候如果 x >= max(y, z) ,则三位也不能换,以此类推。如果 x < any(y, z) ,则可以把 yz 中比 x 大的最小的数拿出来,跟 x 互换位置,剩下的数按顺序排列,就组成下一个更大的数字了。

解法

按照上面的思路,我们可以梳理一下解法:

  • 取出最后两位数字,判断它能否达到要求(通过不同组合生成更大的数字)。如果无法生成更大的数字,换三位试试,以此类推,如果扫描到首位还没有结果,返回 -1 。

  • 如果找到了符合要求的后 x 位数字,则把整个数字单独分割开来,前面的称为 left ,后面 x 位称为 right

  • right 重排,形成下一个更大的数字。重排规则如下:

    • right 而言,找到比 right[0] 的下一个更大数字,把它作为新的 right[0]

    • 剩下的数字升序排列,然后跟新的 right[0] 组合。

  • 组合 leftright 形成新的数字,这就是完整的 "下一个更大的数字" 。

下面来实际编码,我用 JavaScript 实现的。这是主体的 nextBigger 函数:

function nextBigger(n) {
  // 通过 splitDigits 分隔出 left 和 right 两部分
  const [left, right] = splitDigits(`${n}`.split(''), 2)
  if (!left) return -1
  // 对 right 部分重新排列,再跟 left 组合成返回值
  return Number(left.concat(resort(right)).join(''))
}

// 按照 rightSize 分割 digits 数组,如果不和规格,则按 rightSize+1 来递归分割
function splitDigits(digits, rightSize) {
  if (rightSize > digits.length) return []

  const right = digits.slice(-rightSize)
  // 判断 right 是否符合要求
  if (right[0] < right[1]) return [digits.slice(0, -rightSize), right]

  return splitDigits(digits, rightSize + 1)
}

function resort(right) {
  const first = right[0]
  // 这里用 sort 和 reverse 都行
  const rest = right.slice(1).sort()

  // 找到下一个更大数字的索引
  const idx = rest.findIndex(n => n > first)
  const p = rest[idx]
  rest[idx] = first
  return [p].concat(rest)
}

有点注意一下, splitDigits 函数里面判断 right 是否符合要求是用的 right[0] < right[1] ,其中道理可以自己想想。提醒一点,如果代码能走到这里,那么 right[1] 往后的所有数字只可能是 降序排列 的。

源代码和测试可以见我的 GitHub 。如果觉得文章对你有帮助,请帮我点个赞 :)

最后,你可以去 Coderwars 上自己看看 best practise 和 clever 的答案。我觉得这个 kata 的答案思路基本相同,而且 clever 的那个思路其实挺笨的,就没分析它们了。

小结

Kata 的乐趣在于思考和分析问题的规律,然后用合适的编程方式表达出来。这个过程可以有效锻炼逻辑思维和对语言的掌控力。Coderwars 上从低到高的 kata 挺多,主流语言也基本都支持,基本上想放松或想烧脑都能找到合适的选择。

参考链接

Kata: Next bigger number with the same digits
My solution on Coderwars
My solution on GitHub


darkbaby123
1.4k 声望67 粉丝