Part 1 · Lesson 8 · 24 min read
Arrays
Many values in one variable, numbered from 0. Reading, updating, looping and the first real interview questions on arrays.
On this page
- An array is a row of numbered lockers
- Creating, reading and changing
- Looping over an array
- Adding and removing: push and pop
- The four patterns behind most array questions
- Traced: finding the maximum
- Copying arrays: variables share the same array
- Built-in methods you will use every day
- Sorting numbers correctly
- Common array mistakes
- Practice questions (12)
- Make it stick
An array is a row of numbered lockers
Imagine you must store 100 exam marks. Making 100 variables (mark1, mark2, …) would be very hard to manage.
An array is one variable that holds a list of values in order. Think of a row of lockers. Each locker holds one value. Each locker has a number, called its index. The numbering starts at 0, not 1.
A data structure is a way to organise data in memory so that you can use it well. Arrays are the data structure that interviews ask about most. Almost every question in Parts 4–7 uses an array or a string. So this lesson and the next one are very important.
Creating, reading and changing
const marks = [72, 85, 90, 64];console.log(marks[0]); // 72 first item: index 0console.log(marks[2]); // 90console.log(marks.length); // 4 how many itemsconsole.log(marks[marks.length - 1]); // 64 last item: index length - 1console.log(marks[10]); // undefined (there is no locker 10)marks[1] = 88; // change one itemconsole.log(marks); // [ 72, 88, 90, 64 ]- First item:
arr[0]. - Last item:
arr[arr.length - 1]. - Valid indices:
0toarr.length - 1. An index outside this range givesundefined(“no value”).
Looping over an array
In Lesson 4 you saw that “start at 0 and use <” is the most common loop in DSA. Here is why. The variable i takes exactly the values 0 to length − 1. That is every valid index, once.
const nums = [4, 8, 15];// 1. Index loop: use it when you need the position ifor (let i = 0; i < nums.length; i++) { console.log(i, nums[i]);}// 2. for...of: use it when you only need the valuesfor (const x of nums) { console.log(x);}/* Output:0 41 82 154815*/Use the index loop when the position matters. For example, you need it to compare neighbours, to swap items or to return an index. Use for…of when you only need each value. (for…of is a loop that gives you each value of an array, one at a time.)
Adding and removing: push and pop
const stack = [1, 2];stack.push(3); // add to the END → [1, 2, 3]stack.push(4); // → [1, 2, 3, 4]const last = stack.pop(); // remove from the END, returns itconsole.log(last); // 4console.log(stack); // [ 1, 2, 3 ]const empty = []; // start empty and fill it in a loop (very common)for (let i = 1; i <= 3; i++) empty.push(i * i);console.log(empty); // [ 1, 4, 9 ]push is a method that adds an item to the end of an array. pop is a method that removes the last item and gives it back to you. (A method is a function that belongs to an object, here the array. You call it with a dot, like stack.push(3).) Both are fast, because no other item has to move.
There are also unshift and shift. They add and remove at the front. They can be slow on large arrays, because every other item may have to move one place. Lesson 12 explains why that matters.
The four patterns behind most array questions
| Pattern | Before the loop | Inside the loop | Examples |
|---|---|---|---|
| Accumulate | let sum = 0 | sum += arr[i] | sum, average, product |
| Best so far | let max = arr[0] | replace if better | max, min, longest word |
| Count | let count = 0 | if (…) count++ | count evens, count negatives |
| Search | — | if (arr[i] === target) return i | find an index, “does it contain…?” |
These are the loop patterns from Lessons 4–5, now used on arrays. Searching item by item from the start is called a linear search. Part 6 will show a much faster way when the array is sorted.
Traced: finding the maximum
Five numbers at indices 0 to 4.
▶const nums = [3, 7, 2, 9, 4];2let max = nums[0];3for (let i = 1; i < nums.length; i++) {4 if (nums[i] > max) {5 max = nums[i];6 }7}8console.log(max);| nums | [3, 7, 2, 9, 4] |
Copying arrays: variables share the same array
A variable does not hold the array itself. It holds a reference. A reference is the address of the place in memory where the array lives. When you write const b = a, you copy the address, not the lockers. Both variables then point to the same array:
const a = [1, 2, 3];const b = a; // NOT a copy — b points at the SAME arrayb.push(4);console.log(a); // [ 1, 2, 3, 4 ] ← a changed too!const c = [...a]; // a real copy (spread syntax)c.push(5);console.log(a); // [ 1, 2, 3, 4 ] unchangedconsole.log(c); // [ 1, 2, 3, 4, 5 ]This matters when you pass an array into a function. The function can change your array. When a question says “do it in place”, it wants exactly that: change the original array. When a question says “return a new array”, copy the array first. The [...a] form is called spread syntax. It takes every item out of a and puts them in a new array.
Built-in methods you will use every day
JavaScript arrays come with ready-made methods that do common jobs. Each one runs a loop inside, so it still takes time on a large array. But the methods make your code much shorter.
Searching and copying
const nums = [5, 3, 8, 3];console.log(nums.includes(8)); // true is 8 anywhere in the array?console.log(nums.indexOf(3)); // 1 first position of 3, or -1console.log(nums.lastIndexOf(3)); // 3 last position of 3console.log(nums.slice(1, 3)); // [ 3, 8 ] a COPY of positions 1 and 2 (position 3 is not included)console.log(nums.slice()); // [ 5, 3, 8, 3 ] a full copyRemoving and inserting in the middle: splice
const nums = [10, 20, 30, 40];nums.splice(1, 1); // at position 1, remove 1 itemconsole.log(nums); // [ 10, 30, 40 ]nums.splice(1, 0, 15, 25); // at position 1, remove 0 items, insert 15 and 25console.log(nums); // [ 10, 15, 25, 30, 40 ]splice changes the array itself. Every item after the change has to move, so it takes longer on large arrays. slice (without the “p”) never changes the original. It returns a copy of a part of the array. Do not mix them up.
map, filter and reduce
These three methods each take a small function and run it on every item. A function you pass to another function is called a callback. Here the callbacks are arrow functions (Lesson 7).
mapmakes a new array of the same length. Each new item is the callback's result for the old item.filtermakes a new array with only the items for which the callback returns true.reducecombines all items into one value. The callback receives the total so far and the next item. The last argument (here0) is the starting total.
Here they are in code:
const prices = [100, 250, 40, 80];// map: make a NEW array by changing every item (here: add 10 percent)const withTax = prices.map((p) => p * 1.1);console.log(withTax.map(Math.round)); // [ 110, 275, 44, 88 ]// filter: make a NEW array with only the items that pass a testconst cheap = prices.filter((p) => p < 100);console.log(cheap); // [ 40, 80 ]// reduce: combine all items into ONE valueconst total = prices.reduce((sum, p) => sum + p, 0);console.log(total); // 470| Method | Returns | Changes the original? |
|---|---|---|
includes, indexOf | true/false, a position | No |
slice, map, filter | a new array | No |
reduce | one value | No |
push, pop, splice, sort, reverse | (varies) | Yes |
Sorting numbers correctly
sort() is a method that puts the items of an array in order. With no arguments, it compares items as text. So "100" comes before "25", because “1” comes before “2”. For numbers, always pass a comparison function. This is a function that takes two items, a and b. If it returns a negative number, a goes first. If it returns a positive number, b goes first.
const nums = [10, 9, 1, 100, 25];console.log([...nums].sort()); // [ 1, 10, 100, 25, 9 ] ✗ sorted as TEXTconsole.log([...nums].sort((a, b) => a - b)); // [ 1, 9, 10, 25, 100 ] ✓ smallest firstconsole.log([...nums].sort((a, b) => b - a)); // [ 100, 25, 10, 9, 1 ] ✓ largest first(a, b) => a - b sorts from smallest to largest. sort changes the original array, so the examples sort a copy ([...nums]). Part 3 explains how sorting works inside.
Common array mistakes
| Mistake | Fix |
|---|---|
i <= arr.length — reads arr[length], which is undefined | Use i < arr.length |
let max = 0 on an array of negatives returns 0 | Start with arr[0] |
sort() on numbers without a comparison function | sort((a, b) => a - b) |
Forgetting the empty array [] | Ask what to return for it; check arr.length === 0 first |
b = a and expecting a copy | b = [...a] |
Practice questions
Return the sum of all numbers in the array.
Hint 1
Show all 3 solutions with explanations
Start sum at 0 and add arr[i] for every index.
function sumArray(arr) { let sum = 0; for (let i = 0; i < arr.length; i++) { sum += arr[i]; } return sum;}console.log(sumArray([3, 7, 2, 9])); // 21console.log(sumArray([])); // 0For an empty array, the loop never runs and 0 is returned. That is correct, with no special case.
You do not need the position, so loop over the values directly.
function sumArray(arr) { let sum = 0; for (const x of arr) sum += x; return sum;}console.log(sumArray([3, 7, 2, 9])); // 21This is shorter. It also avoids an off-by-one error (a mistake where the loop runs one time too many or too few).
reduce is an array method that combines all items into one value. Here it starts from 0 and adds each item.
const sumArray = (arr) => arr.reduce((sum, x) => sum + x, 0);console.log(sumArray([3, 7, 2, 9])); // 21console.log(sumArray([])); // 0The 0 at the end is the starting value. If you leave it out, reduce throws an error on an empty array.
All three are correct. In interviews, the loop versions are the clearest. reduce is common in everyday JavaScript.
Return the largest number in a non-empty array.
Hint 1
Hint 2
arr[0], not 0. Then arrays of negative numbers also work.Show all 3 solutions with explanations
Start with the first item. Replace it whenever a larger item appears.
function largest(arr) { let max = arr[0]; for (let i = 1; i < arr.length; i++) { if (arr[i] > max) max = arr[i]; } return max;}console.log(largest([3, 7, 2, 9, 4])); // 9console.log(largest([-5, -2, -9])); // -2It goes through the array one time (one pass). This is the traced example in the lesson.
Math.max takes separate numbers, not an array. The spread syntax ...arr turns the array into separate arguments.
const largest = (arr) => Math.max(...arr);console.log(largest([3, 7, 2, 9, 4])); // 9This is short and correct for normal sizes. For extremely large arrays (about a hundred thousand items or more), spreading can fail with an error, so the loop is safer.
After sorting from smallest to largest, the largest value is at the end.
function largest(arr) { const sorted = [...arr].sort((a, b) => a - b); return sorted[sorted.length - 1];}console.log(largest([3, 7, 2, 9, 4])); // 9This is correct, but sorting does far more work than one pass (Lesson 12). Mention it only to show that you know why it is not the best choice.
Use Approach 1. It does the least work and it works for every size of array.
How many numbers in the array are even?
Hint 1
Show all 2 solutions with explanations
This is the count pattern. The counter goes up only when the condition is true.
function countEvens(arr) { let count = 0; for (const x of arr) { if (x % 2 === 0) count++; } return count;}console.log(countEvens([1, 4, 6, 7, 10])); // 3The position does not matter here, so for…of is enough.
filter keeps only the items that pass a test. Keep the even values, then count how many were kept.
const countEvens = (arr) => arr.filter((x) => x % 2 === 0).length;console.log(countEvens([1, 4, 6, 7, 10])); // 3This is easy to read, but it builds a temporary array only to count it.
Approach 1 uses no extra memory. Approach 2 is fine for small inputs, and it reads like the problem.
Return the index of the first occurrence of target, or −1 if it is not there.
Hint 1
Hint 2
Show all 3 solutions with explanations
Return the first matching index. If nothing matches, return -1 after the loop.
function indexOf(arr, target) { for (let i = 0; i < arr.length; i++) { if (arr[i] === target) return i; } return -1;}console.log(indexOf([5, 3, 8, 3], 3)); // 1console.log(indexOf([5, 3, 8], 4)); // -1This is the search pattern. It needs the index, so it uses an index loop and not for…of.
Arrays already have a method for this.
console.log([5, 3, 8, 3].indexOf(3)); // 1console.log([5, 3, 8].indexOf(4)); // -1It does exactly what Approach 1 does: it checks items one by one. This tells you how long it takes on a big array: longer for more items.
findIndex returns the index of the first item that passes a test you give it. It returns -1 if no item passes.
const nums = [5, 3, 8, 3];console.log(nums.findIndex((x) => x > 6)); // 2 first item greater than 6Use it when you search for a condition and not for an exact value.
Be able to write Approach 1. Use the built-in methods in real code. If the array is sorted, binary search (Lesson 28) is much faster.
Reverse the array. Try to do it without creating a second array.
Hint 1
Hint 2
left and right, that move towards each other. Stop when they meet.Show all 3 solutions with explanations
A pointer is a variable that holds an index. Swap the two ends, move both pointers inwards, and repeat until they meet.
function reverseInPlace(arr) { let left = 0; let right = arr.length - 1; while (left < right) { const temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; left++; right--; } return arr;}console.log(reverseInPlace([1, 2, 3, 4, 5])); // [ 5, 4, 3, 2, 1 ]console.log(reverseInPlace([7])); // [ 7 ]| left | right | swap | array after |
|---|---|---|---|
| 0 | 4 | 1 ↔ 5 | [5, 2, 3, 4, 1] |
| 1 | 3 | 2 ↔ 4 | [5, 4, 3, 2, 1] |
| 2 | 2 | left < right is false — stop |
Walk from the last index to the first and push each item into a new array.
function reversed(arr) { const result = []; for (let i = arr.length - 1; i >= 0; i--) { result.push(arr[i]); } return result;}console.log(reversed([1, 2, 3])); // [ 3, 2, 1 ]This is simple, and the original array is unchanged. But it uses a second array of the same size.
arr.reverse() reverses the array in place (it changes the original).
const nums = [1, 2, 3];nums.reverse();console.log(nums); // [ 3, 2, 1 ]It changes the original array. Use [...nums].reverse() if you want to keep the original.
If the question says “in place” or “without extra memory”, use Approach 1. It is your first two-pointer solution. Lesson 21 builds a whole pattern on it.
Return true if the array is in non-decreasing order (each item is at least as big as the one before it).
Hint 1
Hint 2
Show all 3 solutions with explanations
If any item is smaller than the item before it, the array is not sorted.
function isSorted(arr) { for (let i = 1; i < arr.length; i++) { if (arr[i] < arr[i - 1]) return false; } return true;}console.log(isSorted([1, 2, 2, 5])); // trueconsole.log(isSorted([1, 3, 2])); // falseOne drop anywhere is enough to answer false, so the function can stop early.
every is an array method that checks whether a test is true for every item. It returns true or false.
const isSorted = (arr) => arr.every((x, i) => i === 0 || arr[i - 1] <= x);console.log(isSorted([1, 2, 2, 5])); // trueconsole.log(isSorted([1, 3, 2])); // falseThe callback's second argument i is the position of the item. Like Approach 1, every stops at the first failure.
Sort a copy and check whether it matches the original.
function isSorted(arr) { const sorted = [...arr].sort((a, b) => a - b); return sorted.every((x, i) => x === arr[i]);}console.log(isSorted([1, 3, 2])); // falseThis is correct, but sorting is much more work than one pass. Avoid it here.
Use Approach 1.
Return the second largest distinct value, or −1 if there isn't one.
Hint 1
Hint 2
first and second. When a new largest value arrives, the old largest becomes second.Show all 3 solutions with explanations
- Pass 1: find the largest value.
- Pass 2: find the largest value that is strictly smaller.
function secondLargest(arr) { let first = -Infinity; for (const x of arr) if (x > first) first = x; let second = -Infinity; for (const x of arr) if (x < first && x > second) second = x; return second === -Infinity ? -1 : second;}console.log(secondLargest([12, 35, 1, 10, 34, 1])); // 34console.log(secondLargest([10, 10, 10])); // -1-Infinity is a special JavaScript value that is smaller than every other number, so it is a safe starting value. This approach is easy to get right.
Track the largest and the second largest in the same loop.
function secondLargest(arr) { let first = -Infinity, second = -Infinity; for (const x of arr) { if (x > first) { second = first; // the old largest moves down first = x; } else if (x > second && x < first) { second = x; } } return second === -Infinity ? -1 : second;}console.log(secondLargest([12, 35, 1, 10, 34, 1])); // 34console.log(secondLargest([10, 10, 10])); // -1| x | rule | first | second |
|---|---|---|---|
| 12 | new largest | 12 | -∞ |
| 35 | new largest | 35 | 12 |
| 1 | nothing | 35 | 12 |
| 10 | nothing | 35 | 12 |
| 34 | between | 35 | 34 |
| 1 | nothing | 35 | 34 |
A Set is a collection that keeps each value only once (Lesson 10). Put the array in a Set to keep only distinct values. Then sort them and take the second from the end.
function secondLargest(arr) { const unique = [...new Set(arr)].sort((a, b) => a - b); return unique.length < 2 ? -1 : unique[unique.length - 2];}console.log(secondLargest([12, 35, 1, 10, 34, 1])); // 34console.log(secondLargest([10, 10, 10])); // -1This is short, but sorting does more work than needed. It is good to mention, but it is not the best answer.
Approach 2 is the answer interviewers look for (one pass, no extra memory). Approach 1 is a good first answer that you can then improve.
Given a sorted array, keep each value only once.
Hint 1
Hint 2
Show all 3 solutions with explanations
Push an item into the new array if it is the first item or if it is different from the previous one.
function uniqueSorted(arr) { const result = []; for (let i = 0; i < arr.length; i++) { if (i === 0 || arr[i] !== arr[i - 1]) result.push(arr[i]); } return result;}console.log(uniqueSorted([1, 1, 2, 3, 3, 3, 4])); // [ 1, 2, 3, 4 ]console.log(uniqueSorted([])); // []The input is sorted, so checking the neighbour is enough.
Keep a position k where the next unique value will be written (a “write pointer”). Copy each new value to arr[k]. At the end, the first k items are the answer.
function removeDuplicates(arr) { if (arr.length === 0) return 0; let k = 1; // arr[0] is always kept for (let i = 1; i < arr.length; i++) { if (arr[i] !== arr[k - 1]) { arr[k] = arr[i]; k++; } } return k; // number of unique values}const nums = [1, 1, 2, 3, 3, 3, 4];const k = removeDuplicates(nums);console.log(k, nums.slice(0, k)); // 4 [ 1, 2, 3, 4 ]This is exactly LeetCode 26: return the count and leave the unique values at the front of the array. No second array is used.
A Set is a collection that keeps each value only once (Lesson 10). Spread it back into an array.
const uniqueSorted = (arr) => [...new Set(arr)];console.log(uniqueSorted([1, 1, 2, 3, 3, 3, 4])); // [ 1, 2, 3, 4 ]This works even if the array is not sorted. But it uses extra memory and it is not “in place”.
For LeetCode 26 you must use Approach 2. Approach 1 is the easiest to write. Approach 3 is the everyday shortcut.
Move every element one position to the left; the first element goes to the end.
Hint 1
Hint 2
Show all 3 solutions with explanations
Save arr[0], move each item one place left, put the saved item last.
function rotateLeftOne(arr) { if (arr.length === 0) return arr; const first = arr[0]; for (let i = 1; i < arr.length; i++) { arr[i - 1] = arr[i]; } arr[arr.length - 1] = first; return arr;}console.log(rotateLeftOne([1, 2, 3, 4, 5])); // [ 2, 3, 4, 5, 1 ]This is the same idea as the swap in Lesson 2: save a value before you overwrite it.
shift() removes the first item and returns it. push adds it to the end.
const nums = [1, 2, 3, 4, 5];nums.push(nums.shift());console.log(nums); // [ 2, 3, 4, 5, 1 ]This is one line. But shift() may have to move every other item, so it can do about the same amount of work as Approach 1.
slice(1) copies everything after the first item. The answer is that copy, followed by the first item.
const rotateLeftOne = (arr) => [...arr.slice(1), arr[0]];console.log(rotateLeftOne([1, 2, 3, 4, 5])); // [ 2, 3, 4, 5, 1 ]This builds a new array and leaves the original unchanged. Lesson 19 extends the idea to rotating by k positions.
Approach 1 shows that you understand what happens to each item. Approaches 2 and 3 are fine in everyday code.
Move all zeros to the end, keeping the other numbers in their original order. Do it in place. (LeetCode 283.)
Hint 1
k. It means “where the next non-zero value should go”.Hint 2
Show all 3 solutions with explanations
Move the non-zero values to the front. Then fill the rest with zeros.
function moveZeros(arr) { let k = 0; for (let i = 0; i < arr.length; i++) { if (arr[i] !== 0) { arr[k] = arr[i]; k++; } } while (k < arr.length) { arr[k] = 0; k++; } return arr;}console.log(moveZeros([0, 1, 0, 3, 12])); // [ 1, 3, 12, 0, 0 ]| i | arr[i] | action | k after |
|---|---|---|---|
| 0 | 0 | skip | 0 |
| 1 | 1 | arr[0] = 1 | 1 |
| 2 | 0 | skip | 1 |
| 3 | 3 | arr[1] = 3 | 2 |
| 4 | 12 | arr[2] = 12 | 3 |
| — | fill arr[3], arr[4] with 0 | 5 |
When arr[i] is not zero, swap it with arr[k]. The zeros move back on their own.
function moveZeros(arr) { let k = 0; for (let i = 0; i < arr.length; i++) { if (arr[i] !== 0) { [arr[k], arr[i]] = [arr[i], arr[k]]; // swap two items in one line k++; } } return arr;}console.log(moveZeros([0, 1, 0, 3, 12])); // [ 1, 3, 12, 0, 0 ]This uses one loop and no second pass. Each swap moves a non-zero value forward and a zero backward.
Make a new array of the non-zero values. Then add the right number of zeros.
function moveZeros(arr) { const nonZero = arr.filter((x) => x !== 0); const zeros = new Array(arr.length - nonZero.length).fill(0); return [...nonZero, ...zeros];}console.log(moveZeros([0, 1, 0, 3, 12])); // [ 1, 3, 12, 0, 0 ]This is easy to read. But it creates new arrays, so it does not meet the “in place” rule of LeetCode 283.
For LeetCode 283, use Approach 1 or 2 (both are in place). Approach 2 is slightly shorter. Approach 1 is easier to trace.
Return an array where position i holds the sum of nums[0] to nums[i]. (LeetCode 1480.)
Hint 1
Show all 2 solutions with explanations
Add each item to total and push the total into a new array.
function runningSum(nums) { const result = []; let total = 0; for (const x of nums) { total += x; result.push(total); } return result;}console.log(runningSum([1, 2, 3, 4])); // [ 1, 3, 6, 10 ]This is the accumulator pattern. It saves the total after every step.
Each position becomes its own value plus the position before it (which is already updated).
function runningSum(nums) { for (let i = 1; i < nums.length; i++) { nums[i] += nums[i - 1]; } return nums;}console.log(runningSum([3, 1, 2, 10, 1])); // [ 3, 4, 6, 16, 17 ]This uses no extra array. A running total like this is called a prefix sum. Lesson 20 shows how it answers “sum of any range” questions very fast.
Both are correct for LeetCode 1480. Use Approach 2 if you may change the input. Otherwise, use Approach 1.
Given an array of 0s and 1s, return the length of the longest run of consecutive 1s. (LeetCode 485.)
Hint 1
Hint 2
Show all 2 solutions with explanations
Add 1 to current when you see a 1. Reset it to 0 when you see a 0. Keep the largest value it reaches.
function findMaxConsecutiveOnes(nums) { let current = 0, best = 0; for (const x of nums) { if (x === 1) { current++; best = Math.max(best, current); } else { current = 0; } } return best;}console.log(findMaxConsecutiveOnes([1, 1, 0, 1, 1, 1])); // 3console.log(findMaxConsecutiveOnes([1, 0, 1, 1, 0, 1])); // 2This joins two patterns: a counter that resets, and “best so far”. It needs one pass.
Join the array into text like "110111". Split the text at every 0. Then find the longest piece.
function findMaxConsecutiveOnes(nums) { const pieces = nums.join("").split("0"); // "110111" → ["11", "111"] return Math.max(...pieces.map((p) => p.length));}console.log(findMaxConsecutiveOnes([1, 1, 0, 1, 1, 1])); // 3This is a creative shortcut. It builds text and several arrays, so it uses much more memory than Approach 1.
Use Approach 1 for LeetCode 485. Approach 2 shows that a different way of thinking can give an unusual solution.
Make it stick
- Write the four array patterns (accumulate, best so far, count, search) from memory, one line each.
- Reverse
[a, b, c, d]in place on paper with left/right pointers. - Explain why
const b = adoesn't copy an array, and how to copy one.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
Next lesson: strings. A string is text. It works like an array of characters, but you can read it and not change it.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related series