This tutorial is a part of the Data Structures and Algorithms class:
- A typical problem solved by Divide and Conquer algorithm
- Recursion implementation of sorting an array with Divide and Conquer
- FOR Loop (iterative) implementation of sorting an array
- Characteristics of problems can be solved by Divide and Conquer
- Summary
A typical problem solved by Divide and Conquer algorithm
-
Divide (1)
- Divide original problem into 2 (or more) easy to solve subproblems.
-
Conquer (solve (2) + combine (3))
- Solve easy subproblems then combine subproblem solutions.
Recursion implementation of sorting an array with Divide and Conquer
lecture-22/sort-array-recursion.js
// Sort array of numbers small -> big
// plug in your logic to these blocks to use divide and conquer
// Time: O(nlogn), Space: O(n)
const mergeSort = (arr) => {
// exit condition (base case)
if (arr.length === 1) return arr
// divide:
let leftArr = mergeSort(arr.slice(0, arr.length / 2)) // <- bottom up
let rightArr = mergeSort(arr.slice(arr.length / 2)) // <- bottom up
// solve: in this case, there's no solve step.
// because single element array is already a sorted array.
// combine:
const mergedArr = combine(leftArr, rightArr) // <- bottom up
console.log('leftArr:', leftArr)
console.log('rightArr:', rightArr)
console.log('mergedArr:', mergedArr)
console.log('============================')
return mergedArr
}
// Combine sorted left & right arrays
// Time: O(x + y), Space: O(x + y)
const combine = (leftArr, rightArr) => {
const mergedArr = []
// starting indexes
let leftArrIdx = 0
let rightArrIdx = 0
// combine two array in asc order until one is empty
while (leftArrIdx < leftArr.length && rightArrIdx < rightArr.length) {
if (leftArr[leftArrIdx] <= rightArr[rightArrIdx]) {
mergedArr.push(leftArr[leftArrIdx])
leftArrIdx++
} else {
mergedArr.push(rightArr[rightArrIdx])
rightArrIdx++
}
}
// push all numbers left in leftArr to mergedArr
while (leftArrIdx < leftArr.length) {
mergedArr.push(leftArr[leftArrIdx])
leftArrIdx++
}
// push all numbers left in rightArr to mergedArr
while (rightArrIdx < rightArr.length) {
mergedArr.push(rightArr[rightArrIdx])
rightArrIdx++
}
return mergedArr
}
const unsortedNumbers = [15, 26, 13, 7, 3, 5, 2, 22]
const sortedNumbersAscOrder = mergeSort(unsortedNumbers)
console.log(`Unsorted array: [${unsortedNumbers}]`)
console.log(`Sorted array: [${sortedNumbersAscOrder}]`)
$ node sort-array-recursion.js
leftArr: [ 15 ]
rightArr: [ 26 ]
mergedArr: [ 15, 26 ]
============================
leftArr: [ 13 ]
rightArr: [ 7 ]
mergedArr: [ 7, 13 ]
============================
leftArr: [ 15, 26 ]
rightArr: [ 7, 13 ]
mergedArr: [ 7, 13, 15, 26 ]
============================
leftArr: [ 3 ]
rightArr: [ 5 ]
mergedArr: [ 3, 5 ]
============================
leftArr: [ 2 ]
rightArr: [ 22 ]
mergedArr: [ 2, 22 ]
============================
leftArr: [ 3, 5 ]
rightArr: [ 2, 22 ]
mergedArr: [ 2, 3, 5, 22 ]
============================
leftArr: [ 7, 13, 15, 26 ]
rightArr: [ 2, 3, 5, 22 ]
mergedArr: [
2, 3, 5, 7,
13, 15, 22, 26
]
============================
Unsorted array: [15,26,13,7,3,5,2,22]
Sorted array: [2,3,5,7,13,15,22,26]
FOR Loop (iterative) implementation of sorting an array
lecture-22/sort-array-for-loop.js
// Sort array of numbers small -> big
const sortArrayForLoop = (arr) => {
// save final result to sortedArr
const sortedArr = [arr[0]]
console.log('sortedArr:', sortedArr)
// FOR loop to go through all elements
for(let i = 1; i < arr.length; i++) {
console.log('arr[i]:', arr[i])
let left = 0
let right = sortedArr.length - 1
let idxToInsert = null
// Binary Search `sortedArr` to find the position to insert
while(left <= right) {
let mid = Math.ceil((left + right) / 2)
console.log('left:', left, ', mid:', mid, ', right:', right)
// when mid = left and mid = right, our new element position must be
// either before or after mid. Compare elements to decide the idx #
if (mid === left && mid === right) {
if(arr[i] >= sortedArr[mid]) {
idxToInsert = mid + 1
} else {
idxToInsert = mid
}
break
} else if(sortedArr[mid] < arr[i]) { // continue with data points on the right
left = mid + 1
} else if(sortedArr[mid] > arr[i]) { // continue with data points on the left
right = mid - 1
} else { // edge cases (1), left = 1, mid = 2, right = 3
idxToInsert = mid + 1
break
}
}
// edge cases (2), left = 0, mid = 1, right = 1
if (idxToInsert === null) {
idxToInsert = right + 1
}
// insert element to sorted array
sortedArr.splice(idxToInsert, 0, arr[i])
console.log('idxToInsert:', idxToInsert)
console.log('sortedArr after insert:', sortedArr)
console.log('=====================')
}
return sortedArr
}
// Time: Binary Search nested in FOR Loop: O(nlogn), Space: O(n)
sortArrayForLoop([15, 26, 13, 7, 3, 5, 2, 22])
// sortArrayForLoop([-7,-5,-1,7,-4,-1,0,0,4,9]) // edge case (1), (2), loop runs forever
// sortArrayForLoop([-4,0,7,4,9,-5,-1,0,-7,-1]) // edge case (2), left, mid, right don't converge when (left = 0, mid = 1, right = 1)
$ node sort-array-for-loop.js
sortedArr: [ 15 ]
arr[i]: 26
left: 0 , mid: 0 , right: 0
idxToInsert: 1
sortedArr after insert: [ 15, 26 ]
=====================
arr[i]: 13
left: 0 , mid: 1 , right: 1
left: 0 , mid: 0 , right: 0
idxToInsert: 0
sortedArr after insert: [ 13, 15, 26 ]
=====================
arr[i]: 7
left: 0 , mid: 1 , right: 2
left: 0 , mid: 0 , right: 0
idxToInsert: 0
sortedArr after insert: [ 7, 13, 15, 26 ]
=====================
arr[i]: 3
left: 0 , mid: 2 , right: 3
left: 0 , mid: 1 , right: 1
left: 0 , mid: 0 , right: 0
idxToInsert: 0
sortedArr after insert: [ 3, 7, 13, 15, 26 ]
=====================
arr[i]: 5
left: 0 , mid: 2 , right: 4
left: 0 , mid: 1 , right: 1
left: 0 , mid: 0 , right: 0
idxToInsert: 1
sortedArr after insert: [ 3, 5, 7, 13, 15, 26 ]
=====================
arr[i]: 2
left: 0 , mid: 3 , right: 5
left: 0 , mid: 1 , right: 2
left: 0 , mid: 0 , right: 0
idxToInsert: 0
sortedArr after insert: [
2, 3, 5, 7,
13, 15, 26
]
=====================
arr[i]: 22
left: 0 , mid: 3 , right: 6
left: 4 , mid: 5 , right: 6
left: 6 , mid: 6 , right: 6
idxToInsert: 6
sortedArr after insert: [
2, 3, 5, 7,
13, 15, 22, 26
]
Characteristics of problems can be solved by Divide and Conquer
Problem | Input characteristics | Algorithm | Time Complexity |
---|---|---|---|
Can try with any problem type | - pass split test: problem characteristics are the same after splitting input into halves - usually array, string or graph |
Divide and Conquer | O(nlogn) |
lecture-22/sort-array-recursion.js
// plug in your logic to these blocks to use divide and conquer
// Time: O(nlogn), Space: O(n)
const mergeSort = (arr) => {
// exit condition (base case)
// to control how small you want to divide
if (arr.length === 1) return arr
// divide:
let leftArr = mergeSort(arr.slice(0, arr.length / 2)) // <- bottom up
let rightArr = mergeSort(arr.slice(arr.length / 2)) // <- bottom up
// solve: in this case, there's no solve step.
// because single element array is already a sorted array.
// combine:
const mergedArr = combine(leftArr, rightArr) // <- bottom up
return mergedArr
}
lecture-22/sort-array-for-loop.js
// Time: O(nlogn)
// Binary Search inside FOR loop, or FOR loop inside Binary Search
const sortArrayForLoop = (arr) => {
// FOR loop O(n)
for(let i = 1; i < arr.length; i++) {
// Binary Search O(logn) nested in FOR Loop
while(left <= right) {
}
}
}
Summary
Prob. class | P. sub-class | Input characteristics | Algorithm | Time Complexity |
---|---|---|---|---|
(almost) ANY problem class | array, matrix, ... | Brute Force | O(n) & up |
Can try with any problem type | - pass split test - array, string or graph, ... - divide, solve, combine gives a possible solution |
Divide and Conquer | O(nlogn) |
1. Maximization 2. Minimization |
array, string or graph, ... | Greedy choice, no guarantee for most optimal solution |
varies |
|
3. Find all options | array, matrix, ... | Brute Force | O(n) & up |
|
4. Find most optimal solution | ||||
5. Find a truth | search for a number | - sorted array or balanced BST |
Binary Search | O(logn) |
Note: we're assuming
array.splice
is an efficient (O(1)), low level built function in both recursion and iterative implementation of Divide and Conquer algorithm above.
Real life interview questions
All problems below can be solved without the Divide and Conquer algorithm. You can solve them by logical thinking and are not forced to use Divide and Conquer. However, thinking about a Divide and Conquer approach (if possible) is encouraged afterward.
- How does Divide and Conquer solve problems?
- What kind of problems can typically be solved by Divide and Conquer?
- Draw the callstack and recursion tree of the sorting array problem and explain why the console logs out like below. Explain what it means by saying
even with recursion, we're just looking at one data point at a time
.
$ node sort-array-recursion.js
leftArr: [ 15 ]
rightArr: [ 26 ]
mergedArr: [ 15, 26 ]
============================
leftArr: [ 13 ]
rightArr: [ 7 ]
mergedArr: [ 7, 13 ]
============================
leftArr: [ 15, 26 ]
rightArr: [ 7, 13 ]
mergedArr: [ 7, 13, 15, 26 ]
- Not looking at the given code, sort the same array from big -> small from scratch by yourself with both Recursion and FOR Loop. You should be able to quickly explain how the algorithm works and implement it within 15 mins.
- (Easy) Leetcode #169: Majority Element
- (Easy) Leetcode #191: Number of 1 Bits
- (Easy) Leetcode #190: Reverse Bits
- (Medium) Leetcode #912: Sort an Array
- (Medium) Leetcode #53: Maximum Subarray
- (Medium) Leetcode #654: Maximum Binary Tree
- (Medium) Leetcode #148: Sort List
- (Medium) Leetcode #775: Global and Local Inversions
- More on Leetcode #divide-and-conquer.