Code challenges are a great way to level up your skills when learning a new programming language. I've been using Python more recently so I wanted to practice a bit as it's not my main language.
This post will overview a quick code challenge I found on Code Wars, showing my solution as well as steps taken to learn how to write better Python code.
You can use Code Wars, Hacker Rank, or any other challenge site to better yourself. Personally, I like Code Wars as the UI and site design feels like a game which makes it more engaging for me. At the end of the day it's the quality of the challenges that matter so I recommend both in that regard.
The challenge I ended up going with is called Duplicate Encoder which has a difficulty rank of 6 kyu (with 1 being the hardest and 8 the easiest).
For brevity, I'll include the challenge description below as an image.
This problem is straightforward and looks like it can be solved by looping over the characters or using an array map. Then we'll want duplicate characters to return as ")" and non-duplicates to return as "(".
Before continuing, I want to make a quick note on a piece of the Code Wars UI that stands out. That is the "Sample Tests" section and the fact you can check your code against the tests. This forces all users of Code Wars to code from a Test Driven Development perspective.
While these tests are simple, it helps illustrate the beauty of a suite of simply written unit tests. Whether that's the expected output of a public class method or that of a simple function.
Below is my first take that managed to pass (comments added):
def duplicate_encode(word):
i = 0
# Using a dict to check and store char pointer
char_and_pointer = {}
# Iterate over the string
for char in word:
# Avoiding case
char = char.lower()
# Checking if character exists in our dictionary
if char in char_and_pointer:
# A bit of a janky way to change characters in a string
word = word[:i] + ')' + word[i+1:]
if char_and_pointer[char] != 'repeat':
# Want to go back to the first time this character was found and replace
old_pointer = char_and_pointer[char]
word = word[:old_pointer] + ')' + word[old_pointer+1:]
# Set the pointer as 'repeat' so we now know going forward
char_and_pointer[char] = 'repeat'
else:
# If first time character has appeared, set accordingly
word = word[:i] + '(' + word[i+1:]
char_and_pointer[char] = i
i += 1
return word
This managed to solve the problem, but after hitting solve you're greeted with a variety of user submitted solutions ranked by whether they follow "Best Practices" or if it's "Clever".
Typically the "Clever" answers are where you can learn the secret sauces of a programming language. As noted in the first answer in the above image, the user implements Python list comprehension to achieve a one line answer. And by keeping this code Pythonic, aka in line with the principles of the language, the Python compiler will typically prioritize the execution speed of the code.
The second noted solution in the above image still uses list comprehension, but it implements Python's Counter function. This creates a dictionary key for each letter in the string where the value is the count of that character. The user's code checks to see if the count of each character is 1 or not, and returns the proper parenthesis character. Not too much different than the other solution, but it shows off how useful Python's standard library can be.
Performance should always be an important consideration. While code challenge sites don't always factor in performance, it's a fun means to base further refactoring on.
So using my solution and the top 2 solutions, I've gone ahead and created a small testing script to run locally.
# Using timeit for speed tests
import timeit
# Using string 'Mississippi'
print(timeit.timeit("duplicate_encode_mine('Mississippi')", setup="from __main__ import duplicate_encode", number=1000000))
print(timeit.timeit("duplicate_encode_sol1('Mississippi')", setup="from __main__ import duplicate_encode2", number=1000000))
print(timeit.timeit("duplicate_encode_sol2('Mississippi')", setup="from __main__ import duplicate_encode3", number=1000000))
And the times are as follows:
My solution is really slow! Lets see if we can fix this.
The thing that stands out most to me is the janky way I changed the characters in the string. I think it may be best to instead make the string a list and then join them before returning.
I'm also thinking there's no need to run str.lower()
on each character, and can instead run it once on the whole string.
Finally, I want to move away from checking for the string 'repeat', and instead want to check if the value is a -1.
def duplicate_encode(word):
# Using a dict to check if exists and store the first chars pointer
# Setting the characters as keys allows for an index like look up (O(1)) rather than a count()
char_pointers = {}
# Avoiding case
word = word.lower()
# List allows it to replace specific characters by index
word = list(word)
i = 0
for char in word:
if char in char_pointers:
word[i] = ')'
# Only need to replace a past char once
if char_pointers[char] != -1:
word[char_pointers[char]] = ')'
char_pointers[char] = -1
else:
word[i] = '('
char_pointers[char] = i
i += 1
return "".join(word)
And the new times are as follows (using 'Mississippi' as input string)
Nice! After refactoring we were able to make our function faster. I think we could go on and make the function read better, but it's nice to see that increase in speed after learning better ways to write Python!
These challenges are great for both practice and learning. If you're a beginner, start slow and don't get discouraged. Use these challenges as a test and then learn from the solutions of your peers.
That said, there are plenty of really tough challenges on these sites. No matter the skill level, I'm sure you'll learn something new!
Please check out my Code Wars account here: patback118
Happy coding!
Tags: Python, Code-challenge