Problem Solving and Basic Algorithms
By the end of this lesson
Break a problem into steps before coding, and recognise a few patterns that recur constantly.
The hardest part of programming is usually not the code. It is working out what the code should do.
An algorithm is just a plan: a sequence of steps that solves a problem. Writing that plan in plain language before touching a keyboard is not a beginner's crutch — experienced developers do it because it is faster.
Work in plain words first
Take a real requirement: given a list of orders, find how many were placed by customers who have ordered more than once.
Attempting that directly in code usually means restarting twice. In plain words it decomposes cleanly.
1. Count how many orders each customer has
- keep a record of customer -> count
- for every order, add one to that customer's count
2. Work out which customers have a count above 1
3. Count the orders belonging to those customers
4. Return that numberNow each step is small enough to write. Step 1 needs something that maps a customer to a number, which is a dictionary. Steps 2 and 3 are a loop with a condition.
Notice the plan also exposed a question: what counts as "the same customer"? An identifier, presumably — not a name, since two people can share one. Finding that question before writing code is much cheaper than finding it afterwards.
Patterns that come up again and again
Most everyday logic is one of a handful of shapes:
- Accumulate
- Walk a collection, building up a single result. Totals, counts, longest, smallest. Declare the accumulator before the loop.
- Filter
- Walk a collection, keeping only items that satisfy a condition. Produces a smaller collection.
- Transform
- Walk a collection, producing one new item per input item. Same size, different shape.
- Group
- Walk a collection, gathering items under a key. Usually a dictionary from key to list or count.
- Search
- Walk a collection until you find what you want, then stop. Note the stopping — continuing wastes work.
string[] orderCustomerIds = { "C1", "C2", "C1", "C3", "C2", "C1" };
Dictionary<string, int> ordersPerCustomer = new Dictionary<string, int>();
foreach (string customerId in orderCustomerIds)
{
if (ordersPerCustomer.ContainsKey(customerId))
{
ordersPerCustomer[customerId]++;
}
else
{
ordersPerCustomer[customerId] = 1;
}
}
int repeatCustomerOrders = 0;
foreach (string customerId in orderCustomerIds)
{
if (ordersPerCustomer[customerId] > 1)
{
repeatCustomerOrders++;
}
}
Console.WriteLine($"Orders from repeat customers: {repeatCustomerOrders}"); // 5- A Dictionary maps a key to a value. Here it maps a customer identifier to a count.
- The first loop builds the counts. The second uses them. Two passes, each doing one clear job.
- Later, LINQ will express this in a few lines. Writing it out by hand first means you will understand what LINQ is doing rather than treating it as magic.
Summary
- Plan in plain language before writing code — it is faster, not slower
- Writing the plan surfaces the questions you had not thought to ask
- Most logic is accumulate, filter, transform, group or search
- When stuck, solve the problem for two items and look for the pattern
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Think about it
Write a plain-language plan for this: given a list of words, find the word that appears most often. Do not write code yet.
Then decide what should happen if two words tie for most frequent.
Show solution
The plan is a group followed by a search: count each word into a dictionary, then walk the dictionary tracking the highest count seen.
The tie question has no single correct answer, and that is the lesson. You could return the first found, return all of them, or treat it as an error. What matters is that you decide deliberately instead of letting whichever happens to be found first become accidental behaviour.
Challenge
Challenge
Implement it. Given a list of words, print the most frequent one and its count. Handle an empty list sensibly.
Show solution
Group into a dictionary, then accumulate to find the maximum. The empty case is handled up front so the rest of the code can assume there is at least one word.
List<string> words = new List<string> { "api", "data", "api", "cloud", "data", "api" };
if (words.Count == 0)
{
Console.WriteLine("No words supplied.");
return;
}
Dictionary<string, int> counts = new Dictionary<string, int>();
foreach (string word in words)
{
counts[word] = counts.ContainsKey(word) ? counts[word] + 1 : 1;
}
string mostFrequent = words[0];
int highestCount = 0;
foreach (KeyValuePair<string, int> entry in counts)
{
if (entry.Value > highestCount)
{
mostFrequent = entry.Key;
highestCount = entry.Value;
}
}
Console.WriteLine($"Most frequent: '{mostFrequent}' ({highestCount} times)");Saved in this browser only.