Recursion in Python 3.2 - python

I am trying to wrap my head around recursion and have posted a working algorithm to produce all the subsets of a given list.
def genSubsets(L):
res = []
if len(L) == 0:
return [[]]
smaller = genSubsets(L[:-1])
extra = L[-1:]
new = []
for i in smaller:
new.append(i+extra)
return smaller + new
Let's say my list is L = [0,1], correct output is [[],[0],[1],[0,1]]
Using print statements I have narrowed down that genSubsets is called twice before I ever get to the for loop. That much I get.
But why does the first for loop initiate a value of L as just [0] and the second for loop use [0,1]? How exactly do the recursive calls work that incorporate the for loop?

I think this would actually be easier to visualize with a longer source list. If you use [0, 1, 2], you'll see that the recursive calls repeatedly cut off the last item from the list. That is, recusion builds up a stack of recursive calls like this:
genSubsets([0,1,2])
genSubsets([0,1])
genSubsets([0])
genSubsets([])
At this point it hits the "base case" of the recursive algorithm. For this function, the base case is when the list given as a parameter is empty. Hitting the base case means it returns an list containing an empty list [[]]. Here's how the stack looks when it returns:
genSubsets([0,1,2])
genSubsets([0,1])
genSubsets([0]) <- gets [[]] returned to it
So that return value gets back to the previous level, where it is saved in the smaller variable. The variable extra gets assigned to be a slice including only the last item of the list, which in this case is the whole contents, [0].
Now, the loop iterates over the values in smaller, and adds their concatenation with extra to new. Since there's just one value in smaller (the empty list), new ends up with just one value too, []+[0] which is [0]. I assume this is the value you're printing out at some point.
Then the last statement returns the concatenation of smaller and new, so the return value is [[],[0]]. Another view of the stack:
genSubsets([0,1,2])
genSubsets([0,1]) <- gets [[],[0]] returned to it
The return value gets assigned to smaller again, extra is [1], and the loop happens again. This time, new gets two values, [1] and [0,1]. They get concatenated onto the end of smaller again, and the return value is [[],[0],[1],[0,1]]. The last stack view:
genSubsets([0,1,2]) <- gets [[],[0],[1],[0,1]] returned to it
The same thing happens again, this time adding 2s onto the end of each of the items found so far. new ends up as [[2],[0,2],[1,2],[0,1,2]].
The final return value is [[],[0],[1],[0,1],[2],[0,2],[1,2],[0,1,2]]

I am no big fan of trying to visualize the entire call graph for recursive function to understand what they do.
I believe there is a much simpler way:
Enter fairy tale land where recursive functions do the right thing™.
Just assume that genSubsets(L) works:
# This computes the powerset of the list L minus the last element
smaller = genSubsets(L[:-1])
Because this magically worked, the only entries that are missing are those, that contain the last element.
This fragment constructs all those missing subsets:
new = []
for i in smaller:
new.append(i+extra)
Now we have those subsets containing the last element in new and we have those subsets not containing the last element in smaller.
It follows that we must now have all subsets, so we can return new + smaller.
The only thing left is the base case to make sure the recursion stops. Because the empty set (or list in this case) is an element of every power set, we can use that to stop the recursion: Requesting the powerset of an empty set is a set containing the empty set. So our base case is correct. Since every recursive step removes one element off the list, the base case must be encountered at some time.
Thus, the code really does produce the power set.
Note: The principle behind this is that of induction. If something works for some known n0, and we can prove that: The algorithm working for n implies it works for n+1, it must thus work for all n &geq; n0.

Related

Why would a list() method be used on a python list in a recursion problem?

I'm currently learning Depth-First Search in Python and a problem asked that, given a Binary Search Tree and a number N, find all paths from root-to-leaf such that the sum of all the node values of each path equals N.
I did everything right, but my code didn't work (it resulted in an empty 2D array). When looking at the solution, the only difference was "allPath.append(list(currPath))", while the code I wrote was simply 'allPath.append(currPath)'. When I made this change, the code worked perfectly Here's the full code:
def findPathSum(root, sum):
allPath = []
_findPathSum(root, sum, [], allPath)
return allPath
def _findPathSum(currNode, sum, currPath, allPath):
if currNode is None:
return
currPath.append(currNode.val)
if currNode.val == sum and currNode.left is None and currNode.right is None:
print(currPath)
allPath.append(list(currPath))
else:
_findPathSum(currNode.left, sum-currNode.val, currPath, allPath)
_findPathSum(currNode.right, sum-currNode.val, currPath, allPath)
del currPath[-1]
What I'm confused about is that currPath is already a list, and only contains integers (which are the node values). When I print currPath before it is appended to allPaths, it also correctly displays a list with integer values. Yet after I append it to allPaths, allPaths is just an empty array. However, using the list() method on it, for some reason, displays the correct 2D array with the right integer values. I have no clue why this would work.
From my understanding, the list() method simply takes an iterable and turns it into a list...however currPath was already a list. I feel like I'm missing something really obvious.
list creates a brand new list (although the elements are not brand new), a new list object. In your case, without using list you will simply be appending the exact same list object to allPath on each recursive call.
Therefore, since all the elements of allPath are the exact same list, changing that list changes all of the elements of allPath. For example, when at the end of _findPathSum you do del currPath[-1], you are effectively deleting the final element of every element of allPath. Since in the end currPath will be empty, that is what you see at the end in allPath - a list containing empty lists.

How to iterate through a list and use .isdigit to verify if a list of strings is made up of only digits or not

Write a function that takes, as an argument, a list, identified by the variable aList. If the list only contains elements containing digits (either as strings as integers), return the string formed by concatenating all of the elements in the list (see the example that follows). Otherwise, return a string indicating the length of the list, as specified in the examples that follow.
I am just starting to learn how to code and this is my first CS class.
def amIDigits(aList):
for element in range(aList):
if element in aList.isdigit():
bList=[]
bList.append(aList)
return str(bList)
amIDigits([“hello”, 23]) should return the string “The length of the input is 2.”
amIDigits ([“10”, “111”]) should return the string “10111”
If I understand it right the output will be the joined digits even if they are not of the string format. So the best way is to use the all function (returns true if all elements of an iteration are true) and check if the string elements of the list are digits. If so, then return the join of all elements of the list converted to a string. Else, we return the length of the list using the new string formatting syntax (f represents string formatting and the {} return the result of an operation).
code:
def amIDigits(aList):
if all([str(i).isdigit() for i in aList]):
return ''.join(map(str,aList))
else:
return f'The length of the input is {len(aList)}.'
print(amIDigits(['hello', 23]))
print(amIDigits(['10', '111']))
print(amIDigits([55, 33]))
output:
The length of the input is 2.
10111
5533
First, I highly recommend having someone literally sit down and have you walk them through your thought process. It is more useful as a learner to debug your thought process than to have someone give you the answer.
One thing I noticed is that you created your empty list, bList, inside the for block. This will not work. You need to create an empty list to store things into before you begin for looping through the old list, otherwise you will be over-writing your new list every time it loops. So right now, your bList.append() statement is appending an element onto an empty list every time it runs. (You will get only the very last element in the aList stored into your bList.)
Another problem is that you use the range() function, but you don't need to. You want to look at each element inside the list. Range creates a sequence of numbers from 0 to whatever number is inside the parentheses: range() documentation. Your code tries to pass a list into range(), so it is invalid.
The "for blank in blank" statement breaks up whatever list is in the second blank and goes through each of its elements one at a time. For the duration of the for statement, the first blank is the name of the variable that refers to the element being looked at. so for example:
apples = ["Granny Smith","Red Delicious","Green"]
for apple in apples:
eat(apple) #yum!
The for in statement is more naturally spoken as "for each blank in blank:"

Finding the shortest word in a string

I'm new to coding and I'm working on a question that asks to find the shortest word within a sentence. I'm confused what the difference between:
def find_short(s):
for x in s.split():
return min(len(x))
and
def find_short(s):
return min(len(x) for x in s.split())
is, because the former gives me an error and the latter seems to work fine. Are they not virtually the same thing?
Are they not virtually the same thing?
No, they are not the same thing. If s equals "hello world", in the first iteration, x would be "hello". And there are two things wrong here:
You are trying to return in the very first iteration rather than going over all the elements (words) to find out what's the shortest.
min(len(x)) is like saying min(5) which is not only an bad parameter to pass to min(..) but also doesn't make sense. You'd want to pass a list of elements from which min will calculate the minimum.
The second approach is actually correct. See this answer of mine to get an idea of how to interpret it. In short, you are calculating length of every word, putting that into a list (actually a generator), and then asking min to run its minimum computation on it.
There's an easier approach to see why your second expression works. Try printing the result of the following:
print([len(x) for x in s.split()])
The function min takes an array as parameter.
On your 1st block, you have
def find_short(s):
for x in s.split():
return min(len(x))
min is called once on the length of the 1st word, so it crashes because it's expecting an array
You second block is a little different
def find_short(s):
return min(len(x) for x in s.split())
Inside min, you have len(x) for x in s.split() which will return an array of all the lengths and give it to min. Then, with this array, min will be able to return the smallest.
No, they are not the same thing.
In first piece of code you are entering for cycle and trying to calculate min of the first word's length. min(5) doesn't make sense, does it? And even if it could be calculated, return would have stopped executing this function (other words' lengths would not have been taken into consideration).
In second one, len(x) for x in s.split() is a generator expression yielding the lengths of all the words in your sentence. And min will calculate the minimal element of this sequence.
Yes, the examples given are very different.
The first example effectively says:
Take the string s, split it by spaces, and then take each word, x, found and return the minimum value of just the length of x.
The second example effectively says:
Find the minimum value in the list generated by len(x) for x in s.split().
That first example generates an error because the min function expects to compare at least 2 or more elements, and only 1 is provided.
That second example works because the list that is generated by len(x) for x in s.split() converts a string, like say "Python types with ducks?" to a list of word lengths (in my example, it would convert the string to [6, 5, 4, 6]). That list that is generated (this is also why it's called a generator), is what the min function then uses to find the minimum value inside said list.
Another way to write that first example so that it works like you would expect is like this
def find_short(s):
min_length = float("inf")
for x in s.split():
if len(x) < min_length:
min_length = len(x)
return min_length
However, notice how you have to keep track of a variable that you do not have to define using the list generator method in your second example. Although this is not a big deal when you are learning programming for the first time, it becomes a bigger deal when you start making larger, more complex programs.
Sidenote:
Any value that follows the return keyword is what a function "outputs", and thus no more code gets executed.
For example, in your first example (and assuming that the error was not generated), your loop would only ever execute once regardless of the string you give it because it does not check that you actually have found the value you want. What I mean by that is that any time your code encounters a return statement, it means that your function is done.
That is why in my example find_short function, I have an if statement to check that I have the value that I want before committing to the return statement that exits the function entirely.
There is mainly two mistakes here.
First of, seems you are returning the length of the string, not the string itself.
So your function will return 4 instead of 'book', for example.
I will get into how you can fix it in short.
But answering your question:
min() is a function that expects an iterable (entities like array).
In your first method, you are splitting the text, and calling return min(len(word)) for each word.
So, if the call was successfully, it would return on the first iteration.
But it is not successfully because min(3) throws an exception, 3 is not iterable.
On your second approach you are creating a list of parameters to min function.
So your code first resolves len(x) for x in s.split() returning something like 3,2,3,4,1,3,5 as params for min, which returns the minimum value.
If you would like to return the shortest word, you could try:
def find_short(s):
y = s.split()
y.sort(key=lambda a: len(a))
return y[0]

Python Function Not Working

I am trying to create a function, new_function, that takes a number as an argument.
This function will manipulate values in a list based on what number I pass as an argument. Within this function, I will place another function, new_sum, that is responsible for manipulating values inside the list.
For example, if I pass 4 into new_function, I need new_function to run new_sum on each of the first four elements. The corresponding value will change, and I need to create four new lists.
example:
listone=[1,2,3,4,5]
def new_function(value):
for i in range(0,value):
new_list=listone[:]
variable=new_sum(i)
new_list[i]=variable
return new_list
# running new_function(4) should return four new lists
# [(new value for index zero, based on new_sum),2,3,4,5]
# [1,(new value for index one, based on new_sum),3,4,5]
# [1,2,(new value for index two, based on new_sum),4,5]
# [1,2,3,(new value for index three, based on new_sum),5]
My problem is that i keep on getting one giant list. What am I doing wrong?
Fix the indentation of return statement:
listone=[1,2,3,4,5]
def new_function(value):
for i in range(0,value):
new_list=listone[:]
variable=new_sum(i)
new_list[i]=variable
return new_list
The problem with return new_list is that once you return, the function is done.
You can make things more complicated by accumulating the results and returning them all at the end:
listone=[1,2,3,4,5]
def new_function(value):
new_lists = []
for i in range(0,value):
new_list=listone[:]
variable=new_sum(i)
new_list[i]=variable
new_lists.append(new_list)
return new_lists
However, this is exactly what generators are for: If you yield instead of return, that gives the caller one value, and then resumes when he asks for the next value. So:
listone=[1,2,3,4,5]
def new_function(value):
for i in range(0,value):
new_list=listone[:]
variable=new_sum(i)
new_list[i]=variable
yield new_list
The difference is that the first version gives the caller a list of four lists, while the second gives the caller an iterator of four lists. Often, you don't care about the difference—and, in fact, an iterator may be better for responsiveness, memory, or performance reasons.*
If you do care, it often makes more sense to just make a list out of the iterator at the point you need it. In other words, use the second version of the function, then just writes:
new_lists = list(new_function(4))
By the way, you can simplify this by not trying to mutate new_list in-place, and instead just change the values while copying. For example:
def new_function(value):
for i in range(value):
yield listone[:i] + [new_sum(i)] + listone[i+1:]
* Responsiveness is improved because you get the first result as soon as it's ready, instead of only after they're all ready. Memory use is improved because you don't need to keep all of the lists in memory at once, just one at a time. Performance may be improved because interleaving the work can result in better cache behavior and pipelining.

Python: how to split and return a list from a function to avoid memory error

I am currently working with a function that enumerates all cycles within a specific array (a digraph) and I need them all. This function returns all cycles as a list of lists (each sublist being a cycle, e.g. result=[[0,1,0],[0,1,2,0]] is a list containing 2 cycles starting and ending in node 0). However, there are millions of cycles so for big digraphs I get a memory error (MemoryError: MemoryError()) since the list of lists containing all cycles is too big.
I would like that the function splits the result in several arrays so I do not get the memory error. Is that possible? and would that solve the issue?
I tried to do that by splitting the results array as a list of sub-results (the sub-results have a maximum size, say 10 million which is below the 500 million max size stated here: How Big can a Python Array Get? ). The idea is that the result is a list containing sub-results: result=[sub-result1, sub-result2]. However, I get a different memory error: no mem for new parser.
The way I do that is as follows:
if SplitResult == False:
result = [] # list to accumulate the circuits found
# append cycles to the result list
if cycle_found(): #cycle_found() just for example
result.append(new_cycle)
elif SplitResult == True:
result = [[]] # list of lists to accumulate the circuits found
# append cycles to the LAST result SUB-lists
if cycle_found(): #cycle_found() just for example
result[len(result)-1].append(new_cycle)
# create a new sublist when the size of the LAST result SUB-lists
# reaches the size limit (ResultSize)
if len(result[len(result)-1]) == ResultSize:
result.append([])
Maybe the issue is that I merge all sub-results within the results list. In that case, how can I return a variable number of results from a function?
In particular I divide all simple cycles of a 12 node complete digraph in sublists of 10 million cycles. I know there are 115,443,382 cycles in total, so I should get a list with 16 sublists, the first 15 containing 10 million cycles each and the last one containing 443,382 cycles. Instead of that I get a different memory error: no mem for new parser.
This procedure works for an 11 node complete digraph which returns 2 sublists, the first containing the 10 million cycles (10000000) and the other containing 976184. In case it is of any help, their memory footprint is
>>> sys.getsizeof(cycles_list[0])
40764028
>>> sys.getsizeof(cycles_list[1])
4348732
Then, I guess we should add the size of each cycle listed:
>>> sys.getsizeof(cycles_list[0][4])
56
>>> cycles_list[0][4]
[0, 1, 2, 3, 4, 0]
Any help will be most welcome,
Thanks for reading,
Aleix
Thank you for your suggestions. Indeed the right approach to avoid memory issues when returning arrays is simply by avoiding creating so big result arrays. Thus, generator functions are the way forward.
Generator functions are well explained here: What does the "yield" keyword do in Python?
I would just add that a normal function becomes a generator function at the very moment where you add a yield in it. Also, if you add a return statement the generation of iterables will end when reaching it (some generator functions do not have "return" and are thus infinite).
Despite the simple use of generators I had some hard time transforming the original function into a generator function since it was a recursive function (i.e. calling itself). However, this entry shows how a recursive generator function looks like Help understanding how this recursive python function works? and so I could apply it to my function.
Again, thanks to all for your support,
Aleix

Categories

Resources