Problem overview
The method maxNumber(nums1: Array[Int], nums2: Array[Int], k: Int): Array[Int] receives two arrays of digits, which stand for two numbers, and a length k that is at most nums1.length + nums2.length. It must build the largest possible number of exactly k digits by choosing digits from both arrays, with one rule: digits taken from the same array keep their original relative order. Digits from the two arrays can be interleaved freely. The result is an array of k digits.
For nums1 = [3,4,6,5], nums2 = [9,1,2,5,8,3] and k = 5 the answer is [9,8,6,5,3]. The 9 and 8 come from nums2, the 6 and 5 from nums1, and the last 3 from nums2. For nums1 = [6,7], nums2 = [6,0,4] and k = 5 every digit must be used, and the answer is [6,7,6,0,4]. Both arrays have 1 to 500 digits.
All candidate numbers have k digits, so "largest" means: compare from the left, and the first digit that differs decides.
Approach: split, shrink, merge
The problem is hard to attack all at once, so it is cut into three smaller ones.
- Choose the split. Any answer takes
idigits fromnums1andk - ifromnums2. The code tries everyifrom 0 tok, skipping those where either array doesn't have enough digits. - For a given split, pick the best digits from each array. The best
i-digit number that keeps the order ofnums1is computed separately, and so is the best(k - i)-digit number fornums2. This is the job ofmaxSubsequence. - Merge the two results into the largest
k-digit number, keeping the order inside each. This is the job ofmerge.
The best candidate over all splits wins.
The best digits of one array: a stack
maxSubsequence(nums, size) reads the digits from left to right and keeps a stack. When the new digit is bigger than the digit on top, the top digit is a bad choice: removing it moves the bigger digit one place to the left, which makes the number larger. So the stack pops while the top is smaller than the new digit, but only if enough digits remain to still fill size places. That condition is count + nums.length - i > size: the digits on the stack, plus the current one and all the unread ones, must still exceed size after dropping one. Then the new digit is pushed if the stack isn't full.
For [3,4,6,5] and size = 2 the stack goes [3], then [4] (the 3 is popped, since 3 comes before a bigger 4 and enough digits remain), then [6], then [6,5]. The result is [6,5].
Merging two digit lists
To merge, repeatedly take the next digit from whichever list is better. Comparing only the two next digits fails on ties. With a = [6,7] and b = [6,0,4], both start with 6. Taking the first 6 from b leaves the other 6 to come next and pushes the 7 to third place, while taking it from a lets the 7 come second. The rule is to compare everything that remains: take from a if what is left of a is greater than what is left of b. isGreater skips over equal digits and decides at the first difference. If b runs out first, the rest of a counts as greater or equal, so taking from a is right.
On the first example, the splits give these candidates:
i = 0: [] + [9,2,5,8,3] -> 92583
i = 1: [6] + [9,5,8,3] -> 96583
i = 2: [6,5] + [9,8,3] -> 98653
i = 3: [4,6,5] + [9,8] -> 98465
i = 4: [3,4,6,5] + [9] -> 93465
Splits with i = 5 are skipped, because nums1 has only 4 digits. The largest candidate is 98653, the expected answer.
Step by step
- Start with an empty best.
var best = Array.empty[Int]. Any candidate beats it, so the first one replaces it. - Loop over the splits.
i <- 0 to kincludesk. Hereidigits come fromnums1and the otherk - ifromnums2. - Skip impossible splits. Only go on when
i <= nums1.lengthandk - i <= nums2.length. - Get the best digits of each array with
maxSubsequence, intofirstandsecond. - Merge them into
candidate, which haskdigits. - Keep the larger. If
isGreater(candidate, 0, best, 0), replacebest. Both start at index 0. - End with
best, with noreturn: the last expression is the method's value. - Write
maxSubsequence. It is aprivatehelper taking the array andsize. The stack is an array ofsizedigits,stack, andcountsays how many are in use. The top isstack(count - 1). - Visit each index with
nums.indices, because the index tells how many digits remain. - Pop while it helps. The
whileloop runs as long as the stack isn't empty, the top is smaller thannums(i), andcount + nums.length - i > size. Popping is justcount -= 1: the old digit stays in the array and is overwritten by the next push. - Push if there's room. If
count < size, storenums(i)atstack(count)and increasecount. A digit that finds the stack full is left out. End withstack. - Write
merge. It makesresultwith room fora.length + b.lengthdigits and two indices,iandj, both starting at 0. - Loop until both are used.
i < a.length || j < b.length: each pass places one digit. - Choose the source. If
isGreater(a, i, b, j), storea(i)atresult(i + j)and advancei. Otherwise storeb(j)there and advancej. The indexi + jis the number of digits placed so far. End withresult. - Write
isGreater. It takes both arrays and start indices. Parameters can't be reassigned in Scala, so it copies the starts into the variablesiandj. - Skip equal digits. A
whileloop advances both while neither is exhausted anda(i) == b(j). - Decide. The result is
j == b.length || (i < a.length && a(i) > b(j)):awins ifbran out, or ifastill has a digit and it is bigger.
The solution in Scala
class Solution {
def maxNumber(nums1: Array[Int], nums2: Array[Int], k: Int): Array[Int] = {
var best = Array.empty[Int]
for (i <- 0 to k) {
if (i <= nums1.length && k - i <= nums2.length) {
val first = maxSubsequence(nums1, i)
val second = maxSubsequence(nums2, k - i)
val candidate = merge(first, second)
if (isGreater(candidate, 0, best, 0)) {
best = candidate
}
}
}
best
}
private def maxSubsequence(nums: Array[Int], size: Int): Array[Int] = {
val stack = new Array[Int](size)
var count = 0
for (i <- nums.indices) {
while (count > 0 && stack(count - 1) < nums(i) && count + nums.length - i > size) {
count -= 1
}
if (count < size) {
stack(count) = nums(i)
count += 1
}
}
stack
}
private def merge(a: Array[Int], b: Array[Int]): Array[Int] = {
val result = new Array[Int](a.length + b.length)
var i = 0
var j = 0
while (i < a.length || j < b.length) {
if (isGreater(a, i, b, j)) {
result(i + j) = a(i)
i += 1
} else {
result(i + j) = b(j)
j += 1
}
}
result
}
private def isGreater(a: Array[Int], aStart: Int, b: Array[Int], bStart: Int): Boolean = {
var i = aStart
var j = bStart
while (i < a.length && j < b.length && a(i) == b(j)) {
i += 1
j += 1
}
j == b.length || (i < a.length && a(i) > b(j))
}
}
The Scala details worth a closer look:
- Ranges.
0 to kincludesk, while0 until kwould stop atk - 1. The spliti = ktakes every digit fromnums1, so it is needed.nums.indicesis the range0 until nums.length. - Arrays.
new Array[Int](size)creates an array filled with zeros, and elements are read and written with parentheses:stack(count - 1),stack(count) = nums(i). AnArrayhas a fixed size, which is why the stack tracks its fill withcountinstead of resizing. - The stack array can be returned as is. It has exactly
sizeslots, and the pop condition guarantees that all of them are filled by the end. Whensizeis 0 the array is empty. - Last expression as the value.
best,stack,resultand the boolean inisGreaterare the last lines of their methods, so they are the return values. Noreturnkeyword is needed. private. The three helpers are visible only insideSolution. They can call each other in any order, because methods of a class can refer to later methods.&&and||short-circuit. Ini < a.length && a(i) > b(j)the index is only read when it is valid. In thewhilecondition ofisGreater, the two length checks come beforea(i) == b(j)for the same reason.- Parameters are values. A method parameter, like
aStart, can't be assigned to, hence the localvarcopies.
Complexity
Let m and n be the lengths of the arrays.
Time. There are at most k + 1 splits, and at most min(m, n) + 1 of them are valid. For each, maxSubsequence runs in O(m + n), since each digit is pushed and popped at most once. merge places k digits, and each placement may scan up to k digits in isGreater when the remainders are alike, so it is O(k²) in the worst case. Comparing the candidate with best costs O(k). All together, an upper bound is O(k × (m + n + k²)). The scan in isGreater stops at the first differing digit, so usual inputs are much faster than that bound.
Space: O(m + n). Each split creates two stack arrays, a merged array of k digits, and best holds one more k-digit array.
Edge cases and common mistakes
- Taking everything from one array. For
nums1 = [5,1,9],nums2 = [2]andk = 1, the splits arei = 0(digit 2) andi = 1(digit 9). The answer is[9]. - Using all digits. With
k = m + nonly one split is valid, and the result is a merge of the two arrays as they are. The pop condition never allows a pop, because every digit is needed. - Ties in the merge. Comparing just
a(i)andb(j)and always taking frombon a tie gives[6,6,7,0,4]for[6,7]and[6,0,4], which is smaller than the right answer[6,7,6,0,4]. Compare the whole remainders. - Popping too much. Without the
count + nums.length - i > sizetest, the stack could throw away digits it needs, ending with fewer thansizedigits. For[1,9]andsize = 2, the 1 must not be popped when the 9 arrives, and the test is what prevents it. - Zeros. The digits may be 0, and the answer may start with 0 if there is no choice, as
[0,0]and[0]withk = 3gives[0,0,0]. Compare digit by digit, not by converting to anIntorLong, which cannot hold up to 1000 digits. - Range off by one.
0 until kmisses the split that takes allkdigits fromnums1. - Forgetting the split limits. Without the check on
iandk - i,maxSubsequencegets asizelarger than its array and returns a stack that isn't filled.
Testing
nums1 = [3,4,6,5],nums2 = [9,1,2,5,8,3],k = 5gives[9,8,6,5,3].nums1 = [6,7],nums2 = [6,0,4],k = 5gives[6,7,6,0,4].nums1 = [3,9],nums2 = [8,9],k = 3gives[9,8,9].nums1 = [5,1,9],nums2 = [2],k = 1gives[9].nums1 = [0,0],nums2 = [0],k = 3gives[0,0,0].
