The Science of Engineering
Why DSA is Non-Negotiable
So far, we have focused on making our code work. Now, we learn to make it work well.
Writing code is a series of trade-offs, primarily between:
Choosing the right Data Structure and the right Algorithm is how a senior engineer makes these trade-offs intelligently.
How do we talk about the "speed" of an algorithm? We can't use seconds, because that depends on the computer.
Big O Notation is a way to describe how the runtime or space requirement of an algorithm grows as the input size (n) grows.
It describes the worst-case scenario.
Analyzing Algorithmic Efficiency
The algorithm takes the same amount of time regardless of the input size.
Analogy: Picking the first apple from a barrel. It doesn't matter if there are 10 apples or 10,000.
function getFirstElement(arr: any[]): any {
return arr[0]; // Always one operation
}
The runtime grows linearly with the size of the input (n).
Analogy: Reading every page in a book. A 200-page book takes twice as long as a 100-page book.
function findValue(arr: any[], value: any): boolean {
for (let i = 0; i < arr.length; i++) { // Loop runs n times
if (arr[i] === value) {
return true;
}
}
return false;
}
The runtime grows quadratically with the input size. This is common with nested loops.
Analogy: Shaking hands with everyone in a room. If you have `n` people, each person shakes `n-1` hands.
This type of algorithm gets very slow, very quickly and should be avoided if possible for large inputs.
function hasDuplicates(arr: any[]): boolean {
for (let i = 0; i < arr.length; i++) { // Outer loop runs n times
for (let j = 0; j < arr.length; j++) { // Inner loop runs n times
if (i !== j && arr[i] === arr[j]) {
return true;
}
}
}
return false;
}
The runtime grows logarithmically. The algorithm's time increases, but by a diminishing amount as the input size grows. This is incredibly efficient.
These algorithms work by repeatedly dividing the problem in half.
Analogy: Finding a word in a physical dictionary. You open to the middle, decide which half the word is in, and discard the other half. You repeat this until you find the word.
The classic example is **Binary Search**.
Arrays and Linked Lists
Arrays store elements in a contiguous block of memory. This has profound performance implications.
A Linked List stores elements as separate objects called nodes. Each node contains its data and a pointer (or reference) to the next node in the sequence.
Analogy: A scavenger hunt. Each clue tells you the data and where to find the next clue.
class ListNode {
public value: T;
public next: ListNode | null;
constructor(value: T) {
this.value = value;
this.next = null; // Initially, it doesn't point to anything
}
}
// Creating a simple list: A -> B
const nodeA = new ListNode('A');
const nodeB = new ListNode('B');
nodeA.next = nodeB;
Linked Lists have the opposite trade-offs of arrays.
Stacks and Queues
Last-In, First-Out. The last element added is the first one to be removed.
Analogy: A stack of plates. You add a plate to the top, and you remove a plate from the top.
Core Operations:
The JavaScript call stack is a perfect real-world example!
An array is a perfect underlying data structure for a stack, because its `push` and `pop` methods are both fast O(1) operations.
class Stack {
private storage: T[] = [];
push(item: T): void {
this.storage.push(item);
}
pop(): T | undefined {
return this.storage.pop();
}
peek(): T | undefined {
return this.storage[this.storage.length - 1];
}
}
First-In, First-Out. The first element added is the first one to be removed.
Analogy: A line at a checkout counter. The first person in line is the first person to be served.
Core Operations:
The Node.js Event Loop's Callback Queue is a perfect real-world example!
You can use an array, but there's a performance catch.
class Queue {
private storage: T[] = [];
enqueue(item: T): void {
this.storage.push(item); // Fast O(1)
}
dequeue(): T | undefined {
return this.storage.shift(); // 🚨 SLOW! O(n)
}
}
Using `shift()` is inefficient for a queue with many items. A Linked List is actually a much better underlying data structure for implementing a high-performance queue.
This is a classic computer science problem that perfectly demonstrates the power of a Stack. Your task is to write a function that takes a string containing parentheses `()`, brackets `[]`, and braces `{}`, and determines if they are balanced and correctly nested.
Test Cases: `isValid("()")` -> true, `isValid("()[]{}")` -> true, `isValid("(]")` -> false, `isValid("([)]")` -> false, `isValid("{[]}")` -> true.