- Published on
Algorithm Fundamentals
- Authors
- Name
- MASON Joey
- https://x.com/JoeyJoeMA
This article is not about algorithm basics in the traditional sense, but rather some points I've summarized after learning algorithms for a while that I think are quite important for beginners.
Intervals
The concept of intervals is actually very common in real life:
- How many floors do you need to climb from the 7th to the 18th floor?
- How many days are there from January 10th to January 30th?
- How many days are there between January 10th and January 30th?
With the concept of intervals, these can be easily solved:
(7, 18]
: From 7th to 18th floor, excluding 7th but including 18th, then you need to climb 18 - 7 = 11 floors.[10, 30]
: Including both 10th and 30th, then 30 - 10 + 1 = 21 days(10, 30)
: Excluding both 10th and 30th, then 30 - 10 - 1 = 19 days
Those who are good at summarizing have noticed: closed intervals use []
, open intervals use ()
, and the corresponding calculation formulas are:
[i, j]
: j - i + 1(i, j)
: j - i - 1(i, j]
: j - i
Don't underestimate this knowledge point - if you're not familiar with the concept of intervals, binary search can be a nightmare🥲
If you agree, please give me a 👍 at the bottom of the article.
Traversal
Let's look at the role of intervals in traversal. Here's the result:
For loop invariants, if it's a closed interval then include, if it's an open interval then exclude.
// [2, 28]
for (let i = 2; i <= 28; ++i) {}
// [2, 28)
for (let i = 2; i < 28; ++i) {}
There's not much to say about one-dimensional array traversal. Let's mainly look at two-dimensional array traversal. In dynamic programming, the direction and method of traversal have certain techniques.
Spiral Traversal
Spiral traversal is a bit similar to BFS
, requiring while
and for
loops to work together, along with four boundaries for contraction.
let count = 0
let top = 0,
left = 0,
bottom = m - 1,
right = n - 1
while (count < 25) {
// Traverse top row interval [left, right]
for (let i = left; i <= right; ++i) {
console.log(Arr[top][i])
count++
}
top++
// Traverse right column [top, bottom]
for (let i = top; i <= bottom; ++i) {
console.log(Arr[i][right])
count++
}
right--
// Traverse bottom row in reverse [left, right]
for (let i = right; i >= left; --i) {
console.log(Arr[bottom][i])
count++
}
bottom--
// Traverse left column in reverse [top, bottom]
for (let i = bottom; i >= top; --i) {
console.log(Arr[i][left])
count++
}
left++
}
Diagonal Traversal
As shown in the figure, how do we traverse in this order?
First, let's understand:
- Main diagonal: from top-left to bottom-right, coordinates
i === j
; property:i - j
is constant - Secondary diagonal: from bottom-left to top-right, coordinates
i + j === len - 1
; property:i + j
is constant
And this constant value happens to be equal to the index of diagonal line l
in diagonal traversal. (My personal summary using mathematical induction😂)
// Array size is 5 * 5, diagonal indices are [1, 4], so outer for loop traverses diagonals
for (let l = 1; l < len; ++l) {
for (let i = 0; i < len - l; ++i) { // i interval is [0, len - l]
const j = l + i // Main diagonal i - j is constant
console.log(arr[i][j])
}
}
for(let l = 1; l < len; ++l) {
for(let j = 0; j < len - l; ++j) { // Just swap i and j positions
const i = l + j
console.log(arr[i][j])
}
}
The secondary diagonal is left for you🐶 (Of course, I don't recommend memorizing by rote, you should master the basic principles, observe and think more.)
Recursion
Recursion is calling yourself, often used to handle complex problems in a more intuitive and effective way. For conventional recursion teaching, I suggest searching Google directly. Here I'll record some points I think are important for understanding recursion:
- Recursion must have termination conditions to prevent infinite loops
- Pay attention to
return
during recursion, be mindful of the output parameters - Make good use of recursion function input parameters to save and pass information
Recursive Order
Many recursion tutorials don't cover recursive order, but it's actually very important for understanding recursion.
/**
* 1
* / \
* 2 3
* / \ / \
* 4 5 6 7
*
* The go function below performs recursive order traversal of this binary tree
* Each node will be processed 3 times, at positions 1, 2, 3; actual traversal order:
*
* 1(1), 2(1), 4(1), 4(2), 4(3),
* 2(2), 5(1), 5(2), 5(3), 2(3),
* 1(2), 3(1), 6(1), 6(2), 6(3),
* 3(2), 7(1), 7(2), 7(3), 3(3), 1(3)
*
* Pre-order (root-left-right(1)) result: 1, 2, 4, 5, 3, 6, 7
* In-order (left-root-right(2)) result: 4, 2, 5, 1, 6, 3, 7
* Post-order (left-right-root(3)) result: 4, 5, 2, 6, 7, 3, 1
*/
funcion go(head) {
if(head == null) return;
// 1 Pre-order
go(head.left);
// 2 In-order
go(head.right);
// 3 Post-order
}
We often use more recursion with just pre-order + post-order, and the characteristics of post-order are very important - it can access information returned during the previous call stack's return phase.
Let's look at how to solve lc.206 Reverse Linked List (easy) using recursion:
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function (head) {
if (head === null || head.next == null) return head
const newHead = reverseList(head.next)
head.next.next = head
head = null
return newHead
}
Just 5 lines of code, but it's really hard for beginners to understand😂. Why does return newHead
directly give the final result?
The return newHead
in this recursion is below the recursive function, which is in the post-order position. Each return is the node pointer returned by the last round of recursive call stack, which is the pointer to the last node. In tree recursion, for example, when looking for a certain node, if found then return. For recursive calls in for loops, you need to handle them. The core pseudo-code is:
const find = () => {
for (let i = 0; i < list; ++i) {
if (find(list[i].children)) return true
}
}
Finally, one very important point about recursion: never try to challenge recursion depth with your brain, don't get caught up in it, your brain will stack overflow💥~