Lesson 01-Introduction to Data Structures

Basic Concepts of Data Structures

What is a Data Structure

A data structure is a core concept in software engineering, referring to the organization, management, and storage format of data. A proper choice of data structure can significantly improve algorithm efficiency and optimize program performance. Essentially, a data structure is an abstract description of the logical relationships and physical storage methods of data elements in a software system.

In computer science, data structures primarily address two fundamental problems:

  1. How to efficiently store data
  2. How to quickly access and manipulate this data

The choice of data structure directly impacts program performance. For instance, selecting an inappropriate data structure when handling large datasets may lead to slow program execution or even failure to complete. Modern programming languages like JavaScript provide various built-in data structures, but understanding their underlying implementation principles is crucial for writing efficient code.

Data structures can be divided into two categories: static and dynamic:

  • Static Data Structures: Fixed in size, such as arrays
  • Dynamic Data Structures: Variable in size, such as linked lists, trees, etc.

The Relationship Between Data Structures and Algorithms

Data structures and algorithms are two fundamental pillars of computer science, and their relationship is inseparable. In simple terms, algorithms are the steps and methods to solve problems, while data structures are the objects manipulated by algorithms. Without an appropriate data structure, even the most efficient algorithm cannot perform effectively.

Algorithm complexity analysis is typically based on specific data structures. For example, the quicksort algorithm performs well on arrays but is less efficient on linked lists. Similarly, binary search tree algorithms rely on the characteristics of tree structures.

The relationship between data structures and algorithms can be summarized as follows:

  1. Data structures provide the operational foundation for algorithms
  2. Algorithms determine how to efficiently utilize data structures
  3. Together, they determine the performance of a program

In practical development, choosing the right data structure is key to optimizing algorithm performance. For example:

  • For frequent lookups, a hash table is more efficient than a linear list
  • For ordered storage, a balanced binary tree is more suitable than an unordered array
  • For frequent insertions and deletions, a linked list is more advantageous than an array

Differences Between Linear and Nonlinear Structures

Linear and nonlinear structures are two basic classifications of data structures, distinguished primarily by the relationships between data elements.

Characteristics of Linear Structures:

  1. Data elements have a one-to-one linear relationship
  2. Except for the first and last elements, each element has a unique predecessor and successor
  3. Simple logical structure, easy to implement and understand

Common linear structures include:

  • Array
  • Linked List
  • Stack
  • Queue
  • String

Characteristics of Nonlinear Structures:

  1. Data elements have one-to-many or many-to-many relationships
  2. No strict sequential relationship between elements
  3. Complex structure, more challenging to implement

Common nonlinear structures include:

  • Tree
  • Graph
  • Heap
  • Hash Table

Linear structure example (JavaScript implementation):

// Linear structure - Array implementation
class LinearArray {
  constructor() {
    this.data = [];
  }
  
  // Add element
  add(element) {
    this.data.push(element);
  }
  
  // Get element
  get(index) {
    return this.data[index];
  }
  
  // Remove element
  remove(index) {
    return this.data.splice(index, 1)[0];
  }
  
  // Size
  size() {
    return this.data.length;
  }
}

// Usage example
const arr = new LinearArray();
arr.add(10);
arr.add(20);
console.log(arr.get(1)); // Output: 20
console.log(arr.size()); // Output: 2

Nonlinear structure example (JavaScript implementation):

// Nonlinear structure - Binary tree node
class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

// Binary tree implementation
class BinaryTree {
  constructor() {
    this.root = null;
  }
  
  // Insert node
  insert(value) {
    const newNode = new TreeNode(value);
    if (!this.root) {
      this.root = newNode;
      return;
    }
    
    let current = this.root;
    while (true) {
      if (value < current.value) {
        if (!current.left) {
          current.left = newNode;
          return;
        }
        current = current.left;
      } else {
        if (!current.right) {
          current.right = newNode;
          return;
        }
        current = current.right;
      }
    }
  }
  
  // In-order traversal
  inOrderTraversal(node = this.root, result = []) {
    if (node) {
      this.inOrderTraversal(node.left, result);
      result.push(node.value);
      this.inOrderTraversal(node.right, result);
    }
    return result;
  }
}

// Usage example
const tree = new BinaryTree();
tree.insert(10);
tree.insert(5);
tree.insert(15);
console.log(tree.inOrderTraversal()); // Output: [5, 10, 15]

Comparison of linear and nonlinear structures:

CharacteristicLinear StructureNonlinear Structure
Element RelationshipOne-to-oneOne-to-many/Many-to-many
Storage MethodContinuous/LinkedPossibly non-continuous
Operation ComplexityGenerally lowerGenerally higher
Application ScenariosSimple data storageComplex relationship representation
Implementation DifficultySimplerMore complex
Memory UsageTypically more efficientPotentially higher

Understanding the differences between linear and nonlinear structures is crucial for selecting the appropriate data structure. Linear structures are simple and intuitive, suitable for handling ordered data; nonlinear structures have strong expressive power, suitable for representing complex relationships. In practical development, the most suitable data structure should be chosen based on specific requirements.

Time and Space Complexity of Data Structures

Big O Notation

Big O notation is a standard symbol in computer science used to describe the efficiency of algorithms, representing the worst-case time or space requirements as the input size grows. Big O notation focuses on the dominant term as the input size n approaches infinity, ignoring constant factors and lower-order terms.

Characteristics of Big O notation:

  1. Focuses on growth trends rather than specific times
  2. Ignores constant factors and lower-order terms
  3. Describes the worst-case time complexity
  4. Represents the upper bound of algorithm efficiency

Common Big O notation classifications:

SymbolNameDescription
O(1)Constant TimeOperation time does not vary with input size
O(log n)Logarithmic TimeEach operation halves the problem size
O(n)Linear TimeOperation time is proportional to input size
O(n log n)Linear-Logarithmic TimeCommon in efficient sorting algorithms
O(n²)Quadratic TimeTypical complexity of double loops
O(2ⁿ)Exponential TimeComplexity for solving subset problems
O(n!)Factorial TimeComplexity for solving permutation problems

The significance of Big O notation lies in providing a standardized way to compare the efficiency of different algorithms. For example, an O(n) algorithm is always superior to an O(n²) algorithm when n is sufficiently large.

Common examples of Big O notation:

// O(1) - Constant time
function getFirstElement(arr) {
  return arr[0]; // Operation time is the same regardless of array size
}

// O(n) - Linear time
function findElement(arr, target) {
  for (let i = 0; i < arr.length; i++) { // Loop iterations proportional to array size
    if (arr[i] === target) return i;
  }
  return -1;
}

// O(n²) - Quadratic time
function findPairs(arr, target) {
  const pairs = [];
  for (let i = 0; i < arr.length; i++) { // Outer loop runs n times
    for (let j = i + 1; j < arr.length; j++) { // Inner loop runs n/2 times on average
      if (arr[i] + arr[j] === target) {
        pairs.push([arr[i], arr[j]]);
      }
    }
  }
  return pairs;
}

Simplification rules for Big O notation:

  1. Ignore constant factors: O(2n) → O(n)
  2. Ignore lower-order terms: O(n² + n) → O(n²)
  3. Focus on the highest-order term: O(n³ + n² + n) → O(n³)

In practical applications, we typically focus on the worst-case time complexity of algorithms, but some scenarios may require consideration of average-case or best-case time complexity.

Common Complexity Analysis

O(1) – Constant Time Complexity

O(1) indicates that the algorithm’s execution time does not increase with the input size. This is the most efficient time complexity, meaning the operation time remains constant regardless of data size.

Common examples:

  • Array access: arr[0]
  • Hash table lookup (ideal case)
  • Stack/queue basic operations

JavaScript example:

// O(1) - Array access
function getFirstElement(arr) {
  return arr[0]; // Operation time is the same regardless of array size
}

// O(1) - Hash table lookup (ideal case)
const map = new Map();
map.set('key', 'value');
function getValue(key) {
  return map.get(key); // Average O(1) time complexity
}

O(log n) – Logarithmic Time Complexity

O(log n) indicates that the algorithm’s execution time grows logarithmically with the input size. Such algorithms typically improve efficiency by halving the problem size with each operation.

Common examples:

  • Binary search
  • Balanced binary tree operations (search, insert, delete)
  • Certain divide-and-conquer algorithms

JavaScript example:

// O(log n) - Binary search
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

O(n) – Linear Time Complexity

O(n) indicates that the algorithm’s execution time is proportional to the input size. Such algorithms typically require traversing the entire dataset once.

Common examples:

  • Array/linked list traversal
  • Simple search
  • Linear search

JavaScript example:

// O(n) - Array traversal
function sumArray(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) { // Loops n times
    sum += arr[i];
  }
  return sum;
}

// O(n) - Linear search
function findElement(arr, target) {
  for (let i = 0; i < arr.length; i++) { // Worst case requires traversing entire array
    if (arr[i] === target) return i;
  }
  return -1;
}

O(n log n) – Linear-Logarithmic Time Complexity

O(n log n) indicates that the algorithm’s execution time is proportional to the input size multiplied by the logarithm of the input size. Such algorithms often combine divide-and-conquer strategies with linear operations.

Common examples:

  • Efficient sorting algorithms (merge sort, quicksort)
  • Heap sort
  • Certain graph algorithms

JavaScript example:

// O(n log n) - Merge sort
function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  
  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  
  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }
  
  return result.concat(left.slice(i)).concat(right.slice(j));
}

O(n²) – Quadratic Time Complexity

O(n²) indicates that the algorithm’s execution time is proportional to the square of the input size. Such algorithms typically involve nested loops.

Common examples:

  • Bubble sort
  • Selection sort
  • Insertion sort
  • Simple algorithms with double loops

JavaScript example:

// O(n²) - Bubble sort
function bubbleSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // Swap elements
      }
    }
  }
  return arr;
}

O(2ⁿ) – Exponential Time Complexity

O(2ⁿ) indicates that the algorithm’s execution time grows exponentially with the input size. Such algorithms are typically used to solve combinatorial problems.

Common examples:

  • Fibonacci sequence (naive recursive implementation)
  • Subset generation
  • Solving the traveling salesman problem

JavaScript example:

// O(2ⁿ) - Fibonacci sequence (naive recursion)
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2); // Each call generates two sub-calls
}

O(n!) – Factorial Time Complexity

O(n!) indicates that the algorithm’s execution time grows factorially with the input size. Such algorithms are typically used for permutation problems.

Common examples:

  • Permutation generation
  • Traveling salesman problem (brute force solution)

JavaScript example:

// O(n!) - Permutation generation
function permute(arr) {
  const result = [];
  
  function backtrack(start) {
    if (start === arr.length) {
      result.push([...arr]);
      return;
    }
    
    for (let i = start; i < arr.length; i++) {
      [arr[start], arr[i]] = [arr[i], arr[start]]; // Swap
      backtrack(start + 1);
      [arr[start], arr[i]] = [arr[i], arr[start]]; // Restore
    }
  }
  
  backtrack(0);
  return result;
}

Space Complexity and Memory Management

Space complexity analyzes the additional storage space required by an algorithm during execution (excluding the input data itself). Like time complexity, space complexity is described using Big O notation.

Common space complexity classifications:

SymbolNameDescription
O(1)Constant SpaceUses a fixed amount of additional space
O(n)Linear SpaceUses additional space proportional to input size
O(n²)Quadratic SpaceUses additional space proportional to the square of input size
O(log n)Logarithmic SpaceUses additional space proportional to the logarithm of input size

Space Complexity Examples

// O(1) - Constant space
function sumArray(arr) {
  let sum = 0; // Uses a fixed number of variables
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}

// O(n) - Linear space
function copyArray(arr) {
  const newArr = []; // Creates a new array of the same size as input
  for (let i = 0; i < arr.length; i++) {
    newArr.push(arr[i]);
  }
  return newArr;
}

// O(n²) - Quadratic space
function generatePairs(arr) {
  const pairs = []; // Stores all possible pairs of elements
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      pairs.push([arr[i], arr[j]]); // Each pair takes O(1) space, total O(n²)
    }
  }
  return pairs;
}

Memory Management in JavaScript

JavaScript is a language with automatic garbage collection (GC), so developers typically do not need to manually manage memory. However, understanding memory management principles helps in writing more efficient code and avoiding memory leaks.

JavaScript memory lifecycle:

  1. Allocate Memory – When creating objects or variables
  2. Use Memory – Reading/writing object properties or variable values
  3. Release Memory – Garbage collector automatically reclaims memory no longer in use

Garbage collection mechanisms primarily rely on two algorithms:

  1. Reference Counting: Tracks the number of references to each object; memory is reclaimed when the reference count is zero. The drawback is that it cannot handle circular references.
  2. Mark-and-Sweep: Starts from root objects (e.g., window), marks all reachable objects, and clears unreachable ones. Modern JavaScript engines commonly use this algorithm.

Common memory leak scenarios:

  1. Accidental global variables
  2. Forgotten timers/event listeners
  3. References to detached DOM elements
  4. Closures referencing external variables

Memory leak examples and fixes:

// Memory leak example 1: Accidental global variable
function leakMemory() {
  leakedArray = []; // Declared without var/let/const, becomes global
  for (let i = 0; i < 1000000; i++) {
    leakedArray.push(i);
  }
}

// Fix: Use let/const to declare variables
function noLeak() {
  const localArray = [];
  for (let i = 0; i < 1000000; i++) {
    localArray.push(i);
  }
}

// Memory leak example 2: Forgotten timer
function startTimer() {
  const data = getData(); // Assume this is a large data object
  setInterval(() => {
    console.log(data); // Timer holds reference to data even when no longer needed
  }, 1000);
}

// Fix: Clear timer when no longer needed
let timerId;
function startTimer() {
  const data = getData();
  timerId = setInterval(() => {
    console.log(data);
  }, 1000);
}

function stopTimer() {
  clearInterval(timerId);
  // Data will be reclaimed by garbage collector after function execution
}

Techniques for optimizing memory usage:

  1. Use appropriate data structures – Choose space-efficient data structures
  2. Release unused references promptly
  3. Avoid creating unnecessary objects
  4. Process large data in chunks rather than loading all at once
  5. Use WeakMap/WeakSet for temporary references

Data Types in JavaScript

Primitive Types

JavaScript’s primitive data types are the most basic forms of data representation in the language, directly storing values rather than references. Primitive types have the following characteristics:

  1. Accessed by value – Operations work on the actual value
  2. Immutable – Once created, the value cannot be changed (for String and Number)
  3. Stored in stack memory (in simple cases)
  4. Compared by value

JavaScript’s seven primitive data types:

  1. Number – Represents integers and floating-point numbers
let age = 25;
let price = 9.99;
  1. String – Represents textual data
let name = "Alice";
let greeting = 'Hello, world!';
  1. Boolean – Represents logical values
let isActive = true;
let hasPermission = false;
  1. Undefined – Represents an undefined value
let uninitialized; // Default value is undefined
  1. Null – Represents an empty value
let empty = null; // Explicitly assigned as empty
  1. Symbol (ES6+) – Represents a unique identifier
let id = Symbol('unique_id');
  1. BigInt (ES2020+) – Represents integers of arbitrary precision
let bigNum = 9007199254740991n; // Uses 'n' suffix

Examples of primitive type operations:

// Number operations
let a = 10;
let b = 3;
console.log(a + b); // 13
console.log(a % b); // 1

// String operations
let str = "Hello";
console.log(str + " World"); // "Hello World"
console.log(str.length); // 5

// Boolean operations
let flag = true;
console.log(!flag); // false
console.log(flag && false); // false

// Comparison operations
console.log(5 == "5"); // true (loose equality)
console.log(5 === "5"); // false (strict equality)

Storage mechanism for primitive types:

  • In most JavaScript engines, primitive types are stored directly in stack memory (in simple cases)
  • When primitive types are used as object properties, they may be “boxed” into corresponding wrapper objects
  • Primitive types are compared by value, not by reference

Reference Types

JavaScript’s reference types (object types) are used to represent complex data structures. Unlike primitive types, reference types store references (pointers) to objects in memory, not the objects themselves. Reference types have the following characteristics:

  1. Accessed by reference – Operations work on the object reference
  2. Mutable – Object contents can be modified
  3. Stored in heap memory, with references stored in the stack
  4. Compared by reference (unless specifically implemented otherwise)

Common reference types:

  1. Object – A collection of key-value pairs
let person = {
  name: "Bob",
  age: 30,
  greet: function() {
	console.log(`Hello, my name is ${this.name}`);
  }
};
  1. Array – An ordered collection
let numbers = [1, 2, 3];
let mixed = [1, "two", { three: 3 }];
  1. Function – An executable object
function add(a, b) {
  return a + b;
}
  1. Date – Represents dates and times
let now = new Date();
  1. RegExp – Regular expressions
let pattern = /javascript/i;
  1. Other Built-in Objects – Such as Map, Set, Promise, etc.

Examples of reference type operations:

// Object operations
let car = {
  make: "Toyota",
  model: "Camry",
  year: 2020
};
car.color = "red"; // Add new property
console.log(car.make); // "Toyota"

// Array operations
let fruits = ["apple", "banana"];
fruits.push("orange"); // Add element
console.log(fruits[1]); // "banana"

// Function operations
function multiply(a, b) {
  return a * b;
}
console.log(multiply(2, 3)); // 6

// Method as object property
let calculator = {
  add: function(a, b) {
    return a + b;
  }
};
console.log(calculator.add(5, 7)); // 12

Memory mechanism for reference types:

  • Objects are stored in heap memory
  • Variables store references (pointers) to objects in the heap
  • Multiple variables can reference the same object
  • Modifying an object affects all variables referencing that object

Comparison of reference types:

let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;

console.log(obj1 == obj2); // false - Different objects
console.log(obj1 === obj2); // false - Strict equality
console.log(obj1 == obj3); // true - Same reference
console.log(obj1 === obj3); // true - Strict equality

Relationship Between Types and Data Structures

Data types in JavaScript are closely related to data structures. Understanding this relationship helps in selecting the appropriate data structure to solve specific problems.

Primitive Types and Simple Data Structures

Primitive types are typically used to build simple, linear data structures:

  1. Number – Used for counting, indexing, and mathematical calculations
// Simple counter
let count = 0;
function increment() {
  count++;
}
  1. String – Used to represent sequential data
// String as a simple sequence
let letters = "abc";
console.log(letters[1]); // "b"
  1. Boolean – Used for condition checks and state representation
// Status flag
let isActive = true;
if (isActive) {
  console.log("Active");
}

Reference Types and Complex Data Structures

Reference types support the creation of complex, nonlinear data structures:

  1. Object – Used for key-value storage, implementing mappings
// Dictionary/map
let phoneBook = {
  "Alice": "123-456-7890",
  "Bob": "987-654-3210"
};
console.log(phoneBook["Alice"]); // "123-456-7890"
  1. Array – Used for ordered collections, implementing linear lists
// Dynamic array
let queue = [];
queue.push("task1"); // Enqueue
queue.push("task2");
console.log(queue.shift()); // Dequeue "task1"
  1. Function – Used for higher-order functions and closures
// Function as a first-class citizen
function createMultiplier(factor) {
  return function(x) {
	return x * factor;
  };
}
let double = createMultiplier(2);
console.log(double(5)); // 10

Implementation of Advanced Data Structures

JavaScript’s reference types can be combined to implement more complex data structures:

  1. Stack
class Stack {
  constructor() {
	this.items = [];
  }
  
  push(element) {
	this.items.push(element);
  }
  
  pop() {
	return this.items.pop();
  }
  
  peek() {
	return this.items[this.items.length - 1];
  }
  
  isEmpty() {
	return this.items.length === 0;
  }
}

let stack = new Stack();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 2
  1. Queue
class Queue {
  constructor() {
    this.items = [];
  }
  
  enqueue(element) {
    this.items.push(element);
  }
  
  dequeue() {
    return this.items.shift();
  }
  
  front() {
    return this.items[0];
  }
  
  isEmpty() {
    return this.items.length === 0;
  }
}

let queue = new Queue();
queue.enqueue("A");
queue.enqueue("B");
console.log(queue.dequeue()); // "A"
  1. Linked List
class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.size = 0;
  }
  
  add(data) {
    const node = new Node(data);
    if (!this.head) {
      this.head = node;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = node;
    }
    this.size++;
  }
  
  print() {
    let current = this.head;
    let str = "";
    while (current) {
      str += current.data + " -> ";
      current = current.next;
    }
    console.log(str + "null");
  }
}

let list = new LinkedList();
list.add(1);
list.add(2);
list.add(3);
list.print(); // "1 -> 2 -> 3 -> null"
  1. Tree
class TreeNode {
  constructor(value) {
    this.value = value;
    this.children = [];
  }
  
  addChild(childNode) {
    this.children.push(childNode);
  }
}

class Tree {
  constructor() {
    this.root = null;
  }
  
  traverseDFS(node = this.root, callback) {
    if (!node) return;
    callback(node.value);
    node.children.forEach(child => this.traverseDFS(child, callback));
  }
}

let tree = new Tree();
tree.root = new TreeNode(1);
let child1 = new TreeNode(2);
let child2 = new TreeNode(3);
tree.root.addChild(child1);
tree.root.addChild(child2);
tree.traverseDFS(node => console.log(node)); // 1, 2, 3
  1. Graph
class Graph {
  constructor() {
    this.adjacencyList = {};
  }
  
  addVertex(vertex) {
    if (!this.adjacencyList[vertex]) {
      this.adjacencyList[vertex] = [];
    }
  }
  
  addEdge(v1, v2) {
    this.adjacencyList[v1].push(v2);
    this.adjacencyList[v2].push(v1); // Undirected graph
  }
  
  dfs(start) {
    const visited = {};
    const result = [];
    const adjacencyList = this.adjacencyList;
    
    function dfsHelper(vertex) {
      if (!vertex) return;
      visited[vertex] = true;
      result.push(vertex);
      adjacencyList[vertex].forEach(neighbor => {
        if (!visited[neighbor]) {
          dfsHelper(neighbor);
        }
      });
    }
    
    dfsHelper(start);
    return result;
  }
}

let graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addEdge("A", "B");
graph.addEdge("A", "C");
console.log(graph.dfs("A")); // ["A", "B", "C"] (order may vary)

Relationship Between Data Structures and Algorithms

Data structures provide the organizational framework for data manipulated by algorithms, while algorithms define how to operate on these data structures to solve problems. Choosing the right data structure can significantly improve algorithm efficiency.

Example: Search operations in different data structures

// Search in array
function findInArray(arr, target) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) return i;
  }
  return -1;
}

// Search in object (hash table)
function findInObject(obj, target) {
  return obj.hasOwnProperty(target) ? obj[target] : undefined;
}

// Usage example
const arr = [10, 20, 30, 40];
console.log(findInArray(arr, 30)); // 2

const obj = { "10": "a", "20": "b", "30": "c" };
console.log(findInObject(obj, "30")); // "c"

In this example, the average time complexity for searching in an object (hash table) is O(1), while linear search in an array is O(n). This demonstrates the impact of different data structures on algorithm efficiency.

Abstraction and Implementation of Data Structures

JavaScript’s advanced features make it easy to abstract and implement various data structures:

// Abstract data type - Stack interface
class StackADT {
  constructor() {
    if (new.target === StackADT) {
      throw new TypeError("Cannot construct Abstract instances directly");
    }
    if (this.push === undefined || this.pop === undefined) {
      throw new TypeError("Must override push and pop");
    }
  }
}

// Concrete implementation - Array-based stack
class ArrayStack extends StackADT {
  constructor() {
    super();
    this.items = [];
  }
  
  push(element) {
    this.items.push(element);
  }
  
  pop() {
    return this.items.pop();
  }
}

// Usage example
const stack = new ArrayStack();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 2

This separation of abstraction and implementation allows for easy replacement of the underlying implementation without affecting the upper-level code.

Type Conversion and Coercion

JavaScript is a loosely typed language, allowing implicit type conversion. Understanding type conversion rules is crucial for writing robust code.

Implicit Type Conversion

// Implicit conversion with numbers and strings
console.log("5" + 1);    // "51" (string concatenation)
console.log("5" - 1);    // 4 (numeric subtraction)
console.log("5" * "2");  // 10 (numeric multiplication)

// Implicit conversion with booleans
console.log(1 == true);  // true
console.log(0 == false); // true
console.log("" == false); // true

// Special value conversion
console.log(null == undefined); // true
console.log(NaN == NaN);        // false

Explicit Type Conversion

// Convert to number
console.log(Number("123"));    // 123
console.log(parseInt("123px")); // 123
console.log(parseFloat("12.34px")); // 12.34

// Convert to string
console.log(String(123));      // "123"
console.log(123.toString());   // "123"

// Convert to boolean
console.log(Boolean(1));       // true
console.log(Boolean(0));       // false
console.log(Boolean(""));      // false

Type Conversion Rules Summary

  1. ToPrimitive – Convert a value to a primitive
    • Object → Calls valueOf() or toString()
  2. ToNumber – Convert a value to a number
    • undefined → NaN
    • null → 0
    • true → 1, false → 0
    • String → Parsed as a literal
  3. ToString – Convert a value to a string
    • Number → String representation
    • Array → Comma-separated string
    • Object → “[object Object]”
  4. ToBoolean – Convert a value to a boolean
    • Falsy values: false, 0, “”, null, undefined, NaN
    • All other values are truthy

Pitfalls and Best Practices for Type Conversion

// Pitfall example 1: Implicit conversion with ==
console.log(0 == "0");    // true
console.log(0 == false);  // true
console.log("" == false); // true

// Best practice: Use === for strict comparison
console.log(0 === "0");   // false
console.log(0 === false); // false

// Pitfall example 2: Object comparison
const a = [1, 2];
const b = [1, 2];
console.log(a == b);      // false
console.log(a === b);     // false

// Best practice: Deep comparison or custom comparison logic
function arraysEqual(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  for (let i = 0; i < arr1.length; i++) {
    if (arr1[i] !== arr2[i]) return false;
  }
  return true;
}

// Pitfall example 3: Issues with implicit conversion in condition checks
function isFalsy(value) {
  if (value) { // May produce unexpected results
    return false;
  }
  return true;
}

// Best practice: Explicit checks
function isFalsy(value) {
  return value === undefined || value === null || 
         value === false || value === 0 || 
         value === "" || Number.isNaN(value);
}
Share your love