5

I am trying to write a function that takes a positive integer and returns the next smaller positive integer containing the same digits, and -1 when there is no smaller number that contains the same digits.

For example:

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

I wrote a code that solves this, but I don't really know how to optimize it further. Could you please help me? It runs fairly fast on repl.it, but when I submit it, it says it takes more than 1200ms and doesn't allow me to submit it even though all the tests pass.

function nextSmaller(n) {
var nArray= n.toString().split("")
var minimumNum = 1 + Array(nArray.length).join('0')
for(var i=n-1; i >= minimumNum; i--) {
  var newNumArray = i.toString().split('');
  var counter = 0;
  for (var j=0; j<newNumArray.length; j++) {
    if (nArray.indexOf(newNumArray[j]) > -1) {
        counter++
        nArray.splice(nArray.indexOf(newNumArray[j]), 1)
        if (counter === n.toString().split("").length) {
        return i;
        }
     } 
  }
       nArray = n.toString().split("");
       if (i === Number(minimumNum)) return -1;
  }
}
  • 4
    Perhaps this is more suited in [code review](http://codereview.stackexchange.com/) – E. Sundin Feb 03 '17 at 05:22
  • Look for patterns to narrow the problem space. For example, you should only be checking permutations of the digits in the input. – 4castle Feb 03 '17 at 05:27
  • Seems like this is a kata on Codewars – JohanP Feb 03 '17 at 05:29
  • yeah it is a kata on codewars JohanP. thanks E.Sundin, I'll post it there. 4castle, I don't understand what you mean, could you please elaborate? – Abdel Shokair Feb 03 '17 at 05:30
  • It seems the problem is about finding the next lexicographically smaller permutation if you use c++ you can use next_permutation with suitable compare function http://www.cplusplus.com/reference/algorithm/next_permutation/ or you can implement it yourself. But you may need to handle special case where there are leading zero in the next smaller permutation – Petar Petrovic Feb 03 '17 at 05:35
  • [Comments archived](http://chat.stackoverflow.com/rooms/134912/discussion-on-question-by-abdel-aziz-shokair-how-to-optimize-my-function-that-ta) – Bhargav Rao Feb 05 '17 at 17:26

2 Answers2

5

Your code could be optimized a bit, for instance you could use a break statement in your inner loop to move on to the next number as soon as you know the current one isn't going to work (that should make it run in about half the time, but it is still quite slow for an n like 91234567) and instead of n.toString().split("").length in the loop, use a variable so you only need to convert n to an array once.

function nextSmaller(n) {
  var nArray = n.toString().split("")
  var length = nArray.length;
  var minimumNum = 1 + Array(length).join('0')
  for(var i=n-1; i >= minimumNum; i--) {
    var newNumArray = i.toString().split('');
    var counter = 0;
    for (var j=0; j<newNumArray.length; j++) {
      if (nArray.indexOf(newNumArray[j]) < 0)
          break;
      counter++
      nArray.splice(nArray.indexOf(newNumArray[j]), 1)
      if (counter === length) {
          return i;
      }
    }
    nArray = n.toString().split("");
  }
  return -1;
}

There is a very efficient algorithm for computing the next permutation, which can easily be adapted to get the previous one instead (and return -1 if the resulting permutation starts with 0). I adapted this algorithm to do that:

[21,531,2071,912345678,9123545678,915345678].forEach( x => console.log( nextSmaller( x ) ) );

function nextSmaller(n) {
  
    const arr = ( n + '' ).split( '' ).map( Number );
  
    // Find longest non-decreasing suffix
    let i, prev = 9;
    for ( i = arr.length; i--; ) {
        if ( arr[ i ] > prev )
            break;
        prev = arr[ i ];
    }
  
    // If whole sequence is non-decreasing,
    // it is already the smallest permutation
    if ( i < 0 )
        return -1;
  
    const pivot_i = i;
    const pivot = arr[ pivot_i ];
  
    for ( i = arr.length; i--; ) {
        if ( arr[ i ] < pivot )
            break;
    }
  
    arr[ pivot_i ] = arr[ i ];
    arr[ i ] = pivot;

    if ( arr[ 0 ] === 0 )
        return -1;
  
    return +arr.slice( 0, pivot_i + 1 ).concat( arr.slice( pivot_i + 1 ).reverse( ) ).join('');
}
Paul
  • 139,544
  • 27
  • 275
  • 264
  • _"There is a very efficient algorithm"_ How is this substantiated? "very efficient" compared to? – guest271314 Feb 03 '17 at 08:33
  • 1
    @guest271314 It's obvious by looking at it. All it does is loop through the array at most twice and call slice, concat, and reverse a couple times (not in a loop). – Paul Feb 03 '17 at 08:43
  • Nothing is "obvious". Obvious can also be misleading. Find it amazing that when "efficiency" is mentioned in a Question that no actual reproducible measurement of the "efficiency" of a proposed approach is included at Answer. Especially where the tools are available to show and prove the fact of "efficiency". Time is measurable. OP did not ask for theory. Show and prove the facts [Is it more efficient to use find() rather than > for child selector in jQuery?](http://stackoverflow.com/questions/39993827/is-it-more-efficient-to-use-find-rather-than-for-child-selector-in-jquery/39994266#39994266) – guest271314 Feb 03 '17 at 08:47
  • @guest271314 The OP didn't mention "efficiency"; that was you. They have a threshold of 1.2 seconds, but a simple function like this can obviously run thousands of times within that threshold, so it doesn't really matter. – Paul Feb 03 '17 at 08:50
  • Have an inclination you are gathering what am communicating. Some users read the term "optimize" literally. Meaning, show and prove the approach requires less time to complete; that is, the truth. In any event, perhaps OP is not interested in the actual numbers and exacting facts; but rather, a "sense" of "optimize". Here, the numbers are all that matter. The numbers do not lie. There is no possible negotiation with the clock. When iterating or creating 700k+ or more items, each moment matters. Each profile matters. For that matter, everything matters all of the the time. – guest271314 Feb 03 '17 at 08:55
  • @guest271314 Yes, but measuring time doesn't tell you much about an algorithm. If you run the OP's algorithm on `123456798` it will be very quick (thought probably still slower than mine), because it only needs to decrement `9` times, but if you run it on `912345678` it needs to decrement over `10000000` times, whereas mine takes essentially the same amount of time for all inputs of similar length – Paul Feb 03 '17 at 09:05
  • _"but if you run it on 912345678 it needs to decrement over 10000000 times"_ Does it? – guest271314 Feb 03 '17 at 09:16
  • @guest271314 Yes, it does. Over double that number of times, in fact. – Paul Feb 03 '17 at 09:17
  • No, it does not. Though being barred from providing Answer diminishes possibility of sharing what have found, initially by doing the math by hand; at some times for days on end without rest. – guest271314 Feb 03 '17 at 09:19
  • @guest271314 Your answer wouldn't change what the OP's code does. Nor would it outperform my answer, or even come close if you use an algorithm that repeatedly decrements until it finds the answer. – Paul Feb 03 '17 at 09:19
  • Yes it would. Guaranteed. Am not still at this Answer because did not invest many a full day and night into this particular subject matter. First by doing the calculations by hand. Over and over again. Until the relationships were revealed. When found the solution; over and over again to verify. – guest271314 Feb 03 '17 at 09:21
  • @guest271314 It doesn't matter how many days and nights you spent. I spent 30 minutes, but still wrote one that is provably optimal. – Paul Feb 03 '17 at 09:24
  • Am not here to convince. Already know the truth. Because invested better part of last year on this one subject. At least you did produce a link to verify what your pattern compared to OPs. Though you are mistaken to believe that that is the only way to look at the numbers; or the universe for that matter. The universe is linear in some ways, though linked in circles in others. Cheers. – guest271314 Feb 03 '17 at 09:28
2

The algorithm could be like the following:

  1. For the input number n find all numbers that are formed with some permutations of the same digits and sort these numbers. For example, if n=213, we get the sorted list as [123, 132, 213, 231, 312, 321]. (e.g., Permutations in JavaScript? can help you).

  2. Find the index i of the number n in the sorted list. If i>0 return the number at index i-1 else return -1 (if it's the smallest number appearing at the first position of the sorted list).

Another alternative algorithm could be the following:

Decrement the number n until and unless you find one that has exactly same digits (in a different order, you can sort the digits and check for equality).

The most efficient will be similar to the one referred to by @Paulpro(https://www.nayuki.io/page/next-lexicographical-permutation-algorithm)

  1. Find the longest non-decreasing suffix from the decimal string representation of n.
  2. If the entire string n is non-decreasing then return -1 (there can't be any smaller).
  3. Otherwise choose the digit immediately left to the start of the suffix as pivot and swap it with (the leftmost and) the largest digit in the suffix that is smaller than the pivot. Return this number.
Community
  • 1
  • 1
Sandipan Dey
  • 21,482
  • 2
  • 51
  • 63
  • How does this Answer resolve Question _"How to optimize my function"_ ? How can the description of algorithm be reproduced verified as being more efficient that `javascript` at Answer? – guest271314 Feb 03 '17 at 08:34
  • well, it was tagged as `algorithm` so thought of posting one that i could think of and also posted some `javascript` links for implementation, in my view it can be implemented efficiently, since number of digits will be limited, there will be not many permutations, even we can optimize and generate partial list of permutation and stop as soon as we see the number `n`. – Sandipan Dey Feb 03 '17 at 08:39
  • _"in my view it can be implemented efficiently"_ Algorithms begins with theory but is proved with reproducible patterns. Can you show and prove the algorithm that you propose using `javascript`? And that that same algorithm requires less time to complete than `javascript` at Question? Else, what is point? Efficiency is a measurable and reproducible fact, not theory. – guest271314 Feb 03 '17 at 08:41
  • 1
    it's not about a particular programming language i guess, the time & space complexity of this algorithm in the `worst case` will be `O(d!)` where `d` is the number of digits in `n`, since `d` is not going to be very big (assuming based on the examples given by OP), any standard implementation of (partial) permutation generation algorithm is not going to take a lot of time, there is nothing specific about `javascript` i guess, it's yet another programming language. – Sandipan Dey Feb 03 '17 at 08:46
  • Not communicating about `O(d!)`. Communicating about the actual time the process takes to complete. A simple example would be `console.time()`, `console.timeEnd()` – guest271314 Feb 03 '17 at 08:52
  • 1
    @guest271314 Complexity is more relevant than the actual time take. The measured time is hardware dependant, and doesn't tell you anything about how the running-time increases as the length of the input increases, while the complexity class actually tells you how the algorithm compares to other algorithms. The one I posted is `O(d)`. – Paul Feb 03 '17 at 08:55
  • actual time taken can't be way larger than the theoretical complexity (it's matter of constants) unless it's a particularly bad implementation, the link posted (or any standard recursive algorithm for computing permutation) for computing permutation should be efficient enough, OP can test the actual time taken. – Sandipan Dey Feb 03 '17 at 08:57
  • You're welcome @SandipanDey . Oddly it doesn't mention Knuth, but I found it by searching for "Knuth next permutation". I first read about that algorithm in TAOCP. – Paul Feb 03 '17 at 09:27
  • Yes @Paulpro, it's a clever and beautiful algorithm. – Sandipan Dey Feb 03 '17 at 09:29
  • @Paulpro Did you read all 3 books of TAOCP? that's great! which one do you recommend to start first and where to start from? I read CLRS and tardos- kleinberg as texts. – Sandipan Dey Feb 03 '17 at 09:41
  • 1
    @SandipanDey I wish I could say that. One day I will, haha. I have the box set including volume 4A. I read all of volume 1, but I haven't read any of the others cover to cover. I've probably read an average of about 40% of each of them. – Paul Feb 03 '17 at 09:48
  • Protip: The **previous permutation algorithm** is given at the bottom of the web page – Nayuki Feb 04 '17 at 16:33