Fixing Gap in Knowledge about recursion: base conditions and state - python-3.x

I have a gap in knowledge with recursion. I understand the base case should terminate the recursion but I have difficulty choosing the correct one.
Moreover I have a problem in understanding how to manage states without updating the method signature.
Take for instance the problem of largest adjacent element products. My understanding of divide and conquer is:
1) divide the problem into smaller problems:
1.1) create a left array of the first two elements
1.2) create a right array by removing the first element
2) conquer by recursion:
2.1) repeat the function on the right array
2.2) choose a good base case to terminate
3) combine the solution
3.1) this is where things get tricky!?
3.2) for the task of multiplication, how do I persist the result
after each recursion when each new call will re-instantiate the
result list
A concrete example of this gap in knowledge is below: The base case I chose is when the list has fewer than two elements, then return 0. Of course that works except when the product of two elements is less than 0.
Returning None for the base case is a problem with the state, because in python3 None and int comparison throws an error.
TypeError: '>=' not supported between instances of 'int' and 'NoneType'
Complete code is below
def multiply(inputArray):
m = inputArray[0] * inputArray[1]
return m
def adjacentElementsProduct(inputArray):
# [3, 6, -2, -5, 7, 3]
if len(inputArray) <= 1:
# return 0
return None
left = inputArray[:2]
right = inputArray[1:]
result = []
result.append(adjacentElementsProduct(right))
m = multiply(left)
print(result, left, m)
if len(result) == 0:
result.append(m)
else:
if m >= result[0]:
result.insert(0, m)
return result[0]

Seem like you main problem is how to combine solutions. In every single iteration, what you need to combine is results of left array and right array.
how do I persist the result?
Just return max of left result and right result.
def adjacentElementsProduct(inputArray):
# [3, 6, -2, -5, 7, 3]
if len(inputArray) <= 1:
return None
left = inputArray[:2]
right = inputArray[1:]
m = multiply(left)
result = adjacentElementsProduct(right)
# combine solutions
if result is None:
return m
else:
return max(m, result)
Testcases:
print(adjacentElementsProduct([3]))
None
print(adjacentElementsProduct([3,6]))
18
print(adjacentElementsProduct([3, 6, -2, -5, 7, 3]))
21

Related

My second for loop is not working in the below code

nums=[0,5,4,12]
n=len(nums)
temp=1
ans=[]
for i in range(n):
ans.append(temp)
temp*=nums[i]
temp=1
for i in range(n-1,-1):
ans[i]*=temp
temp*=nums[i]
print("yes")
print(ans)
Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i].
The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.
You must write an algorithm that runs in O(n) time and without using the division operation.
This is a solution for this leetcode question but my second for loop is not executing, and i don't know why.
Using range like this will result in zero iterations because the "step" parameter is 1. So because it is 1 it will think it should go upwards, but n is already above -1, so it should be like
range(n, -1, -1)
also you most probably want
import math
n = [0, 5, 4, 12]
ans = []
for num in n:
temp = n[:] # create a copy
temp.remove(num) # remove the number you are on
ans.append(math.prod(temp)) # use math.prod to multiply the rest together
return ans

Conditionally use parts of a nested for loop

I've searched for this answer extensively, but can't seem to find an answer. Therefore, for the first time, I am posting a question here.
I have a function that uses many parameters to perform a calculation. Based on user input, I want to iterate through possible values for some (or all) of the parameters. If I wanted to iterate through all of the parameters, I might do something like this:
for i in range(low1,high1):
for j in range(low2,high2):
for k in range(low3,high3):
for m in range(low4,high4):
doFunction(i, j, k, m)
If I only wanted to iterate the 1st and 4th parameter, I might do this:
for i in range(low1,high1):
for m in range(low4,high4):
doFunction(i, user_input_j, user_input_k, m)
My actual code has almost 15 nested for-loops with 15 different parameters - each of which could be iterable (or not). So, it isn't scalable for me to use what I have and code a unique block of for-loops for each combination of a parameter being iterable or not. If I did that, I'd have 2^15 different blocks of code.
I could do something like this:
if use_static_j == True:
low2 = -999
high2 = -1000
for i in range(low1,high1):
for j in range(low2,high2):
for k in range(low3,high3):
for m in range(low4,high4):
j1 = j if use_static_j==False else user_input_j
doFunction(i, j1, k, m)
I'd just like to know if there is a better way. Perhaps using filter(), map(), or list comprehension... (which I don't have a clear enough understanding of yet)
As suggested in the comments, you could build an array of the parameters and then call the function with each of the values in the array. The easiest way to build the array is using recursion over a list defining the ranges for each parameter. In this code I've assumed a list of tuples consisting of start, stop and scale parameters (so for example the third element in the list produces [3, 2.8, 2.6, 2.4, 2.2]). To use a static value you would use a tuple (static, static+1, 1).
def build_param_array(ranges):
r = ranges[0]
if len(ranges) == 1:
return [[p * r[2]] for p in range(r[0], r[1], -1 if r[1] < r[0] else 1)]
res = []
for p in range(r[0], r[1], -1 if r[1] < r[0] else 1):
pa = build_param_array(ranges[1:])
for a in pa:
res.append([p * r[2]] + a)
return res
# range = (start, stop, scale)
ranges = [(1, 5, 1),
(0, 10, .1),
(15, 10, .2)
]
params = build_param_array(ranges)
for p in params:
doFunction(*p)

Is there a way to sort an unsorted list with some repeated elements?

I am trying to sort an unsorted list [4, 5, 9, 9, 0, 1, 8]
The list has two repeated elements. I have tried to approach the question by having a loop that goes through each element comparing each element with the next in the list and then placing the smaller element at the start of the list.
def sort(ls:
ls[x]
x = [4, 5, 9, 9, 0, 1, 8]
while len(x) > 0:
for i in the range(0, len(x)):
lowest = x[i]
ls.append(lowest)
Please, could someone explain where I am going wrong and how the code should work?
It may be that I have incorrectly thought about the problem and my reasoning for how the code should work is not right
I do not know, if this is exactly what you are looking for but try: sorted(ListObject).
sorted() returns the elements of the list from the smallest to the biggest. If one element is repeated, the repeated element is right after the original element. Hope that helped.
Yes, you can try x.sort() or sorted(x). Check this out https://www.programiz.com/python-programming/methods/built-in/sorted. Also, in your program I don't see you making any comparisons, for example, if x[i] <= x[i+1] then ...
This block of code is just gonna append all the elements in the same order, till n*n times.
Also check this https://en.wikipedia.org/wiki/Insertion_sort
For a built-in Python function to sort, let y be your original list, you can use either sorted(y) or y.sort().Keep in mind that sorted(y) will return a new list so you would need to assign it to a variable such as x=sorted(y); whereas if you use x.sort() it will mutate the original list in-place, so you would just call it as is.
If you're looking to actually implement a sorting function, you can try Merge Sort or Quick Sort which run in O (n log n) in which will handle elements with the same value. You can check this out if you want -> https://www.geeksforgeeks.org/python-program-for-merge-sort/ . For an easier to understand sorting algorithm, Insertion or Bubble sort also handle duplicate as well but have a longer runtime O (n^2) -> https://www.geeksforgeeks.org/python-program-for-bubble-sort/ .
But yea, I agree with Nameet, what you've currently posted looks like it would just append in the same order.
Try one of the above suggestions and hopefully this helps point you in the right direction to if you're looking for a built-in function or to implement a sort, which can be done in multiple ways with different adv and disadv to each one. Hope this helps and good luck!
There are several popular ways for sorting. take bubble sort as an example,
def bubbleSort(array):
x = len(array)
while(x > 1): # the code below make sense only there are at least 2 elements in the list
for i in range(x-1): # maximum of i is x-2, the last element in arr is arr[x-1]
if array[i] > array[i+1]:
array[i], array[i+1] = array[i+1], array[i]
x -= 1
return array
x = [4, 5, 9, 9, 0, 1, 8]
bubbleSort(x)
your code has the same logic as below
def sorts(x):
ls = []
while len(x) > 0:
lowest = min(x)
ls.append(lowest)
x.remove(lowest)
return ls
x = [4, 5, 9, 9, 0, 1, 8]
sorts(x)
#output is [0, 1, 4, 5, 8, 9, 9]

What is the empty dictionary used for in the code?

I'm doing practice problems in python on Leetcode (still learning). This is the problem:
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
Example:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].
my code is
class Solution:
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
dict = {}
for counter, i in enumerate(nums):
a = target- i
if a in dict:
return (dict[a], counter)
dict[i] = counter
It runs fine and passes all the tests however I found a common reason this works is for the dict = {}
What is the reason for this dictionary and how does this code recognize cases for (3,3) target = 6 where there are duplicates and index matters. A basic run down of why the code works would be great!
The dictionary stores as keys the numbers in the list with their index as a value.
For example:
[2, 7, 11, 15] -> {'2':0, '7':1, '11':2, '15':3}
There is never a duplicate inserted, if the same number appears twice, the index will be replaced with the new index where it appears.
In the case of duplicate, it is important to test all value on the first list, and to store index on a separated dict in order to be sur that you will never test in dictionnary the actually tested value.
By using a dictionnary in order to find the index of the right number, you can't store duplicate.
Since in dictionnary you can't have 2 values with the same key, if duplicate, you just change the old index with the new one.
For example, if dict == {'3': 0, '2':1} and the tested value is 2, the dict == {'3': 0, '2':2}.
And if the target is reach by duplicate number (2+2 for target 4 for example), nothing is stored cause of the return in the if a in dict: return (dict[a], counter)

Returning a list of lists using python recursion

I'm brushing up on recursion problems and have no issues when the problem statement asks to print values (eg. BST traversal where you print the node values), and similarly few issues when the problem asks to return a list of values (return a path between 2 nodes in a tree) but am having problems when there are multiple answers, involving multiple lists (or a single 2D list) to be returned. For example the problem asks me to return how many ways a child can reach the top of the stairs, assuming it can jump either 1, 2, or 3 steps at a time. This is no problem and is solved below
def step_helper(steps):
if(steps == 0):
return 1
elif(steps < 0):
return 0
else:
return step_helper(steps-1) +
step_helper(steps-2) +
step_helper(steps-3)
def find_num_ways(steps):
count = step_helper(steps)
print(count)
find_num_ways(10)
Similarly if I need to return a path from two nodes in a BST, returning 1 list is no problem
def another_path_helper(self, n1, n2):
if(n1 == None):
return [None]
elif(n1 == n2):
return [n1.data]
elif(n1.data > n2.data):
return [n1.data] + self.another_path_helper(n1.left, n2)
elif(n1.data < n2.data):
return [n1.data] + self.another_path_helper(n1.right, n2)
else:
return None
def another_path(self, n1, n2):
path = self.another_path_helper(n1, n2)
if(None in path):
return None
else:
return path
However, I'm clueless on how I would return a list of lists. In the child-steps example, instead of returning the number of ways a child can climb the stairs, how could I return a list of paths, which would be a 2d list, where each entry would be a list of steps taken to get from bottom to top? Ideally I would not need to pass a list as argument to my recursive function since I've been told passing mutable objects to recursive functions is a bad practice and no different from using a static counter.
There is absolutely nothing wrong with passing a list as an argument to your recursive function, and not modifying it. In fact, doing so makes it fairly trivial to solve the problem.
Consider a small version of the problem: only 3 steps. You are at the bottom of the stairs. You can take one step, two steps or three steps. You then have 3 sub-problems to solve:
All the solutions starting with a path of [1], going 2 additional steps.
All the solutions starting with a path of [2], going 1 additional step.
All the solutions starting with a path of [3], going 0 additional steps.
Looks like a good start towards the recursive solution.
Let's focus on just the first of these sub-problems. Your path is [1], and you have 2 addition steps to go. From here, you can take 1 step, 2 steps or 3 steps. You again have 3 sub-sub-problems:
All the solutions starting with a path of [1,1], going 1 additional step.
All the solutions starting with a path of [1,2], going 0 additional step.
All the solutions starting with a path of [1,3], going -1 additional steps.
The first sub-sub-problem requires more work ... another recursive call, which should return [[1,1,1]]. The second sub-sub-problem should return just the path we've taken to get here: [[1,2]]. And the last sub-sub-problem should return no solutions: []. We add these solutions together [[1,1,1]] + [[1,2]] + [] to get [[1,1,1],[1,2]], and return that.
Backing up, the second sub-problem, "starting with a path of [2], going 1 additional step" should return [[2,1]] as the set of solutions. The third sub-problem, "starting with a path of [3], going 0 additional steps" should return [[3]]. Adding these solutions together with [[1,1,1],[1,2]] gives the complete solution set: [[1,1,1],[1,2],[2,1],[3]]
As code:
def find_paths(total):
def helper(path, remaining):
paths = []
if remaining == 0:
paths.append(path)
elif remaining > 0:
for step in range(1,3+1):
paths.extend( helper(path + [step], remaining - step))
return paths
return helper([], total)
print(find_paths(3))
The output, as expected, is:
[[1, 1, 1], [1, 2], [2, 1], [3]]
Of course, you don't have to pass path, the current list of the steps, into the recursive call. You could instead just ask for all paths from the current step to the top of the stairs, and prefix those with the step just being taken. We don't even need a helper in this case:
def find_paths(remaining):
paths = []
if remaining == 0:
paths.append([])
for step in range(1,3+1):
if step <= remaining:
subpaths = find_paths(remaining - step)
for subpath in subpaths:
paths.append([step] + subpath)
return paths
print(find_paths(4))
The output, as expected, is:
[[1, 1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 3], [2, 1, 1], [2, 2], [3, 1]]
It should be noted that find_paths(2) will be called -- and will return the same subpaths, [[1,1], [2]] -- after ascending the first two steps, either one at a time with the path [1,1] or as one jump of two steps with the path [2]. Since it returns the same value, instead of recomputing all the subpaths from that point, we can cache the result, and reuse the value on subsequent steps.
from functools import lru_cache
#lru_cache()
def find_paths(remaining):
paths = []
if remaining == 0:
paths.append([])
for step in range(1,3+1):
if step <= remaining:
subpaths = find_paths(remaining - step)
for subpath in subpaths:
paths.append([step] + subpath)
return paths
paths = find_paths(10)
print(len(paths))
print(find_paths.cache_info())
274
CacheInfo(hits=17, misses=11, maxsize=128, currsize=11)
If you set the cache size to zero #lru_cache(maxsize=0), you can see that the find_paths() function is called 600 times in the course of the problem: CacheInfo(hits=0, misses=600, maxsize=0, currsize=0). With the cache enabled, it is called only 28 times, and only executes 11 times; 17 times, the previously stored result is immediately returned, which can be a considerable savings.

Resources