Next Bigger Number when given an int

Question:

I’m having trouble optimizing my algorithm for finding the next biggest number with the same digits when given an integer, returning -1 if there is no bigger number.

Examples of what the function should do:

next_bigger(13): —> 31

next_bigger(201): —> 210

next_bigger(2017): —-> 2071

next_bigger(10) —–> -1

next_bigger(587) —–> 758

I thought using itertools.permutations() would be my best bet for the problem, but the time complexity is demanding.

Here’s my code so far:

import itertools

def next_bigger(n):
    x = list(itertools.permutations(str(n), len(str(n))))
    lst = [int(''.join(x[i])) for i in range(0, len(x))]
    lst.sort(reverse=True)
    if n == lst[0]:
        return -1
    for i in range(0, len(lst)):
        if lst[i + 1] == n:
            return lst[i]

I tried wrapping the list in a set and then back into a list to remove duplicates but that didn’t help. I also tried an if statement in the list comprehension where the list would only include values greater than the original number ‘n’.

Asked By: Wenderer

||

Answers:

I have no idea why itertools.permutations even exists. It is easy code to write yourself, but it is so absurdly inefficient that anyone reaching for it for more than toy examples is almost always making a mistake. As you’ve discovered.

What you have to do is think about the pattern for the next number. For that we need two facts:

  1. We want to increase the smallest digit we can by the least possible by moving a smaller one there
  2. The digits after that should be in ascending order.

So take your 587. We can’t move anything into the 1’s digit. The 7 moved into the 10’s digit would be smaller. The smallest thing we can move into the 100’s digit is the 7. After that the other digits are in ascending order, so 58. Giving us 758.

So what we’ll do is go from the last digit to the first, sorting the end in ascending order as we go until we find that we can make the number bigger. And when we do, we’ll find the smallest thing to put in.

def next_bigger (n):
    # array manipulation is easier with arrays
    digits = list(str(n))
    # i is the position of the digit we're working on.
    # This places it on the 10s digit
    i = len(digits) - 2
    # While i is still in the digits.
    while 0 <= i:
        # We have found the digit to increase!
        if digits[i] < digits[-1]:
            # j will be the digit to exchange with.
            j = len(digits) - 1
            # Scan backwards until we find the smallest digit to place at i
            while digits[i] < digits[j-1]:
                j -= 1
            # swap
            (digits[i], digits[j]) = (digits[j], digits[i])
            # turn it back into a number and return it.
            return int(''.join(digits))
        else:
            # Move the `i`'th digit to the end.
            d = digits.pop(i)
            digits.append(d)
            # And now we'll look at the next largest digit.
            i -= 1
    # We failed to find larger.
    return -1

# Here are all of your test cases.   
for n in [13, 201, 2017, 10, 785]:
    print(n, next_bigger(n))
Answered By: btilly

I would argue that permutations not needed here. You need to rethink/reformulate the task.

If you need to find next bigger number this simply means that when iterating from right to left you need to find the first occurrence where "left" < "right" (i.e. not in ascending order) and "right" is the minimum of all rights satisfying the condition, swap them and then place everything to the right of the new position of "right" in the ascending order. For example naive implementation can look like:

def next_bigger(n):
    reversed = list(str(n))[::-1] # reversed string
    max = reversed[0]
    for left_idx, left_c in enumerate(reversed[1:], start=1):
        if max <= left_c:
            # continue iteration in ascending order
            max = left_c
        else:
            # found the firs left that is not ascending order
            res = []
            for right_idx, right_c in enumerate(reversed):                
                if right_c > left_c:
                    # found the minimum right, sort everything before the found "left" position 
                    sorted_pos = sorted(res + reversed[right_idx+1:left_idx] + [left_c], reverse=True)
                    res = sorted_pos + [right_c] + reversed[left_idx+1:]
                    # build back the number
                    return int(''.join(res[::-1]))  
                else:
                    res.append(right_c)            

    return -1

But since we know that values before the found "left" are in the ascending order we can improve (for "long" numbers) the solution by finding exact new position of left and reversing the parts around it instead of using sort:

def next_bigger(n):
    reversed = list(str(n))[::-1]
    max = reversed[0]
    for left_idx, left_c in enumerate(reversed[1:], start=1):
        if max <= left_c:
            max = left_c
        else:
            for right_idx, right_c in enumerate(reversed):
                if right_c > left_c:
                    res = reversed[right_idx + 1:left_idx][::-1] + [left_c] + reversed[:right_idx][::-1] + [right_c] + reversed[left_idx+1:]
                    return int(''.join(res[::-1]))             

    return -1

Or much more succinct solution by @trincot.

Answered By: Guru Stron

The other answers explain what the approach is. I just reiterate the steps to have a complete answer:

  1. Starting from the right, find the first digit that is less than its successor. If there is none, there is no greater number possible.

  2. Consider the section at the right of that found position. We know the digits in that section are sorted in non-increasing order (because of the previous step). From that section, choose the digit that is the least among those that are greater than the digit found in step 1.

  3. Swap the two found digits. This means that the section of step 2 is still sorted in non-increasing order. Then reverse that section. The swap and reversal can be done in one expression.

I provide here an implementation that stops the explicit iteration when the digit to augment has been identified (step 1), and reconstructs the resulting string by piecing the pieces (reversed) together, using string slicing:

def next_bigger(n):
    s = str(n)
    for i in range(len(s) - 2, -1, -1):
        if s[i] < s[i + 1]:
            for k in range(len(s) - 1, i, -1):
                if s[i] < s[k]:
                    return int(s[:i] + s[k] + s[-1:k:-1] + s[i] + s[k-1:i:-1])
    return -1
Answered By: trincot