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