You may know how to write a loop and still feel stuck when a problem does not tell you which loop to use. The missing step is often a clear model of the problem: what the input means, what result you need, and what must change as you work toward that result.
Programming logic is the reasoning that connects those pieces. You can develop it in C, Python, Java, JavaScript, or another language. The syntax will differ, and the environment will impose its own rules, but you can begin with the same questions. This guide follows one complete problem, then shows how to test your reasoning and practise it on new tasks.
Logic answers “what happens?”; syntax expresses it
Consider a program that adds a delivery charge to small orders. “Use an if statement” is a syntax decision. “Charge delivery when the order total is below the free-delivery threshold, but not when it equals the threshold” is a rule. You need the rule before choosing a comparison operator.
A compiler can reject a missing parenthesis. It generally cannot tell you that you used a strict comparison when the business rule required an inclusive one. A program may run successfully and produce the wrong answer. Keep three questions separate: does the source follow the language, can the program execute in this environment, and does its result satisfy the requirement?
You do need language knowledge. Integer division, overflow, indexing, and string comparison can change how a plan must be implemented. However, learning another keyword is not always the answer to a reasoning problem. First describe the intended behavior without relying on code.
1. Replace a vague request with a small, testable contract
Our worked problem is: find the longest consecutive run of scores that meet a threshold. A list of scores could represent practice attempts in their original order. Before writing a function, define the terms that would otherwise invite guesses:
- Input: an ordered list of integer scores from 0 to 100, and an integer threshold in the same range.
- Qualifying: a score greater than or equal to the threshold.
- Consecutive: neighboring entries in the original list, with no nonqualifying entry between them.
- Output: the length of the longest qualifying run, not the total number of qualifying scores.
- Empty input: return 0. If nothing qualifies, also return 0.
The calculation below assumes these inputs are already valid. An application accepting text from a user must parse and validate it separately. Decide what invalid data means before processing it; silently deleting a bad entry could join two runs that were originally separated.
For scores 65, 42, 78, 91, 38, 80, 84, and 86, with threshold 60, the qualifying runs have lengths 1, 2, and 3. The answer is 3. Counting all qualifying scores would give 6, which solves a different problem. Sorting the scores would also destroy the order the question depends on.
When a task is ambiguous, write a concrete input and ask which output is intended. An example often reveals a disagreement faster than another paragraph of abstract wording.
2. Work through one example before inventing variables
Read the sample from left to right. After 65, you have a qualifying run of length 1. The next score, 42, breaks it. The pair 78 and 91 creates a run of length 2, which 38 breaks. Finally, 80, 84, and 86 create a run of length 3.
What information did you carry while doing that? You needed the length of the run that is currently continuing, and the best length you had seen anywhere. Those become two variables:
current_run: the number of qualifying scores in the run ending at the most recently processed entry.best_run: the largest run length found in the processed part of the list.
One counter is insufficient. If it resets at a failing score, it forgets an earlier best run. If it never resets, it counts qualifying entries that are not consecutive. These variables are useful because they represent distinct facts, not because an example happened to contain two numbers.
Both start at 0: before reading any scores, you have seen no run. After each qualifying score, extend the current run and update the best if necessary. After a nonqualifying score, reset the current run while preserving the best. Nothing else needs to be remembered to return the length.
3. Write instructions that do not hide the difficult step
Pseudocode describes an algorithm without committing to a language’s exact grammar. CS50’s introduction to pseudocode explains how readable instructions connect decisions, repetition, and functions. Your own draft should be precise enough for someone to execute with a pencil.
current_run = 0
best_run = 0
for each score, in its original order:
if score >= threshold:
current_run = current_run + 1
best_run = larger of best_run and current_run
otherwise:
current_run = 0
return best_run“Find the longest run” would not be useful pseudocode here: it merely repeats the question. The version above explains the comparison, the changing state, the reset, and the returned value. A good draft exposes the part you are trying to understand.
The update order matters. Extend current_run before comparing it with best_run; otherwise, you compare against the old length. Updating the best during the loop also handles a longest run at the end of the list, where no later failing score arrives to announce that it has finished.
Give yourself a statement that stays true
After each processed entry, current_run is the qualifying suffix length of the processed entries, and best_run is their longest qualifying run. This is a loop invariant: a statement you maintain across iterations. You do not need formal notation to use it.
It is true initially, when both lengths are 0. A qualifying score extends the suffix and may establish a new best; a failing score makes the suffix length 0 while leaving earlier runs intact. When the loop finishes, the processed part is the whole list, so the best value answers the question. The loop terminates because it processes one entry per iteration from a finite list.
4. Make a dry run visible instead of keeping it in your head
A dry run records what the program would do without executing it. Write down the state after each step. This table uses the sample scores and threshold 60; both counters are 0 before the first row.
| Score | Current run | Best run |
|---|---|---|
| 65 | 1 | 1 |
| 42 | 0 | 1 |
| 78 | 1 | 1 |
| 91 | 2 | 2 |
| 38 | 0 | 2 |
| 80 | 1 | 2 |
| 84 | 2 | 2 |
| 86 | 3 | 3 |
The rows for 42 and 38 reveal the key distinction: the current value resets, but the best does not. The last row shows why the answer is 3. If your implementation produces another answer, compare its state against these rows rather than rereading the entire source repeatedly.
For a different problem, record different state: a remaining balance, a search interval, a previous value, or a collection of items already seen. Include what each column means. A table full of numbers is less helpful when you cannot explain whether they describe the state before or after an operation.
5. Translate each decision into the language you are using
Once the algorithm survives a manual trace, implementation becomes a set of smaller choices. What represents the input? How do you visit each entry? Which type holds a length? How do you return the result? Preserve the meaning of the state while answering these language-specific questions.
A complete C program
#include <stdio.h>
size_t longest_run(const int scores[],
size_t count, int threshold) {
size_t current_run = 0;
size_t best_run = 0;
for (size_t i = 0; i < count; i++) {
if (scores[i] >= threshold) {
current_run++;
if (current_run > best_run) {
best_run = current_run;
}
} else {
current_run = 0;
}
}
return best_run;
}
int main() {
int scores[] = {65, 42, 78, 91, 38, 80, 84, 86};
size_t count = sizeof scores / sizeof scores[0];
size_t best = longest_run(scores, count, 60);
printf("Longest qualifying run: %zu\n", best);
return 0;
}The function receives the element count explicitly because an array parameter does not carry its original length. size_t holds lengths, and %zu prints that type. The caller must provide a valid range of count elements. The sizeof calculation works in main, where scores is an actual array; it is not a way to recover the array’s length inside the function.
Paste this program into the Circuit online C compiler and run it. It uses fixed data deliberately, so you can investigate the calculation before introducing input parsing. See the array guide and function guide if those parts are unfamiliar.
The same reasoning in Python
def longest_run(scores, threshold):
current_run = 0
best_run = 0
for score in scores:
if score >= threshold:
current_run += 1
if current_run > best_run:
best_run = current_run
else:
current_run = 0
return best_run
scores = [65, 42, 78, 91, 38, 80, 84, 86]
best = longest_run(scores, 60)
print(f"Longest qualifying run: {best}")Python’s loop visits the list values directly, and indentation groups the statements. The state updates and qualifying rule remain the same. Its official control-flow tutorial explains this iteration model. Run this version with Python 3 in its own environment; Circuit currently runs C.
Both examples print:
Longest qualifying run: 3This transfer is the useful part of learning logic. You can recognize the same algorithm despite different braces, declarations, and loop forms. It does not mean every language behaves identically: check its numeric types, collection rules, and error handling when you translate.
6. Pick tests that can expose a mistaken assumption
One successful sample is evidence about one input. Design more examples around the rules most likely to go wrong. For threshold 60, use these cases and predict the answer before running them:
| Scores → answer | What it checks |
|---|---|
| Empty list → 0 | No entries to process; initialization must already produce the correct result. |
| 60 → 1 | A score exactly at the threshold qualifies. |
| 59 → 0 | A score just below the threshold does not qualify. |
| 60, 75, 90 → 3 | The longest run reaches the end of the input. |
| 10, 20, 30 → 0 | No score qualifies. |
| 80, 40, 90, 30 → 1 | Separated successes must not be joined. |
| 80, 90, 40, 70 → 2 | The best run occurs earlier than the final run. |
| 70, 80, 30, 90, 95 → 2 | Two equally long runs still return a single length. |
In Python, call longest_run([], 60) for the empty case. In C, call longest_run(NULL, 0, 60): the loop performs no accesses when the count is 0. For nonempty C tests, replace the sample array and retain the count calculation. Do not use a nonzero count with a null pointer or a count larger than the real array.
There are useful relationships to check too. The answer must be between 0 and the number of entries. Raising the threshold cannot increase it. Appending a failing score cannot reduce the best length already seen. These properties complement exact expected answers; they do not replace them.
For repeated work, automate the cases in your language’s test tools. Python’s unittest documentation shows how to compare a function’s actual result with an expected one. Keep tests independent of prompts and display formatting when you are checking a calculation.
7. Debug the first incorrect transition
Suppose you accidentally return current_run instead of best_run. The main sample still prints 3 because its longest run happens to be at the end. But 80, 90, 40, 70 should produce 2 and would produce 1. The smaller test reveals a bug that the larger sample concealed.
Reduce a failure to the shortest input that preserves it, then trace that input. Inspect which variable first stops matching its definition. In this example the state may be correct throughout the loop; the return statement chooses the wrong fact. If you reset best_run along with the current run, the first failing score after a success already breaks the invariant.
- Record the input, expected result, and actual result.
- Identify whether the failure is in reading data, calculating, or presenting the answer.
- Inspect state at the relevant steps with a debugger or temporary output.
- Explain one suspected cause before changing it.
- Rerun the failing case and the previously passing cases.
In C, a temporary print of the index, score, current length, and best length after each iteration can reproduce the dry-run table. Remove those prints when you have answered the question. Adding random statements or changing several conditions at once makes it harder to tell which change corrected the behavior.
8. Split larger tasks at boundaries you can test
If the application must accept scores from a user, do not mix all its responsibilities into the loop. Separate reading text, validating scores, computing the longest run, and displaying the result. Each part should have a clear input and result. The calculation can then be tested with known values without repeatedly typing into a terminal.
This does not require creating a function for every line. A boundary earns its place when it has a useful meaning, can be checked independently, or is reused. The input guide explains the separate concerns around C conversions and invalid input.
Recognize the shape of the work
- Counting or totaling: carry an accumulator, with an initial value that matches the operation.
- Comparing neighbors: remember the previous value and decide how to handle the first entry.
- Finding a best candidate: carry the best found so far; define what happens when no candidate exists.
- Detecting something already seen: consider a set or another lookup structure, with clear equality rules.
- Matching nested openings and closings: consider a stack when the kind and order of unmatched openings matter.
These are starting questions, not recipes to apply blindly. Our streak problem combines counting with a best-so-far value because a break changes what “current” means. A total alone would lose that structure.
The streak algorithm visits each of n scores once and carries two counters: O(n) time and O(1) auxiliary space, excluding the input. Start with this understandable solution. If a real workload later needs improvement, measure it and revisit the algorithm instead of guessing that shorter source must be faster.
When you are stuck, make the next question smaller
A blank editor encourages you to solve everything simultaneously. Replace “How do I write the program?” with a specific question: what should an empty input return, what ends a run, or which fact must survive a reset? Try the smallest example that answers it.
If even a manual solution is unclear, return to the problem statement. If the manual steps are clear but you cannot express them, draft pseudocode. If the pseudocode is clear but the source is not, consult the language’s documentation for that particular operation. Different kinds of difficulty need different next steps.
You can ask a classmate, teacher, or an AI tool for a hint, but ask about the unresolved step. Compare any suggested solution with your contract and tests. Before adopting it, explain why it handles an empty list, a broken run, and a longest run at the end. A pasted answer that you cannot trace gives you little basis for adapting it to the next problem.
Three exercises that change what you need to remember
For each exercise, write the input rules, a manual example, the state you need, and at least three tests before coding. The reasoning notes are included so you can compare approaches after trying it yourself.
Exercise 1: return where the longest run begins
Extend the worked problem to return both a start index and a length. Use zero-based indexes, choose the earliest run when lengths tie, and return “no qualifying run” when the length is 0. For the original sample, the answer is start index 5 and length 3. For 70, 80, 30, 90, 95, choose index 0 and length 2.
Reasoning: remember the current run’s start when its length changes from 0 to 1. When its length becomes strictly greater than the best, copy both its length and its start into the best result. Updating on equality instead would select a later tied run. Choose a suitable representation for “no run” in your language; do not let a valid index 0 silently mean “missing.”
Exercise 2: count changes between neighboring values
For 7, 7, 4, 4, 9, return 2: the value changes from 7 to 4 and from 4 to 9. Empty and one-element lists return 0. The list 3, 3, 3 returns 0; 3, 4, 3 returns 2.
Reasoning: compare each entry after the first with its actual predecessor. Keep a previous value or use neighboring indexes, and increment only when they differ. A fabricated previous value of 0 would incorrectly count the first entry as a change in many inputs. There are only n - 1 adjacent pairs in a nonempty list.
Exercise 3: check a sequence of parentheses
Accept only opening and closing parentheses. Decide whether the sequence is balanced: the empty string and (())() are valid; ())( and (() are not. Reject other characters according to this input contract.
Reasoning: increase a balance for an opening parenthesis and decrease it for a closing one. A negative balance means a closing parenthesis arrived without an unmatched opening, so reject immediately. At the end, require a balance of 0. Checking only the final balance would wrongly accept ())(. If you later add different bracket types, a single counter no longer records enough information about their nesting order.
Build a practice habit around explanations
Choose one small problem, finish a simple version, and write a short note explaining why it works. On a later attempt, change one requirement: include the boundary, return a position instead of a length, or accept empty input. This makes you reconsider the reasoning instead of only memorizing the source.
Keep a record of useful mistakes. “Used the final run instead of the best run” identifies a state-model problem. “Used a count larger than the array” identifies a caller contract problem. Those notes are more actionable than deciding that you are simply bad at logic.
Before calling a program finished, ask whether you can explain every stored value, each condition, the progress of the loop, and the returned result. Then check the boundary cases. This habit applies across languages, while each language’s documentation supplies the implementation details.