Kata: 下一個更大的數字

TL;DR

這是一個 Coderwars 上的練習,等級 4kyu 。Coderwars 上的題目分爲 8 級,數字越小越難。這題算是中等難度。下面是個人分析和解法,語言使用的 JavaScript ,但你也能夠用任何其餘語言來實現。javascript

Kata 描述

你須要提供一個函數,它接受一個正整數爲參數並返回另外一個正整數。返回值必須由輸入的整數的每位數字構造而成,而且是最接近原整數的更大的數字。英文原文是 the next bigger number formed by the same digits 。若是這種數字不存在,函數返回 -1 。java

聽起來挺繞的,看看例子吧。下面的 nextBigger 就是要寫的函數:git

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 。github

思路

我以爲對任何 kata 題目而言,最重要的不是用什麼技巧寫代碼,而是如何發現問題中的規律。這個問題也是如此。想知道如何解題,咱們先想一想 數字是怎麼比大小 的。編程

數字比大小的規則很簡單,大概描述起來以下:數組

  • 先比較位數,位數高的更大。函數

  • 若是位數相同,則從第一位數字開始比較,數字更大的取勝。若是第一位數字相等,則比較第二位數字,以此類推直到末位數。測試

對這個題目而言,構造出來的新數字位數跟原數字是同樣的,因此只用考慮上面的第二條規則。加上題目的描述,咱們就能夠分析出 下一個更大數字 究竟是什麼意思:儘可能只調整末位 x 位數得到滿意的結果,而且 x 儘量小。換句話說,能動最後兩位數字的就別動最後三位。編碼

那麼怎麼知道最少動最後幾位數字能知足要求呢?這就得進一步分析下規律了。讓咱們回顧兩個例子:rest

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

相關文章
相關標籤/搜索