Algorithms and Data structures
Lesson 1
$ 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!
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:
- Introduction
- Is it necessary to learn Algorithms and Data structures?
- Complexity theory
-
Types of algorithms
-
Practical examples using JavaScript
Introduction
An algorithm is any well-defined computational procedure that takes some value, or set of values, as input and produces some value, or set of values, as output. An algorithm is thus a sequence of computational steps that transform the input into the output.
What is an algorithm?
„Software gets slower faster than hardware gets faster.“
Niklaus Virt
Start
Did you learn algorithms and data structures?
Time to learn 🤓
Congratulations 🥳
You are my hero
Resolved!
No
Yes
Let's check uses of Algorithms and Data structures
DOM: Tree
JS Event Loop: Heap, Queue, Stack
OS process priority: Heap & Priority Queue
Google Maps: Dijkstra algorithm and Weighted Graph
Google Search: Trie
Recommendation system/engine: Graph
....and many others
Is it necessary to learn Algorithms and Data structures?
"This is your chance to start learning Algorithms.
But after this, there is no turning back"
Yes?
No?
"Remember... All I'm offering is the truth, nothing more"
"Yes", if you would like to
become a true Software Engineer
pass interviews in the best companies in the world
create quality (productive and reliable) products
be able to solve really difficult problems
receive significantly more than the market average
Resources to train
"No", if you prefer to
make simple websites
don't understand the meaning of LinkedList, Heap, Stack and etc.
work on simple tasks
create a not complicated web servers (CRUD)
don't care about code quality and productivity
Complexity theory
Algorithm complexity
Big O notation, O
Omega notation, Ω
Theta notation, Θ
asymptotic notation, asymptotic complexity
O(n), O(log n), O(n * logn)
logarithmic polynomial
Don't be afraid
The complexity of an algorithm is a measure of the amount of time and/or space (memory) required by an algorithm for an input of a given size (n).
...it's just
Let's train on some JavaScript examples
...and the first example
// Find sum of all numbers in array
function sumNumbersInArray(arr) {
let result = 0;
for (let i=0; i < arr.length; i++) {
result += arr[i];
}
return result;
}
// Test 1
sumNumbersInArray([1,2,3]);
// Test 2
sumNumbersInArray([1,2,3,4,5,6,7,8,9,10]);
Big O notation, O
Big O notation, O
Big O notation is a convenient way to describe how fast a function is growing. It is often used in computer science when estimating time complexity.
Let's try to analyze the an example
// Find sum of all numbers in array
function sumNumbersInArray(arr) {
let result = 0;
for (let i=0; i < arr.length; i++) {
result += arr[i];
}
return result;
}
and the result
// Find sum of all numbers in array
function sumNumbersInArray(arr) {
let result = 0;
for (let i=0; i < arr.length; i++) {
result += arr[i];
}
return result;
}
Data Input
Operations number
100
1000
10000
100
1000
10000
O(n)
Linear
Example 1
// Calculation
function calculate() {
const a = 3 + 1;
const b = 3 + 3;
console.log('Calculating...');
return a + b;
}
calculate();
Let's analyze example 1
Data Input
Operations number
100
1000
10000
100
1000
10000
O(1)
Constant
Example 1.1
// Calculation
function calculate(c) {
const a = 3 + 1;
const b = 3 + 3;
console.log('Calculating...');
return a + b + c;
}
calculate(3);
How about that?
and it's still O(1)
Example 2
// Calculation
function calculate(arr) {
const a = 3 + 1;
const b = 3 + 3;
let c = 0;
for (let i = 0; i < arr.length; i++) {
c += arr[i];
}
console.log('Calculating...');
return a + b + c;
}
calculate([1,2,3,4,5]);
result - O(n)
Example 3
// Calculation
function calculate(arr) {
const a = 3 + 1;
const b = 3 + 3;
let c = arr.length;
console.log('Calculating...');
return a + b + c;
}
calculate([1,2,3,4,5]);
result - O(1)
Example 4
// Calculation
function calculationInArray(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
arr[i] = arr[i] + arr[j];
}
}
return arr;
}
calculationInArray([1,2,3,4,5]);
Data Input
100
1000
10000
100
1000
10000
O(n2)
quadratic
Example 4 - is quadratic
Operations number
Example 5
// Calculation
function calculationInArray(arr) {
const sum = 0;
arr.forEach(n => {
sum += n;
});
return sum;
}
calculationInArray([1,2,3,4,5]);
result - O(n)
Example 6
// Calculation
function calculationInArray(arr) {
arr.forEach(n => console.log(n));
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
arr[i] = arr[i] + arr[j];
}
}
return arr;
}
calculationInArray([1,2,3,4,5]);
result - O(n) + O(n2) = O(n2)
Example 7
// Calculation
function probablySimpleCalculation(arr) {
let result = 0;
arr.forEach(num => {
const additional = arr.indexOf(num) > 5 ? 5 : 1;
result = result + num + additional;
});
return result;
}
probablySimpleCalculation([1,2,3,4,5,6,7,8]);
result - O(n2), because of indexOf()
Example 7 can be improved
// Calculation
function probablySimpleCalculation(arr) {
let result = 0;
arr.forEach((num, index) => {
const additional = index > 5 ? 5 : 1;
result = result + num + additional;
});
return result;
}
probablySimpleCalculation([1,2,3,4,5,6,7,8]);
result - O(n)
Function growing in numbers
O(1) - 1
Input data - 10000 items
O(n) - 10 000
O(n2) - 100 000 000
O(n3) - 1 000 000 000 000
O(n!) - 100 000 000 000 000 0000 000 000 000000 000 0000 0000 000000 000 000 000 000000 0000000 000000 00000.....
FYI: log 1 = 0; log 10 = 1; log 100 = 2
Big O: Phone book example
-
O(1) (best case): Given the page that a person's name is on and their name, find the phone number.
-
O(log n): Given a person's name, find the phone number by picking a random point about halfway through the part of the book you haven't searched yet, then checking to see whether the person's name is at that point. Then repeat the process about halfway through the part of the book where the person's name lies. (This is a binary search for a person's name.)
-
O(n): Given a phone number, find the person or business with that number.
-
O(n log n): There was a mix-up at the printer's office, and our phone book had all its pages inserted in a random order. Fix the ordering so that it's correct by looking at the first name on each page and then putting that page in the appropriate spot in a new, empty phone book.
-
O(n2): A mistake occurred at the office, and every entry in each of the phone books has an extra "0" at the end of the phone number. Take some white-out and remove each zero.
Types of algorithms and their uses
Types of Algorithm
There are many types of Algorithms, but the fundamental types of Algorithms are:
Recursive Algorithm
Dynamic Programming Algorithm
Divide & Conquer Algorithm
Greedy Algorithm
Brute Force Algorithm
Backtracking Algorithm
Brute Force Algorithm
This is the most basic and simplest type of algorithm. A Brute Force Algorithm is the straightforward approach to a problem i.e., the first approach that comes to our mind on seeing the problem. More technically it is just like iterating every possibility available to solve that problem.
function bruteForceSubstringSearch(text, pattern) {
const textLength = text.length;
const patternLength = pattern.length;
for (let i = 0; i < textLength; i++) {
let j;
for (j = 0; j < patternLength; j++) {
if (text.charAt(i + j) !== pattern.charAt(j)) {
break;
}
}
if (j === patternLength) return i;
}
return textLength;
}
bruteForceSubstringSearch('this is a test', 'test');
Recursive Algorithm
This type of algorithm is based on recursion. In recursion, a problem is solved by breaking it into subproblems of the same type and calling own self again and again until the problem is solved with the help of a base condition.
Some common problem that is solved using recursive algorithms: Factorial of a Number, Fibonacci Series, Depth first search for Graph, etc.
// calculate n! using recursion
function factorial(n) {
if (n <= 1) {
return 1;
} else {
return factorial(n-1) * n;
}
}
factorial(5);
Divide & Conquer Algorithm
Divide and conquer algorithm recursively breaks down a problem into two or more sub-problems of the same or related type until these become simple enough to be solved directly.
Applications: Binary Search, Quicksort, Merge Sort, Median Finding, Matrix Multiplication, Closest Pair of Points, Strassen’s Algorithm and etc.
Divide & Conquer Algorithm: Quicksort
const items = [5,3,7,6,2,9];
function quickSort(items, left, right) {
let index;
if (items.length > 1) {
//index returned from partition
index = partition(items, left, right);
//more elements on the left side of the pivot
if (left < index - 1) {
quickSort(items, left, index - 1);
}
//more elements on the right side of the pivot
if (index < right) {
quickSort(items, index, right);
}
}
return items;
}
function swap(items, leftIndex, rightIndex){
const temp = items[leftIndex];
items[leftIndex] = items[rightIndex];
items[rightIndex] = temp;
}
function partition(items, left, right) {
//middle element
const pivot = items[Math.floor((right + left) / 2)],
i = left, //left pointer
j = right; //right pointer
while (i <= j) {
while (items[i] < pivot) { i++; }
while (items[j] > pivot) { j--; }
if (i <= j) {
swap(items, i, j); //sawpping two elements
i++;
j--;
}
}
return i;
}
// first call to quick sort
const sortedArray = quickSort(items, 0, items.length - 1);
console.log(sortedArray); //prints [2,3,5,6,7,9]
Divide & Conquer Algorithm: Palindrome
function palindrome(str) {
const len = str.length;
for (var i = 0; i < len/2; i++) {
if (str[i] !== str[len - 1 - i]) {
// When the characters don't match anymore,
// false is returned and we exit the FOR loop
return false;
}
}
return true;
}
sagas => sagas
tenet => tenet
Divide & Conquer Algorithm: Binary Search
function binarySearch(items, value){
let firstIndex = 0,
lastIndex = items.length - 1,
middleIndex = Math.floor((lastIndex + firstIndex)/2);
while(items[middleIndex] != value && firstIndex < lastIndex) {
if (value < items[middleIndex]) {
lastIndex = middleIndex - 1;
} else if (value > items[middleIndex]) {
firstIndex = middleIndex + 1;
}
middleIndex = Math.floor((lastIndex + firstIndex)/2);
}
return (items[middleIndex] != value) ? -1 : middleIndex;
}
const items = [1, 2, 3, 4, 5, 7, 8, 9];
console.log(binarySearch(items, 1));
console.log(binarySearch(items, 5));
Dynamic Programming Algorithm
Dynamic Programming is mainly an optimization over plain recursion, as a recursive solution can optimize it using Dynamic Programming. The idea is to simply store the results of subproblems so that we do not have to re-compute them when needed later.
Applications: Fibonacci Sequence, Longest Common Subsequence, Longest Increasing Subsequence, Longest Common substring, Bellman-Ford algorithm, Chain Matrix multiplication, Subset Sum and etc.
Dynamic Programming Algorithm: Fibonacci sequence
// recursive way
function fibonacci(num) {
if (num <= 1) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
}
fibonacci(51);
// using memoization
function fibonacci(num, memo) {
memo = memo || {};
if (memo[num]) return memo[num];
if (num <= 1) return 1;
return memo[num] = fibonacci(num - 1, memo) + fibonacci(num - 2, memo);
}
fibonacci(51);
- Time complexity: O(2n)
- Space complexity: O(n)
- Function calls: 20.365.011.074
- Time needed: 176.742ms
- Time complexity: O(2N)
- Space complexity: O(n)
- Function calls: 99
- Time needed: 0.000001ms
Backtracking Algorithm
Backtracking is an algorithmic technique for solving problems recursively by trying to build a solution incrementally, one piece at a time, removing those solutions that fail to satisfy the constraints of the problem at any point of time (by time, here, is referred to the time elapsed till reaching any level of the search tree).
Applications: Generating all Binary strings, N-Queens Problem, Knapsack Problem, Graph coloring Problem and etc.
Greedy Algorithm
Greedy is an algorithmic paradigm that builds up a solution piece by piece, always choosing the next piece that offers the most obvious and immediate benefit. So the problems where choosing locally optimal also leads to a global solution are best fit for Greedy.
Applications: Huffman Coding Compression, Travelling Salesman Problem, Selection Sort, Topological sort, Prim’s & Kruskal’s algorithms, Coin Change problem, Fractional Knapsack Problem and etc.
Greedy Algorithm: Huffman Coding Compression
class Node {
constructor(count, char, left, right) {
this.count = count;
this.char = char;
this.left = left;
this.right = right;
}
}
class HuffmanCoding {
constructor() {
this.code = {};
this.leafs = null;
this.histogram = {};
this.tree = null;
}
createHistogram(input) {
const histogram = {};
for (let i = 0; i < input.length; i++) {
const code = input.charCodeAt(i);
++histogram[code];
}
this.histogram = histogram;
}
// creates the forest with one tree for every char
createLeafs(histogram) {
this.leafs = Object.entries(histogram).map(([code, freq]) => {
const char = String.fromCharCode(code);
return new Node(freq, char, null, null);
});
}
// splits trees into small and big
splitTrees(forest) {
const sorted = forest.sort((a, b) => a.count - b.count);
const small = sorted.slice(0, 2);
const big = sorted.slice(2);
return [small, big];
}
createTree(forest) {
if (forest.length === 1) return forest[0];
const [small_trees, big_trees] = this.splitTrees(forest);
const new_tree = new Node(
small_trees[0].count + small_trees[1].count,
null,
small_trees[0],
small_trees[1]
);
const new_trees = [...big_trees, new_tree];
return this.createTree(new_trees);
}
isASCII(str) {
const test = /^[\x00-\x7F]*$/.test(str);
return test;
}
// Creates the code-words from the created huffman-tree
createCode(prefix, node) {
// empty root node
if (!node) return {};
// leaf node
if (!node.left && !node.right) {
return { [node.char]: prefix };
}
// recursive call
return {
...this.createCode(prefix + "0", node.left),
...this.createCode(prefix + "1", node.right),
};
}
encode(string) {
if (!this.isASCII(string)) {
return 'Invalid text';
}
this.createHistogram(string);
this.createLeafs(this.histogram);
const tree = this.createTree(this.leafs);
const code = this.createCode("", tree);
const encoded = Array.from(string).map((c) => code[c]);
return {
output: encoded,
code,
};
}
}
const huff = new HuffmanCoding();
console.log(huff.encode('Hello there'));
Practical examples
Task 1
const itemsArray = [
{ a: 1, b: 3 },
{ a: 3, b: 3 },
{ a: 6, b: 3 },
{ a: 10, b: 10 },
{ a: 41, b: 1 },
{ a: 0, b: 4 }
];
function filterAndExtendItems(array) {}
We need to filters an array of objects and keep only items, where a > 5 and extend them with a new field sum = a + b
Task 1: solution
const itemsArray = [
{ a: 1, b: 3 },
{ a: 3, b: 3 },
{ a: 6, b: 3 },
{ a: 10, b: 10 },
{ a: 41, b: 1 },
{ a: 0, b: 4 }
];
function filterAndExtendItems(items) {
return items.reduce((acc, item) => {
if (item.a > 5) {
acc.push({
...item,
sum: item.a + item.b,
});
}
return acc;
}, [])
}
Task 2
const inputStr = "Remember, all I'm offering is the truth. Nothing more.";
function calcStringSymbols(str, symbol) {}
We need to calculate the amount of a given symbol in a given string
Task 2: solution
const inputStr = "Remember, all I'm offering is the truth. Nothing more.";
function calcStringSymbols(str, symbol) {
let amount = 0;
for(let i=0; i<str.length; i++) {
if (str.charAt(i) === symbol) {
amount += 1;
}
}
return amount;
}
calcStringSymbols(inputStr, 'm'); // 4
Task 2: solution using Divide & Conquer
const inputStr = "Remember, all I'm offering is the truth. Nothing more.";
function calcStringSymbolsWithDC(str, symbol) {
let amount = 0;
const len = str.length;
for(let i=0; i < len/2; i++) {
if (str.charAt(i) === symbol) {
amount += 1;
}
if (str.charAt(len - i - 1) === symbol) {
amount += 1;
}
}
return amount;
}
calcStringSymbolsWithDC(inputStr, 'm'); // 4
Task 3
const config = {
audio: {
id: 1,
type: 'playlist',
},
video: {
id: 2,
format: '4k',
},
book: {
id: 3,
store: 'yakaboo',
},
podcast: {
id: 4,
online: true,
},
};
function getProductConfig(type, cb) {}
We need to call a given callback function with different config, based on a given product type
Task 3: solution
const config = {
audio: {
id: 1,
type: 'playlist',
},
video: {
id: 2,
format: '4k',
},
book: {
id: 3,
store: 'yakaboo',
},
podcast: {
id: 4,
online: true,
},
};
function getProductConfig(type, cb) {
return cb(config[type] || {});
}
We need to call a function with different config, based on a given product type
Task 4
const itemsArrayN = [
{ n: '1' },
{ n: '2' },
{ n: '3' },
{ n: '4' },
{ n: '5' },
];
function reverse(array) {}
Reverse an array with items
Task 4: solution
const itemsArrayN = [
{ n: '1' },
{ n: '2' },
{ n: '3' },
{ n: '4' },
{ n: '5' },
];
function reverse(array) {
return arr.reduceRight((acc, curr) => acc.concat(curr), [])
}
More algorithms are written using JavaScript:
Quiz time
Flowchart for algorithms representation
What is Flowchart?
A flowchart is a type of diagram that represents a workflow or process. A flowchart can also be defined as a diagrammatic representation of an algorithm, a step-by-step approach to solving a task.
Start
What to do?
Time to learn 🤓
Time to eat 😋
Lamp doesn't work
Lamp plugged in?
Plug in lamp
No
Replace bulb
Yes
Bulb burned out?
Repair lamp
Yes
No
Types
- Document flowcharts, showing controls over a document-flow through a system
- Data flowcharts, showing controls over a data-flow in a system
- System flowcharts, showing controls at a physical or resource level
- Program flowchart, showing the controls in a program within a system
In addition, many diagram techniques are similar to flowcharts but carry a different name, such as UML or activity diagrams.
UML
Flowchart of a Web application
Building blocks: common symbols
Flowline (Arrowhead) |
Shows the process's order of operation. A line coming from one symbol and pointing at another | |
Terminal |
Indicates the beginning and ending of a program or sub-process | |
Process |
Represents a set of operations that changes value, form, or location of data | |
Decision |
Shows a conditional operation that determines which one of the two paths the program will take | |
Input/Output |
Indicates the process of inputting and outputting data, as in entering data or displaying results |
ANSI / ISO shape
Name
Description
Building blocks: other symbols
Data File or Database |
Data is represented by a cylinder symbolizing a disk drive. | |
Document |
Single or multiple documents represented as a stack of rectangles with wavy bases | |
Manual operation |
Represent an operation or adjustment to a process that can only be made manually | |
Manual input |
Represented by quadrilateral, with the top irregularly sloping up from left to right, like the side view of a keyboard | |
Preparation or initialization |
Originally used for steps like setting a switch or initializing a routine |
Shape
Name
Description
As an example let's build a flowchart, that represents the sum operation
Start
sum = a + b
Enter numbers
a and b
Display sum
End
terminal
process
input and output
Start
Enter numbers
a and b
Display result
End
And now let's try to create a flowchart for the division operation
b is not equal to 0
b number is not valid
Yes
No
result = a / b
Before we go further, let's become familiar with Flowcharts
Algorithms and Data structures: Part 1
By Inna Ivashchuk
Algorithms and Data structures: Part 1
- 1,351