A Practical Guide to Algorithms with JavaScript
Introduction
What is an algorithem?
- Algorithm is just the steps that you take to solve a problem
Why should you care?
- Engineer’s job is to solve problems
What will we cover today?
- Estimate generally how fast an algorithms is
- Use some techniques to optimize certain types of algorithms
- Get comfortable with recursion
- Implement a couple sorting and searching algorithms
- Understand the difference between Divide & Conquer and Dynamic Programming
- Learn about the pros and cons of the Greedy technique
- Cover a recursive brute force algorithms
Space & Time Complexity
Introducing Space & Time Complexity
- What make an algorithm fast
- Space complexity
- How much memory is used
- Time complexity
- How many primitive operations are executed
- Space complexity
… with respect to input size
… and assuming worst case scenarios
Problem:
- Given a list of hotels, return the price range of hotels in a given search result
- let’s write an algorithm to do the job
- We’d expect that the more data we have, the longer it will take to figure out the min and max required for the range
- However, as our dataset grows, the cost can grow really fast or slow
Approach #1: compare all numbers to one another
1 | var hotels = [ |
As our data grows, how much does our work increase?
# of hotels(n) | 3 | 5 | 10 | 100 |
---|---|---|---|---|
# Ops | 9 | 25 | 100 | 1000 |
We call this n^2, where n is the number of hotels. As n grows, the amount of work increase at that rate
- Appraoch #2: Track min & max
使用两个循环,第一个寻找最大值,第二个寻找最小值
How many comparisons were made?
Price | 200 | 50 | … | 175 |
---|---|---|---|---|
Max? | 200 | 200 | … | 200 |
Min? | 200 | 50 | … | 50 |
We consider this 2n beaucase as the data grows, the amount of work increases by 2
- Approach #3: Sorted list
如果已经是一个排序过的数组,那么明显只要查找两次即可。
# of Operations | Algorithm |
---|---|
n^2 | compare all numbers |
2n | Find min and max numbers |
2 | Sorted list, find first and last |
Big-O, name | # of Operations | Algorithm |
---|---|---|
O(n^2), quadratic | n^2 | compare all numbers |
O(n),linear | 2n | Find min and max numbers |
O(1), constant | 2 | Sorted list, find first and last |
Super fast | Super Slooooow | ||||
---|---|---|---|---|---|
Name | constant | logarithmic | linear | quadratic | exponential |
Notation | O(1) | O(logn) | O(n) | O(n^2) | O(k^n) |
Native Methods & JS Expressions
1 | // What are some simple, native JS methods/expressions/operations? |
Big O Notation
- Calculating Time
- What do we do if we have multiple expressions/loops/etc?
如果是常数操作,复杂度相加,例如三个加法,可视作O(3),简化为O(1)
如果套嵌循环,那么复杂度相乘,例如三重套嵌循环,复杂度O(n^3)
也就是循环内的操作的时间复杂度要与循环相乘
- Complexity of Common Operations
Complexity | Operation |
---|---|
O(1) | Running a statement |
O(1) | Value look-up on an array, object, variable |
O(logn) | Loop that cuts problem in half every iteration |
O(n) | Looping through the values of an array |
O(n^2) | Double nested loops |
O(n^3) | Tirple nested loops |
Space Complexity & Review
与时间复杂度类似,主要考虑每个操作是否创建了新的数据结构及数据结构的大小
Time complexity of an algorithms signifies the total time required by the program to run to completion. The time complexity of algorithms is most commonly expressed using the Big O notation
Big O notation gives us an industry-standard language to discuss the performance of algorithms.Not knowing how to speak this language can make you stand out as an inexperienced programmer
Did you know there are other notations that are typically used in academic settings? Learn more here
Big O: Loop
- What is the TC(time complexity)
1 | var countChars = function(str) { |
以上操作为线性(O(n))时间复杂度
Big O: Property Lookup
- What is the TC?
1 | var countChars = function (str) { |
上述函数为属性查找操作,时间复杂度为常数级。
Bio O: Push, Shift & Unshift
- What is the TC?
1 | var myList = ['hello', 'hola'] |
就像之前所说,删除或添加数组第一项,我们要将其他的项后移或者前移,时间复杂度O(n)
Optimization with Caching
Faster Algorithms
- Time Complexity?
1 | const isUnique = (arr) => { |
上面我们循环中套嵌了循环,所以时间复杂度为平方级(O(n^2))
- We can do better
1 | isUnique = (arr) => { |
在上面的方法中,我们使用一个新对象来保存已经循环过得数组的项,并不断查找数组项是否已经存在于对象中。用空间换取时间,时间复杂度为线性(O(n)),空间复杂度为(O(n))。
Exercise: Unique Sort
1 | //Task: Transform this simple sorting algorithm into a unique sort. |
Caching with Memoization
Memoization: caching the value that a function returns
1 | const factorial = (n) => { |
Exercise: Basic Memoization
1 | // Task 1: Write a function, times10, that takes an argument, n, and multiples n times 10 |
Exercise: Memoization with Closure
1 | // Task 3: Clean up your global scope by moving your cache inside your function. |
Exercise: Generic Memoize Funciton
1 | const times10 = (n) => (n * 10); |
Recursion
Introducing Recursion
Recursion is simply when a function calls itself; however it doesn’t stop there.
Why recursion?
- Elegant solution to keep your code D.R.Y.
- Expected CS knowledge
Call Stack Walkthrough
- Call Stack Game
- Push called Fn on stack
- Execute Fn body
- until…
- …another fn is called:
- Pause the current execution and start at step 1
- …a return is hit:
- Pop the current Fn off the stack
- Resume executing the previous Fn
递归可以看做是一种循环,为了不进入无限循环,首先要确定停止条件。
1 | var tracker = 0 |
Looping with Recursion
- Recursion in 4 steps
- Identify base case(s)
- Identify recursive case(s)
- Return where appropriate
- Write procedures for each case that bring you closer to the base case(s)
1 | var loopTimes = n => { |
Factorial with a Loop
1 | function computeFactorial(num) { |
1 | function computeFactorial(num, total = 1) { |
Recursion can always be implemented as a loop, but in some situations, believe it or not, it is simpler to use recursion
Tail-Call Optimization
Wrapper Function
- Common Patterns for Recursion
- Wrapper Funcitons
- Accumulators
1 | function wrapperFnLoop(start, end) { // 使用闭包记录变量 |
Accumulators
1 | function joinElements(array, joinString) { |
Exercise: Iterative Loop Exercise
1 | // Task: 将上面的代码使用循环重写 |
Exercise: Recursive Factorial & Memoize
1 | // Task 1: Without peeking, write your own recursive factorial method |
Divide & Conquer
分治法通过递归将复杂问题拆解为小问题进行求解。比较经典的就是二分查找。
Binary Search
- Search for a value in a sorted array by cutting the side of the search area in half
Linear Search
- Search for a value in an array by checking each value in order
Exercise: Linear Search
1 | // TASK: Implement linear search. |
Binary Search
1 | function binarySearch(list, item) { |
Divide & Conquer Review
- Recursive calls to a subset of the problem
- Recongnize base case
- Divide: Break problem down during each call
- Conquer: Do work on each subset
- Combine: Solutions
Sorting Types
Naive Sorts
- Keep looping and comparing values until the list is sorted
- Bubble Sort
- Insertion Sort
- Selection Sort
- Keep looping and comparing values until the list is sorted
Divide & Conquer Sorts
- Recursively divide lists and sort smaller parts of list until entire list is sorted
- Mergesort
- Quiksort
- Recursively divide lists and sort smaller parts of list until entire list is sorted
Bubble Sort
- Loop through an array, comparing adjacent indices and swapping the greater value to the end
- Merge sort
- Recursively merge sorted sub-list
Merge Sort
Concept: Merging Lists
- The merge step takes two sorted lists and merges them into 1 sorted list
Pseudocode: Merge Routine
1 | merge(L, R) |
- Concept: Merge Sort
- Step 1:
- Divide input array into ‘n’ single element subarrays
- Step 2:
- Repeatedly merge subarrays and sort on each merge
- Step 1:
- Pseudocode: Merge Sort
1 | mergeSort(list) |
mergeSort的时间复杂度为O(nlogn),因为递归的将数组拆分为多个子数组,并用线性的方式对子数组进行排序。
Exercise: Bubble Sort
1 | function bubbleSort(list) { |
1 | function swap(array, i, j) { |
Exercise: Merge Sort
1 | function merge(left, right) { |
Greedy Algorithms
Introducing Greedy
- Greedy algorithms always make the locally optimal choice!
例如寻找一个最短路径,使用贪算法,每次都会选择两点之间最短的路径,但实际上这距离可能不是最短的,所以使用贪心算法得出的往往不是最优解。在实际应用时,如果数据过于复杂庞大,那么使用贪心算法得出结果总比没有结果要好。
Greedy Algorithms Wlakthrough
You are the banker in Monopoly with your family who has lost many of the game pieces so you only have bills in these denominations:
$5 $10 $25
You need only pay out your family in the least number of bills possible so you don’t run out before the game is over. Write a function that calculates the least number of bills required for any given dollar amount that is divisible by 5.
1 | // Write a function, makeChange, that returns an integer that represents the least number of coins that add up to an amount where the amount is always divisible by 5. |
Brute Force
Would these values work with your greedy solution?
- coin values: 1, 6, 10
- input: 12
Greedy Approach:
- Always subtract the largest coin possible from the current amount.
Algorithmic Correctness
- Does your algorithm correctly solve the problem?
Brute Force Approach:
- Calculate every single combination possible and keep track of the minimum.
1 | let recursionCounter = 0 |
Dynamic Algorithms
Intriducing Dynamic Programming
Dynamic Approach:
- Cache values to avoid repeated calculations
DP Qualities:
- Optimal Substructure (tends to be recursive)
- Overlapping Subproblems
DP vs Divide & Conquer
DP Approaches:
- Top Down (recursive) vs Bottom Up (iterative)
Memoization with Recursion
1 | const cache = {} |