Search code examples
pythonalgorithmrangeunique

Iterate through a Number With Only Unique Characters


EDIT: It has been pointed out that the step can't be altered in the way I was hoping, a clearer way of explaining what I'm trying to do is find ways to skip numbers with duplicate digits.

I want to iterate through every possible unique combination of a number using step, step being derived from the format: range(start_value, end_value, step).

What I want essentially is for i in for i in range(start_value, end_value, step) to always be a number where all the characters in that number are unique.

A similar showing of what I want is in the code below. It will only print the numbers in which there are no repeats.

def printUnique(l,r):
    for i in range (l, r + 1):
        num = i
        visited = [0,0,0,0,0,0,0,0,0,0]

        while (num):
            if visited[num % 10] == 1:
                break
            visited[num % 10] = 1
            num = (int)(num / 10)

        if num == 0:
            print(i, end = ' ')

l = 123456789;
r = 9876543210;
printUnique(l, r);

The above code is, however, not what I am looking for. It does not utilize step, instead, it iterates through all the numbers with a default step of one.

An example format of the code I'm looking for:

start_value = 123456789
# this is basically 0123456789 but putting zero at the beginning raises an error
end_value = 9876543210

step = 1
# step will probably be a changing variable outside of the loop, maybe not depending on how you do it
# any other potential variables that need to be stored

for i in range(start_value, end_value, step):
    print(i)
    # algorithm here that modifies step variable

If start_value is 98 and end_value is 0 (01), the output should be as follows, if the variables were stored in a list and not printed. Numbers 11, 22, 33, 44, 55, 66, 77, 88 and 99 are skipped.

savedList =  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98]

Solution

  • Since I nerd sniped myself with this question, I'm posting here my answer. It works well even for the whole domain of non-repeating digit numbers (it took about 21 s to generate all of then on my machine).

    The main idea is to use a generator to create the numbers with non-repeated digits in order. Then, instead of going step by step on the original list and checking if it doesn't have repeated numbers, we do the other way round: we get the next number that has no repeated digits and see if it would be part of our original list (this is easy to do with % step).

    This is a lot more efficient as the numbers grow because the numbers without repeated digits become progressively rarer.

    The code below generates the list of numbers with no repeated digits (from smaller to largest). Some drudgery was needed to avoid repeating the permutations that started with 0:

    from itertools import permutations
    
    def gen_all_unique():
        return (sum(i*10**n for n, i in enumerate(reversed(per))) for j in range(1, 11) for per in permutations(range(10), j) if per[0] != 0 or j == 1)
    

    Then, this functions ensures we only select the numbers that fall in the requested step (last if):

    def range_non_repeat(l, r, step=1):
        #  Works when l <= r
        for n in gen_all_unique():
            if n < l:
                continue
            if n >= r:
                break
            if (n - l) % step == 0:
                yield n
    

    This work really well if l <= r. To have something that works in any case, we have to create a special function that generates numbers with unique digits starting from the largest possible to the smallest one. We will do this by changing gen_all_unique to return a different generator depending on the direction requested:

    def gen_all_unique(direction=1):
        if direction >= 0:   # Generate a growing sequence
            return (sum(i*10**n for n, i in enumerate(reversed(per))) for j in range(1, 11) for per in permutations(range(10), j) if per[0] != 0 or j == 1)
        # Generate a diminishing sequence
        return (sum(i*10**n for n, i in enumerate(reversed(per))) for j in range(10, 0, -1) for per in permutations(range(9, -1, -1), j) if per[0] != 0 or j == 1)
    
    
    def range_non_repeat(l, r, step=1):
        direction = step // abs(step)  # Get step's sign
        for n in gen_all_unique(step):
            if direction * n < direction * l:
                continue
            if direction * n >= direction * r:
                break
            if (n - l) % step == 0:
                yield n
    

    I think this code is a good example of the usefulness of generators. On the other hand, if this is an operation that will have to be done several times, especially with large numbers, it might be worth generating all the numbers once and caching it. There are only 8,877,691 numbers without repeated digits, which take about 72 Mb of memory to store (assuming 64 bit ints, since 9876543210 is larger than the largest 32 bit uint). Depending on the problem, that might be a cheap price to pay for the speed.