You can do this in sub-linear time by double-sorting the x & y coordinates. This, of course, assumes that the dictionary of points is fairly static, because there is obviously a (small) cost for adding or moving things.
Below involves using several sorted containers, so each query to getSurrounding
is going to invoke 4 bisection searches, which is still O(log(N)). The added benefit is that this should work for float
values for locations, but I have not really thought that through.
from sortedcollections import SortedDict, SortedList
pos_dict = {(0,0): 'a',
(1,2): 'b',
(1,3): 'c',
(2,2): 'd',
(2,6): 'e',
(3,2): 'f',
(4,4): 'g',
(5,5): 'h'}
sd = SortedDict()
# put everything into a sorted dictionary
for x, y in pos_dict:
temp = sd.get(x, SortedList())
temp.add(y)
sd[x] = temp
# we look for candidate x values in the sorted dictionary
# and be clever with the Manhattan
# distance to find the y values
rng = 2
ctr = (2,2)
points_in_range = []
left_x = ctr[0] - rng
right_x = ctr[0] + rng
low_y = ctr[1] - rng
hi_y = ctr[1] + rng
x_vals = sd.keys()[sd.bisect(left_x) : sd.bisect(right_x)]
# get y values within the remaining Manhattan distance and construct tuples from (x, y)
for x in x_vals:
temp = sd.get(x)
low_y_lim = low_y + abs(x-ctr[0])
hi_y_lim = hi_y - abs(x-ctr[0])
y_vals = temp[temp.bisect(low_y_lim) : temp.bisect(hi_y_lim)]
points_in_range.extend([(x, y) for y in y_vals])
print(points_in_range)
for p in points_in_range:
print(f'{pos_dict.get(p)} is in range at location {p}')
Yields:
[(1, 2), (1, 3), (2, 2), (3, 2)]
b is in range at location (1, 2)
c is in range at location (1, 3)
d is in range at location (2, 2)
f is in range at location (3, 2)
Other Notions...
Depending on size of dictionary and values of range, you might be able to get to near linear time. If the value of rng is small, then fabricating a set of points that are "in range" and intersecting it with a set of the keys in your dictionary is very easy and is near linear if rng is "small."
set_of_in_rng = {(x,y) for x in range(ctr[0]-rng, ctr[0]+rng+1)
for y in range(low_y+abs(x-2), hi_y-abs(x-2)+1)}
points_in_range = set_of_in_rng.intersection(set(pos_dict.keys()))
for p in points_in_range:
print(f'{pos_dict.get(p)} is in range at location {p}')
Yields the same result:
b is in range at location (1, 2)
f is in range at location (3, 2)
c is in range at location (1, 3)
d is in range at location (2, 2)