Lesson 07-Data Structures – Linear Lists

A linear list is a basic data structure consisting of a finite sequence of n (n ≥ 0) elements of the same type. The data elements in a linear list have a one-to-one linear relationship, meaning that except for the first and last elements, each element has a unique predecessor and successor.

Definition of Linear Lists

A linear list (List) is a finite sequence of data elements with the same characteristics, where:

  • n is the length of the linear list (n ≥ 0)
  • When n=0, it represents an empty list
  • If n>0, the first element is called the head and has no predecessor
  • If n>0, the last element is called the tail and has no successor
  • Other elements have exactly one predecessor and one successor

Classification of Linear Lists

Linear lists can be divided into:

  1. Sequential Storage Structure: Uses a group of storage units with consecutive addresses to store the data elements of the linear list sequentially
    • Array implementation
  2. Linked Storage Structure: Uses a group of arbitrary storage units to store the data elements of the linear list
    • Singly linked list
    • Doubly linked list
    • Circular linked list

Sequential Storage Structure Implementation (Array Implementation)

Basic Implementation

class ArrayList {
  constructor(capacity = 10) {
    this.data = new Array(capacity);
    this.length = 0;
    this.capacity = capacity;
  }

  // Get element
  get(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    return this.data[index];
  }

  // Set element
  set(index, value) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    this.data[index] = value;
  }

  // Add element at the end
  append(value) {
    if (this.length === this.capacity) {
      this.resize();
    }
    this.data[this.length] = value;
    this.length++;
  }

  // Insert element at specified position
  insert(index, value) {
    if (index < 0 || index > this.length) {
      throw new Error('Index out of bounds');
    }
    
    if (this.length === this.capacity) {
      this.resize();
    }
    
    // Move elements from index onwards one position back
    for (let i = this.length; i > index; i--) {
      this.data[i] = this.data[i - 1];
    }
    
    this.data[index] = value;
    this.length++;
  }

  // Remove element at specified position
  remove(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    
    const removed = this.data[index];
    
    // Move elements after index one position forward
    for (let i = index; i < this.length - 1; i++) {
      this.data[i] = this.data[i + 1];
    }
    
    this.data[this.length - 1] = undefined; // Clear reference
    this.length--;
    
    return removed;
  }

  // Remove the first matching element
  removeValue(value) {
    const index = this.indexOf(value);
    if (index !== -1) {
      return this.remove(index);
    }
    return undefined;
  }

  // Find the index of an element
  indexOf(value) {
    for (let i = 0; i < this.length; i++) {
      if (this.data[i] === value) {
        return i;
      }
    }
    return -1;
  }

  // Check if contains an element
  contains(value) {
    return this.indexOf(value) !== -1;
  }

  // Clear the linear list
  clear() {
    this.data = new Array(this.capacity);
    this.length = 0;
  }

  // Resize capacity
  resize() {
    const newCapacity = this.capacity * 2;
    const newData = new Array(newCapacity);
    
    for (let i = 0; i < this.length; i++) {
      newData[i] = this.data[i];
    }
    
    this.data = newData;
    this.capacity = newCapacity;
  }

  // Get the size of the linear list
  size() {
    return this.length;
  }

  // Check if empty
  isEmpty() {
    return this.length === 0;
  }

  // Convert to array
  toArray() {
    return this.data.slice(0, this.length);
  }
}

Time Complexity Analysis

OperationTime Complexity
getO(1)
setO(1)
appendAverage O(1), Worst O(n) (when resizing)
insertO(n)
removeO(n)
indexOf/removeValueO(n)
containsO(n)
size/isEmptyO(1)

Linked Storage Structure Implementation

Singly Linked List Implementation

class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // Add node at the end
  append(data) {
    const newNode = new Node(data);
    
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    
    this.length++;
  }

  // Add node at the beginning
  prepend(data) {
    const newNode = new Node(data);
    newNode.next = this.head;
    this.head = newNode;
    
    if (!this.tail) {
      this.tail = newNode;
    }
    
    this.length++;
  }

  // Insert node at specified position
  insert(index, data) {
    if (index < 0 || index > this.length) {
      throw new Error('Index out of bounds');
    }
    
    if (index === 0) {
      this.prepend(data);
      return;
    }
    
    if (index === this.length) {
      this.append(data);
      return;
    }
    
    const newNode = new Node(data);
    const leader = this.getNodeAt(index - 1);
    newNode.next = leader.next;
    leader.next = newNode;
    
    this.length++;
  }

  // Get node at specified position
  getNodeAt(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    
    let currentNode = this.head;
    let counter = 0;
    
    while (counter < index) {
      currentNode = currentNode.next;
      counter++;
    }
    
    return currentNode;
  }

  // Get element
  get(index) {
    const node = this.getNodeAt(index);
    return node.data;
  }

  // Set element
  set(index, data) {
    const node = this.getNodeAt(index);
    node.data = data;
  }

  // Remove node at specified position
  remove(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    
    if (index === 0) {
      const removedNode = this.head;
      this.head = this.head.next;
      
      if (this.length === 1) {
        this.tail = null;
      }
      
      this.length--;
      return removedNode.data;
    }
    
    const leader = this.getNodeAt(index - 1);
    const removedNode = leader.next;
    leader.next = removedNode.next;
    
    if (index === this.length - 1) {
      this.tail = leader;
    }
    
    this.length--;
    return removedNode.data;
  }

  // Remove the first matching element
  removeValue(value) {
    let current = this.head;
    let previous = null;
    
    while (current) {
      if (current.data === value) {
        if (previous === null) {
          this.head = current.next;
          if (this.length === 1) {
            this.tail = null;
          }
        } else {
          previous.next = current.next;
          if (current.next === null) {
            this.tail = previous;
          }
        }
        this.length--;
        return current.data;
      }
      previous = current;
      current = current.next;
    }
    
    return undefined;
  }

  // Find the index of an element
  indexOf(value) {
    let current = this.head;
    let index = 0;
    
    while (current) {
      if (current.data === value) {
        return index;
      }
      current = current.next;
      index++;
    }
    
    return -1;
  }

  // Check if contains an element
  contains(value) {
    return this.indexOf(value) !== -1;
  }

  // Clear the list
  clear() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // Get the size of the list
  size() {
    return this.length;
  }

  // Check if empty
  isEmpty() {
    return this.length === 0;
  }

  // Convert to array
  toArray() {
    const array = [];
    let current = this.head;
    
    while (current) {
      array.push(current.data);
      current = current.next;
    }
    
    return array;
  }
}

Doubly Linked List Implementation

class DoublyNode {
  constructor(data) {
    this.data = data;
    this.prev = null;
    this.next = null;
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // Add node at the end
  append(data) {
    const newNode = new DoublyNode(data);
    
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.prev = this.tail;
      this.tail.next = newNode;
      this.tail = newNode;
    }
    
    this.length++;
  }

  // Add node at the beginning
  prepend(data) {
    const newNode = new DoublyNode(data);
    
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.next = this.head;
      this.head.prev = newNode;
      this.head = newNode;
    }
    
    this.length++;
  }

  // Insert node at specified position
  insert(index, data) {
    if (index < 0 || index > this.length) {
      throw new Error('Index out of bounds');
    }
    
    if (index === 0) {
      this.prepend(data);
      return;
    }
    
    if (index === this.length) {
      this.append(data);
      return;
    }
    
    const newNode = new DoublyNode(data);
    const leader = this.getNodeAt(index - 1);
    const follower = leader.next;
    
    leader.next = newNode;
    newNode.prev = leader;
    newNode.next = follower;
    follower.prev = newNode;
    
    this.length++;
  }

  // Get node at specified position
  getNodeAt(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    
    let current;
    if (index < this.length / 2) {
      // From head
      current = this.head;
      let counter = 0;
      while (counter < index) {
        current = current.next;
        counter++;
      }
    } else {
      // From tail
      current = this.tail;
      let counter = this.length - 1;
      while (counter > index) {
        current = current.prev;
        counter--;
      }
    }
    
    return current;
  }

  // Get element
  get(index) {
    const node = this.getNodeAt(index);
    return node.data;
  }

  // Set element
  set(index, data) {
    const node = this.getNodeAt(index);
    node.data = data;
  }

  // Remove node at specified position
  remove(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    
    let removedNode;
    
    if (index === 0) {
      removedNode = this.head;
      this.head = this.head.next;
      if (this.head) {
        this.head.prev = null;
      } else {
        this.tail = null;
      }
    } else if (index === this.length - 1) {
      removedNode = this.tail;
      this.tail = this.tail.prev;
      if (this.tail) {
        this.tail.next = null;
      } else {
        this.head = null;
      }
    } else {
      removedNode = this.getNodeAt(index);
      removedNode.prev.next = removedNode.next;
      removedNode.next.prev = removedNode.prev;
    }
    
    this.length--;
    return removedNode.data;
  }

  // Remove the first matching element
  removeValue(value) {
    let current = this.head;
    
    while (current) {
      if (current.data === value) {
        if (current.prev) {
          current.prev.next = current.next;
        } else {
          this.head = current.next;
        }
        
        if (current.next) {
          current.next.prev = current.prev;
        } else {
          this.tail = current.prev;
        }
        
        this.length--;
        return current.data;
      }
      current = current.next;
    }
    
    return undefined;
  }

  // Find the index of an element
  indexOf(value) {
    let current = this.head;
    let index = 0;
    
    while (current) {
      if (current.data === value) {
        return index;
      }
      current = current.next;
      index++;
    }
    
    return -1;
  }

  // Check if contains an element
  contains(value) {
    return this.indexOf(value) !== -1;
  }

  // Clear the list
  clear() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // Get the size of the list
  size() {
    return this.length;
  }

  // Check if empty
  isEmpty() {
    return this.length === 0;
  }

  // Convert to array
  toArray() {
    const array = [];
    let current = this.head;
    
    while (current) {
      array.push(current.data);
      current = current.next;
    }
    
    return array;
  }
}

Circular Linked List Implementation

class CircularLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // Add node at the end
  append(data) {
    const newNode = new Node(data);
    
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
      newNode.next = this.head; // Point to itself
    } else {
      newNode.next = this.head; // New node points to head
      this.tail.next = newNode; // Original tail points to new node
      this.tail = newNode; // Update tail
    }
    
    this.length++;
  }

  // Add node at the beginning
  prepend(data) {
    const newNode = new Node(data);
    
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
      newNode.next = this.head;
    } else {
      newNode.next = this.head;
      this.tail.next = newNode;
      this.head = newNode;
    }
    
    this.length++;
  }

  // Insert node at specified position
  insert(index, data) {
    if (index < 0 || index > this.length) {
      throw new Error('Index out of bounds');
    }
    
    if (index === 0) {
      this.prepend(data);
      return;
    }
    
    if (index === this.length) {
      this.append(data);
      return;
    }
    
    const newNode = new Node(data);
    const leader = this.getNodeAt(index - 1);
    newNode.next = leader.next;
    leader.next = newNode;
    
    this.length++;
  }

  // Get node at specified position
  getNodeAt(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    
    let current = this.head;
    let counter = 0;
    
    while (counter < index) {
      current = current.next;
      counter++;
    }
    
    return current;
  }

  // Get element
  get(index) {
    const node = this.getNodeAt(index);
    return node.data;
  }

  // Set element
  set(index, data) {
    const node = this.getNodeAt(index);
    node.data = data;
  }

  // Remove node at specified position
  remove(index) {
    if (index < 0 || index >= this.length) {
      throw new Error('Index out of bounds');
    }
    
    if (this.length === 1) {
      const removedData = this.head.data;
      this.head = null;
      this.tail = null;
      this.length--;
      return removedData;
    }
    
    if (index === 0) {
      const removedData = this.head.data;
      this.tail.next = this.head.next;
      this.head = this.head.next;
      this.length--;
      return removedData;
    }
    
    const leader = this.getNodeAt(index - 1);
    const removedData = leader.next.data;
    leader.next = leader.next.next;
    
    if (index === this.length - 1) {
      this.tail = leader;
    }
    
    this.length--;
    return removedData;
  }

  // Remove the first matching element
  removeValue(value) {
    if (!this.head) {
      return undefined;
    }
    
    if (this.head.data === value) {
      return this.remove(0);
    }
    
    let current = this.head;
    let previous = null;
    
    do {
      previous = current;
      current = current.next;
      
      if (current.data === value) {
        previous.next = current.next;
        
        if (current === this.tail) {
          this.tail = previous;
        }
        
        this.length--;
        return current.data;
      }
    } while (current !== this.head);
    
    return undefined;
  }

  // Find the index of an element
  indexOf(value) {
    if (!this.head) {
      return -1;
    }
    
    let current = this.head;
    let index = 0;
    
    do {
      if (current.data === value) {
        return index;
      }
      current = current.next;
      index++;
    } while (current !== this.head);
    
    return -1;
  }

  // Check if contains an element
  contains(value) {
    return this.indexOf(value) !== -1;
  }

  // Clear the list
  clear() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // Get the size of the list
  size() {
    return this.length;
  }

  // Check if empty
  isEmpty() {
    return this.length === 0;
  }

  // Convert to array
  toArray() {
    if (!this.head) {
      return [];
    }
    
    const array = [];
    let current = this.head;
    
    do {
      array.push(current.data);
      current = current.next;
    } while (current !== this.head);
    
    return array;
  }

  // Print the list
  print() {
    console.log(this.toArray().join(' <-> '));
  }
}

Applications of Linear Lists

Stack Implementation (Based on Array)

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;
  }

  size() {
    return this.items.length;
  }

  clear() {
    this.items = [];
  }
}

Queue Implementation (Based on Linked List)

class Queue {
  constructor() {
    this.items = new DoublyLinkedList();
  }

  enqueue(element) {
    this.items.append(element);
  }

  dequeue() {
    return this.items.removeHead();
  }

  front() {
    return this.items.get(0);
  }

  isEmpty() {
    return this.items.isEmpty();
  }

  size() {
    return this.items.size();
  }

  clear() {
    this.items.clear();
  }
}

Josephus Problem

function josephus(n, m) {
  const circularList = new CircularLinkedList();
  
  // Initialize circular linked list
  for (let i = 1; i <= n; i++) {
    circularList.append(i);
  }
  
  let currentNode = circularList.head;
  let previousNode = null;
  let count = 0;
  
  // While there is more than one node in the list
  while (circularList.size() > 1) {
    count++;
    
    // Find the m-th node
    if (count === m) {
      // Remove current node
      if (previousNode) {
        previousNode.next = currentNode.next;
      } else {
        // If removing the head node
        circularList.head = currentNode.next;
      }
      
      // If the tail node is removed
      if (currentNode.next === circularList.head) {
        circularList.tail = previousNode;
      }
      
      circularList.length--;
      count = 0;
      currentNode = currentNode.next;
    } else {
      previousNode = currentNode;
      currentNode = currentNode.next;
    }
  }
  
  return circularList.head.data;
}

console.log(josephus(7, 3)); // Output the survivor's position

Polynomial Addition

class Polynomial {
  constructor() {
    this.terms = new DoublyLinkedList();
  }

  addTerm(coefficient, exponent) {
    this.terms.append({ coefficient, exponent });
  }

  add(polynomial) {
    const result = new Polynomial();
    let p1 = this.terms.head;
    let p2 = polynomial.terms.head;
    
    while (p1 && p2) {
      if (p1.data.exponent > p2.data.exponent) {
        result.addTerm(p1.data.coefficient, p1.data.exponent);
        p1 = p1.next;
      } else if (p1.data.exponent < p2.data.exponent) {
        result.addTerm(p2.data.coefficient, p2.data.exponent);
        p2 = p2.next;
      } else {
        const sum = p1.data.coefficient + p2.data.coefficient;
        if (sum !== 0) {
          result.addTerm(sum, p1.data.exponent);
        }
        p1 = p1.next;
        p2 = p2.next;
      }
    }
    
    // Add remaining terms
    while (p1) {
      result.addTerm(p1.data.coefficient, p1.data.exponent);
      p1 = p1.next;
    }
    
    while (p2) {
      result.addTerm(p2.data.coefficient, p2.data.exponent);
      p2 = p2.next;
    }
    
    return result;
  }

  toString() {
    let str = '';
    let current = this.terms.head;
    
    while (current) {
      const { coefficient, exponent } = current.data;
      if (str) {
        str += ' + ';
      }
      str += `${coefficient}x^${exponent}`;
      current = current.next;
    }
    
    return str;
  }
}

const p1 = new Polynomial();
p1.addTerm(3, 2);
p1.addTerm(4, 1);
p1.addTerm(5, 0);

const p2 = new Polynomial();
p2.addTerm(2, 2);
p2.addTerm(-4, 1);
p2.addTerm(1, 0);

const sum = p1.add(p2);
console.log(sum.toString()); // Output: 5x^2 + 6x^0

Performance Comparison of Linear Lists

OperationSequential Storage (Array)Linked Storage (Singly Linked List)Linked Storage (Doubly Linked List)
Access ElementO(1)O(n)O(n)
Insert/Delete at HeadO(n) (requires shifting elements)O(1)O(1)
Insert/Delete at TailO(1) (if capacity is known)O(1)O(1)
Insert/Delete in MiddleO(n) (requires shifting elements)O(n) (requires traversal)O(n) (requires traversal)
Memory UsageContiguous memory, may waste spaceDynamic allocation, no wasteDynamic allocation, no waste
Implementation ComplexitySimpleMediumMore complex

Summary

Linear lists are one of the most basic data structures, providing a foundation for other complex data structures (such as stacks, queues, trees, etc.). Depending on different application scenarios and performance requirements, sequential storage (array) or linked storage (singly linked list/doubly linked list) can be chosen to implement linear lists.

  • Sequential Storage is suitable for scenarios with frequent access but less insertion/deletion
  • Linked Storage is suitable for scenarios with frequent insertion/deletion but less random access
  • Doubly Linked List is more efficient than singly linked list when frequent operations at head and tail are needed

Understanding the implementation principles and performance characteristics of linear lists is very important for solving practical problems, especially when optimizing data structure performance.

Share your love