Devops Interview Prep: Leetcode as Code

In a competitive job market it’s important to be able to demonstrate the technologies you’re proficient with, and the undisputed best test for this takes the form of a whiteboard exercise or an algorithm test to see if you’re able to implement common data structures and the sort.

But, could I succeed without functions, true if/else statements, and loops?

Fizzbuzz is often a first interview question for candidates and we’ll cut our teeth on it:


locals {
  iterations = 100 # First we specify the number of iterations
  fizzbuzz = [for i in range(local.iterations) : {
    index = i
    value = i % 15 == 0 ? "FizzBuzz" : 
      i % 3 == 0 ? "Fizz" : 
      i % 5 == 0 ? "Buzz" : 
      i # Otherwise, just return the index
  }]
  fizzbuzz_result = [
    for i in local.fizzbuzz : length(i.value) > 0 ? i.value : i.index
  ]
}
# ["1", "2", "Fizz", "4", "Buzz", "Fizz", <snip>..., "14", "FizzBuzz"]

Okay, not so bad, what about sorting? Surely this will scale well. Can it sort?

locals {
  mixed_list = [
    1, 3, 5, 1,1,1
  ]

  bubble_sorted_list = [
    for i, val in range(length(local.mixed_list)) :
    i < length(local.mixed_list) - 1 ?              # Make sure we don't go out of bounds
    local.mixed_list[i] < local.mixed_list[i + 1] ? # Determine if we need to swap
    local.mixed_list[i] : local.mixed_list[i + 1] : # Handle the swap
    local.mixed_list[i]                             # We were on the last element, just return the value
  ]
}
# Not quite. We're not sorted, and we misplaced a 5 somewhere.
# [1,3,1,1,1] 

Let’s update that to use a module:

locals {
  state = [5, 4, 3, 1, 2]
}

# Five passes, each pass performs odd+even swaps
module "pass_0" {
  source = "./mods/pass"
  in     = local.state # [5,4,3,1,2]
}

# <snip> pass_1 through pass_4 </snip>

module "pass_4" {
  source = "./mods/pass"
  in     = module.pass_3.out # [1,2,3,4,5]
}

The timeless product of cs50 students everywhere, but we want to set our sights higher.

P.S. unfortunately importing ./mods/pass from within mods/pass led to a circular dependency, bringing terraform to its knees. I also considered putting terraform apply --auto-approve into a while loop and letting it mutate a file input, but I think I’m getting distracted.

# Here's the module:
variable "in" {
  type        = list(number)
  description = "Input list for a single odd+even bubble pass"
}

module "swap_odd" {
  count  = floor(length(var.in) / 2)
  source = "../swap"
  in     = [var.in[count.index * 2], var.in[count.index * 2 + 1]]
}

locals {
  pass_odd = concat(
    flatten([for i in range(0, length(module.swap_odd)) : module.swap_odd[i].out]),
    length(var.in) % 2 == 1 ? [var.in[length(var.in) - 1]] : []
  )
}

module "swap_even" {
  count  = floor((length(local.pass_odd) - 1) / 2)
  source = "../swap"
  in     = [local.pass_odd[count.index * 2 + 1], local.pass_odd[count.index * 2 + 2]]
}

locals {
  pass_even = concat(
    [local.pass_odd[0]],
    flatten([for i in range(0, length(module.swap_even)) : module.swap_even[i].out]),
    length(local.pass_odd) % 2 == 0 ? [local.pass_odd[length(local.pass_odd) - 1]] : []
  )
}

A bit messier, but it works. I think I could get rid of the swap module but I need a new challenge. Let’s try something with a cleaner approach: is this a palindrome?

locals {
  is_palindromes = [
    "", "a", "aa", "aaa", "kayak", "radar", "racecar", "madam", "dog", "god",
    "cat", "act", "rat", "mat", "hat", "bat", "tab", "cab", "bac", "aca", "cac",
    "aac", "cca", "aaa", "ccc", "aaaa"
  ]
  palindromes = [
    for str in local.is_palindromes :
    length(str) <= 1 ? true : # empty string and single characters are palindromes
    alltrue([
      for i in range(floor(length(str) / 2)) :
      substr(str, i, 1) == substr(str, length(str) - 1 - i, 1)
    ])
  ]
}

The next challenge is Two Sum, which we can conquer with a map:

locals {
  # Test data for Two Sum
  two_sum_array = [2, 7, 11, 15, 3, 6, 1, 8, 4, 9]
  two_sum_target = 23
  
  two_sum_value_map = {
    for i, val in local.two_sum_array : tostring(val) => i
  }
  
  # For each number, check if (target - number) exists in the map
  two_sum_raw = [
    for i, num in local.two_sum_array :
    contains(keys(local.two_sum_value_map), tostring(local.two_sum_target - num)) && 
    local.two_sum_value_map[tostring(local.two_sum_target - num)] != i ?
    {
      indices = [i, local.two_sum_value_map[tostring(local.two_sum_target - num)]]
      values = [num, local.two_sum_target - num]
      sum = local.two_sum_target
    } : null
  ]
  
  # Filter out null results from optimized solution
  two_sum_results = [
    for result in local.two_sum_raw: result if result != null
  ]
}
# {
#     "indices" = [3,7]
#     "sum" = 23
#     "values" = [15,8]
# }

Ah yes Mitchell would be proud.