Simplest way of generate a circular pattern around a number

Viewed 117

I am trying to get all the surrounding integers of a given one, starting from the actual integer and going outwards from both sides at a time, for example:

generateNeighborhood(10, 3), where 10 is the origin and 3 the radius of the neighbourhood, should return [10,11,9,12,8,13,7].

My attempt so far is this:

def generateNeighborhood(c: int, r: int):
    yield c
    for i in range(1, r + 1):
        for j in range(2):
            yield c + i * (-1) ** j

Do you know any simpler ways of achieving these results?

4 Answers

These are both variants I could come up with. Decide for yourself if you think one is better. The problem is if you want to generate 1, -1 and 2, -2 at the same part of the loop, you would get a nested list. Therefore, you need to flatten it like I did with itertools. Also, in the first method, 0 is generated twice, therefore I need to skip one 0 by starting at index 1.

def generateNeighborhoodA(c: int, r: int):
    li = list(itertools.chain.from_iterable((-i, i) for i in range(r + 1)))
    for i in range(1, len(li)):
        yield li[i] + c


def generateNeighborhoodB(c: int, r: int):
    yield c
    for i in range(1, r + 1):
        yield c + i
        yield c - i

If the order of elements wouldn't matter, this would be super short.

A.

Here's a simple one-liner:

>>> c, r = 10, 3
>>> sorted(range(c-r, c+r+1), key=lambda n: (abs(c-n), -n))
[10, 11, 9, 12, 8, 13, 7]

range() can easily give us the numbers we need but to get them in the right order, sort each by its distance from the center c, using abs(). And to get the larger neighbour before the smaller in that pair (11 before 9), use -n which is more negative (smaller) for the larger neighbours.

If you want the smaller neighbour before the larger one, then flip the sign:

>>> sorted(range(c-r, c+r+1), key=lambda n: (abs(c-n), n))
[10, 9, 11, 8, 12, 7, 13]

In a generator:

def generateNeighborhood(c: int, r: int):
    yield from sorted(range(c-r, c+r+1), key=lambda n: (abs(c-n), -n))

But note that sorted() returns a list anyway, so may as well put return sorted(...). And for very very long lists (large radii r), use (B) a modified version of your original code or (C) the variation below it:


B.

As user3386109 suggested in the comments, you can remove the j loop and yield the neighbours for that radius:

def generateNeighborhood(c: int, r: int):
    yield c
    for i in range(1, r + 1):
        yield c + i
        yield c - i

C.

Here's a variation, with generator expressions:

def generateNeighborhood(c: int, r: int):
    yield c
    pairs = ((c + i, c - i) for i in range(1, r + 1))  # generator, not expanded here
    yield from (n for pair in pairs for n in pair)     # processed one at a time

Performance with small and large radii:

Small: list(generateNeighborhood(10, 3)) - B is faster than A is faster than C

  1. B: 1.19 µs
  2. A: 2.53 µs
  3. C: 2.6 µs

Large: list(generateNeighborhood(10, 3_000_000)) - B is faster than C is faster than A:

  1. B: 877 ms
  2. C: 1.43 s
  3. A: 2.97 s

So for simplicity, go with A. For speed, go with B. (I thought C would beat B but it doesn't.)

An efficient way of calculating this is vectorizing the operations. You can use numpy and the zip function to generate your result:

import numpy as np
def generateNeighborhood(c: int, r: int):
    return np.hstack((np.array(c), np.array(list(zip(np.array(c) + np.array(range(1, r+1)), np.array(c) - np.array(range(1, r+1))))).ravel())).tolist()

The surrounding integer arrays are combined using zip so that they are arranged as requested, and then horizontally stacked with the central value. If you don't need the result as a list, simply remove the .tolist() at the end of the function.

Here is a neat way to do it with NumPy (or without Numpy, as in comments). Just replace the np.arange with the list comprehensions in comments to avoid numpy.-

def generateNeighborhood(n, m):

    #Get elements front and behind
    f = np.arange(n+1,n+m+1)  #[i for i in range(n+1, n+m+1)]
    l = np.arange(n-m,n+1)    #[i for i in range(n-m, n+1)]
    
    #Alternating merge between 2 lists
    result = [None]*(len(l)+len(f))
    result[::2] = reversed(l)
    result[1::2] = f
    print(result)
    
generateNeighborhood(10,3)
[10, 11, 9, 12, 8, 13, 7]
Related