算法可视化面板使用说明
Visualize Your Code
The visual panel editor is now available online and can be accessed via:
https://labuladong.online/algo-visualize/
Additionally, an 'Edit' button has been added to the algorithm visualization panels embedded in this site and all related plugins. Clicking it allows you to directly modify and execute your code for visualization.
Currently, the visualization panel only supports JavaScript code. If you find it difficult to understand the JavaScript code in the visualization panel, I have written a Minimal JavaScript Tutorial specifically for using the visualization panel, which will help you get started in 5 minutes.
I hope you read this article to the end, because in the last part, I will teach you practical techniques for using the visualization panel in specific algorithm problem scenarios, guiding you step-by-step to efficiently analyze problems and the execution process of algorithms. Once you recognize the power of the visualization panel, you will find that it greatly enhances your understanding of algorithms.
Basic Usage
At the top are mainly control buttons and sliders, which can control the step-by-step execution of the code.
On the left is the code panel, where the currently executing line of code is highlighted. Clicking on the code can directly run it to the specified location. After clicking the 'Edit' button at the top, the left code panel turns into an editor, allowing you to directly modify and run the code.
Most Useful Technique
Clicking on the code to run it to a specific location is the most commonly used feature. This function is similar to the 'Run to Cursor' feature in an IDE when debugging code.
Usually, we don't start from the beginning and step through the algorithm execution one by one, but rather click on the code to jump to an interesting location and then step through it to observe the execution process.
On the right is the visualization area, which displays variables, data structures, stack information, recursion counts, etc. Hovering over a data structure allows you to view detailed information.
In the bottom left is the Log Console. If your code uses console.log
, the output will be printed here. The console.log
function has been enhanced to automatically add indentation to the output content based on the recursion depth, making it easier to observe the recursive process. You can take a close look at the output in the Log Console in the above image.
In the bottom right are some floating buttons that provide functions such as refreshing the panel, full-screen display, and showing/hiding the Log Console.
I recorded a video on Bilibili explaining and demonstrating the basic functions mentioned above. If you're interested, you can check it out:
Understanding these is enough to get you started with the visualization panel. You can play around with these features on the two panels.
The following content mainly covers the functions of the visual editor for executing your own code.
Enhanced console.log
A long time ago, I shared an article on Test-taking "Cheating" Techniques, where I introduced a trick for debugging recursive algorithms using only standard output during written exams. Therefore, I enhanced the console.log
function (JavaScript's print output function) in the visualization panel to help everyone observe the output of complex algorithms more intuitively.
The enhancements are as follows:
If you use
console.log
in the visualization panel, the output will be printed in the Log Console at the bottom left, and it will automatically add indentation based on the recursion depth.Clicking on the output in the console will mark all outputs with the same indentation with a dashed line, making it easier to understand which outputs are at the same recursion depth.
If you don't want to use automatic indentation, you can use the
console._log
method. This method is the original print output function and does not automatically add indentation.
Enough talk, let's see this panel:
The code outputs at the start and end of recursion. You can see that the console at the bottom left displays the following content, with outputs from recursive functions at the same level having the same indentation, making it easy to distinguish:
进入 traverse(1)
进入 traverse(2)
进入 traverse(3)
进入 traverse(4)
退出 traverse(4)
退出 traverse(3)
退出 traverse(2)
退出 traverse(1)
You can click the first character "进" or "退" in each line of output in the console. This will show a dashed line aligning all outputs with the same indentation, making it easier to see that "Entering traverse(1)" corresponds to "Exiting traverse(1)", "Entering traverse(2)" corresponds to "Exiting traverse(2)", and so on.
If you don't want to use the automatic indentation feature, you can change console.log
in your code to console._log
. This way, the output won't be automatically indented. Feel free to modify the code and try it out.
Visualizing Data Structures
The panel can visualize common data structures such as arrays, linked lists, binary trees, hash tables, and hash sets. Below is a detailed introduction on how to create these data structures.
Standard Library Data Structures
For JavaScript built-in data structures like arrays and hash tables, you can initialize and operate them as usual, for example:
What needs special attention are some specific data structures in LeetCode, such as the singly linked list ListNode
and the binary tree TreeNode
. Their basic definitions and usage are the same as on LeetCode. Here are some examples.
Singly Linked List
First, let's talk about the singly linked list. Each node in a singly linked list has val
and next
attributes, which are exactly the same as defined on LeetCode:
// Definition of a singly linked list node
// This type is built-in and can be used directly without re-declaration
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val)
this.next = (next===undefined ? null : next)
}
You can use the ListNode.create
method to quickly create a singly linked list. Here's a simple example:
Doubly Linked List
Each node in a doubly linked list has val, prev, next
attributes. The constructor is exactly the same as on LeetCode:
// Definition of a doubly linked list node
// This type is built-in and can be used directly without needing to declare it again
function DoubleListNode(val, prev, next) {
this.val = (val===undefined ? 0 : val)
this.prev = (prev===undefined ? null : prev)
this.next = (next===undefined ? null : next)
}
You can use the DoubleListNode.create
method to quickly create a doubly linked list, or you can manually assemble it. Here are some simple examples:
Binary Trees
Now let's talk about binary trees. Each node in a binary tree has val, left, right
attributes, and the constructor is consistent with LeetCode:
// Definition of a binary tree node
// This type is built-in and can be used directly without further declaration
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
You can use the TreeNode.create
method to quickly create a binary tree. Note that we use lists to represent binary trees, in the same way as how LeetCode represents binary trees. Here are two examples:
Each binary tree node has val, left, right
attributes, which are exactly the same as the definitions on LeetCode.
N-ary Tree
Each node in an N-ary tree has val, children
attributes. The constructor is identical to LeetCode's definition:
// Definition of a multi-way tree node
// This type is built-in and can be used directly without needing to declare it again
function Node(val, children) {
this.val = val;
this.children = children;
}
You can use Node.create
to create a multi-way tree. Pay attention to the representation method of multi-way trees. We still use lists to represent multi-way trees, in the order of level-order traversal, and use null
to separate each group of child nodes. For reference, you can check the LeetCode problem 429. N-ary Tree Level Order Traversal for examples.
Each multi-way tree node has val, children
attributes, which are exactly the same as the definitions on LeetCode.
Binary Heap (Priority Queue)
In my article Binary Heap Basics and Implementation, I explained the basic concepts of binary heaps, the code implementation of priority queues, and the core operations of floating up (swim
) and sinking down (sink
) using a visual panel.
Tip
For binary heaps (priority queues), the main APIs are the push, pop, peek
methods, which are used to insert elements and remove the top element of the heap.
In Binary Heap Basics and Implementation, I also used methods like _swim, _sink, _getLastNode, _makeNode
, but these are internal helper methods for binary heaps and are only used for teaching purposes, so they won't be introduced here.
The visual panel provides some shortcut methods to create max heaps and min heaps, and also supports using custom comparison functions to create binary heaps. Here are some examples:
Variable Hiding and Scope Promotion
Using @visualize global
can elevate a variable's scope to global, allowing some closure variables to remain visible in the right-side visualization panel.
Using @visualize hide
can hide variables, enabling you to conceal irrelevant data structures and prevent overcrowding in the visualization panel.
Let's illustrate the usage of these comments with an example. Suppose I want to implement a simple Stack
class with methods push, pop, peek
. I can leverage JavaScript function closures like this:
However, if you scroll to the last step, you'll notice that the stack
variable persists as an Object in the visualization panel, which is not meaningful and takes up a lot of space.
What we're more interested in is the items
variable, as we can observe the changes in this array to understand how the push, pop
methods work.
Since items
is declared within the Stack
function, it is not in the current scope when the code runs outside the function body, and thus it won't be visualized in the right panel.
To address this, we can use the @visualize global
comment to promote the items
variable to the global scope, and then use the @visualize hide
comment to hide the stack
variable in the right panel.
This achieves the desired effect. Click on the line stack.push(1);
and execute downwards. You'll see that the stack
variable no longer appears, and the update process of the items
array is displayed on the right:
Visualizing the Backtracking/DFS Recursive Process
Tip
Beginners can skip this section.
This content will be useful when you have studied the binary tree chapter or the dynamic programming/backtracking chapter, and need to visualize your recursive functions.
Recursive algorithms can be a headache for many readers. I previously wrote an article Understanding All Recursive Algorithms from a Tree Perspective, which theoretically abstracts the most basic thinking model and writing techniques for recursive algorithms.
Simply put: Think of the recursive function as a pointer on a recursive tree. Backtracking algorithms traverse a multi-way tree and collect the values of leaf nodes; dynamic programming decomposes problems and uses the answers to subproblems to derive the answer to the original problem.
Now, I have integrated the thinking model described in the article into the algorithm visualization panel, which will definitely help you understand the execution process of recursive algorithms more intuitively.
For recursive algorithms, comments like @visualize status
, @visualize choose
, and @visualize unchoose
can be very helpful. Let's go through them one by one.
Example of @visualize status
In a nutshell, the @visualize status
comment can be placed above a recursive function that needs visualization, to control the values of nodes in the recursive tree.
Let's look at a simple example. When explaining the Core Framework of Dynamic Programming, I drew the recursive tree for the Fibonacci problem, showing how larger problems at the top are gradually broken down:
How do we describe the process of the algorithm running? The recursive function fib
acts like a pointer on the recursive tree, continuously breaking down the original problem into subproblems. When the problem is broken down to the base case (leaf nodes), it starts returning the answers of subproblems layer by layer, deducing the answer to the original problem.
With the @visualize status
comment, this process can be seen very intuitively. The following panel shows the 27th step of the Fibonacci algorithm, attempting to calculate the value of fib(3)
:
The @visualize status
comment is written above the fib
function, and its specific meanings are:
Enable the recursive tree visualization for this
fib
function. Each recursive call will be visualized as a node on the recursive tree, and the value ofn
in the function parameters will be displayed as the state on the node.The
fib
function is regarded as a pointer traversing this recursive tree, and the branches on the stack path will be highlighted in bold.If the function has a return value, when the function ends and calculates the return value of a node, moving the mouse over this node will display the return value.
Practice
Please enter 27 in the step bar and press Enter to jump to the 27th step. Move the mouse over the node (2)
, and you can see fib(2) = 1
, indicating that the value of fib(2)
has been calculated.
Nodes (5), (4), (3)
on the recursive path have not been calculated yet, and moving the mouse over them will not display any return value.
Practice
Try clicking the forward and backward buttons on the visualization panel to let the algorithm run a few steps forward, understanding the growth process of the recursive tree in dynamic programming.
Practice
When the entire recursive tree is traversed, the original problem fib(5)
will be calculated. At that point, all nodes on the recursive tree will display return values.
Try dragging the progress bar to the end to see what the recursive tree looks like, and move the mouse over various nodes to see what is displayed.
The Fibonacci solution code shown earlier did not include a memoization, making it an exponential time complexity algorithm. Now let's see what impact adding memoization has on the growth of the entire recursive tree.
Practice
👇 Please drag the progress bar to the end of the algorithm to see what the recursive tree looks like.
Practice
👇 This is a version with memoization optimization. Please drag the progress bar to the end of the algorithm to see what the recursive tree looks like, and compare it with the recursive tree without memoization.
By visualizing the entire tree, can you now intuitively understand how dynamic programming eliminates overlapping subproblems through memoization?
Example of @visualize choose/unchoose
In a nutshell, @visualize choose/unchoose
comments are placed before and after recursive function calls to control the values on the branches of the recursion tree.
Let's create a simple backtracking problem. Suppose you need to write a generateBinaryNumber
function to generate binary numbers of length n
. For example, when n = 3
, it should generate the 8 binary numbers: 000, 001, 010, 011, 100, 101, 110, 111
.
The solution code for this problem is not complicated. Let me show you how to use @visualize choose/unchoose
comments to visualize the backtracking process:
You can move your mouse over any node in the recursion tree to display all the information on the path from the root node to that node.
Doesn't it make understanding the recursive algorithm code much more intuitive when you combine it with this recursion tree?
Visualizing the BFS Process
As mentioned in Binary Tree Thinking (Overview), the BFS algorithm is an extension of level-order traversal of binary trees.
Since recursion trees can be visualized, the BFS process can also be visualized. You can use @visualize bfs
to enable automatic generation of the exhaustive tree for BFS algorithms. The values on the nodes represent the elements in the queue, and @visualize choose/unchoose
comments can control the values on the branches.
Here's a simple example to demonstrate the visualization of the BFS process:
Scenario Practice Questions
Next, I will ask you questions based on specific algorithm problems, requiring you to efficiently analyze the problem and the algorithm's execution process using the visualization panel. I will provide the answers, so you can think about them first and then see my solutions.
This section primarily uses the "Execute to Specific Code Position" feature of the visualization panel: you can click on a part of the code, and the algorithm will execute up to that point, visualizing the process. Using this feature well can greatly enhance your understanding of algorithms.
Warm-Up
First, let's get familiar with the "Execute to Specific Code Position" feature. In the previously demonstrated linked list cycle detection algorithm, I already guided you through using it. I asked you to repeatedly click on the line if (slow === fast)
to observe the chasing process of the fast and slow pointers:
Why does clicking this part show the chasing of the fast and slow pointers? Because when this part of the code is executed, the fast
and slow
pointers have just moved, so clicking here allows you to see their complete movement process.
In the following exercises, please focus on thinking about the state of the algorithm when the code execution reaches a certain part. This way, you can better use the visualization panel to precisely control the execution of the algorithm.
Scenario One: Preorder, Inorder, and Postorder Traversal of a Binary Tree
Hands-On
The code above demonstrates the preorder, inorder, and postorder traversal of a binary tree, which should be familiar to everyone.
Please click the play button to view the execution flow of this code. During playback, the previous/next step buttons will turn into speed up/slow down buttons to adjust the playback speed.
Questions
You can see that the binary tree will be visualized, and the root
variable acts like a pointer moving through the tree. I want you to use the "Execute to Specific Code Position" feature to click on the appropriate places in the code to precisely control the movement order of the root
pointer.
Reset the panel and make the
root
pointer traverse the entire binary tree in preorder, i.e., in the sequence1,2,4,3,5,6
.Reset the panel and make the
root
pointer traverse the entire binary tree in inorder, i.e., in the sequence2,4,1,5,3,6
.Reset the panel and make the
root
pointer traverse the entire binary tree in postorder, i.e., in the sequence4,2,5,6,3,1
.
Answers
These questions are not difficult:
Repeatedly click on the part of the code
preorderResult.push(root.val)
.Repeatedly click on the part of the code
inorderResult.push(root.val)
.Repeatedly click on the part of the code
postorderResult.push(root.val)
.
Scenario Two: Observing the Growth of the Recursive Tree
Question
👇 This is the Fibonacci algorithm without memoization that we just discussed. Now, based on your understanding of recursive trees, click the appropriate location in the code so that with each click, the recursive tree grows by one node, until the recursive tree is fully formed.
Answer
You can continuously click on the line if (n < 2)
in the code to observe the growth process of the recursive tree.
Why does clicking this part show the growth process of the recursive tree? Because this part of the code is the base case of the recursive function, which is executed in every recursive call. Each node in the recursive tree corresponds to each recursive call, so clicking this if statement allows you to see the growth process of the nodes in the recursive tree.
This is a very commonly used technique to observe the recursive process.
Scenario Three: Quickly Obtain an Exhaustive Result
Question One
👇 This is the visualization panel for the permutation algorithm shown in the previous text. In Scenario Two, you observed the growth of the recursive tree for the Fibonacci problem. Now, please click on the appropriate location in the code to observe the growth of the recursive tree for the permutation problem.
Answer One
This is simple. Just keep clicking on the if (track.length === nums.length)
part of the code to observe the growth of the recursive tree.
Question Two
Reset the panel.
Now, I don't want to see the recursive tree grow node by node; it's not very interesting. I want you to click on the appropriate location in the code so that each click grows a "branch" of the recursive tree.
This is a common scenario in backtracking algorithms because the end of each "branch" is a leaf node, and leaf nodes usually store one of the exhaustive results generated by the backtracking algorithm. For example, in this problem, the leaf node at the end of each branch represents a permutation result.
Answer Two
Keep clicking on the res.push([...track])
part of the code. Each click will cause the backtracking tree to grow a "branch."
Because leaf nodes are essentially the nodes where the recursive tree stops growing, i.e., the base case of the backtrack
function. So, by clicking on the code in the base case, we can directly jump to the leaf nodes of the recursive tree, which is equivalent to growing a branch.
Scenario Four: Understanding Top-Down and Bottom-Up Thinking Patterns
Question
In my article Dynamic Programming Core Framework, I explained that the top-down recursive approach using a memo
memoization and the bottom-up iterative approach using a dp
array are essentially the same. The values in the dp
array and the memo
memoization are identical, and the two methods can be converted into each other.
Below, I will provide you with both the top-down recursive solution and the bottom-up iterative solution. Please complete the following tasks:
In the panel for the top-down recursive solution, click on the appropriate location in the code so that each click updates an element in the
memo
.In the panel for the bottom-up iterative solution, click on the appropriate location in the code so that each click updates an element in the
dp
.Compare the updates of the
dp
array and thememo
array in both solutions to understand that the top-down recursive solution and the bottom-up iterative solution are essentially the same.
Answer
In the panel for the top-down recursive solution, each click on the code segment
memo[n] = dp(memo, n - 1) + dp(memo, n - 2)
will update an element in thememo
array.In the panel for the bottom-up iterative solution, each click on the code segment
dp[i] = dp[i - 1] + dp[i - 2]
will update an element in thedp
array.It should be easy to understand with the help of the visualization panels. However, you might notice that the values of
memo[1]
anddp[1]
are different. This is because we setdp[1]
as a base case with an initial value, whereas in the recursive solution, the base case is directly returned and does not need to be set in thememo
.