Hack 1

def collatz(n):
    itns = 0
    while n != 1:
        print(int(n))
        if n % 2 == 0: n /= 2
        else: n = n * 3 + 1
        itns += 1
    return itns
print("iterations:", collatz(int(input("number to start from? "))))
20
10
5
16
8
4
2
iterations: 7

Hack 2

def sum_range_slow(n):
    ret = 0
    c = 0
    while c <= n:
        ret += c
        c += 1
    return ret

print("inefficient loop time: ")
%timeit sum_range_slow(1000000)

# efficient
def sum_range_fast(n):
    return n * (n + 1) / 2

print("efficient loop time: ")
%timeit sum_range_fast(1000000)

# 180ms >>> 230ns
inefficient loop time: 
180 ms ± 83.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
efficient loop time: 
230 ns ± 8.54 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

The main difference between the two functions is that the first function uses a loop to iterate over the range of numbers from 0 to n, while the second function uses a mathematical formula to compute the sum directly.

In the first function, sum_range_slow, the loop will execute n+1 times, which means that the time it takes to execute this function will be proportional to n, the input size. This is known as having a linear time complexity.

On the other hand, the second function, sum_range_fast, uses a mathematical formula to compute the sum of the range of numbers from 0 to n in a single step, without using a loop. As a result, the time it takes to execute this function does not depend on the size of the input, n. This is known as having a constant time complexity.

In general, algorithms with a lower time complexity will be more efficient and faster than algorithms with a higher time complexity, especially when the input size is large. In this case, the second function is much more efficient than the first one because it has a constant time complexity, while the first one has a linear time complexity.

Algorithm efficiency: refers to how quickly an algorithm can solve a problem, or how much time and resources it requires to do so.