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)
,则可以把 y
和 z
中比 x
大的最小的数拿出来,跟 x
互换位置,剩下的数按顺序排列,就组成下一个更大的数字了。
解法
按照上面的思路,我们可以梳理一下解法:
取出最后两位数字,判断它能否达到要求(通过不同组合生成更大的数字)。如果无法生成更大的数字,换三位试试,以此类推,如果扫描到首位还没有结果,返回 -1 。
如果找到了符合要求的后 x 位数字,则把整个数字单独分割开来,前面的称为
left
,后面 x 位称为right
。-
对
right
重排,形成下一个更大的数字。重排规则如下:对
right
而言,找到比right[0]
的下一个更大数字,把它作为新的right[0]
。剩下的数字升序排列,然后跟新的
right[0]
组合。
组合
left
和right
形成新的数字,这就是完整的 "下一个更大的数字" 。
下面来实际编码,我用 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