Algotutor: Using AI to Actually Get Better at Algorithms

· Medium ·

6 min read Original article ↗

Andrei Boar

Solving algorithmic problems is a lot like writing: you always assume you’re good at it until you actually try it.

Working as a programmer for several years, I always thought that when I would get a LeetCode-style problem in an interview, I should be fairly decent at solving it, since I was fairly decent at my job.

Apparently, that was not the case: I found out I suck at solving them.

How I got better

When I decided to get better, I bought an AlgoExpert subscription, did their Data Structures Crash Course, and started crunching one problem a day until I found a job.

I still do this every time I’m interviewing: buy the AlgoExpert subscription again and solve problems until I land a job. Below is my progress from the last time I was interviewing (green dots are solved problems):

Press enter or click to view image in full size

My current progress at AlgoExpert

What I was still missing

There are two problems with AlgoExpert and similar platforms:

  1. Peeking at a solution is not optimal for learning
  2. Lack of a review process

If you don’t know how to solve a problem, you can watch the video explanation and try again. The time to solve decreases, but if you really want to learn, I think you need to arrive at the solution yourself. This isn’t always 100% possible, but even well-known algorithms that required a genius to discover are made up of sub-problems you can learn to solve and internalize.

By solving these micro-problems, you learn your prerequisites.

I believe Justin Skycak’s thoughts on struggling with math due to missing prerequisite knowledge apply to struggling with coding as well:

Press enter or click to view image in full size

Another problem is the lack of a review program. To remember how to solve the problems, I think you need to review them regularly; otherwise, they fade away. I experienced this when I studied for my Linux Foundation Certified Systems Administrator exam. The only way to pass that exam is actually doing the problems yourself.

However, a year later, if I were to take it again without regular practice, I’m sure I would fail it.

Introducing algotutor

During my Easter holiday, I created a project that I think solves the two problems above. Introducing algotutor, an AI-powered algorithmic training system that uses Claude to teach you to solve coding challenges using Golang.

It works like this:

Just type train in a Claude session and it will prompt you for a problem. For this article, I chose Sonnet 4.6 model. I had a very good experience with it, but feel free to experiment with other models.

Press enter or click to view image in full size

Once you type train you get a problem:

Press enter or click to view image in full size

The problem is ready in your editor of choice inside the main.go file. To keep things simple, I made it so the user always edits just one file.

package main

// Problem 001 — Sorted Squared Array
// Given a sorted array of integers (may include negatives), return a new sorted
// array containing the squares of each integer.
//
// sortedSquaredArray([]int{-4, -1, 0, 3, 10}) → [0, 1, 9, 16, 100]
// sortedSquaredArray([]int{-7, -3, 2, 3, 11}) → [4, 9, 9, 49, 121]
// sortedSquaredArray([]int{1, 2, 3}) → [1, 4, 9]
// sortedSquaredArray([]int{}) → []

import (
"fmt"
"sort"
)

func main() {
fmt.Println(sortedSquaredArray([]int{-4, -1, 0, 3, 10})) // [0 1 9 16 100]
fmt.Println(sortedSquaredArray([]int{-7, -3, 2, 3, 11})) // [4 9 9 49 121]
fmt.Println(sortedSquaredArray([]int{1, 2, 3})) // [1 4 9]
fmt.Println(sortedSquaredArray([]int{})) // []
}

func sortedSquaredArray(arr []int) []int {
// your code here
_ = sort.Ints // hint: use sort.Ints(result) to sort your result slice
return nil
}

I apply a solution and verify it works with go run .:

func sortedSquaredArray(arr []int) []int {
var result []int

for _, v := range arr {
result = append(result, v*v)
}

sort.Ints(result)

return result
}

➜  algotutor git:(main) go run .
[0 1 9 16 100]
[4 9 9 49 121]
[1 4 9]
[]

Once I see I get a good output I type check in the Claude session and get the feedback:

Press enter or click to view image in full size

Notice that it created 4 review cards and also scheduled the problem to be solved in a future training session.

You can start a review process with the make review command:

Press enter or click to view image in full size

Example of a question & answer card:

Press enter or click to view image in full size

Press enter or click to view image in full size

Press enter or click to view image in full size

What if you don’t know?

Let’s take another problem:

Press enter or click to view image in full size

mapped to:

package main

// Problem 002 — Palindrome Check
// Given an ASCII string, return true if it reads the same forwards and backwards.
//
// isPalindrome("racecar") → true
// isPalindrome("hello") → false
// isPalindrome("abba") → true
// isPalindrome("a") → true
// isPalindrome("") → true

import "fmt"

func main() {
fmt.Println(isPalindrome("racecar")) // true
fmt.Println(isPalindrome("hello")) // false
fmt.Println(isPalindrome("abba")) // true
fmt.Println(isPalindrome("a")) // true
fmt.Println(isPalindrome("")) // true
}

func isPalindrome(s string) bool {
// your code here
return false
}

Just type I don’t know

Press enter or click to view image in full size

Spelling mistakes are OK as you can see

I got

package main

// Problem 002a — Last Character
// Given a non-empty ASCII string s, return its last byte (character).
//
// lastChar("hello") → 'o'
// lastChar("racecar") → 'r'
// lastChar("a") → 'a'

import "fmt"

func main() {
fmt.Println(lastChar("hello")) // 111 (byte value of 'o')
fmt.Println(lastChar("racecar")) // 114 (byte value of 'r')
fmt.Println(lastChar("a")) // 97 (byte value of 'a')
}

func lastChar(s string) byte {
// your code here
return 0
}

Let’s type I don't know again. We get:

package main

// Problem 002b — Last Index
// For "hello" (length 5), the last index is 4.
// Given a non-empty string s, return the index of its last character.
//
// lastIndex("hello") → 4
// lastIndex("hi") → 1
// lastIndex("a") → 0

import "fmt"

func main() {
fmt.Println(lastIndex("hello")) // 4
fmt.Println(lastIndex("hi")) // 1
fmt.Println(lastIndex("a")) // 0
}

func lastIndex(s string) int {
// your code here
return 0
}

and again

package main

// Problem 002c — Index from Length
// Given a length n, return the last valid index.
// n=5 → 4, n=3 → 2, n=1 → 0
//
// What single arithmetic operation on n gives the answer?

import "fmt"

func main() {
fmt.Println(lastIndexFromLength(5)) // 4
fmt.Println(lastIndexFromLength(3)) // 2
fmt.Println(lastIndexFromLength(1)) // 0
}

func lastIndexFromLength(n int) int {
// your code here
return 0
}

Notice how the problems gradually become easier.

Solving the current problem, it creates review cards and takes you back to the previous problem until you manage to solve the palindrome one.

Press enter or click to view image in full size

I’m still fiddling with it. I recently added mistake tracking, problem re-solve, mixed interleaved practice, and reset progress.

My focus will be on improving the spaced repetition cards it generates and on better splitting a problem into sub-problems. I really want to make the experience similar to a real tutoring session.

Conclusion

I hope this project will help anyone struggling with LeetCode-style interviews or who wants to become a better engineer.

If you do try it and have some ideas for improvement or questions, do let me know in a comment. I’m genuinely curious to find out whether it’s actually useful for someone using it for the first time.

Happy practice!