Cell by Cell
AboutRings

TechEssay

After 24 Kata: Learning To Ask A Better Question

After the first 24 Kata exercises, the biggest lesson was not a specific algorithm, but learning to ask what information each step should produce.

  • Published
  • Last edited
  • Reading time8 min
  • LanguagesENZH
  • Kata
  • Problem Solving
  • Algorithms
  • Learning Notes

I started Kata as a place to practice solving problems, not to collect them.

The habit I wanted was ordinary: read the problem slowly, write down a first idea, find where that idea breaks, then turn the mess into something clean. Twenty-four problems in, the thing that stuck with me was not any single algorithm. It was a smaller, quieter shift in how I ask the question in the first place.

Full project: Kata

Remembering the right thing

The early problems were the usual arrays, strings, and lookups, and they turned out to be less about tricks than about memory: what, exactly, am I trying to remember as I move through the data?

Two Sum is the cleanest example. My first idea was the honest brute-force one, check every pair, and it works, it is just slow. What collapses it into a single pass is not a technique but a change of question. Instead of "which two numbers add up to the target," I ask, "the number in my hand right now, what is its missing half, and have I seen that half already?" Once the question is phrased that way, a hash map is the obvious place to keep the answer.

The same instinct carries the next few. Contains Duplicate only ever asks "have I seen this before," so a set fits better than a list. Valid Anagram asks one step more, not just whether a character showed up but how many times, so the right memory is a frequency map rather than a set. Different structures, same underlying move: decide what question the data structure has to answer before reaching for one.

Boundaries do more work than they look

A few problems were really about edges, not elements.

In Binary Search the tempting image is "cut the array, look at the middle." The actual work is quieter than that: hold two boundaries, left and right, and never lose track of the real index while you shrink them. First Bad Version sharpened this for me, because it is not a search for a value at all. It is a search for the first place where a condition flips from good to bad. That single reframing rewrites the update rules. If mid is good it cannot be the answer, so I move past it to mid + 1. If mid is bad it might still be the earliest bad one, so I keep it in range. Binary search, it turns out, is less about the middle element and more about what I am allowed to throw away.

When the window will not behave

Maximum Subarray was one of the first problems that quietly changed how I think.

It looks like an interval problem, which makes you want to slide a left and right boundary and ask "where should the best subarray start." But negative numbers break that picture. Sometimes the right move is not to nudge the left edge one step, it is to abandon the whole prefix behind you. The question that actually holds up is narrower: what is the largest subarray sum that has to end right here, at this position?

That state is easy to carry:

current = max(nums[i], current + nums[i])

This was the first time a solution stopped being about moving pointers by hand and started being about naming a state and letting it roll forward. Climbing Stairs has the same skeleton. You can try to count how many 1-steps and 2-steps you used and get tangled fast, or you can ask what the last move onto step n could have been. It is either a step from n - 1 or a step from n - 2, and suddenly the whole thing depends on just those two.

Pointers are not values

The linked list problems barely felt like algorithms. They were about being honest about what a variable actually points at.

In Merge Two Sorted Lists the comparison is trivial. The real question is where I attach a node once I have picked it, which is the whole reason the dummy head and tail pointer exist: they let me build the result without treating the first node as a special case. Reverse Linked List looked easier and taught me more, because it forced a basic question I had been sliding past, is head the value or the node? It only clicked once I separated the three things that had been blurred together: current.val is the value inside, current.next is the next node, and current = current.next is the variable moving on. Reversing is not printing values backward, it is rewiring the next pointers, and before I overwrite one I have to save what came after it:

next_node = current.next
current.next = prev

Linked List Cycle made the same point from the other side. A cycle means meeting the same node object again, not the same value. In a linked list values can repeat freely, but nodes are identities, and the whole problem depends on respecting that difference.

One small rule under many cases

Some problems show up wearing a costume of special cases.

Roman to Integer looks like a pile of exceptions: IV, IX, XL, XC, CD, CM. You could handle each subtractive form by name, or you could notice the single local rule sitting underneath all of them, which is that a numeral smaller than the one to its right gets subtracted, and otherwise gets added. Plus One has the same flavor. The lazy read is "isn't this just the number plus one," and in Python you can cheat by converting the whole array, but that dodges the actual problem, which is how a carry travels through digits. Walk from the end: if a digit is not 9, add one and stop; if it is 9, set it to 0 and let the carry keep moving. A problem with many named cases often has one quiet rule holding it together.

Trees changed which question I was answering

The binary tree run was the real turn in this first batch.

Invert Binary Tree starts simple, swap every node's two children, and the thing that makes it easy is admitting the shape is recursive: a tree is just a root plus a left subtree plus a right subtree. So the function handles the root and hands the subtrees back to itself. Maximum Depth made the return value say something out loud, maxDepth(root) gives the depth of the tree under root, and once that sentence is clear the body is just 1 + max(left depth, right depth).

Same Tree and Symmetric Tree pushed it further, and the useful move was choosing the right arguments before writing any logic. isSameTree(p, q) compares two current nodes; isMirror(left, right) compares two mirrored positions. Get the function's shape right and the recursive calls almost write themselves. Subtree of Another Tree then just reuses Same Tree, with one function asking "are these two trees identical" and another walking the root to try each candidate start. Keeping those two jobs separate is what keeps the code readable.

The lesson I actually want to keep

The clearest arc runs through three problems that all lean on subtree height: Maximum Depth, Balanced Binary Tree, and Diameter of Binary Tree. What changes is what height is for.

In Maximum Depth, height is the answer, full stop. In Balanced Binary Tree it is not enough on its own, because the function has to compute height and notice, on the way up, whether some subtree is already unbalanced. A sentinel like -1 can carry that failure upward. That was the first crack in an assumption I had not known I was holding: that a recursive function returns the answer. It does not have to. It can return whatever the parent call needs.

Diameter made it obvious. My first instinct was to ask whether the longest path runs through the root or hides inside a subtree, which drags you into awkward comparisons between branches on the left and branches on the right, and quietly mixes two different things: the best path already formed somewhere below, and the single downward height a node can report to its parent. The clean question is local. For each node, if a path has its highest point right here, how long is it? That length is just left depth + right depth. So the function keeps returning height, and the diameter gets updated off to the side as I visit each node.

That is the line worth writing on the wall:

What 24 problems added up to

Looking back across the batch, the repeated lesson was never "use a hash map" or "use recursion" or "use two pointers." It was to ask a sharper question before writing anything.

Less "how do I find the answer," more "what information do I actually need at this step." Less "which case does this fall into," more "is there one local rule that covers all of them." Less "what should the code look like," more "what should this variable, or this function, actually mean?"

That is the part I want Kata to keep training.

Full project: Kata