Study Guide 2023+

datastructures

Warning: These notes are partial, ongoing, incomplete, and may contain typos/inaccuracies. (They are kept factually accurate, time permitting.)

They are being united from many disparate notes created in the past and the layout/organization will gradually improve with time!

Please view them on a computer as they are not optimized for mobile (although you can still view them on Mobile along with the Flashcards at your own risk)!

Topics and code examples are lazy-loaded and may require two-clicks from the TOC to correctly calculate the updated x,y coordinates (after rendering). Thanks!

Data Structures: Trees

A "tree-like", derived, Data Structure that has zero or more child instances, each instance taking a value:

  1. Trees are a kind of Graph which has a Root node.
  2. Each parent node has one or more child nodes.

Attributes

Implementations

var TreeNode = function(val, children = []) {
    this.val = val;
    this.children = children;
}
  1. Depth First Search - refer to the Search article.
    • Top to Bottom, Left to Right
    • By Column
  2. Breadth First Search - consult the Level Recursion article.
    • Left to Right, Top, to Bottom
    • By Row or Level
  3. N-Trees vs BST
    • For N-Trees replace Left and Right child nodes with Children List or Array.

Notes

  1. A LinkedList can be thought of as Tree with only one child element at each node.

Code samples:

  1. https://github.com/Thoughtscript/cplusplus-coursera/tree/master/course/11%20-%20Generic%20Tree

Data Structures: Binary Trees

A "tree-like", derived, Data Structure that connects left and right child instances, each instance taking a value.

Useful resource: https://trees-visualizer.netlify.app/trees

Binary Trees

  1. A Tree with at most two children.
  2. Ordered by constraints.
    • Binary Heap
      • A Binary Tree
      • Max: the value of any child of a parent node is greater than or equal to the parent.
      • Min: the value of any child of a parent node is less than or equal to the parent.
      • Average Read: O(n)
      • Average Add: O(1)
    • Binary Search
      • A Binary Tree
      • The value of the left child node of a parent node must be less than or equal to the value of the parent node.
        • L <= P
      • The value of the right child node of a parent node must be greater than or equal to the value of the parent node.
        • R >= P
      • Therefore, any value of any right node of the root node will be greater than the value of any left node.
      • Therefore, any value of any left node of the root node will be less than the value of any right node.
      • Average Read: O(log(n))
      • Average Add: O(log(n))
    • AVL Tree (Georgy Adelson-Velsky and Evgenii Landis)
      • A Self Balancing Binary Search Tree (dynamic)
      • Heights of each branch differ by at most 1.
      • A self-balancing algorithm executes if heights differ by more than 1.
  3. Perfect
    • Balanced and all leaves are at the same depth.
    • Each branch is complete, has two children, and is at the same height as the rest.
  4. Balanced
    • Each branch differs in height no more than one compared to any other in the tree.
  5. Complete
    • Each node except the last is completely filled (has two children).
    • Each node in the last row is as far left as possible.

Attributes

Implementations

var TreeNode = function(val, left, right) {
    this.val = val;
    this.left = left;
    this.right = right;
}
  1. Pre Order - current node first, left node, then right node last
  2. In Order - left node first, then current node, then right node last
  3. Post Order - left node first, right node second, then current node last
const preOrder = node => 
{
  const traverse = (node, result) => {
    if (!node) result.push(null)
    else {
      result.push(node.data)
      if (node.left) traverse(node.left, result)
      if (node.right) traverse(node.right, result)
    }
  }
  let result = []
  if (!node) return result
  traverse(node, result)
  return result
}

const inOrder = node => 
{
  const traverse = (node, result) => {
    if (node.left) traverse(node.left, result)
    result.push(node.data)
    if (node.right) traverse(node.right, result)
  }
  let result = []
  if (!node) return result
  traverse(node, result)
  return result
}

const postOrder = node => 
{
  const traverse = (node, result) => {
    if (!node) {}
    else {
      if (node.left) traverse(node.left, result)
      if (node.right) traverse(node.right, result)
      result.push(node.data)
    }
  }
  let result = []
  if (!node) return result
  traverse(node, result)
  return result
}

Refer to: https://www.codewars.com/kata/5268956c10342831a8000135

  1. Level Recursion - Traverse by each level of a Binary Search Tree
def tree_by_levels(node):
    result = []
    
    if node is None:
        return result
    
    level = [node]
    nxt = []
    
    while len(level) > 0:
        temp = []
        count = 0
        l = len(level)
        
        while count < l:
            if (len(level) > 0):
                c = level.pop(0)
                if c is None:
                    continue
                temp.append(c.value)
                nxt.append(c.left)
                nxt.append(c.right)
            count += 1
        
        if len(temp) > 0:
            for x in range(0, len(temp)):
                result.append(temp[x])
        level = nxt

    return result

Refer to: https://www.codewars.com/kata/52bef5e3588c56132c0003bc

Make a Balanced Binary Search Tree

Given a sorted Array or List:

const sortedArrayToBST = A => {
    const L = A.length
    if (L === 0) return null
    if (L === 1) return new TreeNode(A[0], null, null)
    if (L === 2) return new TreeNode(A[1], new TreeNode(A[0], null, null), null)
    if (L === 3) return new TreeNode(A[1], new TreeNode(A[0], null, null), new TreeNode(A[2], null, null))
    if (L > 3) return recurse(A)
}

const findPivot = arr => arr[Math.floor(arr.length / 2)]

const firstHalf = arr => arr.slice(0, Math.floor(arr.length / 2))

const secondHalf = arr => arr.slice(Math.floor(arr.length / 2) + 1, arr.length)

const recurse = A => {
    const L = A.length
    
    if (L === 0) return null
    if (L === 1) return new TreeNode(A[0], null, null)
    if (L === 2) return new TreeNode(A[1], new TreeNode(A[0], null, null), null)
    if (L === 3) return new TreeNode(A[1], new TreeNode(A[0], null, null), new TreeNode(A[2], null, null))
    if (L > 3) {
        let node = new TreeNode(findPivot(A), null, null)
        node.left = recurse(firstHalf(A))
        node.right = recurse(secondHalf(A))
        return node
    }
}
  1. https://trees-visualizer.netlify.app/trees
  2. https://www.codewars.com/kata/5268956c10342831a8000135
  3. https://www.codewars.com/kata/52bef5e3588c56132c0003bc
  4. https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree

Code samples:

  1. https://github.com/Thoughtscript/cplusplus-coursera/tree/master/course/9%20-%20BST
  2. https://github.com/Thoughtscript/cplusplus-coursera/tree/master/course/10%20-%20AVL

Data Structures: Singly Linked List

A "chain-like", derived, Data Structure that connects instances head to tail through a next attribute, each instance taking a value.

Attributes

Time Complexity

Generally, the average time complexity for most operations will be O(N). One needs to traverse through a Linked List to find the right instances to alter, insert a new instance between, remove, etc.

The best case is O(1) - the desired instance can be right at the beginning of the Linked List at the head.

Implementations

// JavaScript
var LinkedList = function(val, next) {
    this.val = val;

    if (next === undefined || next === null) this.next = null;
    else this.next = next;
}

Here, I implement a very simple Linked List. Java's default LinkedList tracks head and tail via index. The implementation below explicitly stipulates a head and tail and uses Node objects (specifically the next property) to create a chain.

// Java
public class Node {
  private Node next;
  private Object data;

  public Node(Object data, Node next) {
    this.data = data;
    this.next = next;
  }

  //... Getters and Setters
}

public class LinkedList {
  private Node head;
  private Node tail;

  public LinkedList() {
    Node tail = new Node(null, null);
    this.head = new Node(null, tail);
    this.tail = tail;
  }

  public LinkedList(Node head, Node tail) {
    this.head = head;
    this.tail = tail;
  }
}

Common Operations

// JavaScript
function append(head, val) {
  if (head == null) head = new LinkedList(val, null);
  else {
    var current = head;
    while (current.next != null) {
      current = current.next;
    }
    current.next = new LinkedList(val, null);
  }
  return head;
};

function prepend(head, val) {
  if (head == null) head = new LinkedList(val, null);
  else {
    var current = head;
    head = new LinkedList(val, current);
  }
  return head;
};

/** Remove by value not by index. */
function del(head, val) {
  let current = head, arr = []
    
  while (current != null) {
    if (current.val != val) arr.push(current.val)
    current = current.next
  }
  
  return buildList(arr);
};

/** - Starting At 1 */
function insrt(head, val, position) {
  if (head == null) head = new LinkedList(val, null);
  else {
    var current = head;
    var last = head;
    for (var i = 1; i < position; i++) {
      last = current;
      current = current.next;
    }
    current = new LinkedList(val, current);
    last.next = current;
  }
  return head;
};

function checkCycle(head) {
  if (head == null) return false;
  else {
    var alreadyIn = [];
    var current = head;
    while (current != null) {
      if (alreadyIn.indexOf(current.val) == -1) alreadyIn.push(current.val);
      else return true;
      current = current.next;
    }
    return false;
  }
};

Notes

  1. A LinkedList can be thought of as Tree with only one child element at each node.

Data Structures: Stack

A "paper-stack" like derived object that's LIFO (last in, first out).

Often implemented with an Array or LinkedList.

Fun Algorithms

  1. Stacks are often useful when two potentially non-contiguous items must be paired and/or canceled out (such as (, ) pairs).
  2. In such cases, items can be added to a Stack (push), compared against the top of the Stack (peek), and removed (pop).

Consider Reverse Polish Logic Notation:

// Assumptions/Constraints: WFF, whitespaces correctly spaced
String[] A = "T T → T → T →".split(" ");

Stack<String> S = new Stack<>();

for (int i = 0; i < A.length; i++){
    String P = A[i];

    if (P.equals("∨")) {
        String X = S.pop();
        String Y = S.pop();
        if (X.equals("T") || Y.equals("T")) S.push("T");
        else S.push("F");

    } else if (P.equals("∧")) {
        String X = S.pop();
        String Y = S.pop();
        if (X.equals("T") && Y.equals("T")) S.push("T");
        else S.push("F");

    } else if (P.equals("→")) {
        String X = S.pop();
        String Y = S.pop();
        if (X.equals("T") && Y.equals("F")) S.push("F");
        else S.push("T");

    } else if (P.equals("¬")) {
        String X = S.pop();
        if (X.equals("T")) S.push("F");
        else S.push("T");

    } else S.push(P);
}

//S.pop()

T T → T → T → is equivalent to ((T → T) → T) → T in standard notation. The key insight here is that every operator is surrounded by a pair of Booleans.

Consider Reverse Polish Notation:

// Assumptions/Constraints: WFF, whitespaces correctly spaced
String[] A = "2 2 + 2 1 + * 2 +".split(" ");

Stack<String> S = new Stack<>();

for (int i = 0; i < A.length; i++){
    String X = A[i];

    if (X.equals("+")) S.push(S.pop() + S.pop());

    else if (X.equals("-")) {
        Double N = S.pop();
        Double M = S.pop();
        S.push(M - N);

    } else if (X.equals("/")) {
        Double N = S.pop();
        Double M = S.pop();
        S.push(M / N);

    } else if (X.equals("*")) S.push(S.pop() * S.pop());

    else S.push(Double.parseDouble(X));
}

//S.pop()

2 2 + 2 1 + * 2 + is equivalent to ((2 + 2) * (2 + 1)) + 2 in standard notation. The key insight here is every that every operator is surrounded by a pair of Numbers.

Also, Valid Parentheses:

String input = "({{{}[][][][][][][]}}()[])";

Stack<String> S = new Stack<>();

for (int i = 0; i < input.length(); i++){
    String P = String.valueOf(input.charAt(i));

    if (P.equals(")")) {
        if (S.size() > 0 && S.peek().equals("(")) S.pop();
        else S.push(P);

    } else if (P.equals("}")) {
        if (S.size() > 0 && S.peek().equals("{")) S.pop();
         else S.push(P);

    } else if (P.equals("]")) {
        if (S.size() > 0 && S.peek().equals("[")) S.pop();
        else S.push(P);

    } else S.push(P);
}

//S.pop()

Every pair of Parentheses must pair and cancel each other out.

A Stack variant of Math Equation Solver (eval or the like not allowed):

// Assumptions/Constraints: WFF, parentheses valid
String inputString = "((((12/3)/2)-(-5*4))+10)";

Stack<Character> P_stack = new Stack<>();
Stack<Double> N_stack = new Stack<>();
Stack<Character> O_stack = new Stack<>();
String currentNum = "";

for (int i = 0; i < inputString.length(); i++) {
    Character C = inputString.charAt(i);

    if (C.equals(left)) P_stack.push(C);

    else if (C.equals(right)) {
        if (currentNum.length() > 0) {
            N_stack.push(Double.valueOf(currentNum));
            currentNum = "";
        }

        if (P_stack.peek().equals(left)) {
            P_stack.pop();

            if (N_stack.size() > 1) {
                Double X = N_stack.pop();
                Double Y = N_stack.pop();
                Character O = O_stack.pop();

                if (O.equals(add)) N_stack.push(X + Y);
                if (O.equals(multi)) N_stack.push(X * Y);
                if (O.equals(div)) N_stack.push(Y / X);
                if (O.equals(sub)) N_stack.push(Y - X);
            }
        }
    }

    else if (C.equals(add) || C.equals(sub) || C.equals(multi) || C.equals(div)) {
        if (C.equals(sub)) {
            Character L = inputString.charAt(i-1); // Will never be first number with correct parentheticals
            if (L.equals(add) || L.equals(sub) || L.equals(multi) || L.equals(div) || L.equals(left)) currentNum += "-";
            else {
                O_stack.push(C);

                if (currentNum.length() > 0) {
                    N_stack.push(Double.valueOf(currentNum));
                    currentNum = "";
                }
            }

        } else {
            O_stack.push(C);

            if (currentNum.length() > 0) {
                N_stack.push(Double.valueOf(currentNum));
                currentNum = "";
            }
        }
    }

    else currentNum += C;
}

//N_stack.pop()

Same intuitions as before but using alternating Stacks.

  1. https://leetcode.com/problems/min-stack/
  2. https://leetcode.com/problems/maximum-frequency-stack/
  3. https://leetcode.com/problems/implement-stack-using-queues/
  4. https://leetcode.com/problems/design-a-stack-with-increment-operation/
  5. https://leetcode.com/problems/build-an-array-with-stack-operations/
  6. https://www.geeksforgeeks.org/problems/stock-span-problem-1587115621
  7. https://www.geeksforgeeks.org/problems/first-non-repeating-character-in-a-stream1216
  8. https://www.geeksforgeeks.org/problems/next-element-with-greater-frequency--170637

Code samples:

  1. https://github.com/Thoughtscript/java_algos/blob/main/src/main/java/io/thoughtscript/algos/ReversePolishNotationLogic.java
  2. https://github.com/Thoughtscript/java_algos/blob/main/src/main/java/io/thoughtscript/algos/ReversePolishNotation.java
  3. https://github.com/Thoughtscript/java_algos/blob/main/src/main/java/io/thoughtscript/algos/ValidParentheses.java
  4. https://github.com/Thoughtscript/java_algos/blob/main/src/main/java/io/thoughtscript/algos/MathEquationSolverStack.java

Data Structures: Queue

A "waiting line" (British "queue") and derived object that's FIFO (first in, first out).

Often implemented with an Array or LinkedList.

  1. https://www.codewars.com/kata/52a64cf14009fd59c6000994
  2. https://leetcode.com/problems/queue-reconstruction-by-height/
  3. https://leetcode.com/problems/implement-stack-using-queues/
  4. https://www.geeksforgeeks.org/problems/k-largest-elements4206
  5. https://www.geeksforgeeks.org/problems/kth-largest-element-in-a-stream2220
  6. https://www.geeksforgeeks.org/problems/kth-smallest-element5635

Code samples:

  1. https://www.codewars.com/kata/52a64cf14009fd59c6000994 <- using implicit ordering of JS keys for Priority Queue
  2. https://leetcode.com/problems/implement-stack-using-queues/ <- top answer
  3. https://github.com/Thoughtscript/java_algos/blob/main/src/main/java/io/thoughtscript/algos/gfg/queues/MinimumTotalCostReducingToOneElement.java

Data Structures: Sets

Most languages provide some implementation (or approximation) of the (the Pure Mathematics) Set Theoretic conception.

In Pure Mathematics, Sets are defined by the following:

Programmatically, Sets are generally characterized by the following (slightly weaker) constraints:

Refer to: Algorithmic Implementations of Common Set Operations

There are some important differences between the above and the pure math conception:

Note that Apache's Java Common Collection Utils supplies both a customized retainAll() and intersection() methods that return distinct Sets.

  1. https://docs.oracle.com/javase/8/docs/api/java/util/List.html#retainAll-java.util.Collection-
  2. https://www.baeldung.com/apache-commons-collection-utils

Data Structures: Useful

Some interesting Data Structures of note (that are typically variants of the above):

  1. Priority Queue - a Queue that sorts elements by some specified Comparison or Priority.

    • Define a Comparator to sort elements.
    • Exposes Queue functionalities: add() - Heap Sorted, and in O(logN) time, contains(), peek() - returns first element without removing it, poll() - removes and returns first element, etc.
    • Much faster than either (a) scratch bulding a Data Structure that's required to both store and sort each element or to (b) naively sort some existing Data Structure on each pass through a loop.
    • Remember that PriorityQueue doesn't keep all elements in its Heap sorted.
    • Instead, use poll() like so to extra the elements in successive order:
      // Function to merge k sorted arrays.
      public static ArrayList<Integer> mergeKArrays(int[][] arr, int K) {
        Queue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a));
            
        // O(K)
        for (int i = 0; i < K; i++) {
          // O(K)
          for (int j = 0; j < K; j++) {
              // O(log(K))
              pq.add(arr[i][j]);
          }
        }
      
        ArrayList<Integer> result = new ArrayList<>();
        while (pq.size() > 0) {
          result.add(pq.poll());
        }
      
        return result;
      }
      

      https://www.geeksforgeeks.org/problems/merge-k-sorted-arrays/1

  2. Tree Map - a Map that supports sorted Key Value pairs.

    • Define a Comparator to sort elements.
    • Exposes Map functionalities: put() - Heap Sorted, and in O(logN) time, containsKey(), get(), keySet(), etc.
  3. Tries and Prefix Trees.

    • Often used to store Strings, words efficiently.
    • Enables efficient individual character by character spellchecking or autocompletion.

    https://leetcode.com/problems/implement-trie-prefix-tree/

    Simple Trie for sorted lexicons:

    function buildTrie(...words) {
      words.sort()
      let trie = {}
    
      // evaluate each word
      for (let i = 0; i < words.length; i++) {
        const W = words[i]
      
        let current = trie
      
        // Simplified trie - no isWord
        for (let j = 0; j < W.length; j++) {
          const K = W.substring(0, j+1)
          if (!current[K]) current[K] = {}        
          if (j === W.length - 1) current[K] = null
          current = current[K]
        }  
      }
    
      return trie
    }
    

    https://www.codewars.com/kata/5b65c47cbedf7b69ab00066e/solutions/javascript

  1. https://geeksforgeeks.org/priority-queue-set-1-introduction/
  2. https://www.geeksforgeeks.org/treemap-in-java/
  3. https://www.geeksforgeeks.org/problems/merge-k-sorted-arrays/
  4. https://leetcode.com/problems/implement-trie-prefix-tree/
  5. https://www.codewars.com/kata/5b65c47cbedf7b69ab00066e/solutions/javascript