📕
Dan Fitz's Notes
  • README
  • Ai
    • Supervised Machine Learning
      • Introduction To Machine Learning
      • Regression With Multiple Input Variables
      • Classification
  • Csharp
    • C Sharp Advanced
      • Generics
      • Delegates
      • Lambda Expressions
      • Events
    • C Sharp Fundamentals
      • Intro To C
      • Primitive Types And Expressions
      • Non Primitive Types
      • Control Flow
      • Arrays And Lists
      • Working With Dates
      • Working With Text
      • Working With Files
      • Debugging Applications
    • C Sharp Intermediate
      • Classes
      • Association Between Classes
      • Inheritance
      • Polymorphism
      • Interfaces
  • Java
    • Inheritance Data Structures Java
      • Inheritance Polymorphism Using Overriding And Access Modifiers
      • Abstract Classes And Debugging
      • File I O And Exceptions
      • Collections Maps And Regular Expressions
    • Intro To Java
      • Introduction To Java Classes And Eclipse
      • Unit Testing Arrays And Array Lists
      • Static Variables Methods And Polymorphism Using Overloading
  • Javascript
    • Algorithms Data Structures
      • Big O Notation
      • Analyzing Performance Of Arrays And Objects
      • Problem Solving Approach
      • Problem Solving Patterns
      • Recursion
      • Searching Algorithms
      • Bubble Selection And Insertion Sort
      • Merge Sort
      • Quick Sort
      • Radix Sort
      • Data Structures Introduction
      • Singly Linked Lists
      • Doubly Linked Lists
      • Stacks And Queues
      • Binary Search Trees
      • Tree Traversal
      • Binary Heaps
    • Complete Nodejs
      • Understanding Node.js
      • REST AP Is And Mongoose
      • API Authentication And Security
      • Node.js Module System
      • File System And Command Line Args
      • Debugging Node.js
      • Asynchronous Node.js
      • Web Servers
      • Accessing API From Browser
      • Application Deployment
      • Mongo DB And Promises
    • Complete React Native
      • Working With Content
      • Building Lists
      • Navigating Users Between Screens
      • State Management
      • Handling Screen Layout
      • Setting Up An App
      • More On Navigation
      • Advanced Statement Management With Context
      • Building A Custom Express API
      • In App Authentication
    • Epic React
      • React Fundamentals
      • React Hooks
      • Advanced React Hooks
      • Advanced React Patterns
      • React Performance
    • Fireship Firestore
      • Firestore Queries And Data Modeling Course
      • Model Relational Data In Firestore No SQL
    • Functional Light Javascript
      • Intro
      • Function Purity
      • Argument Adapters
      • Point Free
      • Closure
      • Composition
      • Immutability
      • Recursion
      • List Operations
      • Transduction
      • Data Structure Operations
      • Async
    • Js Weird Parts
      • Execution Contexts And Lexical Environments
      • Types And Operators
      • Objects And Functions
      • Object Oriented Java Script And Prototypal Inheritance
      • Defining Objects
    • Mastering Chrome Dev Tools
      • Introduction
      • Editing
      • Debugging
      • Networking
      • Auditing
      • Node.js Profiling
      • Performance Monitoring
      • Image Performance
      • Memory
    • React Complete Guide
      • What Is React
      • React Basics
      • Rendering Lists And Conditionals
      • Styling React Components
      • Debugging React Apps
      • Component Deep Dive
      • Building A React App
      • Reaching Out To The Web
      • Routing
    • React Testing
      • Intro To Jest Enzyme And TDD
      • Basic Testing
      • Redux Testing
      • Redux Thunk Testing
    • Serverless Bootcamp
      • Introduction
      • Auction Service Setup
      • Auction Service CRUD Operations
      • Auction Service Processing Auctions
    • Testing Javascript
      • Fundamentals Of Testing
      • Static Analysis Testing
      • Mocking Fundamentals
      • Configuring Jest
      • Test React Components With Jest And React Testing Library
    • Typescript Developers Guide
      • Getting Started With Type Script
      • What Is A Type System
      • Type Annotations In Action
      • Annotations With Functions And Objects
      • Mastering Typed Arrays
      • Tuples In Type Script
      • The All Important Interface
      • Building Functionality With Classes
    • Web Performance With Webpack
      • Intro
      • Code Splitting
      • Module Methods Magic Comments
  • Other
    • Algo Expert
      • Defining Data Structures And Complexity Analysis
      • Memory
      • Big O Notation
      • Logarithm
      • Arrays
      • Linked Lists
      • Hash Tables
      • Stacks And Queues
      • Strings
      • Graphs
      • Trees
    • Aws Solutions Architect
      • AWS Fundamentals IAM EC 2
    • Fundamentals Math
      • Numbers And Negative Numbers
      • Factors And Multiples
      • Fractions
    • Mysql Bootcamp
      • Overview And Installation
      • Creating Databases And Tables
      • Inserting Data
      • CRUD Commands
      • The World Of String Functions
      • Refining Our Selections
      • The Magic Of Aggregate Functions
    • Random Notes
      • Understanding React Hooks
  • Python
    • Data Analysis Using Python
      • Loading Querying And Filtering Data Using The Csv Module
      • Loading Querying Joining And Filtering Data Using Pandas
      • Summarizing And Visualizing Data
    • Intro To Python
      • Course Introduction Intro To Programming And The Python Language Variables Conditionals Jupyter Notebook And IDLE
      • Intro To Lists Loops And Functions
      • More With Lists Strings Tuples Sets And Py Charm
      • Dictionaries And Files
Powered by GitBook
On this page
  • Definition
  • Comparison to Arrays
  • Singly Linked List Class
  • Basic constructors
  • Push
  • Pop
  • Shift
  • Unshift
  • Get
  • Set
  • Insert
  • Remove
  • Reverse
  • Big O
  1. Javascript
  2. Algorithms Data Structures

Singly Linked Lists

  1. What is a singly linked list?

  2. How does it differ from arrays?

  3. Defining SinglyLinkedList class with insertion, removal, traversal methods

Definition

A linked list stores any data types in an order just like an array.

However, arrays are indexed with a number, and you can select elements at specific indices.

In contrast, linked lists just consist of nodes that each contain a value and a pointer to the next node (or null for the last node).

Additionally, linked lists keep track of the head node, the tail node, and the length of the list.

Now a singly linked list means that each node is only connected one-directionally to the next node. (A doubly linked list is connected both ways.)

Comparison to Arrays

Because arrays have indices, it's easy to perform random access: to request the 4th element, we just go arr[3]. With singly linked lists, there is no random access; we have to start at the head and follow the pointers to the 4th element.

However, one thing that singly linked lists are good at is insertion and deletion. For example, arrays have a hard time adding to the beginning because everything has to be re-indexed. With singly linked lists, you simply define a new node as the head and set the pointer to the first element.

Singly Linked List Class

Basic constructors

To start, we want to define a Node with a value and a reference to the next node.

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

// Now we have the foundation to connect nodes
const firstNode = new Node('Hello')
firstNode.next = new Node('World')

class SinglyLinkedList {
  // This is enough to initialize
  constructor() {
    this.head = null
    this.tail = null
    this.length = 0
  }
}

const list = new SinglyLinkedList()

Push

The basic requirements of a push are that you

  1. Set the new node to both head and tail if it's the first element.

  2. Otherwise, set the current tail's next to the new node and then repoint the tail to the new node.

  3. Finally, increment the length.

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

  push(val) {
    const newNode = new Node(val)
    
    if (this.length === 0) {
      this.head = newNode
    } else {
      this.tail.next = newNode
    }

    this.tail = newNode
    this.length++
    return this
  }
}

Pop

With a pop, your goal is to

  1. Store the current tail in a variable.

  2. Change the second-last element to the new tail and set its next to null.

  3. Decrement the length.

  4. Return the stored variable.

Note: In order to get to the secon-last element, you actually have to traverse from the head, making pop O(n).

pop() {
  if (this.length === 0) return undefined

  const poppedNode = this.tail

  if (this.length === 1) { // <= special case
    this.head = null
    this.tail = null
  } else {
    let newTail = this.head
    while (newTail.next !== null) {
      newTail = newTail.next
    }
    this.tail = newTail
    this.tail.next = null
  }

  this.length--
  return poppedNode
}

Shift

Shift is simple. All you have to do is

  1. Store the current head in a variable.

  2. Set the head's next as the new head.

  3. Set the saved old head's next to null.

  4. Decrement the length.

  5. Return the stored variable.

shift() {
  if (this.length === 0) return undefined

  const shiftedNode = this.head

  if (this.length === 1) this.tail = null // <= special case

  this.head = this.head.next
  shiftedNode.next = null

  this.length--
  return shiftedNode
}

Unshift

Unshift is also very simple. You just

  1. Point the new node to the current head.

  2. Set the new node as the new head.

  3. Increment the length.

unshift(val) {
  const newNode = new Node(val)

  if (this.length === 0) { // <= special case
    this.tail = newNode
  } else {
    newNode.next = this.head
  }

  this.head = newNode
  this.length++
  return this
}

Get

The basic idea of get is to

  1. Accept an index.

  2. If the index is out of bounds, return undefined.

  3. Otherwise, loop through the list until you reach the index.

get(index) {
  if (index < 0 || index >= this.length) return undefined

  let counter = 0
  let targetNode = this.head

  while (counter < index) {
    targetNode = targetNode.next
    counter++
  }

  return targetNode
}

Set

Set is super simple in that you can just use get and re-set the val property of the node it returns.

set(index, val) {
  const targetNode = this.get(index)

  // Leverages logic check in get
  if (!targetNode) return false

  targetNode.val = val
  return true
}

Insert

Insert accepts an index and val and adds a node at that index, pointing the node before it to the new node and pointing the new node to what the node before it was previously pointing at.

What makes insert unique is that you can leverage other methods!

  1. If the index is less than zero or greater than the length, return false.

  2. If the index is equal to the length, use push.

  3. If the index is 0, use unshift.

  4. Otherwise, use get to find the element at index - 1.

  5. Then create a new node and set its pointer to the previous element's pointer.

  6. Finally, set the previous element's pointer to the new node.

  7. Increment length and return true.

insert(index, val) {
  if (index < 0 || index > this.length) return false

  if (index === this.length) {
    this.push(val)
  } else if (index === 0) {
    this.unshift(val)
  } else {
    const previousNode = this.get(index - 1)
    const newNode = new Node(val)
    newNode.next = previousNode.next
    previousNode.next = newNode
    this.length++
  }

  return true
}

Remove

Remove works just like insert but takes away an element:

  1. If the index is less than zero or greater than or equal to the length, return undefined.

  2. If the index is equal to length - 1, use and return pop.

  3. If the index is equal to zero, use and return shift.

  4. Otherwise, find the node at index - 1.

  5. Store that node's pointer in a variable. That's your target node.

  6. Set that node's pointer to its next.next.

  7. Decrement the length and return the target node.

remove(index) {
  if (index < 0 || index >= this.length) return undefined

  let removedNode

  if (index === this.length - 1) {
    removedNode = this.pop()
  } else if (index === 0) {
    removedNode = this.shift()
  } else {
    const previousNode = this.get(index - 1)
    removedNode = previousNode.next
    previousNode.next = removedNode.next
  }
  
  removedNode.next = null
  this.length--
  return removedNode
}

Reverse

The basic idea behind reverse is to loop through your linked list and keep track of 3 nodes: the current node, the one before it, and the one after. The goal is to set the current node's pointer to the node before it. Then you shift the 3 nodes being tracked to the right.

Note: Before starting this loop though, you want to swap the head and tail.

// Reverses in place
reverse() {
  // Swap head and tail
  let node = this.head
  this.head = this.tail
  this.tail = node

  let prev = null // <= start null to set tail to null @ first iteration
  let next
  for (let i = 0; i < this.length; i++) {
    next = node.next
    node.next = prev // <= this is where pointer is re-set
    previous = node
    node = next
  }
  
  return this
}

Think of reverse as looping through the list and flipping the direction of the pointer as it goes:

// 1 => 2 => 3 => 4 => null (starting position)

// null *<=* 1 - 2 => 3 => 4 => null
// prev    node next

// null <= 1 *<=* 2 - 3 => 4 => null
//       prev   node next

// null <= 1 <= 2 *<=* 3 - 4 => null
//            prev   node next

// null <= 1 <= 2 <= 3 *<=* 4 - null
//                  prev   node next

// null <= 1 <= 2 <= 3 <= 4 (final position)

The pointers are now correctly reversed. You then just flip the head and tail properties for sake of record.

// BONUS: Returns reversed copy
reverse() {
  const reversedList = new SinglyLinkedList()

  let counter = 0
  let currentNode = this.head
  while (counter < this.length) {
    reversedList.unshift(currentNode.val)
    counter++
    currentNode = currentNode.next
  }

  return reversedList
}

Big O

Insertion (push and unshift) is O(1) because all you're doing is re-setting pointers and redefining the head or tail. In contrast, arrays are only O(1) for push. Unshift requires re-indexing.

Removal is O(1) (for shift) or O(n) (for pop). That's because pop requires following the pointers to the second-last node.

Search is O(n). In contrast, search in arrays vary from O(n) (e.g. linear search) to O(log n) (e.g. binary search).

Random access is O(n) via get because you have to follow the pointers to reach your desired node. In contrast, arrays are always O(1) because indexing makes access super quick.

TLDR: Singly linked lists are best when you're realy concerned insertion or removal times but don't really care about random access. This is especially true when you care about insertion at the beginning.

PreviousData Structures IntroductionNextDoubly Linked Lists

Last updated 3 years ago