# Steps of Problem Solving in Data Structures and Algorithms

Every solution starts with a strategy, and an algorithm is a strategy for solving a coding problem. So, we must learn to design an efficient algorithm and translate this 'algorithm' into the correct code to get the job done.

But there are many coding problems available in data structures and algorithms, and most of the time, these problems are new to us. So as programmers, we need to develop ourselves as confident problem-solvers who are not intimidated by the difficulty of the given problem.

Our long-term goal should be simple: Learn to design correct and efficient code within a given time. As we practice more and more, we will gain experience in problem-solving, and our work will become easier. Here are some essential skills that we should practice for every DSA problem:

• Developing an approach to understanding the problem
• Thinking of a correct basic solution
• Designing step-by-step pseudocode solutions
• Analyzing the efficiency of a solution
• Optimizing the solution further
• Transforming pseudocode into correct code

Now, the critical question would be: Is there a well-defined, guided strategy to approach and solve a coding problem? If yes, then what are the critical steps? Let's think and explore!

### Step 1: Understanding the problem

Solving a problem requires a clear understanding of the problem. Unfortunately, sometimes we read only the first few lines and assume the rest of the problem or ignore this step because we have seen something similar in the past. We should view these as unfair practices and develop a clear approach to understanding problems.

During problem-solving, every small detail can help us design an efficient solution. Sometimes, a small change in the question can alter the solution approach. Taking extra time to understand the problem will give us more confidence later on. The fact is, we never want to realize halfway through that we misunderstood the problem.

It doesn't matter if we have encountered a question before or not; we should read the question several times. So, take a paper and write down everything while going through the problem. Exploring some examples will also help us clarify how many cases our algorithm can handle and the possible input-output patterns. We should also explore scenarios for large input, edge cases, and invalid input.

Sometimes, it is common for problem descriptions to suffer from these types of deficiencies:

• The problem description may rely on undefined assumptions
• The problem description may be ambiguous or incomplete
• The problem description may have various contradictions.

These deficiencies may be due to the abstract explanation of the problem description in our natural languages. So, it is our responsibility to identify such deficiencies and work with the interviewer or problem provider to clarify them. We should start by seeking answers to the following questions:

• What are the inputs and outputs?
• What type of data is available?
• What is the size or scale of the input?
• How is the data stored? What is the data structure?
• Are there any special conditions or orders in the data?
• What rules exist for working with the data?

### Step 2: Thinking of a correct basic solution

The best approach would be to think of a correct solution that comes immediately to our mind. It does not matter even if it is an inefficient approach. Having a correct and inefficient answer is much better than an incorrect solution or significant delay in finding the solution. This could help us in so many ways:

• Help us to build good confidence or motivation at the start.
• Provide an excellent point to start a conversation with the interviewer.
• Sometimes, it provides a hint to improve efficiency by reducing some loops, removing some intermediate steps, or performing some operations efficiently.

Here are some examples of brute force patterns: three nested loops, two nested loops, solution using extra memory, solution using sorting, double traversal in the binary tree, considering all sub-arrays or substrings, exhaustive search, etc.

After thinking and communicating the brute force idea, the interviewer may ask for its time and space complexity. We need to work on paper, analyze each critical operation, and write it in the form of Big-O notation. Clear conceptual idea of time and space complexity analysis is essential at this stage.

### Step 3: Designing efficient solution with pseudocode

This is a stage to use the best experience of DSA problem-solving and apply various problem-solving strategies. One practical truth is: moving from a basic algorithm to the most efficient algorithm is a little difficult in a single step. Each time, we need to optimize the previous algorithm and stop when there is no further optimization possible. Revisiting the problem description and looking for some additional information can help a lot in further optimization. For example:

• If the input array is sorted or nearly sorted, we can apply optimized algorithms such as a single loop, two-pointer approach, or binary search.
• If we need to find a subarray of size k, we can use the sliding window technique, which involves maintaining a window of size k over the array and sliding it over the elements to find the desired subarray.
• When searching is a critical operation, we can use optimized search algorithms or data structures like binary search, BST, or hash table.
• For optimization problems, we can consider divide and conquer, dynamic programming, or greedy algorithm approaches.
• If we need to find a solution with a given constraint, we can use backtracking.
• When working with string data, direct address tables, hash tables, or trie data structures can be useful.
• To frequently access and process max or min elements, we can use a priority queue or heap data structure.
• For dictionary operations such as insert, search, and delete, we can use hash tables or BST.
• If we need to perform both dictionary and priority queue operations, a BST may be useful.
• For range query operations such as range max, range min, or range sum, we can use data structures like segment trees or Fenwick trees.
• To process binary tree data level by level, BFS or level-order traversal can be used.

The idea would be simple: we should learn the use case of efficient problem-solving patterns on various data structures. Continuously thinking, analyzing, and looking for a better solution is the core idea.

Here are some best examples of problems where several levels of optimisations are feasible. Practicing such types of coding questions helps a lot in building confidence.

Find equilibrium index of an array

• Using nested loops: Time = O(n²), Memory = O(1)
• Using prefix sum array: Time = O(n), Memory = O(n)
• Using single scan: Time = O(n), Memory = O(1)

Trapping rain water

• Using nested loops: Time = O(n²), Memory = O(1)
• Using Dynamic Programming: Time = O(n), Memory = O(n)
• Using Stack: Time = O(n), Memory = O(n)
• Using two pointers: Time = O(n), Memory = O(1)

Check for pair with a given sum

• Using nested loops: Time = O(n²), Memory = O(1)
• Using sorting and binary search: Time = O(nlogn), Memory = O(1)
• Using sorting and Two Pointers: Time = O(nlogn), Memory = O(1)
• Using a Hash Table: Time = O(n), Memory = O(n)

Find the majority element in an array

• Using two nested loops: Time = O(n²), Memory = O(1)
• Using Sorting: Time = O(nlogn), Memory = O(1)
• Using the divide and conquer: Time = O(nlogn), Memory = O(logn)
• Using a Hash Table: Time = O(n), Memory = O(n)
• Using Bit Manipulation: Time = O(n), Memory = O(1)
• Using Randomisation: Time = O(nlogn), Memory = O(1)
Note: If value of n is very large.
• Boyer-Moore Voting Algorithm: Time = O(n), Memory = O(1)

Maximum Subarray Sum

• Using three nested loops: Time = O(n^3), Memory = O(1)
• Using two nested loops: Time = O(n^2), Memory = O(1)
• Using divide and conquer: Time = O(nlogn), Memory = O(logn)
• Using dynamic programming: Time = O(n), Memory = O(n)
• Kadane algorithm: Time = O(n), Memory = O(1)

Before you jump into the end-to-end code implementation, it’s good practice to write pseudocode on paper. It would be helpful in defining code structure and critical operations. Some programmers skip this step, but writing the final code becomes easier when we have well-designed pseudocode.

### Step 4: Transforming pseudocode into a clean, correct, and optimized code

Finally, we need to replace each line of pseudocode with actual code in our favorite programming languages like C++, Java, Python, C#, JavaScript, etc. Never forget to test actual code with sample test data and check if the actual output is equal to the expected output. When writing code in your interviews, discuss sample data or test cases with the interviewer.

Simplifying and optimizing the code may require a few iterations of observation. We need to ask these questions once we are done writing the code:

• Does this code run for every possible input, including the edge cases?
• Can we optimize the code further? Can we remove some variables or loop or some extra space?
• Are we repeating some steps a lot? Can we define it separately using another function?
• Is the code readable or written with a good coding style?

Enjoy learning, Enjoy coding, Enjoy algorithms!