from random import shuffle
from time import time


def fibonacci(n):
    """
    Return the nth fibonacci number, that is n if n < 2,
    or fibonacci(n-2) + fibonacci(n-1) otherwise.

    @param int n: a non-negative integer
    @rtype: int

    >>> fibonacci(0)
    0
    >>> fibonacci(1)
    1
    >>> fibonacci(3)
    2
    """
    if n < 2:
        return n
    else:
        return fibonacci(n - 2) + fibonacci(n - 1)


def fib_memo(n, seen):
    """
    Return the nth fibonacci number reasonably quickly.

    @param int n: index of fibonacci number
    @param dict[int, int] seen: already-seen results
    """
    if n not in seen:
        seen[n] = (n if n < 2
                   else fib_memo(n - 2, seen) + fib_memo(n - 1, seen))
    return seen[n]


def merge(lst1, lst2):
    """return merge of lst1 and lst2

    >>> merge([1, 3, 5], [2, 4, 6])
    [1, 2, 3, 4, 5, 6]
    >>> merge([1, 2, 3], [0, 4, 5])
    [0, 1, 2, 3, 4, 5]
    >>> merge([0], [1, 2, 3, 4])
    [0, 1, 2, 3, 4]
    """
    sorted = []
    index1, index2 = 0, 0
    while index1 < len(lst1) and index2 < len(lst2):
        if lst1[index1] < lst2[index2]:
            sorted.append(lst1[index1])
            index1 += 1
        else:
            sorted.append(lst2[index2])
            index2 += 1
    # Now either i1 = len(L1) or i2 = len(L2). The remaining elements of the
    # other list can all be added to the end of sorted.
    # So, note that at most ONE of L1[i1:] and L2[i2:] is non-empty.
    return sorted + lst1[index1:] + lst2[index2:]


def merge_sort(lst):
    """Produce copy of L in non-decreasing order

    >>> merge_sort([1, 5, 3, 4, 2])
    [1, 2, 3, 4, 5]
    >>> L = list(range(20))
    >>> shuffle(L)
    >>> merge_sort(L) == list(range(20))
    True
    """
    if len(lst) < 2:
        return lst[:]
    else:
        return merge(merge_sort(lst[:len(lst) // 2]),
                     merge_sort(lst[len(lst) // 2:]))


def qs(list_):
    """
    Return a new list consisting of the elements of list_ in ascending order.

    @param list list_: list of comparables
    @rtype: list

    >>> qs([1, 5, 3, 2])
    [1, 2, 3, 5]
    >>> qs([1, 5, 3, 2, 7])
    [1, 2, 3, 5, 7]
    >>> L = [9, 1, 5, 3, 2, 7, 8]
    >>> qs(L)
    [1, 2, 3, 5, 7, 8, 9]
    """
    if len(list_) < 2:
        return list_[:]
    else:
        return (qs([i for i in list_[1:] if i < list_[0]]) +
                [list_[0]] +
                qs([i for i in list_[1:] if i >= list_[0]]))


if __name__ == "__main__":
    import doctest
    doctest.testmod()

    N = 30

    start = time()
    print(fib_memo(N, {}))
    print("Memoized Fibonacci run time: {}"
          .format(time() - start))

    start = time()
    print(fibonacci(N))
    print("Classic Fibonacci run time:  {}"
          .format(time() - start))
