Algorithms and Data structures

 

Lesson 2

$ whoami

Senior Software Engineer

I like JS, React, movies, music and I'm a big fan of LOTR and Star Wars 🤓

May the Force be with you!

about 6 years with GlobalLogic

about 8 years in Web Development

        GitHub page

Speaker and mentor at GL JS community

Part of the program committee at Fwdays

Inna Ivashchuk

Agenda:

  • What is Data structure?
  • Types of Data structures
  • Linear DS
  • Non-Linear DS
  • Big O() of сommon Data Structure operations

What is Data structure?

A data structure is a specialized format for organizing, processing, retrieving and storing data.

What is a Data structure?

    How are data structures used

Storing data (DB)

Managing resources and services (OS)

Data Exchange (TCP/IP)

Ordering and sorting

Indexing

Scalebility (Apache Spark)

and many other places

Types of Data structures

JavaScript Data Types

Data Types

Primitive

Non-Primitive

null

undefined

number

boolean

string

symbol

Object

Data structure hierarchy

Linear Data Structures

An array is a collection of items stored at contiguous memory locations. The idea is to store multiple items of the same type together.

Can be one and multi-dimensional.

 

Array

      0                1                  2                 3

Array

20

10

3

13

9

0

1

2

3

4

20

10

3

13

9

How we perceive an array:

How it is stored in memory

Consecutive memory locations

RAM

Some data types

 An array

Array: types

20

10

3

13

9

0

1

2

3

4

20

10

3

13

9

2

11

9

3

9

0

1

2

3

4

20

10

3

13

9

2

11

9

3

9

0

1

2

3

4

100

1

33

3

One-dimensional array

Multidimensional array

Two-dimensional array

0

1

2

0

1

Array: types

// One-dimensional
const movies = [
  "Star Wars",
  "The Lord of the Rings",
  "Harry Potter",
  "The Matrix",
  "Dune",
];

console.log(movies[3]);

// Two-dimensional
const matrix = [
  [1, 2, 3],
  [4, 5, 6]
];

console.log(matrix[1][1]);

// Multidimensional
const  terrains = [
  ['desert', 'desert', 'grass', 'grass'],
  ['desert', 'grass', 'water', 'grass'],
  ['grass', 'grass', 'water', 'water'],
  ['grass', 'grass', 'grass', 'grass']
];

console.log(terrains[1][0]);

Stack is a linear data structure that follows a particular order in which the operations are performed. The order may be LIFO(Last In First Out) or FILO(First In Last Out).

 

Stack

Stack: JS implementation

// Stack class
class Stack {

    // Array is used to implement stack
    constructor() {
        this.items = [];
    }

	// push method
    push(element) {
        // push element into the items
        this.items.push(element);
    }

    // pop method
    pop() {
        // return top most element in the stack
        // and removes it from the stack
        // Underflow if stack is empty
        if (this.items.length == 0)
            return "Underflow";
        return this.items.pop();
    }

    // peek method
    peek() {
        // return the top most element from the stack
        // but does'nt delete it.
        return this.items[this.items.length - 1];
    }

    // isEmpty method
    isEmpty() {
        // return true if stack is empty
        return this.items.length == 0;
    }

    // printStack method
    printStack() {
        var str = "";
        for (var i = 0; i < this.items.length; i++)
            str += this.items[i] + " ";
        return str;
    }

}

const stack = new Stack();

// Adding element to the stack
stack.push(10);
stack.push(20);
stack.push(30);

// Printing the stack element
// prints [10, 20, 30]
console.log(stack.printStack());

// returns 30
console.log(stack.peek());

// returns 30 and remove it from stack
console.log(stack.pop());

// returns [10, 20]
console.log(stack.printStack());

A Queue is a linear structure that follows a particular order in which the operations are performed. The order is First In First Out (FIFO)

Queue

Queue: JS implementation

// Queue class
class Queue {
    // Array is used to implement a Queue
    constructor() {
        this.items = [];
    }

    // Functions to be implemented
    // enqueue method
    enqueue(element) {
        // adding element to the queue
        this.items.push(element);
    }

    // dequeue method
    dequeue() {
        // removing element from the queue
        // returns underflow when called
        // on empty queue
        if (this.isEmpty())
            return "Underflow";
        return this.items.shift();
    }

    // front method
    front() {
        // returns the Front element of
        // the queue without removing it.
        if (this.isEmpty())
            return "No elements in Queue";
        return this.items[0];
    }

    // isEmpty method
    isEmpty() {
        // return true if the queue is empty.
        return this.items.length == 0;
    }
    // printQueue method
    printQueue() {
        var str = "";
        for (var i = 0; i < this.items.length; i++)
            str += this.items[i] + " ";
        return str;
    }
}

// creating object for queue class
const queue = new Queue();
			

// Testing dequeue and pop on an empty queue
// returns Underflow
console.log(queue.dequeue());

// returns true
console.log(queue.isEmpty());

// Adding elements to the queue
// queue contains [10, 20, 30, 40, 50]
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
queue.enqueue(40);
queue.enqueue(50);
queue.enqueue(60);

// returns 10
console.log(queue.front());

// removes 10 from the queue
// queue contains [20, 30, 40, 50, 60]
console.log(queue.dequeue());

// returns 20
console.log(queue.front());

// removes 20
// queue contains [30, 40, 50, 60]
console.log(queue.dequeue());

// printing the elements of the queue
// prints [30, 40, 50, 60]
console.log(queue.printQueue());

Stack vs Queue

Last In First Out

LIFO

FIFO

First In First Out

A Linked List stores a collection of items in a linear order. Each element, or node, in a Linked list, contains a data item, as well as a reference, or link, to the next item in the list.

Linked List

Linked List

Node

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

Linked List: JS implementation

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

class List {
    constructor() {
        this.head = null;
        this.tail = null;
    }

    getNode(index) {
        if (index < 0) return 0;
        if (index === 0 && this.head) return this.head;

        let currentNode = this.head;
        while (index > 0 && currentNode && currentNode.next) {
            currentNode = currentNode.next;
            index--;
        }

        if (index !== 0) {
            return null;
        }
        return currentNode;
    }

    get(index) {
        return this.getNode(index).value;
    }

    push(value) {
        if (!this.head) {
            this.head = new Node(value);
            this.tail = this.head;
            return this;
        }

        this.tail.next = new Node(value);
        this.tail = this.tail.next;

        return this;
    }

    pop() {
        if (!this.head) {
            return null;
        }

        if (!this.head.next) {
            const temp = this.head.value;
            this.head = this.tail = null;
            return temp;
        }

        let currentNode = this.head;

        while (currentNode.next.next !== null) {
            currentNode = currentNode.next;
        }

        const temp = this.tail.value;
        this.tail = currentNode;
        currentNode.next = null;

        return temp;
    }

    remove(index) {
        const prev = this.getNode(index - 1);
        const current = this.getNode(index);

        if (prev && current && current.next) {
            prev.next = current.next;
        } else if (!prev && current) {
            this.head = current.next;
        }

        return current;
    }

    shift() {
        if (this.head) {
            const temp = this.head.value;
            if (this.head.next === null) {
                this.tail = null;
            }
            this.head = this.head.next;
            return temp;
        }

        return null;
    }

    unshift(value) {
        const newNode = new Node(value);
        newNode.next = this.head;
        this.head = newNode;
        return this;
    }

    toString() {
        let result = '';
        let current = this.head;
        while (current) {
            result += `${current.value}${current.next ? ', ' : ''}`;
            current = current.next;
        }
        return result;
    }
}

const list = new List();

list.push(1).push(2).push(3).push(4);

console.log(list.toString());

console.log(list.remove(1));
console.log(list.toString());
console.log(list.remove(0));
console.log(list.toString());
console.log(list.remove(333));
console.log(list.toString());

Non-Linear Data Structures

 A Hash table (also known as a Hash map) - stores a collection of items in an associative array that plots keys to values. A Hash table uses a hash function to convert an index into an array of buckets that contain the desired data item.

Hash Table

Hash Table: JS implementation

const WEIRD_NUMBER = 23;

function hash(str, limit = 53) {
    let result = 0;

    for(let i=0; i < Math.min(str.length, 100); i++) {
        result += (str.charCodeAt(i) - 96) * WEIRD_NUMBER;
    }

    return Math.abs(result % limit);
}


class HashTable {
    constructor() {
        this.array = new Array(10);
    }

    set(key, value) {
        const keyHash = hash(key, this.array.length);
        
        if(!this.array[keyHash]) {
            this.array[keyHash] = [];
        }
        const bucket = this.array[keyHash];
        bucket.push([key, value]);
    }

    get(key) {
        const keyHash = hash(key, this.array.length);
        const bucket = this.array[keyHash];

        for(let i = 0; i < bucket.length; i++) {
            const entries = bucket[i];
            if (entries[0] === key) {
                return entries[1];
            }
        }
            
        return;
    }

    keys() {
        const result = [];

        for (let i = 0; i < this.array.length; i++) {
            if(this.array[i]) {
                this.array[i].forEach(entry => {
                    result.push(entry[0]);
                });
            }
        }

        return result;
    }
}


const test = new HashTable();

test.set('black', '#000');
test.set('white', '#fff');
test.set('red', '#f00');
test.set('blue', '#00f');
test.set('green', '#0f0');

console.log(test);

console.log(test.get('green'));
console.log(test.get('black'));
console.log(test.get('red'));
console.log(test.get('white'));
console.log(test.keys());

Map vs Object

Map is a data collection type (in a more fancy way — abstract data structure type), in which, data is stored in a form of pairs, which contains a unique key and value mapped to that key. And because of the uniqueness of each stored key, there is no duplicate pair stored.

Use Map, when many changes expected

const map1 = new Map();

map1.set('a', 1);
map1.set('b', 2);
map1.set('c', 3);

console.log(map1.get('a'));
// Expected output: 1

map1.set('a', 97);

console.log(map1.get('a'));
// Expected output: 97

console.log(map1.size);
// Expected output: 3

map1.delete('b');

console.log(map1.size);
// Expected output: 2

Set

     A JavaScript Set is a collection of unique values of any type, whether primitive values or object references. The main things to know:

  • each value can only occur once in a Set
  • can hold any value of any data type.

'Ron'

25

'QA'

'QA lead'

MAP

Keys

'name'

'age'

'job'

'title'

Values

'Ron'

'John'

'Alex'

'Juli'

SET

Indices

0

1

2

3

Values

 A Tree stores a collection of items in an abstract, hierarchical way. Each node is associated with a key-value, with parent nodes linked to child nodes - or subnodes. There is one root node that is the ancestor of all the nodes in the tree.

Trees

Binary Tree

Root (onle one)

Child

Leaf (no children)

Binary Search Tree: JS implementation

class Node {
	constructor(value) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

class BinarySearchTree {
	constructor() {
		this.root = null;
	}
	// Add new Node with {value}
	add(value) {
		if(!this.root) {
			this.root = new Node(value);
			return this;
		}

		let temp = this.root;

		while(true) {
			if(temp.value < value) {
				if (!temp.right) {
					temp.right = new Node(value);
					return this;
				}

				temp = temp.right;
			} else {
				if(!temp.left) {
					temp.left = new Node(value);
					return this;
				}

				temp = temp.left;
			}
		}
	}

	// Find Node with {value}
	find(value) {
		let temp = this.root;

		if(!temp) {
			return null;
		}

		while(true) {
			if(temp.value === value) {
				return temp;
			} else if (temp.value < value) {
				if(!temp.right) {
					return null;
				} 
				temp = temp.right;
			} else {
				if (!temp.left) {
					return null;
				}
				temp = temp.left;
			}
		}
	}

	// Check if Node with {value} exists
	contains(value) {
		let node = this.find(value);
		if (node) {
			return true;
		}
		return false;
	}

	// Traversing
	//    B
	// A     C
	
	// A, B, C
	traverseInOrder(node = this.root) {
	   let res = []; 
	   function traverse(node) {
			if (node.left) {
				traverse(node.left)
			} 

			res.push(node.value)

			if (node.right) {
				traverse(node.right)
			}
	   }

	   traverse(node);

	   return res;
	}

	// B, A, C
	traversePreorder(node = this.root) {
		return node
		? [node.value, ...this.traversePreorder(node.left), ...this.traversePreorder(node.right)]
		: [];        
	}

	// A, C, B
	traversePostorder() {
		let res = []; 
		function traverse(node) {
			 if (node.left) {
				 traverse(node.left)
			 } 
 			 
			 if (node.right) {
				 traverse(node.right)
			}

			res.push(node.value)
		}
 
		traverse(this.root);
 
		return res;
	}
}

const tree = new BinarySearchTree();
tree.add(5).add(2).add(-1).add(6).add(100);

// console.log(tree);

//    5
//  2    6
//         100

console.log(tree.find(2));
console.log(tree.find(100));
console.log(tree.find(1000000));
console.log(tree.contains(2));
console.log(tree.contains(17));

console.log('Preorder  ', tree.traversePreorder());
console.log('Inorder ', tree.traverseInOrder());
console.log('Postorder ', tree.traversePostorder());

     A Heap is a tree-based structure in which each parent node's associated key value is greater than or equal to the key values of any of its children's key values.

    Heap is a special case of balanced binary tree data structure where the root-node key is compared with its children and arranged accordingly.

Heap

Heap: max and min

Max-Heap: In a Max-Heap the key present at the root node must be greatest among the keys present at all of it’s children. The same property must be recursively true for all sub-trees in that Binary Tree.

Min-Heap: In a Min-Heap the key present at the root node must be minimum among the keys present at all of it’s children. The same property must be recursively true for all sub-trees in that Binary Tree.

Heap: JS implementation

function Heap(data = []) {
	this.data = data;
	this.size = data.length;

	if (data.length > 1) {
		this.buildMaxHeap();
	}
}

Heap.prototype = {
	swap: function(i, j) {
		let temp = this.data[i];
		this.data[i] = this.data[j];
		this.data[j] = temp;
	},

	maxHeapify: function(i) {
		let leftIdx = 2*i + 1;
		let rightIdx = 2*i + 2;
		let largest = i;

		if ( leftIdx < this.size && this.data[leftIdx] > this.data[largest]) {
			largest = leftIdx;
		}

		if (rightIdx < this.size && this.data[rightIdx] > this.data[largest]) {
			largest = rightIdx;
		}

		if (largest !== i) {
			this.swap(largest, i);
			this.maxHeapify(largest);
		}
	},

	buildMaxHeap: function() {
		for(let i = Math.floor(this.size / 2); i >= 0; i--) {
			this.maxHeapify(i);
		}
	},

	bubbleUp: function() { 
		let curIdx = this.size - 1;	

		while(curIdx > 0) { // O(log(n)) as we deviding parent index by 2 every iteration
			let parentIdx = Math.floor((curIdx - 1)/ 2);
			let lastVal = this.data[curIdx];	
			let parentData = this.data[parentIdx];
			
			if (parentData >= lastVal) { return; }
			else {
				this.data[parentIdx] = lastVal;
				this.data[curIdx] = parentData;
				curIdx = parentIdx;
			}
		}
	},

	push: function(val) {
		this.data.push(val);
		this.size++;
		this.bubbleUp();
	},

	decrementSize: function() { this.size--; },

	print: function() { console.log(this.data) }
}



const heap = new Heap();

heap.push(20);
heap.push(10);
heap.push(30);
heap.push(1);
heap.push(100);

heap.print();

/*
Output: 
[ 100, 30, 20, 1, 10 ]

The tree looks like:

   100
   / \
  30 20
 / \
1  10

*/

 A Graph stores a collection of items in a nonlinear fashion. Graphs are made up of a finite set of nodes, also known as vertices, and lines that connect them, also known as edges. These are useful for representing real-world systems such as computer networks.

Graph

Graph: Weighted Graph

Weighted Graph: JS implementation

class PriorityQueue {
    constructor(){
      this.values = [];
    }
    enqueue(val, priority) {
      this.values.push({val, priority });
      this.sort();
    }
    dequeue() {
      return this.values.shift();
    }
    sort() {
      this.values.sort((a, b) => a.priority - b.priority);
    };
  }


  
class WeightedGraph {

    /**
     *  In graph theory and computer science, an adjacency list is a collection of unordered lists used to represent a finite graph
     */
    constructor() {
        this.adjacencyList = {};
    }

    /**
     * "Vertex" is a synonym for a node of a graph, i.e., one of the points on which the graph is defined and which may be connected by graph edges. 
     *  
     * Time complexity: O(1)
     */
    addVertex(vertex) {
        if(this.adjacencyList[vertex]) {
            console.warn(`"${vertex}" vertex already present into adjacency list. Overriding ${vertex} `);
        }

        this.adjacencyList[vertex] = [];
    }

    /**
     * For an undirected graph, an unordered pair of nodes that specify a line joining these two nodes are said to form an edge.
     * 
     * Time complexity: O(1)
     */
    addEdge(vertex1, vertex2, weight) {
        if(!this.adjacencyList[vertex1]) {
            console.info(`Creating vertex "${vertex1}"`)
            this.addVertex(vertex1);
        }

        if(!this.adjacencyList[vertex2]) {
            console.info(`Creating vertex "${vertex2}"`)
            this.addVertex(vertex2);
        }

        this.adjacencyList[vertex1].push({ node: vertex2, weight });
        this.adjacencyList[vertex2].push({ node: vertex1, weight });
    }

    // O(|E|)
    removeEdge(vertex1, vertex2) {
        this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter(val => val !== vertex2);
        this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter(val => val !== vertex1);
    }

    // O(|V| + |E|)
    removeVertex(vertex) {
        this.adjacencyList[vertex]
            .forEach(vertex2 => this.removeEdge(vertex, vertex2));

        delete this.adjacencyList[vertex];
    }

    /**
     * Breadth-first search (BFS) is an algorithm for traversing or searching tree or graph data structures. 
     * It starts at the tree root and explores all of the neighbor nodes at the present depth prior to moving on to the nodes at the next depth level.
     */
    breadthFirstSearch() {
        const res = [];
        const keys = Object.keys(this.adjacencyList);
        const queue = [keys[0]];
        const visited = { [keys[0]]: true };

        while(queue.length) {
            const node = queue.shift();
            res.push(node);
            this.adjacencyList[node].forEach(vrt => {
                if(!visited[vrt.node]) {
                    visited[vrt.node] = true;
                    queue.push(vrt.node);
                } 
            });
        }

        return res;
    }

    /**
     * Depth-first search (DFS) is an algorithm for traversing or searching tree or graph data structures.
     * The algorithm starts at the root node (selecting some arbitrary node as the root node in the case of a graph)
     * and explores as far as possible along each branch before backtracking.
     */
    depthFirstSearch() {
        const res = [];
        const keys = Object.keys(this.adjacencyList);
        const visited = {};

        const traverse = node => {
            if (!visited[node]) {
                visited[node] = true;
                res.push(node);
                this.adjacencyList[node].forEach(vrt => traverse(vrt.node))
            } 
        }

        traverse(keys[0]);

        return res;
    }

    /**
     * 
     * Dijkstra's algorithm (or Dijkstra's Shortest Path First algorithm, SPF algorithm)[1] is an algorithm for finding the shortest paths
     * between nodes in a graph, which may represent, for example, road networks. 
     * 
     * It was conceived by computer scientist Edsger W. Dijkstra in 1956 and published three years later.
     */
    dijkstra(vertex1, vertex2) {
        const previous = {};
        const dist = {};
        const keys = Object.keys(this.adjacencyList);
        const priorityQueue = new PriorityQueue();
        let smallest;

        // init
        keys.forEach(key => {
            if (key === vertex1) {
                dist[key] = 0;
                priorityQueue.enqueue(key, 0);
            } else {
                dist[key] = Infinity;
                priorityQueue.enqueue(key, Infinity);
            } 
            previous[key] = null;
        });

        // take smallest dist vertex
        while(priorityQueue.values.length) {
            smallest = priorityQueue.dequeue().val;

            if(smallest === vertex2) {
                // wohoo
                const res = [];
                let curr = previous[vertex2]
                while(curr) {
                    res.push(curr);
                    curr = previous[curr];
                }

                return res.reverse().concat(vertex2);
            } 

            this.adjacencyList[smallest].forEach(el => {
                // calc dist to neighbour node
                let candidate = el.weight + dist[smallest];
                if (dist[el.node] > candidate) {
                    dist[el.node] = candidate;
                    previous[el.node] = smallest;
                    priorityQueue.enqueue(el.node, candidate)
                }
            })
        }


    }

}

/**
*  Example:
*         
*         7
*      C --- D
*   1 /   3   \ 2
*    A ------- B
*  3 |    2    | 5
*    F ------- K
*  4 \    1   / 1
*     J ---- I
* 
*/   

const graph = new WeightedGraph();

graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('F');
graph.addVertex('K');
graph.addVertex('J');
graph.addVertex('I');

graph.addEdge('C', 'D', 7);
graph.addEdge('A', 'B', 3);
graph.addEdge('F', 'K', 2);
graph.addEdge('J', 'I', 1);

graph.addEdge('C', 'A', 1);
graph.addEdge('A', 'F', 3);
graph.addEdge('F', 'J', 4);
graph.addEdge('D', 'B', 2);
graph.addEdge('B', 'K', 5);
graph.addEdge('K', 'I', 1);

console.log(graph.breadthFirstSearch());
console.log(graph.depthFirstSearch());
console.log('A->D', graph.dijkstra('A', 'D'));
console.log('A->K', graph.dijkstra('A', 'K'));
console.log('A->I', graph.dijkstra('A', 'I'));

A Trie, also known as a keyword tree, is a data structure that stores strings as data items that can be organized in a visual graph.

 

Trie

Big O() of сommon Data Structure operations

Common Data Structure Operations

Big O for JavaScript array methods

const movies = [
  'LoTR',
  'Back to the Future',
];

// .push() - add a new element
values.push('Star Wars', 'Matrix');

// .pop() - remove the last one
values.pop();

// .reverse() - reverse an array
values.reverse();

// .unshift() // add a new elemnt
// to the beginning of an array
values.unshift('Avatar');

// .sort()
values.sort();

// .filter()
values.filter();

=>  O(1)

=>  O(1)

=>  O(n)

=>  O(n)

=>  O(n log (n))

=>  O(n)

Array Sorting Algorithms

Quiz time

Useful resources

Time complexity Big 0 for Javascript Array methods and examples can be found in this article

Q & A

Thank you!

Thanks to AFU

Keep working, learning, developing and definitely donate and help our Ukrainian Army and volunteers

Algorithms and Data structures: Part 2

By Inna Ivashchuk

Algorithms and Data structures: Part 2

  • 1,064