Python List Processing - Advanced Techniques
using CodingBat's List-2 exercises as examples
-- P. Brooks, Stuyvesant High School, 2013

Contents

CodingBat's Exercises (below):

Tutorial:

count_evens

Return the number of even ints in the given array. Note: the % "mod" operator computes the remainder, e.g. 5 % 2 is 1.

count_evens([2, 1, 2, 3, 4]) → 3
count_evens([2, 2, 0]) → 3
count_evens([1, 3, 5]) → 0
1. Straightforward answer:
def count_evens(nums):
  count = 0
  for n in nums:
    if n % 2 == 0:
      count = count + 1
  return count
This is a straightforward application of the for in looping construct.
2. Create a list of only the even numbers, and return its length:
def count_evens(nums):
  newlist = []
  for n in nums:
    if n%2 == 0:
      newlist.append(n)
  return len(newlist)
 
3. Use a "list comprehension" to do the same thing as #2:
def count_evens(nums):
  return len([n for n in nums if n%2 == 0])
This does the same thing as #2 above, except more compactly.

The list comprehension expression: "[n for n in nums if n%2 == 0]" selects and creates a new list based on the nums list, but containing only its even elements.  The len() of this new list returns the answer requested.

4. Using reduce() and a list comprehension
def OneIfEven(n):
    return (n+1)%2

def SumList(L):
    return reduce(lambda x,y: x+y, L, 0)
    
def count_evens(nums):
    return SumList([OneIfEven(k) for k in nums])
Here's our strategy: create a new list of ones and zeros, with the ones where the even numbers in nums were, and zeros where the odd ones were.  So, for instance, from [2, 1, 2, 3, 4] create: [1, 0 , 1, 0, 1], then add up the numbers in this new list.

OneIfEven(n) will return a 1 if n is even, otherwise 0.
SumList(L) will add up the numbers in a list
The list comprehension in count_evens() does the conversion from nums to the new list of ones and zeros.

5. Using .count() and a list comprehension
def count_evens(nums):
    return [k%2 for k in nums].count(0)
As in #4 above, create a new list with ones and zeros where the evens and odds were in the nums list, but this time assign a 0 for the evens, and 1 for the odds.  Having created that list, use the .count() method to count the number of zeros.

big_diff

Given an array length 1 or more of ints, return the difference between the largest and smallest values in the array. Note: the built-in min(v1, v2) and max(v1, v2) functions return the smaller or larger of two values.

big_diff([10, 3, 5, 6]) → 7
big_diff([7, 2, 10, 9]) → 8
big_diff([2, 10, 7, 2]) → 8
1. Straightforward answer:
def big_diff(nums):
  
  # find the smallest
  smallest = nums[0]
  for n in nums:
    smallest = min(smallest,n)
    
  # find the largest
  largest = nums[0]
  for n in nums:
    largest = max(largest,n)
  
  return largest - smallest
They give you a hint about using the functions min() and max() to compare 2 numbers..
2. But if we look up min() and max()...
def big_diff(nums):
  return max(nums) - min(nums)
...we'd find that they also work on entire lists!
3. Alternatively, we could sort the list first ...
def big_diff(nums):
  nums.sort()
  return nums[-1] - nums[0]
It's useful to be aware of useful list methods...
4. But if we didn't know about min(list) and max(list) ...
def big_diff(nums):
  return reduce(lambda x,y: x if x>y else y,nums) - reduce(lambda x,y: x if x<y else y,nums)
...we can create the equivalents of max(list) and min(list) using the reduce() function

centered_average

Return the "centered" average of an array of ints, which we'll say is the mean average of the values, except ignoring the largest and smallest values in the array. If there are multiple copies of the smallest value, ignore just one copy, and likewise for the largest value. Use int division to produce the final average. You may assume that the array is length 3 or more.

centered_average([1, 2, 3, 4, 100]) → 3
centered_average([1, 1, 5, 5, 10, 8, 7]) → 5
centered_average([-10, -4, -2, -4, -2, 0]) → -3
1. Straightforward answer:
def centered_average(nums):
    
  # find the smallest
  smallest = nums[0]
  for n in nums:
    smallest = min(smallest,n)
    
  # find the largest
  largest = nums[0]
  for n in nums:
    largest = max(largest,n)

  # find the sum
  sum = 0
  for n in nums:
    sum += n
  
  return (sum - smallest - largest) / (len(nums) - 2)
Same as "big_diff" above: using min() and max()...
2. Knowing about min(list) and max(list) and sum(list)...
def centered_average(nums):
  
  return (sum(nums) - min(nums) - max(nums)) / (len(nums) - 2)
 
3. Again, creating the equivalent of min(list), etc. using reduce()...
def smallest(L):
  return reduce(lambda x,y: x if x<y else y,L)
  
def largest(L):
  return reduce(lambda x,y: x if x>y else y,L)
  
def SumList(L):
    return reduce(lambda x,y: x+y, L)

def centered_average(nums):
  return (SumList(nums)-smallest(nums)-largest(nums))/(len(nums)-2)
  
If we didn't know about min(list) and max(list), we could create smallest(list) and largest(list) using the reduce() function, and also create the SumList(list) function that way.
4. We can put all the helper functions (above) directly inside our main function...
def centered_average(nums):
  def smallest(L):
    return reduce(lambda x,y: x if x<y else y,L)
  
  def largest(L):
    return reduce(lambda x,y: x if x>y else y,L)
  
  def SumList(L):
    return reduce(lambda x,y: x+y, L)

  return (SumList(nums)-smallest(nums)-largest(nums))/(len(nums)-2)
The 3 helper functions are (perhaps) only needed to help us with our current task, and so the rest of the world need not know about them, and so we can hide them inside the main (centered_average()) function.  And then we can use them immediately.

sum13

Return the sum of the numbers in the array, returning 0 for an empty array. Except the number 13 is very unlucky, so it does not count and numbers that come immediately after a 13 also do not count.

sum13([1, 2, 2, 1]) → 6
sum13([1, 1]) → 2
sum13([1, 2, 2, 1, 13]) → 6
1. Straightforward solution...
def sum13(nums):
  sum = 0
  pos = 0
  while pos < len(nums):
    if nums[pos] == 13:
      pos += 1
    else:
      sum += nums[pos]
    pos += 1
  return sum
Notice that we end up incrementing the pos variable twice if the number is 13 (thereby skipping the number right after the 13).
2. Remove each offending 13 (and the number right after it)
def sum13(nums):
  while 13 in nums:
    pos = nums.index(13)
    del nums[pos:pos+2]  # remove the 13 and the number after it
  
  # now add up the numbers that remain in the list
  sum = 0
  for n in nums:
    sum += n
  
  return sum  
 
3. Create a helper function telling us whether to add or not..
def sum13(nums):
  def NoAdd(L,i):
    if L[i] == 13:
      return True
    if i > 0 and L[i-1] == 13:
      return True
    return False
    
  sum = 0
  pos = 0
  while pos < len(nums):
    if not NoAdd(nums,pos):
      sum += nums[pos]
    pos += 1
    
  return sum
    
We know that we cannot add if the current number is 13 or the previous number was 13 (provided that there is a previous number) -- so let's create a helper function to tell us, at any position that we are at in the list, whether we can add the number or not.

And since this helper function is not likely to be useful for any purpose outside the sum13() function, let's hide it inside sum13().

sum67

Return the sum of the numbers in the array, except ignore sections of numbers starting with a 6 and extending to the next 7 (every 6 will be followed by at least one 7). Return 0 for no numbers.

sum67([1, 2, 2]) → 5
sum67([1, 2, 2, 6, 99, 99, 7]) → 5
sum67([1, 1, 6, 7, 2]) → 4
1. Keep track of whether we are able to add or not...
def sum67(nums):

  YesWeCanAdd = True
  sum = 0
  
  for n in nums:
  
    if n == 6:
      YesWeCanAdd = False
      
    elif YesWeCanAdd:
      sum += n
    
    elif not YesWeCanAdd and n == 7:
      YesWeCanAdd = True
  
  return sum
The variable YesWeCanAdd tells us whether we're in a region of the list where we can add the numbers.  We immediately make it False if we see a 6, and keep it False until we see the following 7.
2. Remove the regions of the list between 6 and 7
def sum67(nums):

  # remove the regions between 6 and 7
  while 6 in nums:
    pos6 = nums.index(6)
    pos7 = nums[pos6:].index(7) + pos6
    del nums[pos6:pos7+1]
   
  # add up every number that remains in the list
  return reduce(lambda x,y: x+y, nums, 0)
We want to remove the regions of the list between a 6 and the next 7.

We know that when we have removed all of those regions, there will be no 6s left in the list (though there may be 7s left over).

So we find position of the first 6 in the list, and then the position of the first 7 after that (carefully), and remove that region -- note that we are guaranteed that there will be a 7 after a 6

We do this until there are no more 6s left.

has22

Given an array of ints, return True if the array contains a 2 next to a 2 somewhere.

has22([1, 2, 2]) → True
has22([1, 2, 1, 2]) → False
has22([2, 1, 2]) → False
1. Keep track of whether we've just seen a 2 a moment ago...
def has22(nums):
  JustSawA2 = False
  
  for n in nums:
    if JustSawA2 and n == 2:
      return True
    JustSawA2 = (n == 2)
  
  return False
Note: the expression (n == 2) results in (returns) a True or False, which we just assign to JustSawA2, and we do this assignment just before going on to the next number.
2. Convert the list into a string, and do a string search...
def has22(nums):
  arr_of_strings = [str(n) for n in nums]
  str_with_separators = '-' + '-'.join(arr_of_strings) + '-'
  return '-2-2-' in str_with_separators
Suppose we executed has22([2, 3, 22, 2, 1, 2]).  The correct answer would be False.  Going through the code...
arr_of_string is ['2', '3', '22', '2', '1', '2']
str_with_separators is '-2-3-22-2-1-2-'
'-2-2-' in str_with_separators is False