1

I have a question about my Python code to find the max value within a list. The functional code is as following:

def large(x):
    if len(x) == 1:
        return x.pop(0)
    else:
        temp = x.pop(0)
        previous = large(x)
        if previous >= temp:
            return previous
        else:
            return temp

But before that, I tried:

def large(x):
    if len(x) == 1:
        return x.pop(0)
    else:
        temp = x.pop(0)
        if large(x) >= temp:
            return large(x)
        else:
            return temp

And it will return the error message as:

<ipython-input-74-dd8676a7c4e6> in large(x)
      3         return x.pop(0)
      4     else:
----> 5         temp = x.pop(0)
      6         if large(x) >= temp:
      7             return large(x)

IndexError: pop from empty list

The toy data would be:

inputlist = [6,1,3,2,3,4,5,6]
large(inputlist)

Thank you for your help in advance. I can't find the main cause of this error. As for me, these two codes are completely same.

Pynchia
  • 10,996
  • 5
  • 34
  • 43
Qiang Super
  • 323
  • 1
  • 11
  • I assume you cannot use `max()` function? And are you sure you want a mutable parameter? – OneCricketeer Aug 26 '20 at 02:41
  • 1
    You call large(x) twice at the same stack frame in the second version, one in if and another in return – geckos Aug 26 '20 at 02:45
  • Consider using an index instead of pop. That would avoid a sneaky mutation _and it will also have much better performance bounds_.. for each recursive call, pass the current index + 1. This is a well established ‘pattern’ to recursively iterate array-like data. – user2864740 Aug 26 '20 at 02:51
  • If I was to use pop, I’d 1) pop from the _back_ to avoid bad complexity shifting data, 2) use a loop instead of recursion to keep the side-effects close. – user2864740 Aug 26 '20 at 02:59
  • @OneCricketeer My goal is to apply the recursion to build up a ```max``` function. – Qiang Super Aug 26 '20 at 14:20
  • @user2864740 Thank you for your help. I am wondering that how should I eliminate the number of elements in each round with index rather than ```pop```? I am kind of new to the Python and algorithm. – Qiang Super Aug 26 '20 at 14:22
  • @user2864740 It would be better to make the questions more clear. The goal of this question is to implement the recursion to find the max within a list. – Qiang Super Aug 26 '20 at 14:24
  • @geckos Thank you for your help. Would you mind explaining more on this? In the functional program, I also called the large(x) twice to implement the recursion. Why it won't cause any error? I am kind of new to the Python and algorithm. – Qiang Super Aug 26 '20 at 14:26
  • Think about this `if large(x) >= temp: return large(x)` it will recur on if, and then again on return. Put a print before each pop and you will see it. In the first version you save it in previous and return that value, in the second version you call large twice, this is the error – geckos Aug 26 '20 at 18:42
  • Another way to understand what is happening, it's a little archaic but it works, get a pencil and paper and track inputs and returns, you will see how recursion is stacking doing this – geckos Aug 26 '20 at 18:44
  • @geckos Thank you for your explanation. I would like to confirm if the first version I ran the large(x) twice in each if statement -> the data will get passed to the ```large``` firstly in the conditional judgement, and then the return clause will ran the ```large``` again. – Qiang Super Aug 26 '20 at 21:27
  • I don't really understand what you mean, in the first version you save the return value in previous then use that value, you dont' call large twice as in second version – geckos Aug 27 '20 at 02:12

4 Answers4

3

The problem with

if large(x) >= temp:
    return large(x)

is that you end up calling large(x) (and therefore pop) more than once, which is removing elements from the list.

Personally, I would more go for this style than using a mutating function like pop.

def large(x):
    if len(x) == 1:
        return x[0]
    remainder = large(x[1:])
    return x[0] if x[0] > remainder else remainder
OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
  • And if he MUST use pop, I suggest using something like `cache=large(x)` `return cache if cache>temp else temp` – xkcdjerry Aug 26 '20 at 02:57
  • @xkcdjerry Outside of the "exercise", I would still express caution on mutaing the parameter – OneCricketeer Aug 26 '20 at 02:59
  • Agreed. Also,`pop(0)` has terrible performance.It's best not to use it if not explictly asked to do so. – xkcdjerry Aug 26 '20 at 03:01
  • Thank you for your suggestion, @OneCricketeer. I am curious why should I use the remainder = large(x[1:]). I am pretty new to the algorithm, and based on my understanding, the recursion will take each element in the list into consideration. That is why I used the ```pop``` function here. Would you mind explaining more? Thx. – Qiang Super Aug 26 '20 at 14:13
  • @xkcdjerry Thank you for your suggestion. I used the pop according to the textbook 'grokking the algorithm', where it suggested to take out an element from the list and then apply the recursion to the rest of the elements. I am not sure why I can't use the if large(x) >= temp: return large(x) here. Thanks. – Qiang Super Aug 26 '20 at 14:16
  • @QiangSuper We explained why you cannot call the function in the if statement. You are modifying list `x` each time you call the function. In the if statement, you are not saving the result that is returned, and then you end up calling the function again with less data in `x` than before. The way in this answer does the same thing as your code without modifying `x`, and looking at the remaning elements with a "list slice" `[1:]` – OneCricketeer Aug 26 '20 at 16:11
  • 1
    @OneCricketeer Thank you for your explanation. I appreciate your time and patience. I think the issue was caused by I called the ```large``` twice, which will eliminate the elements within the list twice. That is why we should use the ```previous = large(x)``` instead of calling ```large(x)``` directly. – Qiang Super Aug 27 '20 at 15:09
2

The same solution as OneCricketeer's but without creating list slices upon every recursive call unnecessarily. It also handles an empty list.

def large(x):
    def rec(y):
        try:
            v = next(y)
        except StopIteration:
            return None
        r = rec(y)
        if r is None:
           return v
        return v if v > r else r
    return rec(iter(x))

inputlist = [6,1,3,2,3,4,5,6]
print(large(inputlist))
print(large([]))

which produces

6
None
Pynchia
  • 10,996
  • 5
  • 34
  • 43
1

Since this is a recursion exercise, and not something we'd do in system code, I'd go with descriptive code over efficient code and do something like:

def largest(array):
    if array:
        head, *tail = array

        if tail and head < (result := largest(tail)):
            return result

        return head

    return None

if __name__ == "__main__":
    from random import choices

    array = choices(range(100), k=10)

    print(array, '->', largest(array))

OUTPUT

> python3 test.py
[46, 67, 0, 22, 23, 20, 30, 7, 87, 50] -> 87
> python3 test.py
[83, 77, 61, 53, 7, 65, 68, 43, 44, 47] -> 83
> python3 test.py
[36, 99, 47, 93, 60, 43, 56, 90, 53, 44] -> 99
> 

If you really need to be efficient, I'd recommend doing so safely. Specifically, not exposing an API with special arguments that the caller is not supposed to use, e.g.:

def my_max(lst, m=None, i=0):

As they can supply values for these extra arguments that will make your code fail, and ultimately blame it on you. Ditto for exposing internal functions that the caller might call instead of the intended one:

def my_max(lst, m=None, i=0):

def my_max_helper(lst, i):

Accidentally calling my_max_helper() with a bogus value for the poorly named i argument. Instead, I'd consider nesting your functions to avoid such calling errors:

def largest(array):

    def largest_recursive(array, index):
        a = array[index]

        if len(array) - index != 1:
            if (b := largest_recursive(array, index + 1)) > a:
                return b

        return a

    if array:
        return largest_recursive(array, 0)

    return None
cdlane
  • 40,441
  • 5
  • 32
  • 81
1

This does not answer why the original is incorrect. Rather, it lays down a 'standard pattern' that can be used for implementing a number of recursive problems.

I am wondering that how should I eliminate the number of elements in each round with index rather than pop?

Don't "eliminate" the elements :-)

Many recursive-friendly problems operate by reducing the range each step. This includes finding the max value (or any operation that can be expressed as a fold), a binary search, a top-down merge sort, etc. Many of these problems are themselves expressed in pseudo-code using arrays and sub-problem reduction by adjusting the ranges of each recursive call. In the case of a max/binary-search this also avoids any mutations to the original object.

Thus, a recursive max function can written as the following. Note that this form of passing in the working state is Tail-Call Friendly. While I find this form is easier to express certain problems, it does not really matter in Python as [C]Python does not support Tail-Call Optimizations^.

def my_max(lst, m=None, i=0):
  # base-case: return result of work
  if len(lst) == i:
    return m
  # Compute max through here
  c = lst[i]
  m = c if m is None or c > m else m
  # Call recursive function increasing the index by 1.
  # This is how the problem advances.
  return my_max(lst, m, i + 1)

The above example also use default arguments instead of a helper method. Here is an alternative that uses the recursive result — which is often how recursive functions are introduced — as well as a discrete helper method.

def my_max(lst):
  # Wrapper can ensure helper pre-conditions.
  # In this case that is a non-Empty list per the base case check.
  if not lst:
    return None
  return my_max_helper(lst, 0)

def my_max_helper(lst, i):
  # base case: last item in list returns itself
  if len(lst) - 1 == i:
    return lst[i]
  c = lst[i]
  m = my_max_helper(lst, i + 1)
  return c if c > m else m

In both cases temporarily variables are used to avoid duplicate expressions; while sometimes merely a stylistic choice, this consistency would have mitigated the original issue due to avoiding the unexpected side-effect of additional pop-mutation.

The above methods should be called with a list or other sequence that supports O(1) indexed lookups. In particular, the 'index' approach is not suitable, and will not work with, generator objects. There are other answers that cover this — just beware to avoid potential list slices like h,*t=l or l[1:] which can lead to bad performance bounds.

^There are modules in Python which can emulate TCO through spring-boarding.

user2864740
  • 60,010
  • 15
  • 145
  • 220
  • Thank you for your help. Might I ask additional question? Does ```m = c if m is None or c > m else m``` equals to ```if m is None or c>m: return m=c else: return m```? Would you mind explaining more about the helper method? For the ```my_max_helper```, I am wondering how should we define the boundary of ```i```? Will it grow above the ```len(lst)```? Thanks. – Qiang Super Aug 27 '20 at 15:22