Given a list of integers, return the nth smallest integer in the list. Only distinct elements should be considered when calculating the answer. n will always be positive (n > 0)
If the nth small integer doesn't exist, return -1 (C++) / None (Python) / nil (Ruby) / null (JavaScript).
Notes:
- "indexing" starts from 1
- huge lists (of 1 million elements) will be tested
Examples
nth_smallest([1, 3, 4, 5], 7) ==> None # n is more than the size of the list
nth_smallest([4, 3, 4, 5], 4) ==> None # 4th smallest integer doesn't exist
nth_smallest([45, -10, 4, 5, 4], 4) ==> 45 # 4th smallest integer is 45 If you get a timeout, just try to resubmit your solution. However, if you always get a timeout, review your code.
I wrote several functions to try to solve this.
My first attempt was:
def nS1(arr = z, n = 16):
st = set(arr)
return sorted(st)[n-1] if n <= len(st) else None
My second:
def nS2(arr = z, n = 16):
st = set()
count = 0
for i in sorted(arr):
if i not in st:
count += 1
if count == n:
return i
st.add(i)
return None
I even tried implementing quickselect:
#Given a list, it modifies it so that the element at `pvtIdx` (pivot index) is the `pvtIdx` smallest element.
def partition(lst, lft, rght, pvtIdx):
pvtVal = lst[pvtIdx] #The value of the pivot element that would be used in comparison.
lst[pvtIdx], lst[rght] = lst[rght], lst[pvtIdx] #Swap `lst[rght]` and `lst[pvtIdx]`.
strIdx = lft #The store index that contains the location that is partitioned.
for i in range(lft, rght): #Iterate through the list.
if lst[i] < pvtVal: #If the current element is less than the pivot element.
lst[i], lst[strIdx] = lst[strIdx], lst[i] #Swap the current element and the partitioner.
strIdx += 1 #Increment the partitioner.
lst[rght], lst[strIdx] = lst[strIdx], lst[rght] #Swap the pivot element and the partitioner.
#The list is now partitioned into elements < the pivot elements and elements > the pivot element around the partition location.
return strIdx #Return the partition location.
def select(lst, lft, rght, k):
if lft == rght: #Return the sole element of the list if it is already sorted.
return lst[lft]
pvtIdx = lft + int(random()*(rght - lft)) #Generate a random pivot index between `lft` and `rght` (both inclusive).
pvtIdx = partition(lst, lft, rght, pvtIdx) #The index of the pivot value in it's sorted position.
if k == pvtIdx: #If that index corresponds to the desired index.
return lst[k]
elif k < pvtIdx: #Insert another element to its sorted position in the partition of the list that the desired element resides in.
return select(lst, lft, pvtIdx - 1, k)
else:
return select(lst, pvtIdx + 1, rght, k) #Insert another element to its sorted position in the partition of the list that the desired element resides in.
def nS3(lst = z, k = 16):
st = set(lst)
ln = len(st)
return None if k > ln else select(list(st), 0, ln-1, k-1)
Switched to an iterative implementation cause python:
def select2(lst, lft, rght, k):
while True:
if lft == rght: #Return the sole element of the list if it is already sorted.
return lst[lft]
pvtIdx = lft + int(random()*(rght - lft)) #Generate a random pivot index between `lft` and `rght` (both inclusive).
pvtIdx = partition(lst, lft, rght, pvtIdx) #The index of the pivot value in it's sorted position.
if k == pvtIdx: #If that index corresponds to the desired index.
return lst[k]
elif k < pvtIdx: #Insert another element to its sorted position in the partition of the list that the desired element resides in.
right = pvtIdx - 1
continue
else:
left = pvtIdx+1 #Insert another element to its sorted position in the partition of the list that the desired element resides in.
continue
def nS4(lst = z, k = 16):
st = set(lst)
ln = len(st)
return None if k > ln else select2(list(st), 0, ln-1, k-1)
I tried using a heap:
def nS5(lst = z, k = 16):
lst = list(set(lst))
ln = len(lst)
if k > ln:
return None
heapify(lst)
for i in range(k):
current = heappop(lst)
return current
I tried optimising my heap:
def nS6(lst = z, k = 16):
heapify(lst)
st = set()
count = 0
while count < k:
if not lst:
return None
current = heappop(lst)
if current not in st:
st.add(current)
count += 1
return current
I tried combining multiple functions together to leverage asymptotics:
def nS7(lst, k):
if len(lst) < 100000:
return nS6(lst, k)
return nS4(lst, k)
I benchmarked my functions (using a fifty element list):
print("t(nS1):\t\t", t(nS1, number = 1000000))
print("t(nS2):\t\t", t(nS2, number = 1000000))
# print("t(nS3):\t\t", t(nS3, number = 1000000))
# print("t(nS4):\t\t", t(nS4, number = 1000000))
print("t(nS5):\t\t", t(nS5, number = 1000000))
print("t(nS6):\t\t", t(nS6, number = 1000000))
(I commented out the two implementations of quickselect because previous benchmarks had shown they took orders of magnitude more time than some of the other implementations).
If others hadn't solved the kata, I would have called bullshit. As it is, I've invested way too much effort into it already.
heapq.nsmallest(k, set(lst))[-1]
fair? \$\endgroup\$