Lessons
1 · Foundations (Think Like a Programmer)
2 · Data Structures and Recursion
- 05Arrays and Array-Based Problem Solving
- 06Strings: Characters, Immutability, and Efficient Manipulation
- 07Linked Lists: Nodes, Pointers, and Pointer Reasoning
- 08Stacks and Queues
- 09Hashing: HashMap, HashSet, and Frequency Patterns
- 10Recursion and Backtracking
- 11Trees and Binary Search Trees
- 12Heaps and Priority Queues
3 · Core Algorithms and Reusable Patterns
4 · Advanced Interview Topics
Lesson 2 · Foundations (Think Like a Programmer)
Problem Solving Fundamentals: From a Question to an Algorithm
How to turn a vague problem statement into a clear plan: pin down inputs, outputs and constraints, list edge cases, write pseudocode, build a brute-force answer first and then improve it.
For
Beginners who understand basic loops and arrays and want a reliable method for approaching unfamiliar coding problems.
Level
Before you start
Article 1 (Start Here). You should be able to read a short Java method with a loop and an if statement.
On this page
Key takeaways
- A problem is not solved until you can state its input, output, constraints and edge cases in your own words.
- Examples come before code. Work at least one normal case and one awkward case by hand.
- Write a brute-force solution first. It gives you a correct baseline, and its wasted work points to the optimization.
- Constraints tell you which complexity is acceptable. Check the input size before you commit to an approach.
- Pseudocode is for thinking, Java is for running. Do not mix them up, and do not skip the thinking.
Imagine a friend who runs a small online shop says, "Can you tell me which stretch of days we did well?" Is "well" above a certain amount, or above the average? Is a stretch any group of days, or consecutive days only? Should the answer be the longest stretch, the best one, or the most recent?
A problem statement is a request from a person who knows what they mean and is not telling you everything. Most beginners start coding straight away and solve the wrong problem very efficiently. This article teaches you to slow down for two minutes at the start so you do not lose two hours at the end.
What you will learn
#- How to extract inputs, outputs, constraints and assumptions from a problem statement.
- How to use the input size to decide how fast your solution needs to be.
- How to pick examples that expose mistakes, and how to list edge cases.
- How to break a problem into steps and write pseudocode.
- How to write a brute-force solution, find its wasted work and improve it.
Step 1: Understand the problem
#Before you write anything, answer these questions in your own words.
Question | Why it matters |
|---|---|
What exactly is the input? Its type, and what it represents. |
|
What exactly is the output? A value, a list, true or false, an index? | Returning a count is different from returning the items themselves. |
What are the constraints? How large can the input be? What range can values take? | They decide which approaches are fast enough and whether |
What are the assumptions? Sorted? Positive only? Unique values? | An assumption you were not told is a bug waiting to happen. |
What should happen for unusual input? Empty, one element, all equal? | These cases cause most wrong answers. |
When you are in an interview, ask these questions out loud. Interviewers like it, because it is what a good colleague does. When you are practising alone, write the answers down, because writing forces precision.
Our running example
#Here is a problem we will use through this article. It is original, small and rich enough to show every step.
Longest good streak. A shop records its sales amount for each day, in order, in an array
sales. A day is "good" if its sales are at leasttarget. Return the length of the longest run of consecutive good days.
Let us run the questions on it.
- Input: an array of integers
salesand an integertarget. - Output: one integer, the length of the longest run of consecutive good days.
- Constraints: none were given. We would ask. Suppose the answer is "up to 100,000 days".
- Assumptions: sales can be zero. Negative values are possible if returns are recorded. We treat each element on its own, nothing needs to be sorted.
- Unusual input: an empty array has no days, so the answer is
0. If no day is good, the answer is also0.
Notice that we did not write code and we already know a great deal.
Step 2: Read the constraints
#The size of the input tells you how much work your solution may do. A computer can do roughly tens or hundreds of millions of simple operations in a second. The exact number depends on the machine, the language and the platform, so treat the following as a rough guide and not as a law.
Input size | What usually works | Why |
|---|---|---|
up to about 10 | Anything, including trying all orderings | Even |
up to about 20 to 25 | Trying all subsets, which is |
|
up to about 500 | O(n^3) |
|
up to about 5,000 | O(n^2) |
|
up to about 100,000 | O(n log n) or O(n) |
|
1,000,000 or more | O(n), sometimes O(n log n) | You can afford very few passes |
about a billion or more | O(log n) or O(1) | You cannot even look at every value |
The next article, Time and Space Complexity Without the Confusion, explains the O(...) notation properly. For now, read "O(n^2)" as "the work grows with the square of the input size".
For our shop, n is up to 100,000, so a solution that compares every pair of days is too slow. We will keep that in mind.
Constraints on values matter too. If values can reach two billion and you add a few of them, an int may overflow. We will meet that in the next two articles.
Step 3: Choose examples, including awkward ones
#Good examples are chosen to try to break your idea. For each problem, write:
- One normal example you can solve by eye.
- The smallest case. Empty input, a single element.
- An all-same case. Everything equal, everything good, everything bad.
- A boundary case. A value exactly equal to the threshold. A streak at the very start or the very end.
- A case where the answer is not at the start. This catches code that only looks at the beginning.
For the shop, with target = 7:
| Longest good streak | Why this example |
|---|---|---|
| 3 | Normal. Two streaks of different lengths, and the longer one is later. |
| 0 | No good day. |
| 3 | Every day is good, so the streak touches both ends. |
| 0 | Empty. |
| 1 | Single good element. |
| 1 | Exactly equal to |
Write these before the code, and you have a test suite waiting.
Step 4: Break the problem into smaller steps
#Large problems feel impossible. Small steps do not. Ask: "If I were doing this by hand on a printed list, what would I do?"
By hand you would move your finger along the days. Each time you see a good day you add one to a running count. When you see a bad day, the streak is over, so you reset the count to zero. Whenever the running count beats the best streak so far, you remember it.
That description is already an algorithm. Writing it in a structured way is called pseudocode.
Pseudocode: thinking in a half-way language
#Pseudocode is a plain-English version of code. It has no strict syntax. Its purpose is to let you design the steps without fighting a compiler.
best = 0current = 0for each value in sales: if value >= target: current = current + 1 best = max(best, current) else: current = 0return bestA few rules for good pseudocode:
- Use short, precise verbs: set, add, compare, return, repeat.
- Name things the way you will name them in code.
- Keep indentation to show structure, as in the example.
- Do not hide hard steps behind vague words. "Process the array" is not a step.
Step 5: Brute force first
#A brute-force solution tries everything simple and obvious. It is usually slow but it is easy to get right. There are three reasons to write it first:
- It gives you a correct answer to compare against.
- It proves you understand the problem.
- Its wasted work shows you where the speed-up is hiding.
For the shop, the brute force is: "Try every day as the starting day. From there, keep going while the days are good. Remember the longest."
best = 0for start from 0 to n - 1: end = start while end < n and sales[end] >= target: end = end + 1 best = max(best, end - start)return bestLet us make both versions real Java and run them side by side.
// File: Main.javapublic class Main { // Brute force: try every starting day and extend as far as the streak lasts. static int longestStreakBrute(int[] sales, int target) { int best = 0; for (int start = 0; start < sales.length; start++) { int end = start; while (end < sales.length && sales[end] >= target) { end++; } best = Math.max(best, end - start); } return best; } // One pass: keep the length of the streak that is running right now. static int longestStreak(int[] sales, int target) { int best = 0; int current = 0; for (int value : sales) { if (value >= target) { current++; best = Math.max(best, current); } else { current = 0; } } return best; } public static void main(String[] args) { int[][] tests = { {5, 8, 9, 3, 7, 7, 7, 2}, {1, 2, 3}, {10, 10, 10}, {}, {9}, {7}, {-3, -1, -2} }; int target = 7; for (int[] sales : tests) { System.out.println(longestStreakBrute(sales, target) + " " + longestStreak(sales, target)); } }}3 30 03 30 01 11 10 0Both columns match the "Longest good streak" column in our table. Having two independent solutions agree is a strong sign, and it is a technique you can reuse: write the slow obvious version and use it to check the fast clever version.
Step 6: Find the wasted work
#Trace the brute force on [5, 8, 9, 3, 7, 7, 7, 2] with target = 7.
| What the inner loop does | Streak found |
|---|---|---|
0 |
| 0 |
1 |
| 2 |
2 |
| 1 |
3 |
| 0 |
4 |
| 3 |
5 |
| 2 |
6 |
| 1 |
7 |
| 0 |
Look at starts 1 and 2. When we started at day 1 we already walked over day 2 and learned that it begins a good run of length 1 that ends at the same place. Starting again at day 2 re-reads the same days. The same happens for starts 4, 5 and 6.
That is the wasted work: we keep re-walking days we have already seen. The improvement writes itself. Walk through the days once, and carry along what we learned: the length of the streak running right now.
Dry run of the one-pass version
#Same input, target = 7:
|
| Good? |
|
|
|---|---|---|---|---|
0 | 5 | no | 0 | 0 |
1 | 8 | yes | 1 | 1 |
2 | 9 | yes | 2 | 2 |
3 | 3 | no | 0 | 2 |
4 | 7 | yes | 1 | 2 |
5 | 7 | yes | 2 | 2 |
6 | 7 | yes | 3 | 3 |
7 | 2 | no | 0 | 3 |
Returns 3. Every day was looked at exactly once.
Complexity
#- Brute force. In the worst case, every day is good. Start 0 walks
ndays, start 1 walksn - 1, and so on, so the total isn + (n - 1) + ... + 1, which isn * (n + 1) / 2. We say the time is O(n^2) and the extra space is O(1). Forn = 100,000that is about five billion steps, which is too slow. - One pass. The loop runs
ntimes and each iteration does a constant amount of work. Time is O(n) and extra space is O(1), because we only keepbestandcurrent.
For this particular problem the brute force would barely pass a test with n = 5,000 and would fail one with n = 100,000. The constraints told us that in advance.
A second worked example: second largest distinct value
#Our second example has more traps, which makes it a better workout for the edge-case habit.
Second largest. Given an array of integers, return the second largest distinct value. If there are fewer than two distinct values, there is no answer.
The word distinct is a decision. In [9, 9, 4], is the second largest 9 or 4? We chose distinct, so it is 4. If the problem statement does not say, you ask.
Brute force: sort a copy of the array, then walk backwards from the end until you find a value different from the largest.
Optimization: sorting does more work than we need, because we only care about the top two values. One pass can track the largest and the second largest seen so far.
Here is the trap: how do you represent "nothing seen yet"? If you start first and second at Integer.MIN_VALUE, the array [Integer.MIN_VALUE, 5] breaks, because the real minimum value looks like "nothing yet". We avoid this by tracking the two values in long variables and using Long.MIN_VALUE, which is smaller than any int.
// File: Main.javaimport java.util.Arrays;public class Main { // Brute force: sort a copy, then look for the first value below the maximum. static Integer secondLargestSorted(int[] numbers) { int[] copy = Arrays.copyOf(numbers, numbers.length); Arrays.sort(copy); for (int i = copy.length - 2; i >= 0; i--) { if (copy[i] != copy[copy.length - 1]) { return copy[i]; } } return null; // fewer than two distinct values } // One pass: track the two largest distinct values seen so far. static Integer secondLargest(int[] numbers) { long first = Long.MIN_VALUE; // no int is this small, so it safely means "nothing yet" long second = Long.MIN_VALUE; for (int value : numbers) { if (value > first) { second = first; first = value; } else if (value < first && value > second) { second = value; } } return second == Long.MIN_VALUE ? null : (int) second; } public static void main(String[] args) { int[][] tests = { {4, 9, 2, 9, 7}, {5, 5, 5}, {}, {3}, {Integer.MIN_VALUE, 5} }; for (int[] test : tests) { System.out.println(secondLargestSorted(test) + " " + secondLargest(test)); } }}7 7null nullnull nullnull null-2147483648 -2147483648A method that returns Integer (the object version of int) can return null to say "there is no answer". We explain why in Java Essentials for DSA.
Walk through [4, 9, 2, 9, 7] in the one-pass version:
|
|
| Branch taken |
|
|
|---|---|---|---|---|---|
4 | none | none |
| 4 | none |
9 | 4 | none |
| 9 | 4 |
2 | 9 | 4 | neither (2 is not above | 9 | 4 |
9 | 9 | 4 | neither (equal to | 9 | 4 |
7 | 9 | 4 |
| 9 | 7 |
The answer is 7. The value < first check is what makes the second 9 get ignored, which is exactly the "distinct" part of the requirement.
Complexity: sorting version O(n log n) time and O(n) extra space for the copy. One-pass version O(n) time and O(1) extra space.
Edge cases and common mistakes
#- Solving a slightly different problem. Re-read the statement after your first example. Did you assume sorted input, positive numbers or unique values?
- Treating "empty" as impossible. If it is allowed, handle it. If it is not, say so.
- Off-by-one errors. Decide whether your range is inclusive or exclusive.
end - startis a length;end - start + 1would be too long. - Bad sentinels. Starting
maxat0fails for all-negative arrays. Starting it atInteger.MIN_VALUEis safer, but starting it at the first element is the safest when the array is not empty. - Ignoring overflow. If values or sums can exceed about two billion, use
long. - Optimising before you have a correct baseline. A fast wrong answer is worth nothing.
- Forgetting to test the boundary of the condition.
>versus>=is the most common single-character bug.
Exercises
#Exercise 1
Trace
#Run longestStreak by hand on sales = [7, 7, 2, 8, 9, 10, 1] with target = 8. Fill in current and best after each element. What is returned?
Exercise 2: Edge cases
#You must write a method that returns the average of an array of marks. List at least six inputs you would test, and say what the method should do for each. Pay special attention to anything that could crash or overflow.
Exercise 3: Brute force, then improve
#Given an array a with at least two elements, find the largest value of a[j] - a[i] where i < j. Write the brute force, then a one-pass version. State the complexity of each. (Hint: for each position, what is the best earlier element to subtract?)
Exercise 4: Debug
#This method is meant to return the length of the longest good streak, but it has two bugs. Find them, give an input that exposes each, and fix the code.
static int longestStreakBuggy(int[] sales, int target) { int best = 0; int current = 0; for (int value : sales) { if (value > target) { current++; } best = Math.max(best, current); } return best;}Exercise 5: Use a property of the input
#A sorted array of integers is given. Count how many distinct values it contains. First describe a solution that works for any array, then use the fact that the array is sorted to simplify it.
Show solutionHide solution
With target = 8, a value is good if it is at least 8.
| Good? |
|
|
|---|---|---|---|
7 | no | 0 | 0 |
7 | no | 0 | 0 |
2 | no | 0 | 0 |
8 | yes | 1 | 1 |
9 | yes | 2 | 2 |
10 | yes | 3 | 3 |
1 | no | 0 | 3 |
The method returns 3. Note that current is reset to 0 on the final 1, but best keeps the earlier maximum. That is why we track both.
Exercise 2
Show solutionHide solution
Here are useful tests and expected behaviour:
Input | Expected behaviour | Reason |
|---|---|---|
| Decide explicitly: return | Dividing by zero is the obvious trap. With ints it throws; with doubles it gives |
|
| Single element. |
|
| All equal, and all zero. |
|
| Maximum marks. |
|
| The sum of the two values exceeds the |
|
| Negative values, if they are allowed at all. |
|
| The average is not a whole number, so integer division would wrongly give |
The overflow and the integer division are the two bugs people ship most often. A safe version sums into a long and divides as a double:
// File: Main.javapublic class Main { static double average(int[] marks) { if (marks.length == 0) { throw new IllegalArgumentException("marks must not be empty"); } long sum = 0; for (int mark : marks) { sum += mark; } return (double) sum / marks.length; } public static void main(String[] args) { System.out.println(average(new int[] {75})); System.out.println(average(new int[] {3, 4})); System.out.println(average(new int[] {2_000_000_000, 2_000_000_000})); int badSum = 2_000_000_000 + 2_000_000_000; // overflows int System.out.println(badSum); }}75.03.52.0E9-294967296The last line shows the overflow: two billion plus two billion is four billion, which does not fit in an int, so it wraps around to a negative number. The method returns the double 2.0E9, which is Java's way of printing 2,000,000,000.0.
Exercise 3
Show solutionHide solution
Brute force. Check every pair (i, j) with i < j. Keep the maximum difference. That is O(n^2) time, O(1) space.
One pass. For a fixed j, the best i is the earlier position with the smallest value. So walk through the array, remember the smallest value seen so far (minSoFar), and at each j compute a[j] - minSoFar. Update minSoFar after using it, because i must be strictly before j. This is O(n) time, O(1) space.
We use long for the differences, because a[j] - a[i] can overflow an int when the values are extreme (for example Integer.MIN_VALUE - Integer.MAX_VALUE).
// File: Main.javapublic class Main { static long maxDifferenceBrute(int[] a) { long best = (long) a[1] - a[0]; for (int i = 0; i < a.length; i++) { for (int j = i + 1; j < a.length; j++) { best = Math.max(best, (long) a[j] - a[i]); } } return best; } static long maxDifference(int[] a) { int minSoFar = a[0]; long best = (long) a[1] - a[0]; for (int j = 1; j < a.length; j++) { best = Math.max(best, (long) a[j] - minSoFar); minSoFar = Math.min(minSoFar, a[j]); } return best; } public static void main(String[] args) { int[][] tests = { {7, 2, 9, 4}, {9, 5, 3}, {1, 2}, {Integer.MAX_VALUE, Integer.MIN_VALUE} }; for (int[] test : tests) { System.out.println(maxDifferenceBrute(test) + " " + maxDifference(test)); } }}7 7-2 -21 1-4294967295 -4294967295The second test shows that the answer can be negative when the array is decreasing, which is why best starts at the first valid difference and not at 0. The fourth test would have given a wrong answer with plain int arithmetic.
Exercise 4
Show solutionHide solution
There are two bugs.
>should be>=. The problem says a good day has sales at leasttarget. Withsales = [7, 7]andtarget = 7, the buggy code returns0and the correct answer is2.currentis never reset. When a bad day appears, the streak must restart from zero. Withsales = [9, 3, 9]andtarget = 8, the buggy code counts both good days even though a bad day sits between them, so it returns2. The longest streak is1.
Fixed version, run against both examples:
// File: Main.javapublic class Main { static int longestStreakBuggy(int[] sales, int target) { int best = 0; int current = 0; for (int value : sales) { if (value > target) { current++; } best = Math.max(best, current); } return best; } static int longestStreakFixed(int[] sales, int target) { int best = 0; int current = 0; for (int value : sales) { if (value >= target) { current++; } else { current = 0; } best = Math.max(best, current); } return best; } public static void main(String[] args) { System.out.println(longestStreakBuggy(new int[] {7, 7}, 7) + " " + longestStreakFixed(new int[] {7, 7}, 7)); System.out.println(longestStreakBuggy(new int[] {9, 3, 9}, 8) + " " + longestStreakFixed(new int[] {9, 3, 9}, 8)); }}0 22 1The first column is the buggy method and the second is the fixed one. Notice that the fixed version updates best after every element, which is fine, because best can only grow when current has just grown.
Exercise 5
Show solutionHide solution
Any array. Put every value in a HashSet and return the set's size. This is O(n) time and O(n) space. (We study HashSet in Hashing.) Another general approach is to sort first, which brings you to the sorted case below.
Using the sorted property. In a sorted array, equal values sit next to each other. So a value is "new" exactly when it differs from the value just before it. Count the first element, then count every position where sorted[i] != sorted[i - 1]. Time O(n), extra space O(1).
// File: Main.javapublic class Main { static int countDistinctSorted(int[] sorted) { if (sorted.length == 0) { return 0; } int count = 1; // the first element is always a new value for (int i = 1; i < sorted.length; i++) { if (sorted[i] != sorted[i - 1]) { count++; } } return count; } public static void main(String[] args) { System.out.println(countDistinctSorted(new int[] {1, 1, 2, 3, 3, 3})); System.out.println(countDistinctSorted(new int[] {})); System.out.println(countDistinctSorted(new int[] {7})); System.out.println(countDistinctSorted(new int[] {2, 2, 2})); }}3011The lesson is general: whenever the problem gives you a property (sorted, small range, unique), ask what it lets you skip. Here, sortedness lets us find duplicates by looking only at neighbours.
Where to go next
#- Next: Java Essentials for DSA gives you the language tools: types, overflow, arrays, strings and basic input.
- Then: Time and Space Complexity Without the Confusion turns "too slow" into a number.
- Looking ahead: you will see the "remember what you have already learned" idea again in Sliding Window and Prefix Sums.
Connected lessons
Linked from 4 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
How to Learn Data Structures and Algorithms Without Burning Out
Guide · Career
The OA Landscape: Platforms, Formats, Proctoring and Rules
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

