This post focuses on writing code in Java. If you feel you’re lacking in the basics, we highly recommend reading these posts first: Java Programming Language, Data Structures, Big O Notation
Introduction
In this post, we team up to tackle some basic algorithmic coding problems. The goal? To apply the programming concepts we’ve been learning so far, like loops, conditionals, and data structures—in real-world problem-solving scenarios.
Throughout the session, we bounce ideas back and forth, and explore both brute-force and optimized solutions in Java. Whether you’re preparing for technical interviews or brushing up on your algorithm skills, this step-by-step walkthrough will guide you through the thought process, the pitfalls, and the small wins of solving coding problems.
Ready to see what we’ve built? Let’s jump into the first challenge!
Cut! Just a thing to consider: code with us! Open your IDE and try to solve these problems on your own. We’d not be surprised if you come up with even a better idea!
Challenge 1: Two Sum ∑
This classic problem asks us to find two numbers in an array that add up to a given target. We’ll start with a brute-force approach, then optimize using a HashMap for faster lookups.
Problem Description
Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to the target. You may assume that each input would have exactly one solution, and you may not use the same element twice.
How to break down this problem in the simplest way? For starters, let’s use the simplest possible logic: “brute force”.
Step-by-Step: Brute Force Approach
We’ll use two nested loops to compare every pair of elements:
1. Outer loop: Iterate over each element.
1
2
3
for (int x = 0; x < nums.length - 1; x++) {
    // inner loop goes here
}
2. Inner loop: For each element x, iterate again from x + 1 onward.
1
2
3
for (int y = x + 1; y < nums.length; y++) {
    // compare nums[x] + nums[y]
}
Apart from looping, we need to check if our assumption is already met. Let’s use condition:
3. Condition check: If their sum matches the target, return the indices.
1
2
3
if (nums[x] + nums[y] == target) {
    return new int[]{x, y};
}
Let’s combine it all into a ready-made solution:
Final Brute Force Code
1
2
3
4
5
6
7
8
9
10
public int[] twoSum(int[] nums, int target) {
    for (int x = 0; x < nums.length - 1; x++) {
        for (int y = x + 1; y < nums.length; y++) {
            if (nums[x] + nums[y] == target) {
                return new int[]{x, y};
            }
        }
    }
    return new int[]{};
}
Brute Force Summary
The brute force approach is the most straightforward way to solve the Two Sum problem: simply try all pairs and check if their sum equals the target.
Pros:
- Simple to understand: Ideal for beginners learning nested loops and pair comparisons.
 - No extra memory: Doesn’t require additional data structures like a HashMap.
 - Deterministic: Always finds a solution if one exists (since it checks all possibilities).
 
Cons:
- Inefficient for large arrays: Time complexity is O(n²), which becomes slow as the input size grows.
 - Redundant comparisons: It checks the same pairs more than once (e.g., (i, j) and (j, i)).
 - Not scalable: This approach won’t perform well in interview settings or large datasets.
 
Use this as a learning tool, not a long-term solution. It’s a good baseline to build intuition before moving to optimized approaches like HashMap-based lookups.
The hint above is tempting, isn’t it?😀 Let’s try to do this by using a HashMap!
Step-by-Step: Optimized HashMap Approach
Now let’s improve efficiency using a HashMap for constant-time lookups:
1. Create a HashMap to store required complements and their indices:
1
Map<Integer, Integer> reminders = new HashMap<>();
2. Loop once through the array - we need to check if current number exists in the map.
1
2
3
for (int i = 0; i < nums.length; i++) {
    // check and store logic goes here
}
3. If a match is found, return the stored index and the current index.
1
2
3
if (reminders.containsKey(nums[i])) {
    return new int[]{reminders.get(nums[i]), i};
}
4. Otherwise, store the complement (target - current number) with the index:
1
reminders.put(target - nums[i], i);
Let’s combine it all into a ready-made solution:
Final HashMap Version
1
2
3
4
5
6
7
8
9
10
11
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> reminders = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        if (reminders.containsKey(nums[i])) {
            return new int[]{reminders.get(nums[i]), i};
        }
        reminders.put(target - nums[i], i);
    }
    return new int[]{};
}
Why This Is Better
- Time Complexity: Reduced from O(n²) to O(n)
 - Space Complexity: O(n) extra memory for the map
 - Clean Logic: Easier to read and avoids duplicate pairs
 
Challenge 2: Roman to Integer 🏛️
Let’s try to understand and implement the logic behind Roman numeral parsing, including handling the tricky subtractive combinations like IV, IX, CM, and more. We’ll start with a readable switch-case implementation and later explore alternative methods.
Problem Description
Convert a Roman numeral string into an integer. Some numerals act as subtractors (like IV = 4, CM = 900).
Having many different conditions to check has great potential for using the switch-case statement:
Step-by-Step Solution Using switch-case and Conditionals
We begin by initializing a variable to hold our result:
1
int value = 0;
Then we loop through each character of the input string:
1
2
3
for (int i = 0; i < s.length(); i++) {
    // logic goes here
}
Inside this loop, we will use a switch statement to map each Roman character to its respective integer value:
1
2
3
4
5
6
7
8
9
switch(s.charAt(i)) {
    case 'I': value += 1; break;
    case 'V': value += 5; break;
    case 'X': value += 10; break;
    case 'L': value += 50; break;
    case 'C': value += 100; break;
    case 'D': value += 500; break;
    case 'M': value += 1000; break;
}
Now we need to handle the subtraction rules, e.g., IV (4), IX (9), XL (40), and so on. This can be done using conditionals within each case. Let’s enhance the case for V with a conditional check to handle the case where I comes before it:
1
2
3
4
case 'V':
    value += 5;
    if (i >= 1 && s.charAt(i - 1) == 'I') value -= 2;
    break;
We repeat this for X, L, C, D, and M, subtracting previously added values when necessary. Let’s combine it all into a ready-made solution:
Final Implementation (switch-case)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
    public int romanToInt(String s) {
        int value = 0;
        for (int i = 0; i < s.length(); i++) {
            switch(s.charAt(i)) {
               case 'I' : value += 1; break;
               case 'V' : value += 5; if (i >= 1 && s.charAt(i - 1) == 'I') value -= 2; break;
               case 'X' : value += 10; if (i >= 1 && s.charAt(i - 1) == 'I') value -= 2; break;
               case 'L' : value += 50; if (i >= 1 && s.charAt(i - 1) == 'X') value -= 20; break;
               case 'C' : value += 100; if (i >= 1 && s.charAt(i - 1) == 'X') value -= 20; break;
               case 'D' : value += 500; if (i >= 1 && s.charAt(i - 1) == 'C') value -= 200; break;
               case 'M' : value += 1000; if (i >= 1 && s.charAt(i - 1) == 'C') value -= 200; break;
           }
        }
        return value;
    }
}
But do we have any alternatives? Yes! We can use Java’s built-in method, specifically the contains() method for String objects. Let’s see what such a solution might look like:
Alternate Solution Using contains()
This version first pre-deducts subtractive cases like IV and IX using String.contains(), then proceeds with character iteration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int romanToInt(String s) {
    int value = 0;
    if (s.contains("IV") || s.contains("IX")) value -= 2;
    if (s.contains("XL") || s.contains("XC")) value -= 20;
    if (s.contains("CD") || s.contains("CM")) value -= 200;
    for (char c : s.toCharArray()) {
        if(c == 'I') value += 1;
        if(c == 'V') value += 5;
        if(c == 'X') value += 10;
        if(c == 'L') value += 50;
        if(c == 'C') value += 100;
        if(c == 'D') value += 500;
        if(c == 'M') value += 1000;
    }
    return value;
}
Summary
- The 
switch-caseapproach is detailed and direct, good for clarity. - The 
contains()method offers a clever but less granular way to handle subtraction. - Both solve the problem correctly, so the choice depends on readability vs compactness.
 
Challenge 3: Longest Common Prefix ↪
This challenge is all about comparing strings in an array to find the longest sequence of characters they all share at the beginning. We’ll first implement a character-by-character approach, then refactor using a more concise method involving Java’s built-in string operations.
Problem Description
Given an array of strings, return the longest common prefix among them. If there is none, return an empty string.
Strings, characters within them, well, I guess we need to start slowly, but it looks like we can’t do it without looping over, and checking this or that:
Step-by-Step: Character-by-Character Comparison
First, we need to identify the length of the shortest string in the array to avoid index errors during comparison. Based on our knowledge, we immediately create a snippet which uses variable assignment, a for loop, and an if condition:
1
2
3
4
5
6
7
8
int shortest = strs[0].length();
for (String str : strs) {
    int currentLength = str.length();
    if (currentLength < shortest) {
        shortest = currentLength;
    }
}
We need to consider the case in which any of the strings is empty - then the common prefix must be empty as well:
1
if (shortest == 0) return "";
We then loop through each character index up to the length of the shortest string:
1
2
3
4
5
6
7
StringBuilder lcp = new StringBuilder();
for (int charIndex = 0; charIndex < shortest; charIndex++) {
    boolean isCommon = true;
    char firstWordChar = strs[0].charAt(charIndex);
    // comparison loop goes here
}
Now we can compare this character against all other strings at the same position:
1
2
3
4
5
6
for (String str : strs) {
    if (str.charAt(charIndex) != firstWordChar) {
        isCommon = false;
        break;
    }
}
Finally, if all match, append the character to the result. Otherwise, break early:
1
2
3
4
5
if (isCommon) {
    lcp.append(firstWordChar);
} else {
    break;
}
Let’s combine it all into a ready-made solution:
Final Version (Loop-Based)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public String longestCommonPrefix(String[] strs) {
    int shortest = strs[0].length();
    for (String str: strs) {
        if (str.length() < shortest) {
            shortest = str.length();
        }
    }
    if (shortest == 0) return "";
    StringBuilder lcp = new StringBuilder();
    for (int charIndex = 0; charIndex < shortest; charIndex++) {
        boolean isCommon = true;
        char firstWordChar = strs[0].charAt(charIndex);
        for (String str: strs) {
            if (str.charAt(charIndex) != firstWordChar) {
                isCommon = false;
                break;
            }
        }
        if (isCommon) {
            lcp.append(firstWordChar);
        } else {
            break;
        }
    }
    return lcp.toString();
}
This is a lot! Let’s think about an alternative solution, which, hopefully, will also be shorter, therefore easier to understand:
Optimized Version Using substring
This version starts with the first word as the assumed prefix and trims it until it matches all strings:
1
2
3
4
5
6
7
8
9
10
public String longestCommonPrefix(String[] strs) {
    if (strs.length == 0) return "";
    String prefix = strs[0];
    for (String s : strs) {
        while (s.indexOf(prefix) != 0) {
            prefix = prefix.substring(0, prefix.length() - 1);
        }
    }
    return prefix;
}
Key Insight
- Use the shortest string length to limit comparisons.
 - For larger datasets, substring shrinking can be more efficient than comparing every character.
 
Challenge 4: Remove Duplicates from Sorted Array 👥
This problem teaches you how to manipulate arrays in-place using a two-pointer technique, which is a key concept in array-based algorithm problems. We’ll build up the solution step-by-step and explain the logic behind every move.
Problem Description
Given a sorted integer array nums, remove the duplicates in-place such that each unique element appears only once. The relative order of the elements should be kept the same. Return the number of unique elements.
You must solve this problem with O(1) extra memory.
Phew, that sounds scary! But don’t worry, we have all the tools we need at our fingertips - let’s dive into the implementation!
Step-by-Step: Two-Pointer Approach
We use two pointers: one for the current unique index and one to iterate through the array. Let’s start by initializing the unique pointer:
1
int unique = 1;
We then loop from the second element onwards using a simple for loop:
1
2
3
for (int index = 1; index < nums.length; index++) {
    // comparison logic goes here
}
If the current element is not equal to the previous one, it’s unique — so, we may copy it into the unique position and increment unique:
1
2
3
4
if (nums[index] != nums[index - 1]) {
    nums[unique] = nums[index];
    unique++;
}
Let’s combine it all into a ready-made solution:
Final Version
1
2
3
4
5
6
7
8
9
10
11
public int removeDuplicates(int[] nums) {
    int unique = 1;
    for (int index = 1; index < nums.length; index++) {
        if (nums[index] != nums[index - 1]) {
            nums[unique] = nums[index];
            unique++;
        }
    }
    return unique;
}
Key Takeaways
- The array is sorted, so duplicates are adjacent.
 uniquepoints to the position where the next distinct value should go.- This technique avoids using extra memory and meets the O(1) space requirement.
 
Summary
We need to emphasize it: the purpose of tackling challenges like these isn’t just about getting the right answer. It’s about developing a thought process, exploring multiple strategies, and practicing how to communicate and reason through code—skills that are just as valuable in interviews as they are in real-world development. Speaking of which:
Interview Challenges vs Real-World Development
Interview challenges are not always representative of actual software development work. While platforms with such challenges are great for sharpening your algorithmic thinking, they often focus on abstract problems that are optimized for speed, recursion, or clever tricks—rather than long-term maintainability or practical business logic.
In real-world projects, developers rarely need to reverse a binary tree or calculate Roman numerals from scratch. Instead, they deal with clarity, collaboration, and maintainability—things like:
- Designing readable and testable code.
 - Choosing between built-in methods or writing custom logic.
 - Making trade-offs between performance and understandability.
 - Working with frameworks, databases, APIs, and messy real-world data.
 
Moreover, interviews are mainly about ..filtering candidates. They’re artificial gates, designed not to mimic your job responsibilities but to test your ability to:
- Think under pressure.
 - Communicate your logic.
 - Apply core concepts efficiently.
 
Finally, if you remember one thing from this post, let it be this: how you think and explain your solution is often more valuable than getting it 100% right. That’s why these coding platforms exist—to trace your thought process and simulate the kind of reasoning an interviewer is watching for.
Whether you’re preparing for technical interviews or just sharpening your problem-solving skills, this kind of session is a powerful way to grow as a developer🖥️💡
Key Takeaways
- Focus on thinking, not just answers: How you approach and explain a problem often matters more than whether you solve it perfectly.
 - Interview != Real Work: Coding interviews test algorithmic thinking under pressure, while real-life dev work is more collaborative and context-driven.
 - Know your tools: Built-in Java functions and standard libraries are usually more efficient and thoroughly tested than handwritten alternatives.
 - Data structures matter: Choosing the right data structure (e.g., HashMap for lookup speed) can drastically improve performance.
 - Use what’s proven: Recursion isn’t always better—loops are often faster and easier to debug.
 - Train like it’s a workout: Regular problem-solving sessions are like gym sessions for your brain.
 
“It’s not about making it perfect. It’s about thinking it through and showing your reasoning.” – Krzysztof
Stay tuned!
We hope you enjoyed this deep dive into solving coding challenges. More sessions are on the way, so follow the blog or subscribe (if you have’t done it yet) to get notified when the next post drops!
Until then, happy coding! 💻
