What are the base cases in this recursive BST algorithm? - python

I have not often written recursive functions/methods. I successfully understand the "base case" in simpler functions such as the following:
def countdown(num):
if num == 0: # Base case
return
print(num) # Action
countdown(num-1) # Reduction and recurse
def factorial(num):
if num == 0 or num == 1: # Base cases
return 1
return factorial(num-1) * num # reduction + recurse + action
However, I have programmed an algorithm that finds the closest value to a given value in a Binary Search Tree (BST), as well as insertion, print all nodes, and other such algorithms. I've found these algorithms to be much harder to understand especially with regard to identify the base cases. This question specifically pertains to the following implementation of findClosest and _findClosest. Consider:
class BSTNode:
def __init__(self,value=None):
self.value = value
self.leftChild = None
self.rightChild = None
class BST:
def __init__(self):
self.root = None
def findClosest(self, value):
if self.root is None: # Base case 0
print("There is no tree, so can't find closest!")
return None
else:
return self._findClosest(value, self.root, self.root.value)
def _findClosest(self, searchValue, currentNode, closest):
if currentNode is None: # Base case #1
return closest
if abs(currentNode.value - closest) < abs(closest - searchValue): # ACTION
closest = currentNode.value
if searchValue > currentNode.value: # Reduce and recurse
return self._findClosest(searchValue,currentNode.rightChild, closest)
elif searchValue < currentNode.value: # Reduce and recurse
return self._findClosest(searchValue,currentNode.leftChild,closest)
else:
return currentNode.value # Base case #2
You can see that I attempted to identify the base cases. As I am using a sort of "driver, pseudo-public" method which handles the first base case of no root separately, I labeled that "base case 0". In any event, my point of confusion is that there are 4 separate return statements in _findClosest(), and my current knowledge of recusion would have me surmising that the two I've labeled Base case #1 and Base case #2 with comments are in fact the only two base cases in that method. However, in order for this method to work correctly, I've also had to return the results of _findClosest() when called on the leftChild and rightChild, so would these also be more base cases? I am having a difficult time determining what is a base case vs. what is the reduction/recursive step. The fact that there are multiple base cases and separate "paths of recursion" if you will, is far more difficult for me to understand than those simple recursive functions that I mentioned earlier. Finally, the base cases in the _findClosest method are also spread out, with the recursive calls sandwiched in the middle, further adding to my confusion.
Please note that the code provided runs fine on Python 3.7.9, however I purposely did not include the rest of the BST methods and driver as I was concerned with including too much code. I also am not sure that this questions would require those remaining items. If a suitable answer does, I will edit and add them.

For a recursive function, any state that doesn't require recursive computation can be a base case. Even if calculating the base value requires some computation (that isn't recursive), it qualifies as a base case, since it ends the recursion there.
Similarly, any statement that does or does not do any computation but makes a recursive call to some sub-state can be qualified as a recursive step (reduction depends on the computation; if the search space isn't reduced, you'll technically be stuck in infinite recursion).
As for your implementation, you've correctly classified the statements in _findClosest(). However, the classification for findClosest() wouldn't make much sense, since it isn't a recursive function anyways.

From
https://www.sparknotes.com/cs/recursion/whatisrecursion/section1/page/3/
The base case, or halting case, of a function is the problem that we know the answer to, that can be solved without any more recursive calls. The base case is what stops the recursion from continuing on forever. Every recursive function must have at least one base case (many functions have more than one).
When you are calling your recursive function _findClosest with left_child or right child, you are not sure if you find the the closest number of searchValue. You will be sure you found the closest number when one of the base case happen (currentNode is Null or currentNode.value == searchValue).
By the way, you could easily implement your function _findClosest as an iterative function (because you never let call of your function on the stack, '_findClosest' never called itself two time).
I think you made a mistake in the comparison, when you want to see if searchValue is closer with the currentNode.value then with the closest:
if abs(currentNode.value - closest) < abs(closest - searchValue):
should be:
if abs(currentNode.value - searchValue) < abs(closest - searchValue):

Related

I want to add values while a recursive loop unfolds

This is a bottom up approach to check if the tree is an AVL tree or not. So how this code works is:
Suppose this is a tree :
8
3 10
2
1
The leaf node is checked that it is a leaf node(here 1). It then unfolds one recursion when the node with data 2 is the current value. The value of cl = 1, while it compares the right tree. The right branch of 2 is empty i.e does not have any children so the avl_compare will have (1, 0) which is allowed.
After this i want to add one value to cl so that when the node with data 3 is the current value, the value of cl = 2. avl_check is an assignment question. I have done this on my own but i need some help here to play with recursive functions.
def avl_check(self):
cl = cr = 0
if(self.left):
self.left.avl_check()
cl+=1
if(self.right):
self.right.avl_check()
cr += 1
if(not self.avl_compare(cl,cr)):
print("here")
Your immediate problem is that you don't seem to understand local and global variables. cl and cr are local variables; with the given control flow, the only values they can ever have are 0 and 1. Remember that each instance of the routine gets a new set of local variables: you set them to 0, perhaps increment to 1, and then you return. This does not affect the values of the variables in other instances of the function.
A deeper problem is that you haven't thought this through for larger trees. Assume that you do learn to use global variables and correct these increments. Take your current tree, insert nodes 4, 9, 10, and 11 (nicely balanced). Walk through your algorithm, tracing the values of cl and cr. By the time you get to node 10, cl is disturbingly more than the tree depth -- I think this is a fatal error in your logic.
Think through this again: a recursive routine should not have global variables, except perhaps for the data store of a dynamic programming implementation (which does not apply here). The function should check for the base case and return something trivial (such as 0 or 1). Otherwise, the function should reduce the problem one simple step and recur; when the recursion returns, the function does something simple with the result and returns the new result to its parent.
Your task is relatively simple:
Find the depths of the two subtrees.
If their difference > 1, return False
else return True
You should already know how to check the depth of a tree. Implement this first. After that, make your implementation more intelligent: checking the depth of a subtree should also check its balance at each step. That will be your final solution.

How to multiply without the * sign using recursion?

so as homework for a programming class on python we're supposed to multiply to integers (n,m) with each other WITHOUT using the * sign (or another multiplication form). We're supposed to use recursion to solve this problem, so i tried just adding n with itself, m number of times. I think my problem is with using recursion itself. I have searched on the internet for recursion usage, no results. Here is my code. Could someone point me in the right direction?
def mult(n,m):
""" mult outputs the product of two integers n and m
input: any numbers
"""
if m > 0:
return n + n
return m - 1
else:
return 1
I don't want to give you the answer to your homework here so instead hopefully I can provide an example of recursion that may help you along :-).
# Here we define a normal function in python
def count_down(val):
# Next we do some logic, in this case print the value
print(val)
# Now we check for some kind of "exit" condition. In our
# case we want the value to be greater than 1. If our value
# is less than one we do nothing, otherwise we call ourself
# with a new, different value.
if val > 1:
count_down(val-1)
count_down(5)
How can you apply this to what you're currently working on? Maybe, instead of printing something you could have it return something instead...
Thanks guys, i figured it out!!!
i had to return 0 instead of 1, otherwise the answer would always be one higher than what we wanted.
and i understand how you have to call upon the function, which is the main thing i missed.
Here's what i did:
def mult(n,m):
""" mult outputs the product of two integers n and m
input: any numbers
"""
if m == 0:
return 0
else:
return n + mult(n, m - 1)
You have the right mechanics, but you haven't internalized the basics you found in your searches. A recursive function usually breaks down to two cases:
Base Case --
How do you know when you're done? What do you want to do at that point?
Here, you've figured out that your base case is when the multiplier is 0. What do you want to return at this point? Remember, you're doing this as an additive process: I believe you want the additive identity element 0, not the multiplicative 1.
Recursion Case --
Do something trivial to simplify the problem, then recur with this simplified version.
Here, you've figured out that you want to enhance the running sum and reduce the multiplier by 1. However, you haven't called your function again. You haven't properly enhanced any sort of accumulative sum; you've doubled the multiplicand. Also, you're getting confused about recursion: return is to go back to whatever called this function. For recursion, you'll want something like
mult(n, m-1)
Now remember that this is a function: it returns a value. Now, what do you need to do with this value? For instance, if you're trying to compute 4*3, the statement above will give you the value of 4*2, What do you do with that, so that you can return the correct value of 4*3 to whatever called this instance? You'll want something like
result = mult(n, m-1)
return [...] result
... where you have to fill in that [...] spot. If you want, you can combine these into a single line of code; I'm just trying to make it easier for you.

Understanding Recursion on Binary Trees

Hello I am new to programing so at school we are learning recursion and binary trees. But for me recursion is something that really melts my mind away.
so I was trying out a recursive function on a Binary Tree trying to output the preorder format
def preorder(self, preorder_list):
""" (BinaryTree) -> list
Return a list of the items in this tree using a *preorder* traversal.
"""
if self.root is not EmptyValue:
if self.left and self.right != None:
preorder_list.append(self.left.preorder(preorder_list))
preorder_list.append(self.right.preorder(preorder_list))
def helper_preorder(self):
preorder_list = []
self.preorder(preorder_list)
return preorder_list
When I run this code the output I get is:
for example;
[None, None, None, None]
Now I suspect this has to be a problem with my recursive reasoning. So I would like to know what is the problem with my recursive reasoning and how I can better my self in recursion.
Thanks for your time.
Your problem is that you're never returning anything from preorder, and Python is implicitly returning None. So you append the return value of the function to your list, but that value is None. Your function should look like this (In pseudo code, not valid Python)1:
preorder(node, list):
if node is Empty:
return list
else:
new_list.append(node.data)
preorder(node.left, new_list);
preorder(node.right, new_list);
return list
Note that I am duplicating the return statement, so you could optimize this, but I set it up this way because it's easiest to understand recursion if you think of it as a base case or a recursive call.
To understand recursion2, think about breaking a hard problem into smaller, easier problems. I'll take a simple example, one of the classic recursion examples: factorial(n).
What is factorial(n)? That's a really hard question to answer, so let's find something simpler.
From math class, we know that n! = n*(n-1)*(n-2)*...*(2)*(1), so let's start there. We can immediately see that n! = n * (n-1)!. That helps; now we can write a starter of our factorial function:
factorial(n):
return n * factorial(n-1)
But it's not done yet; if we run that, it'll go forever and not stop3. So we need what's called a base case: a stopping point for the recursion, where the problem is now so simple that we know the answer.
Fortunately, we have a natural base case: 1! = 1, which is true by definition. So we add that to our function:
factorial(n):
if n == 1: return 1
else return n * factorial(n)
Just to make it clear how that works, let's do a trace for something small, say n=4.
So we call factorial(4). Obviously 4 != 1, so factorial(4) = 4*factorial(3). Now we repeat the process, which is the beautiful thing about recursion: applying the same algorithm to smaller subsets of the original problem, and building up a solution from those parts.
Our trace looks something like this:
factorial(4) = 4*factorial(3)
factorial(4) = 4*3*factorial(2)
factorial(4) = 4*3*2*factorial(1)
Now, we know that factorial(1) is: it's just 1, our base case. So finally we have
factorial(4) = 4*3*2*1
factorial(4) = 24
1 This is only one possible way; there are others, but this is what I came up with on the spot
2 You must first understand recursion (Sorry; I couldn't resist)
3 In the real world, it will eventually stop: because each recursive call uses some memory (To keep track of function parameters and local variables and the like), code that recurses infinitely will eventually exceed the memory allocated to it, and will crash. This is one of the most common causes of a stack overflow error

Memoized to DP solution - Making Change

Recently I read a problem to practice DP. I wasn't able to come up with one, so I tried a recursive solution which I later modified to use memoization. The problem statement is as follows :-
Making Change. You are given n types of coin denominations of values
v(1) < v(2) < ... < v(n) (all integers). Assume v(1) = 1, so you can
always make change for any amount of money C. Give an algorithm which
makes change for an amount of money C with as few coins as possible.
[on problem set 4]
I got the question from here
My solution was as follows :-
def memoized_make_change(L, index, cost, d):
if index == 0:
return cost
if (index, cost) in d:
return d[(index, cost)]
count = cost / L[index]
val1 = memoized_make_change(L, index-1, cost%L[index], d) + count
val2 = memoized_make_change(L, index-1, cost, d)
x = min(val1, val2)
d[(index, cost)] = x
return x
This is how I've understood my solution to the problem. Assume that the denominations are stored in L in ascending order. As I iterate from the end to the beginning, I have a choice to either choose a denomination or not choose it. If I choose it, I then recurse to satisfy the remaining amount with lower denominations. If I do not choose it, I recurse to satisfy the current amount with lower denominations.
Either way, at a given function call, I find the best(lowest count) to satisfy a given amount.
Could I have some help in bridging the thought process from here onward to reach a DP solution? I'm not doing this as any HW, this is just for fun and practice. I don't really need any code either, just some help in explaining the thought process would be perfect.
[EDIT]
I recall reading that function calls are expensive and is the reason why bottom up(based on iteration) might be preferred. Is that possible for this problem?
Here is a general approach for converting memoized recursive solutions to "traditional" bottom-up DP ones, in cases where this is possible.
First, let's express our general "memoized recursive solution". Here, x represents all the parameters that change on each recursive call. We want this to be a tuple of positive integers - in your case, (index, cost). I omit anything that's constant across the recursion (in your case, L), and I suppose that I have a global cache. (But FWIW, in Python you should just use the lru_cache decorator from the standard library functools module rather than managing the cache yourself.)
To solve for(x):
If x in cache: return cache[x]
Handle base cases, i.e. where one or more components of x is zero
Otherwise:
Make one or more recursive calls
Combine those results into `result`
cache[x] = result
return result
The basic idea in dynamic programming is simply to evaluate the base cases first and work upward:
To solve for(x):
For y starting at (0, 0, ...) and increasing towards x:
Do all the stuff from above
However, two neat things happen when we arrange the code this way:
As long as the order of y values is chosen properly (this is trivial when there's only one vector component, of course), we can arrange that the results for the recursive call are always in cache (i.e. we already calculated them earlier, because y had that value on a previous iteration of the loop). So instead of actually making the recursive call, we replace it directly with a cache lookup.
Since every component of y will use consecutively increasing values, and will be placed in the cache in order, we can use a multidimensional array (nested lists, or else a Numpy array) to store the values instead of a dictionary.
So we get something like:
To solve for(x):
cache = multidimensional array sized according to x
for i in range(first component of x):
for j in ...:
(as many loops as needed; better yet use `itertools.product`)
If this is a base case, write the appropriate value to cache
Otherwise, compute "recursive" index values to use, look up
the values, perform the computation and store the result
return the appropriate ("last") value from cache
I suggest considering the relationship between the value you are constructing and the values you need for it.
In this case you are constructing a value for index, cost based on:
index-1 and cost
index-1 and cost%L[index]
What you are searching for is a way of iterating over the choices such that you will always have precalculated everything you need.
In this case you can simply change the code to the iterative approach:
for each choice of index 0 upwards:
for each choice of cost:
compute value corresponding to index,cost
In practice, I find that the iterative approach can be significantly faster (e.g. *4 perhaps) for simple problems as it avoids the overhead of function calls and checking the cache for preexisting values.

Empty zeroth element in array/list to eliminate repeated decrementing. Does this improve performance?

I am using Python to solve Project Euler problems. Many require caching the results of past calculations to improve performance, leading to code like this:
pastResults = [None] * 1000000
def someCalculation(integerArgument):
# return result of a calculation performed on numberArgument
# for example, summing the factorial or square of its digits
for eachNumber in range(1, 1000001)
if pastResults[eachNumber - 1] is None:
pastResults[eachNumber - 1] = someCalculation(eachNumber)
# perform additional actions with pastResults[eachNumber - 1]
Would the repeated decrementing have an adverse impact on program performance? Would having an empty or dummy zeroth element (so the zero-based array emulates a one-based array) improve performance by eliminating the repeated decrementing?
pastResults = [None] * 1000001
def someCalculation(integerArgument):
# return result of a calculation performed on numberArgument
# for example, summing the factorial or square of its digits
for eachNumber in range(1, 1000001)
if pastResults[eachNumber] is None:
pastResults[eachNumber] = someCalculation(eachNumber)
# perform additional actions with pastResults[eachNumber]
I also feel that emulating a one-based array would make the code easier to follow. That is why I do not make the range zero-based with for eachNumber in range(1000000) as someCalculation(eachNumber + 1) would not be logical.
How significant is the additional memory from the empty zeroth element? What other factors should I consider? I would prefer answers that are not confined to Python and Project Euler.
EDIT: Should be is None instead of is not None.
Not really an answer to the question regarding the performance, rather a general tip about caching previously calculated values. The usual way to do this is to use a map (Python dict) for this, as this allows to use more complex keys instead of just integer numbers, like floating point numbers, strings, or even tuples. Also, you won't run into problems in case your keys are rather sparse.
pastResults = {}
def someCalculation(integerArgument):
if integerArgument not in pastResults:
pastResults[integerArgument] = # calculation performed on numberArg.
return pastResults[integerArgument]
Also, there is no need to perform the calculations "in order" using a loop. Just call the function for the value you are interested in, and the if statement will take care that, when invoked recursively, the function is called only once for each argument.
Ultimately, if you are using this a lot (as clearly the case for Project Euler) you can define yourself a function decorator, like this one:
def memo(f):
f.cache = {}
def _f(*args, **kwargs):
if args not in f.cache:
f.cache[args] = f(*args, **kwargs)
return f.cache[args]
return _f
What this does is: It takes a function and defines another function that first checks whether the given parameters can be found in the cache, and otherwise calculates the result of the original function and puts it into the cache. Just add the #memo annotation to your function definitions and this will take care of caching for you.
#memo
def someCalculation(integerArgument):
# function body
This is syntactic sugar for someCalculation = memo(someCalculation). Note however, that this will not always work out well. First, the paremters have to be hashable (no lists or other mutable types); second, in case you are passing parameters that are not relevant for the result (e.g., debugging stuff etc.) your cache can grow unnecessarily large, as all the parameters are used as the key.

Categories

Resources