The meta lessons:

  • Learn patterns not problems.
  • Visualize. Visualize. Visualize.
  • Think about how to solve the problem before writing code.
  • Think out loud.
  • Its ok to start with brute force.
  • Never assume.

  1. Comparision in Loops.
  • To solve comparision based problems, my first instinct was to always loop through two arrays.
  • However, if a comparision (a,b) is the same as (b,a), a simple trick is to start the second array from current position of the first array i.e., instead of comparing items in the full square shown below, only compare items in the upper triangle.

If the comparision between (1, 2) and (2,1) are the same, we can just do (1,2) which is present in the upper triangle.

1for i in range(len(nums)):
2	for j in range(i+1, len(nums)): # This is the upper triangle
3		print(nums[i], nums[j])
  1. The advantage of using a while loop over a for loop.
1# I am habituated to think like a for loop, to convert for loop to while loop
2for i in range(k): 
3
4# To make this a while loop it requires initialization, update and termination for the loop variable
5i = 0 
6while i< k: 
7	# somewhere inside  
8	i+=1
  1. Principle of not materializing unnecessary data structures Or “there are ways to know about things without creating the thing”.

For example:

a. To know the length of the longest sequence, you don’t have to create the actual sequence. You can just keep track of the length as you go.

1# Instead of creating the sequence, keep track of the length as you go
2longest = 0
3for i in range(len(nums)):
4	longest = max(longest, nums[i])

b. When dealing with paths in a graph or tree, you might not need to store the entire path, but just keep track of relevant information about it (like its length, start and end points, or some aggregated value).

This can lead to more efficient algorithms in terms of both time and space complexity.


Python related (syntax and more) tricks, which I never bothered to learn until I started solving LeetCode:

  1. Deletion in lists and dicts
 1# delete an item from a list using index
 2a = [10, 12, 13, 14, 15]
 3del a[3] # deletes the item in index 3
 4
 5# Interestingly, the same method works to remove an item from dictionary based on key
 6mydict = {5: 3, 2: 1, 3: 3, 1: 2}
 7del mydict[5]
 8print(mydict)
 9
10# delete an item from a list using value
11a = [10, 10, 12, 13, 14, 15]
12a.remove(10)
  1. On Sets.

Sets and dictionaries are both implemented using hash tables, this means that they have constant lookup time O(1) i.e., better than a list which is O(n) lookup.

Before Python 3.7, sets and dicts were unordered collections i.e., the concept of indexing did not apply. When we print them, they are in random order. Since Python 3.7, dictionaries preserve insertion order (!important). For sets, this is still true. When we print them, they are in random order.

1# Simple set operations
2lista = [10, 12, 13, 14, 15, 14, 14]
3seen = set()
4for a in lista:
5    if a in seen:
6        print(f"Repeated: {a}")
7    else: 
8        seen.add(a)
  1. On Dictionaries.
1# Dict get method with default return values
2chars = "believers"
3mydict= {}
4for char in chars: 
5	# If it finds, it returns the stored value. If not return a 0
6	mydict[char] = mydict.get(char, 0) + 1 # Genius way to count chars in a string
7print(mydict)

Dict keys and values are of type dict_keys and dict_values respectively. We need to convert them to list explicitly. The order of keys and values in dict_keys and dict_values are same as that in the dict (preserves insertion order).

1mydict = {1: 3, 2: 2, 3: 1}
2print(mydict)
3print(list(mydict.keys()))
4print(list(mydict.values()))
  1. On chars and strings.
1# You can also sort characters. In some cases this may be helpful (such as checking equality)
1ord('a') returns the unicode of the alphabet. then you can do +1 to increment it.
2then you can use chr() to convert it back to the word
  1. On Lists and Tuples.

Lists are mutable (can be changed after creation) but tuples are immutable. We can perform type conversion with tuple() and list() to convert between them.

 1# Tuples are immutable
 2b = tuple([1, 2, 3]) # tuple expects single iterable argument like a list during creation
 3b[0] = 10 # This raises TypeError: 'tuple' object does not support item assignment
 4
 5# Lists are mutable
 6a = [1, 2, 3]
 7a[0] = 10 # This works! Lists can be modified
 8print(a) # [10, 2, 3]
 9
10# If we do tuple() on a list, it becomes immutable
11a = [1, 2, 3]
12b = tuple(a)
13b[0] = 10 # This is NOT allowed
  1. Why do we need a semicolon after break? break;
  • We don’t, its optional. Both break and break; work exactly the same way.

Additionally, here are some Python tricks I learned from watching Youtube videos from Karpathy (a wizard):

  1. Using zip to grab pairs of items.
 1ids = [1, 2, 3, 4]
 2for item in zip(ids, ids[1:]): 
 3    print(item) # Gets the pairs (1,2), (2,3), (3,4)
 4                # If its changed to ids[2:] we get (1,3), (2,4)
 5
 6# Utilizing zip to traverse the upper triangle (discussed above).
 7nums = [1, 2, 3, 4, 5]
 8for i in range(1, len(nums)):
 9    for item in zip(nums, nums[i:]):
10        print(item)
  1. Returning conditions instead of if-else.
1# 1. Instead of using if and else like this:
2if pattern_word == pattern:
3    return True
4else:
5    return False
6
7# 2. Return the condition itself
8return pattern_word == pattern
  1. Printing variables with their names.
1# Print a variable with its name
2print(f'{log_likelihood=}')
3# prints log_likelihood=tensor(-38.7856)
  1. Joining a list of strings.
 1# Join a list of strings
 2words = ["Hello", "World!"]
 3
 4# Join with empty string
 5result1 = ''.join(words)
 6print(result1)  # HelloWorld!
 7
 8# Instead of empty string, you can use space, comma, newline etc.
 9# Join with space
10result2 = ' '.join(words)
11print(result2)  # Hello World!
12
13# Join with newline
14result4 = '\n'.join(words)
15print(result4)
16# Hello
17# World!
  1. Initializing an array of given size.
1# Initializing an array of given size
2new_arr = [0] * size