Knowing how to solve a problem is only half the skill. The other half is knowing how to solve it well.
In Data Structures and Algorithms, two candidates can arrive at the same correct answer and still perform very differently in an interview. One writes clean, readable, well-reasoned code. The other writes something that works but is hard to follow, misses edge cases, or uses unnecessary memory. The difference between them is not intelligence or talent — it is habits.
This tutorial is about building the right habits early, before bad ones get reinforced through repetition.
1. Concept
What are best and bad practices in DSA?
Best practices are the habits, patterns, and code qualities that make your solutions correct, efficient, readable, and easy to explain. Bad practices are the habits that undermine one or more of those qualities, even when the output looks correct.
The distinction matters because:
- A solution that is correct but unreadable is hard to debug and hard to explain in an interview
- A solution that is readable but inefficient may time out on large inputs
- A solution that handles common cases but ignores edge cases will fail in production and in tests
- A solution built on bad naming or logic shortcuts will confuse the next person who reads it — including future you
In DSA specifically, best practices cover:
- How you read and understand a problem before coding
- How you name variables and structure your code
- How you handle edge cases
- How you analyze and communicate complexity
- How you test and verify your solution
None of these are optional in a real interview or a real codebase. They are exactly what separates a passing solution from a strong one.
2. Real-world Intuition
Think of writing DSA solutions like writing a recipe.
A bad recipe says: "cook the thing until done." It might produce edible food if the cook already knows what they are doing. But if someone else tries to follow it, or if you return to it six months later having forgotten the context, it fails completely.
A good recipe says: "sauté the onions over medium heat for 8 minutes until translucent and soft, stirring occasionally." It is precise, self-explanatory, and reproducible by anyone.
Your DSA code is a recipe. When you write x, temp2, or flag as variable names, you are writing "cook the thing until done." When you write leftPointer, maxLengthSoFar, or hasVisited, you are writing a recipe someone else can follow.
The code you write during study and practice is where your habits form. If you practice writing clear code on easy problems, you will naturally write clear code on hard ones. If you practice writing quick-and-dirty code to get problems done, that habit will follow you into interviews.
3. Syntax / Implementation
This section gives you a side-by-side reference of best practices and their bad counterparts across the most common habits in DSA.
Habit 1: Naming variables clearly
// Bad
function fn(a, t) {
let x = new Map();
for (let i = 0; i < a.length; i++) {
let y = t - a[i];
if (x.has(y)) return [x.get(y), i];
x.set(a[i], i);
}
}
// Best
function twoSum(nums, target) {
const seen = new Map(); // maps value → index
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (seen.has(complement)) {
return [seen.get(complement), i];
}
seen.set(nums[i], i);
}
return [];
}The logic is identical. The difference is that the best practice version can be read and explained without mental overhead. In an interview, you want to spend cognitive energy on the algorithm, not on remembering what y means.
Habit 2: Handling edge cases explicitly
// Bad — crashes or returns wrong answer on empty input
function findMax(nums) {
let max = nums[0]; // will crash if nums is empty
for (let i = 1; i < nums.length; i++) {
if (nums[i] > max) max = nums[i];
}
return max;
}
// Best — handles edge cases before main logic
function findMax(nums) {
if (nums.length === 0) return null;
let max = nums[0];
for (let i = 1; i < nums.length; i++) {
if (nums[i] > max) max = nums[i];
}
return max;
}Edge case handling is not about being defensive. It is about understanding your problem fully. If you have not thought about empty input, you have not finished reading the problem.
Habit 3: Stating complexity before submitting
// Bad — solution submitted with no complexity awareness
function hasDuplicate(nums) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] === nums[j]) return true;
}
}
return false;
}
// O(n^2) time — did the solver know this? Unclear.
// Best — complexity understood and stated, optimized
/**
* Contains Duplicate
* Time: O(n) | Space: O(n)
*/
function hasDuplicate(nums) {
const seen = new Set();
for (const num of nums) {
if (seen.has(num)) return true;
seen.add(num);
}
return false;
}The bad version works. But if n is one million, it runs one trillion operations. The best version runs one million. That difference is not theoretical — it determines whether your solution passes.
Habit 4: Using strict equality in JavaScript
// Bad
if (a == b) { ... } // loose equality, type coercion surprises
// Best
if (a === b) { ... } // strict equality, no surprisesIn JavaScript, 0 == false is true. "" == false is true. null == undefined is true. These are not edge cases you can ignore — they are traps that produce bugs that are very hard to trace. Always use === in DSA code unless you have a deliberate reason not to.
Habit 5: Avoiding unnecessary data structures
// Bad — builds an entire reversed array just to compare
function isPalindrome(nums) {
const reversed = [...nums].reverse();
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== reversed[i]) return false;
}
return true;
}
// O(n) extra space unnecessarily
// Best — two pointers, O(1) extra space
function isPalindrome(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
if (nums[left] !== nums[right]) return false;
left++;
right--;
}
return true;
}Every data structure you create costs memory. Before creating one, ask: do I actually need this, or am I creating it because it is the first approach that came to mind?
Habit 6: Writing helper functions for complex logic
// Bad — everything jammed into one function
function isValidSudoku(board) {
for (let r = 0; r < 9; r++) {
let seen = new Set();
for (let c = 0; c < 9; c++) {
if (board[r][c] === '.') continue;
if (seen.has(board[r][c])) return false;
seen.add(board[r][c]);
}
}
// ... then another 40 lines for columns and boxes inline
}
// Best — logic separated into a helper
function isValidSudoku(board) {
for (let r = 0; r < 9; r++) {
if (!hasUniqueValues(board[r])) return false;
}
for (let c = 0; c < 9; c++) {
if (!hasUniqueValues(board.map(row => row[c]))) return false;
}
// check 3x3 boxes
for (let br = 0; br < 3; br++) {
for (let bc = 0; bc < 3; bc++) {
const box = [];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
box.push(board[br * 3 + r][bc * 3 + c]);
}
}
if (!hasUniqueValues(box)) return false;
}
}
return true;
}
function hasUniqueValues(cells) {
const seen = new Set();
for (const cell of cells) {
if (cell === '.') continue;
if (seen.has(cell)) return false;
seen.add(cell);
}
return true;
}Complex logic broken into named helpers reads like documentation. Each function does one thing and has a name that describes what it does.
4. Dry Run
Let us take one problem and walk through what applying best practices looks like from start to finish.
Problem: Given an integer array, move all zeros to the end while maintaining the relative order of non-zero elements. Do it in place.
Input: [0, 1, 0, 3, 12]
Output: [1, 3, 12, 0, 0]Step 1: Understand before coding
- Input: array with zeros and non-zeros
- Output: same array modified in place, zeros at end, order of non-zeros preserved
- Constraint: in place means no new array
Step 2: Identify the pattern
We need to "collect" non-zeros at the front and fill the rest with zeros. This is a two-pointer / write-pointer pattern.
Step 3: Plan with complexity
- Use a
writeIndexpointer that tracks where the next non-zero should go - Iterate once, writing non-zeros forward
- Fill remaining positions with zero
- Time: O(n), Space: O(1)
Step 4: Write clean code
/**
* Move Zeroes
* Approach: Write pointer — place non-zeros at front, fill rest with 0
* Time: O(n) | Space: O(1)
*/
function moveZeroes(nums) {
if (nums.length === 0) return;
let writeIndex = 0; // next position to place a non-zero element
// Pass 1: move all non-zeros to the front
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) {
nums[writeIndex] = nums[i];
writeIndex++;
}
}
// Pass 2: fill remaining positions with zero
for (let i = writeIndex; i < nums.length; i++) {
nums[i] = 0;
}
}
// Test
const arr = [0, 1, 0, 3, 12];
moveZeroes(arr);
console.log(arr); // [1, 3, 12, 0, 0]
const allZeros = [0, 0, 0];
moveZeroes(allZeros);
console.log(allZeros); // [0, 0, 0]
const noZeros = [1, 2, 3];
moveZeroes(noZeros);
console.log(noZeros); // [1, 2, 3]Dry run on [0, 1, 0, 3, 12]:
writeIndex = 0
i=0: nums[0] = 0, skip
i=1: nums[1] = 1, nums[0] = 1, writeIndex = 1 → [1, 1, 0, 3, 12]
i=2: nums[2] = 0, skip
i=3: nums[3] = 3, nums[1] = 3, writeIndex = 2 → [1, 3, 0, 3, 12]
i=4: nums[4] = 12, nums[2] = 12, writeIndex = 3 → [1, 3, 12, 3, 12]
Pass 2: fill from writeIndex=3 to end with 0
nums[3] = 0, nums[4] = 0
Final: [1, 3, 12, 0, 0]Each step has a meaningful variable name, each pass has a comment, edge cases are tested explicitly, and complexity is stated upfront. This is what a best-practice solution looks like.
5. Best Practice Example
Problem: Find if a linked list has a cycle.
/**
* Linked List Cycle Detection
* Approach: Floyd's slow and fast pointer algorithm
* Time: O(n) | Space: O(1)
*
* Intuition: if there is a cycle, the fast pointer will eventually
* lap the slow pointer and they will meet inside the cycle.
* If there is no cycle, the fast pointer reaches null.
*/
function hasCycle(head) {
if (head === null || head.next === null) return false;
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}Why this is strong:
- The function name and variable names (
slow,fast) make the algorithm self-documenting - Edge cases (null head, single node) are handled at the top
- The comment explains the intuition, not just the mechanics
fast !== null && fast.next !== nullis checked before accessingfast.next.next— no null pointer errors- Complexity is stated and it is optimal for this problem
If an interviewer asks "why do you check fast.next !== null?", you can answer immediately because you thought about it rather than copying the pattern blindly.
6. Bad Practice Example
Problem: Same linked list cycle problem, approached without discipline.
function cycle(h) {
let arr = [];
let cur = h;
while (cur != null) {
if (arr.includes(cur)) return true;
arr.push(cur);
cur = cur.next;
}
return false;
}What is wrong with this:
-
O(n) space unnecessarily: An array stores every visited node. The optimal solution uses O(1) space. This is a well-known problem with a well-known optimal solution — using the naive approach in an interview signals lack of preparation.
-
arr.includes(cur)is O(n): This makes the overall time complexity O(n^2), not O(n). A Set lookup would be O(1), but the whole approach should be avoided anyway. -
!= nullinstead of!== null: Loose equality in a case that should be strict. -
No edge case handling: What if
his null? The while loop condition handles it implicitly, but an explicit check at the top makes intent clear. -
Variable names
h,arr,cur: Two of three are understandable in context, buthgives nothing. In a longer function this becomes a real problem. -
No complexity comment: The solver almost certainly did not analyze the complexity. If they had, they would have noticed the O(n^2) from
includes.
The function produces the correct answer on valid inputs. But it demonstrates none of the qualities that distinguish a strong DSA practitioner.
7. Time Complexity
Understanding best practices for time complexity means more than knowing Big O notation. It means actively applying that knowledge as a filter at every stage of problem-solving.
Best practice: estimate before you code
Before writing a single line, estimate the complexity of your planned approach. If it is O(n^2) and n can be 100,000, your plan is wrong. Fix it before coding, not after.
Best practice: recognize complexity from structure
Nested loops over the same input → O(n^2) — often avoidable
Sorting before processing → O(n log n) minimum
Hash map lookup or insert → O(1) average
Binary search on sorted input → O(log n)
Recursive with memoization → usually O(n) or O(n^2) depending on statesBad practice: optimizing the wrong thing
Some beginners spend time micro-optimizing minor details (like choosing for over forEach) while leaving an O(n^2) nested loop untouched. The nested loop dominates. Fix the algorithmic complexity first.
Bad practice: ignoring built-in method complexity
// This looks like O(n), but is actually O(n^2)
for (let i = 0; i < arr.length; i++) {
arr.splice(i, 1); // splice is O(n) — shifts all elements
}
// arr.includes(), arr.indexOf(), arr.splice() are all O(n)
// Calling them inside a loop makes the overall complexity O(n^2)Knowing the complexity of the built-in methods you use is part of knowing the complexity of your solution.
8. Space Complexity
Best practice: prefer in-place solutions when constraints allow
Many problems can be solved either by creating new data structures or by modifying the input. When the problem says "in place" or you need O(1) space, two pointers and write-pointer patterns are your main tools.
Best practice: count recursion stack space
// This function uses O(n) stack space for an array of length n
function sumRecursive(nums, i = 0) {
if (i === nums.length) return 0;
return nums[i] + sumRecursive(nums, i + 1);
}
// The iterative version uses O(1) space
function sumIterative(nums) {
let total = 0;
for (const num of nums) total += num;
return total;
}Recursion depth is space. A recursive DFS on a tree with height h uses O(h) stack space. For a balanced tree that is O(log n). For a skewed tree it is O(n).
Bad practice: creating data structures "just in case"
// Bad — results array built up but only last element needed
function lastElement(nums) {
const results = [];
for (const num of nums) {
results.push(num * 2);
}
return results[results.length - 1];
}
// Best — O(1) space
function lastElement(nums) {
if (nums.length === 0) return null;
return nums[nums.length - 1] * 2;
}Only create data structures you actually need.
9. Common Mistakes
Mistake 1: Using bad variable names during "quick" practice
Beginners often tell themselves they will clean up the code later, or that naming does not matter during practice. But habits are formed through repetition, not intention. Every time you write x instead of maxFrequency, you are practicing the wrong habit.
How to avoid it: Treat every practice problem as if it is a real interview. There is no difference between "practice mode" and "performance mode" when it comes to habits.
Mistake 2: Submitting without testing edge cases
Getting the happy path to work and immediately submitting is one of the most common failure points. The first failing test case is almost always an edge case: empty input, single element, all same values, maximum constraint values.
How to avoid it: Before submitting, manually test: empty input, single element, all same elements, and the largest possible input if you can estimate it.
Mistake 3: Ignoring the "in place" or "without extra space" constraint
When a problem says "in place", it is telling you which approach to take. Many beginners read "in place" and still create a new array because they are more comfortable with that approach.
How to avoid it: Read constraints carefully. Constraints are not warnings — they are directions.
Mistake 4: Mixing problem-solving with typing speed
Some beginners type as fast as they think, treating code as a scratchpad. This produces code full of dead variables, commented-out abandoned attempts, and logic that changed halfway through. It is hard to debug and hard to explain.
How to avoid it: Write a plain-English plan before writing code. Code should be the translation of a plan, not the process of forming one.
Mistake 5: Not refactoring after a solution works
Getting a solution to pass and immediately moving to the next problem skips the most valuable step: reflection and cleanup. The working-but-messy version is a draft. The clean version is the lesson.
How to avoid it: After every passing solution, spend five minutes refactoring. Improve variable names, add a complexity comment, remove dead code, and verify edge cases. This is where the learning solidifies.
10. Practice Problems
These problems are chosen to reward the habits described in this tutorial. Pay attention not just to whether you solve them, but how clean your solution is.
-
Easy: "Reverse String" — practice in-place two-pointer solution with clear variable names. Pattern: two pointers.
-
Easy: "Running Sum of 1D Array" — practice prefix sum with proper variable naming and edge case handling. Pattern: prefix sum.
-
Medium: "Longest Consecutive Sequence" — a problem where the naive O(n^2) solution is obvious and the O(n) solution requires recognizing the hash set pattern. Pattern: hash set.
-
Medium: "Find All Anagrams in a String" — requires sliding window with a character frequency map. Practice writing the helper logic cleanly. Pattern: sliding window with hash map.
-
Hard: "Minimum Window Substring" — a problem that tests whether you can maintain a clean implementation of a complex sliding window with two pointers and a frequency map without losing readability. Pattern: sliding window.
11. Summary
Best practices in DSA are not cosmetic. They are functional. Clean variable names reduce bugs. Edge case handling prevents failures. Complexity analysis determines whether a solution is actually correct for the given constraints.
What to remember:
- Name variables after what they represent, not what they are
- Handle edge cases before the main logic, not as an afterthought
- State time and space complexity as a comment on every solution
- Prefer in-place solutions when the problem allows it
- Test empty input, single element, and boundary values before submitting
- Refactor every passing solution before moving to the next problem
When to apply these practices: Always. Not just on hard problems, not just in interviews. The habits you build on easy problems are the habits you bring to hard ones.
Why complexity matters: A correct solution with the wrong complexity is an incorrect solution at scale. Complexity analysis is what connects "works on examples" to "works in production."