<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Apoorva on Medium]]></title>
        <description><![CDATA[Stories by Apoorva on Medium]]></description>
        <link>https://medium.com/@apoorvasn?source=rss-c1ddc9d92b89------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*TvL-fzSTW8q7SxCY6wkTgQ.jpeg</url>
            <title>Stories by Apoorva on Medium</title>
            <link>https://medium.com/@apoorvasn?source=rss-c1ddc9d92b89------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 11:58:33 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@apoorvasn/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[MiniShell : Building a Simple shell in C]]></title>
            <link>https://apoorvasn.medium.com/minishell-building-a-simple-shell-in-c-55a64a401a4f?source=rss-c1ddc9d92b89------2</link>
            <guid isPermaLink="false">https://medium.com/p/55a64a401a4f</guid>
            <category><![CDATA[42-network]]></category>
            <category><![CDATA[minishell]]></category>
            <category><![CDATA[system-programming]]></category>
            <category><![CDATA[shell]]></category>
            <category><![CDATA[c]]></category>
            <dc:creator><![CDATA[Apoorva]]></dc:creator>
            <pubDate>Sun, 06 Apr 2025 14:32:32 GMT</pubDate>
            <atom:updated>2025-04-06T14:32:32.049Z</atom:updated>
            <content:encoded><![CDATA[<h3>MiniShell : Building a Simple shell in C</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/481/1*A6sCqsPYv77lo5Fa0lpByw.png" /><figcaption>Credits: <a href="https://docs.rockylinux.org/books/bash_programming/01-Shell-overview/">docs.rockylinux.org</a></figcaption></figure><p>A year ago, when I was in the 42 piscine, I had to use the terminal for the first time to push code to GitHub. I can vividly remember how nervous and confused I was, playing around with the commands and finding even the most basic tasks difficult. Fast forward to today, and I am proud of the progress I’ve made, culminating in building a shell that operates similarly to the one I once found daunting. This project, a collaborative effort of a two-person team, marks a significant milestone in my journey.</p><p>In the world of Unix-like systems, the shell acts as the primary interface between the user and the system’s deeper functionalities.</p><p>This blog post examines the development of our “MiniShell,” a project intended to mimic the key aspects of sophisticated shells like Bash, but on a more rudimentary level. Our MiniShell is also an educational aid, providing for a hands-on experience with command parsing, process management, and the nuance of environment manipulation required to accommodate more sophisticated systems.</p><p>Working on this project solidified my C programming and widened my understanding of system internals such as file descriptors, process lifecycle, and signal handling. As one of the most challenging assignments at 42 school — and casually known as “miniHell” by our team — this challenge was as rewarding as it was difficult. In this post, I will describe the obstacles we encountered, the methods we employed to navigate through them, and the learnings we accumulated in the process.</p><h3>Project Overview</h3><p>what is Shell?</p><p>Shell is called a <strong>command interface</strong> or <strong>command interpreter</strong>. It provides a system-level interface program for users to send requests to the Linux kernel for running programs.</p><p>The objective of MiniShell project was to create a simplified yet functional version of a Unix-like shell using C programming language. This involved implementing several core features that are typical of more sophisticated shells like Bash.</p><h3>Key Functionalities</h3><ol><li><strong>Command Parsing and Execution:</strong></li></ol><ul><li><strong>Prompt Display:</strong> The shell continuously displays a prompt, waiting for user input.</li><li><strong>Input Tokenization:</strong> User inputs are broken down into tokens to interpret commands and their arguments.</li><li><strong>Command Execution:</strong> Based on the parsed input, the shell executes the corresponding commands either by invoking built-in functions or external programs.</li></ul><p><strong>2. Handling Redirections and Piping:</strong></p><ul><li><strong>Redirection: </strong>Handles input (&lt;), output (&gt;), and append (&gt;&gt;) redirections, allowing the user to direct the flow of data into and out of commands and files.</li><li><strong>Pipes:</strong> By using pipes (|), the output of one command can serve as the input to another, facilitating a chain of command executions that share data seamlessly.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/678/1*m4IvT-hW66kHBTkzmT8V4w.png" /><figcaption>Understanding a pipe</figcaption></figure><p><strong>3. Built-in Commands:</strong></p><ul><li>We included several built-in commands like echo, cd, pwd, env, export, and exit, which are handled internally without spawning new processes. This not only improves the performance but also simplifies the management of environment variables and the current working directory.</li></ul><p><strong>4. Environment and Signal Handling:</strong></p><ul><li><strong>Environment Management:</strong> The shell supports setting and retrieving environment variables.</li><li><strong>Signal Handling:</strong> We implemented handlers for CTRL-C, CTRL-D, and CTRL-\\ to manage shell termination and interruption, mimicking the behaviour seen in Bash.</li></ul><p><strong>5. Error Handling and Process Management:</strong></p><ul><li><strong>Robust Error Handling:</strong> The shell gracefully handles errors like incorrect commands or permissions issues without crashing.</li><li><strong>Process Management:</strong> Each command runs in a separate process, where the parent manages the lifecycle and ensures that all resources are cleaned up properly.</li></ul><h3>Development Approach</h3><p>The project was split into two main parts: parsing and execution, handled by my teammate and me, respectively. We used a modular design to keep components like parsing, executing, and signal handling distinct and manageable. Regular code reviews and paired programming sessions helped us maintain code quality and consistency across the project.</p><h3>Basic lifetime of a shell</h3><p>Let’s look at a shell from the top down. A shell does three main things in its lifetime.</p><ul><li><strong>Initialize</strong>: In this step, a typical shell would read and execute its configuration files. These change aspects of the shell’s behavior.</li><li><strong>Interpret</strong>: Next, the shell reads commands from stdin (which could be interactive, or a file) and executes them.</li><li><strong>Terminate</strong>: After its commands are executed, the shell executes any shutdown commands, frees up any memory, and terminates.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/678/1*vMGMSlAxHmYKaJKaNkUH4Q.png" /></figure><pre>int main(int argc, char **argv)<br>{<br> // Load config files, if any.<br>// Run command loop.<br> lsh_loop();<br>// Perform any shutdown/cleanup.<br>return EXIT_SUCCESS;<br>}</pre><h3>Program Flow of MiniShell</h3><h4>Initialization</h4><p>The journey of our MiniShell starts with the main function, where the shell environment is prepared. This includes:</p><ul><li><strong>Argument Verification:</strong> Using check_args(), the shell ensures it is started with the correct parameters.</li><li><strong>Environment Setup:</strong> The init_shell() function sets up the necessary shell environment, initializing variables and configuring the shell&#39;s state.</li></ul><pre>void init_shell(t_shell *mini, char **envv)<br>{<br> mini-&gt;running_status = 1;<br> mini-&gt;input = NULL;<br> mini-&gt;commands = NULL;<br> mini-&gt;last_exit_status = 0;<br> mini-&gt;expand_heredoc = 0;<br> set_envv(mini, envv);<br>}</pre><h4>Input Processing and Signal Handling</h4><p>Once initialized, MiniShell enters its main operational loop, continuously processing user input:</p><ul><li><strong>Signal Configuration:</strong> setup_sig_handling() configures how the shell responds to external signals like SIGINT (Ctrl+C) and SIGQUIT, ensuring that the shell remains responsive and user-friendly.</li></ul><pre>void setup_sig_handling(t_shell *mini)<br>{<br> struct sigaction sa_int;<br> struct sigaction sa_quit;<br><br> if (pipe(mini-&gt;signal_pipe) == -1)<br> {<br>  perror(&quot;pipe&quot;);<br>  exit (1);<br> }<br> sa_int.sa_handler = handle_sigint;<br> sigemptyset(&amp;sa_int.sa_mask);<br> sa_int.sa_flags = SA_RESTART;<br> sa_quit.sa_handler = SIG_IGN;<br> sigemptyset(&amp;sa_quit.sa_mask);<br> sa_quit.sa_flags = SA_RESTART;<br> if (sigaction(SIGINT, &amp;sa_int, NULL) == -1 || <br>  sigaction(SIGQUIT, &amp;sa_quit, NULL) == -1)<br> {<br>  perror(&quot;sigaction&quot;);<br>  exit(1);<br> }<br>}</pre><ul><li><strong>Environment Variables:</strong> set_env() is responsible for populating the shell’s environment, crucial for the proper execution of commands.</li></ul><h4>Command Loop</h4><p>The core of MiniShell’s functionality lies in its ability to process and execute commands:</p><ul><li><strong>Reading Input:</strong> take_input() leverages the readline library to capture user input, benefiting from built-in line editing and history features.</li><li><strong>Tokenization:</strong> The input string is broken down into manageable pieces or tokens by tokenize(), distinguishing between commands, options, and arguments.</li></ul><h4>Execution</h4><p>After parsing the input:</p><ul><li><strong>Token Expansion:</strong> expand_tokens() handles the expansion of environment variables and other necessary token transformations.</li><li><strong>Command Grouping:</strong> group_tokens_to_cmd() organizes tokens into commands and manages setups for piping and redirection.</li><li><strong>Command Execution:</strong> Finally, execute_minishell() takes the structured commands and either executes built-in functions or external programs.</li></ul><h4>Clean-up</h4><p>Post-execution, the shell cleans up to maintain efficiency and prevent memory leaks:</p><ul><li><strong>Freeing Resources:</strong> Functions like free_tokens() and free_commands() are called to release any allocated memory.</li><li><strong>Continuation Check:</strong> The loop continues based on the status flags set during execution, ensuring the shell only exits when prompted by the user.</li></ul><h3>Technical Challenges and Solutions</h3><h4>Challenge: Signal Handling</h4><p><strong>Problem:</strong> Ensuring that the shell correctly handles Unix signals like SIGINT without terminating unexpectedly or freezing.</p><p><strong>Solution:</strong> We implemented custom signal handlers that intercept these signals and appropriately dictate the shell’s response, allowing for a seamless user experience similar to more advanced shells.</p><h4>Challenge: Command Tokenization and Parsing</h4><p><strong>Problem:</strong> Correctly parsing user input into tokens while managing different command types, options, arguments, and handling special characters and quotations.</p><p><strong>Solution:</strong> We developed a tokenizer that respects quoted strings and correctly identifies and separates commands and their parameters. Implementing specific checks for the context, such as skipping expansion inside single quotes.</p><h4>Challenge: Memory Management</h4><p><strong>Problem:</strong> Managing memory efficiently in a shell that continuously allocates memory for new commands and their arguments.</p><p><strong>Solution:</strong> Through meticulous tracking and management, including regular use of gdb, Valgrind and a lot of print statements, we ensured that each allocation was paired with a corresponding deallocation, preventing memory leaks even during complex command executions.</p><h4>Challenge: Handling Redirections and Pipes</h4><p><strong>Problem:</strong> Implementing a flexible system for input/output redirection and inter-command piping that respects the precedence of redirections over pipes.</p><p><strong>Solution:</strong> We crafted a system that sets up file descriptors before command execution to manage the flow of data correctly between commands and external files, ensuring that the shell behaves predictably and efficiently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/496/1*TttcuFj_GztJ88fUpcIx0A.png" /></figure><p>Let’s consider an example command to check the flow:</p><p>cat file.txt | grep &#39;pattern&#39; &gt; output.txt</p><h4><strong>1. Input Reception</strong></h4><p>As with any command, the MiniShell starts by waiting for user input in its main loop. When the user enters cat file.txt | grep &#39;pattern&#39; &gt; output.txt, the command string is captured using the readline library via the take_input() function.</p><h4>2. Tokenization</h4><p>The received input string is passed to the tokenize() function, which processes the string to break it into manageable pieces (tokens):</p><ul><li>The tokenizer identifies cat, file.txt, |, grep, &#39;pattern&#39;, &gt;, and output.txt as individual tokens.</li></ul><h4>3. Token Expansion</h4><p>The expand_tokens() function processes each token to handle environment variables or other expansions. In this case, since there are no environment variables or expandable elements in this command, the tokens remain unchanged.</p><h4>4. Command Grouping</h4><p>Using group_tokens_to_cmd(), tokens are organized into command structures:</p><ul><li>The tokens are grouped by recognizing the pipe (|) as a command separator. This results in two command groups: one for cat file.txt and another for grep &#39;pattern&#39;.</li><li>The redirection (&gt; output.txt) is noted in the command structure of the grep command to redirect its output.</li></ul><h4>5. Execution Preparation</h4><p>The execute_minishell() function sets up the command execution:</p><ul><li>It validates the tokens using validate_tokens() to ensure there are no misplaced pipes or redirection symbols.</li></ul><h4>6. Command Execution</h4><p>The execute() function processes the command groups:</p><ul><li><strong>Piping Setup:</strong> The shell sets up a pipe between cat file.txt and grep &#39;pattern&#39; using the pipe() system call. This links the standard output of cat directly to the standard input of grep.</li><li><strong>Redirection Setup:</strong> For grep &#39;pattern&#39;, the output redirection is set up to write to output.txt instead of the terminal.</li><li><strong>Command Execution:</strong> Each command group is executed in turn:</li><li>cat file.txt reads the contents of file.txt and writes it to the pipe.</li><li>grep &#39;pattern&#39; reads from the pipe, filters lines containing &#39;pattern&#39;, and writes the result to output.txt.</li></ul><h4>7. Clean-up</h4><p>After executing the commands, resources are cleaned up:</p><ul><li><strong>Freeing Resources:</strong> free_tokens() and free_commands() are called to release memory used for tokens and command structures.</li><li><strong>Handling Pipes and Redirection:</strong> Any open file descriptors from pipes or redirections are closed to prevent resource leaks.</li></ul><h4>8. Continuation</h4><p>The shell checks if it should continue running and returns to the input prompt, waiting for the next command.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ce6JXSKRi4vAd6v2KCfiNA.png" /><figcaption>Minishell-Program flow</figcaption></figure><h3><strong>Error Handling</strong></h3><h4>Types of errors to look out for</h4><p>Examples of the types of errors that a simple shell may encounter and how they can be handled</p><p><strong>Incorrect command:</strong></p><p>If the user enters an incorrect command, the shell should display an error message indicating that the command was not recognized.</p><p><strong>Incorrect number of arguments:</strong></p><p>If the user enters a valid command but with an incorrect number of arguments, the shell should display an error message indicating the correct usage of the command.</p><p><strong>Permission denied:</strong></p><p>If the user attempts to execute a command for which they do not have the necessary permissions, the shell should display an error message indicating that permission was denied.</p><p><strong>System call error:</strong></p><p>If a system call made by the shell fails, the shell should display an error message indicating that the system call failed.</p><p>In summary, error handling is an important aspect of building a shell, and it requires careful attention to system calls, library functions, and signals. A shell should check return values, handle signals correctly, and provide meaningful error messages to the user when necessary. It’s important to provide informative error messages to the user in order to aid in debugging and troubleshooting.</p><p>Some C functions that can be used for error handling in a shell include:</p><p><strong>perror():</strong> This function is used to print an error message to the console. It takes a string argument that is printed before the error message and uses the value of the global errno variable to determine the error message to display.</p><p><strong>strerror()</strong>: This function takes an error code as an argument and returns a string describing the error. It can be used to provide more detailed error messages to the user.</p><p><strong>signal()</strong>: This function is used to register a signal handler function for a specific signal. The signal handler function is called when the specified signal is received by the process.</p><p><strong>waitpid()</strong>: This function is used to wait for a child process to exit. It takes the PID of the child process and a pointer to an integer that will hold the exit status of the child process when it exits. It returns the PID of the exited child process.</p><h3>Running the program</h3><p>Check and run the <a href="https://github.com/Apoorva-S-Natesh/minishell.git">code</a></p><pre>git clone<br>make<br>./minishell<br>cat input.txt | grep -i &quot;error&quot; | sort | uniq -c &gt; final.txt</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/764/1*dcPbi-RbaR0rw3uR2P3y-Q.jpeg" /></figure><h3>Reflections</h3><p>When we first started this project, I felt completely lost, unsure of where to begin. The idea of building a shell from scratch seemed overwhelming, but step by step, things started to make sense. Looking at examples and watching videos helped me grasp the fundamentals, and from there, we began with a simple hardcoded implementation before gradually expanding its functionality.</p><p>Throughout this journey, I gained a deeper appreciation for system programming, debugging, memory management and team work. Working on MiniShell was both a challenge and a learning experience that strengthened my understanding of Unix-like systems and C programming. I must also express my gratitude to my incredible teammate, <a href="https://github.com/arekiu">Alex!</a>. Her collaboration made this demanding project feel much more manageable and less stressful.</p><p>There’s always room for improvement. We still have issues with unclosed file descriptors that need to be fixed, and refining the handling of edge cases could make the shell even more robust.</p><p>Despite the hurdles, this project reinforced an essential lesson: the best way to learn is by building.</p><h4><strong>References and tools</strong></h4><p><a href="https://github.com/LucasKuhn/minishell_tester.git">Tester</a> by LucasKuhn</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=55a64a401a4f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Solving the Dining Philosophers Problem with Mutex Locks and C Programming]]></title>
            <link>https://apoorvasn.medium.com/solving-the-dining-philosophers-problem-with-mutex-locks-and-c-programming-cee5ac8d35e7?source=rss-c1ddc9d92b89------2</link>
            <guid isPermaLink="false">https://medium.com/p/cee5ac8d35e7</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[practice]]></category>
            <category><![CDATA[concurrency]]></category>
            <category><![CDATA[dining-philosophers]]></category>
            <category><![CDATA[c]]></category>
            <dc:creator><![CDATA[Apoorva]]></dc:creator>
            <pubDate>Sun, 12 Jan 2025 19:49:37 GMT</pubDate>
            <atom:updated>2025-01-12T20:03:32.423Z</atom:updated>
            <content:encoded><![CDATA[<h4>Threads of Thought: Philosophers at the Dinner Table</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eB_qh6nsMMsSx-ZEbaLxBw.jpeg" /></figure><h3>Introduction</h3><p>The Dining Philosophers Problem, originating from Edsger Dijkstra as a thought experiment in 1965 is one of computer science’s enduring challenges. It is often used to illustrate the challenges of allocating limited resources among multiple processes in a concurrent system. It provides a vivid scenario to explore the fundamentals of process synchronization and concurrency within computer science.</p><h3>Understanding the Problem</h3><p><strong>Description</strong>: X amount of philosophers sit at a round table with bowl of food. Forks are placed in front of each philosopher. There are as many forks as philosophers. All day the philosophers take turns eating, sleeping, and thinking. A philosopher must have two forks in order to eat, and each fork may only be used by one philosopher at a time. At any time a philosopher can pick up or set down a fork, but cannot start eating until picking up both forks. The philosophers alternatively eat, sleep, or think. While they are eating, they are not thinking nor sleeping, while thinking, they are not eating nor sleeping, and, of course, while sleeping, they are not eating nor thinking.</p><p><strong>Challenges Faced</strong>: The challenge is to design a system where each philosopher can think, eat and sleep without starvation. The main challenges include deadlock, where philosophers wait on each other indefinitely for forks, and starvation, where a philosopher never gets enough resources to eat. Race conditions can also occur, complicating the synchronization of thread operations.</p><h3>Technical Setup</h3><p><strong>Tools and Technologies Used</strong>: This solution was implemented in C as part of 42 Projects, using POSIX threads (pthreads) for handling multiple philosophers simultaneously and mutexes for managing access to the forks.</p><p><strong>Environment Setup</strong>: The solution is developed on a Linux system, utilizing GCC for compilation with flags for thread safety and performance optimization.</p><p>Before getting into the implementation details let us look at the basis of programming to understand what a process, thread is.</p><h3>Process vs Thread</h3><p>Building a house is a process, but the people doing the work to install the pipes, paint the walls and do the electrical work are all the working threads within the process of building the house.</p><p>I like to use threads synonymously with “unit of work” or “thing that executes a specific piece of work / action for the process to complete successfully.</p><p>So if I run chrome, and have 10 tabs open, I will have 10 processes running. Within each process are 1 or many units of work (called threads) responsible for memory management, access, logic, etc.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zUbbrGM8lEnFgckLcs_J-w.png" /><figcaption>Image credits: <a href="https://www.youtube.com/watch?v=4rLW7zg21gI&amp;ab_channel=ByteByteGo">https://www.youtube.com/watch?v=4rLW7zg21gI&amp;ab_channel=ByteByteGo</a></figcaption></figure><p>Processes are isolated executable files in memory, while threads are units of execution within those processes sharing memory space.</p><h4>Summary</h4><p>🖥️ Program Definition: A program is an executable file containing processor instructions. Recognizing that a program is merely a file until executed helps clarify the transition to processes, emphasizing the importance of execution context.</p><p>🚀 Process Creation: When a program is executed, it becomes a process and requires resources managed by the OS.</p><p>🔒 Process Isolation: Each process has its own memory address space, preventing corruption by other processes.</p><p>🧵 Thread Function: A thread is a unit of execution within a process, with its own stack but sharing memory space with other threads.</p><p>⚙️ Context Switching: The OS uses context switching to manage process and thread execution on the CPU, which is resource-intensive.</p><p>⏩ Efficiency of Threads: Context switching is faster between threads than processes due to shared memory, reducing overhead.</p><p>🔄 Alternative Scheduling: Mechanisms like fibers and coroutines minimize context switching costs by allowing applications to manage scheduling.</p><p>🔗 Memory Protection: Process isolation is vital for system stability, particularly in multi-process applications like web browsers, which utilize this to enhance user experience.</p><p>🧩 Threads vs. Processes: Understanding that threads share memory space while processes don’t is key for developers in designing efficient applications and managing data.</p><h3>Common Issues in Thread Programming</h3><p>When working with threads in C, there are several common issues that programmers may encounter. These include:</p><ul><li><strong>Race Conditions</strong>: A race condition occurs when two or more threads can access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don’t know the order in which the threads will attempt to access the shared data. Therefore, the result of the change in data is dependent on the thread scheduling algorithm, i.e. both threads are “racing” to access/change the data.</li><li><strong>Deadlocks</strong>: Improper handling of resource acquisition and release can lead to deadlocks, where threads are stuck indefinitely waiting for each other. Deadlocks happen when we lock the same mutex twice or more or more often due to the order in which the mutexes are locked and unlocked.</li><li><strong>Resource Management</strong>: Managing shared resources such as memory or file handles among multiple threads requires careful coordination to avoid conflicts and corruption.</li><li><strong>Synchronization</strong>: Coordinating the execution of multiple threads to ensure correct program behaviour often involves the use of synchronization primitives like mutexes, semaphores, and condition variables.</li><li><strong>Performance Overhead</strong>: Creating and managing threads can introduce overhead in terms of memory and CPU usage, impacting the overall performance of the application if not managed efficiently.</li><li><strong>Debugging Challenges</strong>: Debugging multi-threaded programs can be challenging due to the non-deterministic nature of thread execution and timing-dependent bugs.</li><li><strong>Platform Dependency</strong>: Thread behaviour may vary across different operating systems and platforms, requiring platform-specific considerations for thread programming.</li></ul><h3>Key Implementation Details</h3><ol><li><strong>Thread Management</strong>: Each philosopher is represented as a thread. This parallelism simulates the simultaneous actions of thinking, eating, and sleeping.</li><li><strong>Mutex Usage</strong>: Mutex locks are crucial. Each fork has an associated mutex to ensure that no two philosophers can hold the same fork at the same time, thus preventing deadlock.</li><li><strong>State Management</strong>: I maintained the state of each philosopher (eating, thinking, sleeping) using shared variables protected by mutexes, ensuring that changes in state were mutually exclusive among threads.</li><li><strong>Monitoring and Control:</strong> A separate monitoring thread checks continuously to ensure no philosopher is starved and the program can terminate correctly once all philosophers have eaten sufficiently or when one of the philosopher dies.</li></ol><p>🔑 <strong><em>Simulating Deadlocks:</em></strong> Introducing delays can help demonstrate deadlocks in a controlled manner, useful for educational purposes and debugging.</p><p>🔑 <strong><em>Preventive Measures:</em></strong> Standardizing lock acquisition order across threads can significantly reduce the risk of deadlocks in complex applications.</p><h3>Core Functions Explained</h3><ol><li><strong>Checking Philosophers’ Status</strong></li></ol><pre>char	control_simu(t_rcs *data)<br>{<br>	int	i;<br>	i = 0;<br>	pthread_create(&amp;data-&gt;monitor, NULL, monitor_routine, (void *)data);<br>	while (i &lt; data-&gt;num_philo)<br>	{<br>		if (pthread_create(&amp;data-&gt;philos[i].thread, NULL, philo_routine, <br>				(void *)&amp;data-&gt;philos[i]) != 0)<br>		{<br>			printf(&quot;\\033[0;31mFailed to create philosopher thread\\033[0m \\n&quot;);<br>			return (0);<br>		}<br>		i++;<br>	}<br>	join_threads(data);<br>	return (1);<br>}</pre><p>It begins by creating a monitoring thread that oversees the entire simulation, ensuring conditions like starvation or task completion are appropriately handled. Then, individual threads for each philosopher are spawned, each executing a routine that simulates the philosopher’s activities such as eating, thinking, and sleeping. If any thread fails to initiate, an error is reported, and the function exits with failure. After initializing all philosopher threads and the monitor thread, join_threads(data) is called to ensure that the main thread (typically the one running main()) waits for all other threads to complete before proceeding. This is essential to prevent the program from exiting prematurely and to ensure all resources are properly released.</p><p><strong>2. Action: Eating</strong></p><pre>void	eating(t_philo *philo)<br>{<br>	if (philo-&gt;id % 2)<br>	{<br>		pthread_mutex_lock(philo-&gt;right_fork);<br>		log_action(&quot;has taken a fork&quot;, philo-&gt;id, philo-&gt;s_data, &quot;\\033[0;32m&quot;);<br>		pthread_mutex_lock(philo-&gt;left_fork);<br>		log_action(&quot;has taken a fork&quot;, philo-&gt;id, philo-&gt;s_data, &quot;\\033[0;32m&quot;);<br>	}<br>	else<br>	{<br>		pthread_mutex_lock(philo-&gt;left_fork);<br>		log_action(&quot;has taken a fork&quot;, philo-&gt;id, philo-&gt;s_data, &quot;\\033[0;32m&quot;);<br>		pthread_mutex_lock(philo-&gt;right_fork);<br>		log_action(&quot;has taken a fork&quot;, philo-&gt;id, philo-&gt;s_data, &quot;\\033[0;32m&quot;);<br>	}<br>	ft_start_eating(philo);<br>	log_action(&quot;is eating&quot;, philo-&gt;id, philo-&gt;s_data, &quot;\\033[0;34m&quot;);<br>	precise_sleep(philo-&gt;eat_time);<br>	pthread_mutex_unlock(philo-&gt;left_fork);<br>	pthread_mutex_unlock(philo-&gt;right_fork);<br>	pthread_mutex_lock(&amp;philo-&gt;s_data-&gt;meal_lock);<br>	philo-&gt;eating = 0;<br>	philo-&gt;meals_eaten++;<br>	pthread_mutex_unlock(&amp;philo-&gt;s_data-&gt;meal_lock);<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/964/1*0_r9stTd2GznGlMzWX2ohg.png" /></figure><p>Philosophers with odd IDs first pick up the right fork and then the left, while those with even IDs do the opposite, preventing a circular wait condition that could lead to deadlock. Once both forks are secured, the philosopher is logged as eating, and the action is timed to simulate the duration of the meal. After eating, the forks are released in the opposite order they were picked up to further minimize potential deadlocks. Finally, the meal’s completion is recorded under the protection of a meal_lock mutex to safely update the shared state variables eating and meals_eaten, ensuring that these critical sections are not accessed concurrently by other threads, thus maintaining data integrity throughout the simulation.</p><p><strong>3. Logging Actions</strong></p><pre>void	log_action(char *action, int i, t_rcs *data, char *color)<br>{<br>	unsigned long	timestamp;	<br>        pthread_mutex_lock(&amp;data-&gt;logging);<br>	timestamp = get_time_ms() - data-&gt;philos[i -1].start_time;<br>	pthread_mutex_lock(&amp;data-&gt;death_lock);<br>	if (data-&gt;dead != 1)<br>	{<br>		printf(&quot;%lu %s%d %s\\033[0m\\n&quot;, timestamp, color, i, action);<br>	}<br>	pthread_mutex_unlock(&amp;data-&gt;death_lock);<br>	pthread_mutex_unlock(&amp;data-&gt;logging);<br>}</pre><p>The function first locks the logging mutex to prevent other threads from writing simultaneously, ensuring that log entries are not interleaved and remain readable. It then calculates the time elapsed since the start of the simulation for that particular philosopher. Another lock, death_lock, is used to check if the simulation is still active before logging the action. This dual-lock mechanism ensures that actions are only logged when the philosopher is active and the simulation has not been terminated.</p><p><strong>4. Precise Timing</strong></p><p>This function marks the start time and continuously checks against the current time until the desired duration has passed, pausing execution in very short intervals (0.5 milliseconds) to prevent the thread from monopolizing CPU resources.</p><pre>void	precise_sleep(unsigned long duration)<br>{<br>		unsigned long	start;<br>		start = get_time_ms();<br>		while (get_time_ms() - start &lt; duration)<br>		{<br>		usleep(500);<br>		}<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yLRuCsVCj7D_y0qKglYT9w.jpeg" /><figcaption>Simulation Visualization forInput : ./philo 5 610 200 200 9</figcaption></figure><h3>Challenges and Solutions</h3><p><strong>Challenge 1: Circular Wait Condition</strong></p><ul><li><strong>Problem Faced</strong>: The circular wait condition arose when each philosopher attempted to pick up the left and right forks in no particular order. This issue could lead to a deadlock, where all philosophers hold one fork and wait indefinitely for the other, preventing any philosopher from eating.</li><li><strong>Solution Implemented</strong>: To prevent this deadlock scenario, a strict order was enforced in which philosophers pick up their forks. Philosophers with odd IDs first pick up the right fork and then the left, while those with even IDs do the opposite. This strategy breaks the cycle of circular wait and effectively prevents deadlock.</li></ul><p><strong>Challenge 2: Favouritism in Fork Acquisition</strong></p><ul><li><strong>Problem Faced</strong>: Some philosophers were favoured in terms of accessing forks due to their position at the table or the sequence of actions, leading to unfair resource allocation where some philosophers would always pick up the fork first, they would finish eating and then pick up the fork to eat again immediately.</li><li><strong>Solution Implemented</strong>: The solution involved implementing an algorithm that considers fairness; each philosopher must put down their forks and wait a random minimal duration before attempting to pick them up again if they were not successful in their first attempt. This randomized delay reduces the chance of favoritism and ensures that all philosophers get an equal opportunity to eat.</li></ul><p><strong>Challenge 3: Delayed Death Logging</strong></p><ul><li><strong>Problem Faced</strong>: The simulation had an issue where a philosopher’s death would only be logged after they had completed their full sleep cycle or finished eating, which could misleadingly suggest that the philosopher was alive for longer than actually was the case.</li><li><strong>Solution Implemented</strong>: To address this, the monitoring system was refined to check the status of philosophers more frequently and independently of their eating or sleeping cycles. The implementation of a dedicated monitoring thread allowed for immediate recognition and logging of a philosopher’s death, thus ensuring that death events are recorded and handled promptly and accurately.</li></ul><h3><strong>Running the program</strong></h3><p>Check and run the <a href="https://github.com/Apoorva-S-Natesh/philosophers.git">code</a></p><pre>git clone<br>make<br>./philo 5 610 200 200 3<br>or <br>valgrind --tool=helgrind ./philo 5 610 200 200</pre><p><em>*All time in milliseconds</em></p><p>./philo [number_of_philosophers] [time_to_die] [time_to_eat] [time_to_sleep] [number_of_times_each_philosopher_must_eat(optional)]</p><p><em>**Helgrind is a Valgrind tool for detecting synchronisation errors in C, C++ and Fortran programs that use the POSIX pthreads threading primitives.</em></p><h3>Mood Check</h3><ul><li>What is a thread even?😲</li><li>Deadlocks! 😣</li><li>Finally they follow a routine 😴🍽️🤔</li></ul><h3>Reflections</h3><p>The real-world applications of such a solution are vast, particularly in operating systems and database transaction systems, where resource allocation must be managed meticulously to prevent deadlocks and ensure fairness.</p><p>Feel free to share your thoughts, experiences, or any questions in the comments below! Whether you are a student grappling with similar problems or an experienced developer with insights to share, I look forward to your feedback and discussion.</p><h3>References and tools</h3><p>Play and try: <a href="https://nlp.nd.edu/justin/dining/interactiveExplanation.php">Interactive Explanation</a></p><p>Visualizer : <a href="https://nafuka11.github.io/philosophers-visualizer/">visualizer</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cee5ac8d35e7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Chaos to Order : Navigating the Push_Swap Challenge]]></title>
            <link>https://apoorvasn.medium.com/from-chaos-to-order-navigating-the-push-swap-challenge-df3ac13c9188?source=rss-c1ddc9d92b89------2</link>
            <guid isPermaLink="false">https://medium.com/p/df3ac13c9188</guid>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[sorting-algorithms]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[visualization]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Apoorva]]></dc:creator>
            <pubDate>Tue, 07 Jan 2025 15:54:54 GMT</pubDate>
            <atom:updated>2025-01-12T13:38:34.594Z</atom:updated>
            <content:encoded><![CDATA[<h3>Push_Swap Challenge : Navigating from Chaos to Order</h3><h4>A tale of stacks and chunks. A sorting algorithm for the push_swap project of 42 Programming School.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5t2BllNfm0u9eDfJFBZfcA.png" /></figure><p>Welcome to a detailed exploration of the Push_Swap challenge, a complex puzzle that tests the limits of algorithm design and efficiency. In this journey, we delve deep into the mechanics of sorting algorithms, particularly focusing on the use of two stacks to sort data with a minimal number of operations. This challenge not only sharpens problem-solving skills but also enhances understanding of data structures and algorithm optimization. Join me as we unpack the strategies that lead to an effective solution, learn from trials, and ultimately master the art of stack sorting.</p><h3>Introduction : Entering the Chaos</h3><p>The Push_Swap problem involves two stacks, a and b. Stack a starts filled with a random assortment of integers, and stack b starts empty. The objective is to sort the integers in stack a in ascending order using a limited set of operations. Oh, and you better keep it snappy because every operation counts. 😅</p><h3>Operations in Toolbox</h3><p><strong>sa</strong> (swap a): Swap the first 2 elements at the top of stack a. Do nothing if there is only one or no elements. <br><strong>sb</strong> (swap b): Swap the first 2 elements at the top of stack b. Do nothing if there is only one or no elements. <br><strong>ss</strong> : sa and sb at the same time. <br><strong>pa</strong> (push a): Take the first element at the top of b and put it at the top of a. Do nothing if b is empty. <br><strong>pb</strong> (push b): Take the first element at the top of a and put it at the top of b. Do nothing if a is empty. <br><strong>ra</strong> (rotate a): Shift up all elements of stack a by 1. The first element becomes the last one. <br><strong>rb</strong> (rotate b): Shift up all elements of stack b by 1. The first element becomes the last one. <br><strong>rr </strong>: ra and rb at the same time. <br><strong>rra</strong> (reverse rotate a): Shift down all elements of stack a by 1. The last element becomes the first one. <br><strong>rrb </strong>(reverse rotate b): Shift down all elements of stack b by 1. The last element becomes the first one. <br>rrr : rra and rrb at the same time</p><h3>Getting Hands-On with the Problem</h3><p>Before I could even type a line of code, I decided to get physical with the problem. Picture this: cutting squares of paper, each with a number, and laying them out on my living room floor. I was manually swapping these paper squares, pushing them from one side to the other, essentially performing the stack operations in real life.</p><p>Through this tactile approach, I could see the algorithms come to life. It was a practical way to see the problem literally unfold before my eyes, helping me grasp the mechanics behind each algorithmic twist and turn. I experimented with different algorithms like the bubble sort, quick sort and even the mechanical turk algorithm.</p><p>I referred to this <a href="https://www.youtube.com/watch?v=rbbTd-gkajw&amp;ab_channel=CodingwithLewis">video</a> to understand different sorting algorithms and their Time complexity.</p><p>But I finally took inspiration from Ulysse Gerkerns’ approach of considering the two stacks as 4 stacks.</p><figure><img alt="42 push swap : stack A and stack B serve as 4 stacks" src="https://cdn-images-1.medium.com/max/283/1*sPo05AcG68gTLWolfPHnjg.png" /><figcaption>2 stacks serve as 4 stacks</figcaption></figure><h3>Sorting Strategy</h3><p>Now, let’s talk strategy. The program doesn’t just <em>sort</em> — it strategizes. Here’s the flow:</p><ol><li><strong>Small Stack Handling:</strong></li></ol><ul><li>For stacks with 2–5 elements, we use specific functions (sort_two_a, sort_three_a, etc.) to sort them directly.<em>(Small stacks = less drama. Quick and clean.)</em></li></ul><p><strong>2. Divide and Conquer:</strong></p><ul><li>For larger stacks, we split them into chunks of three and use functions like sort_from_left to tackle them in segments.</li></ul><p><strong>3. Control Checks:</strong></p><ul><li>Functions like n_ascending and n_descending ensure we’re staying on track.</li></ul><p>To reduce the number of operations:</p><ul><li>Check If an element in Stack A has to be pushed to stack B and then rotated in B(rb) and if the next element in Stack A also has to be rotated(ra) then <strong>rr</strong> is used instead.</li><li>Check if an element in Stack A has to be swapped and also an element in Stack B has to be swapped then <strong>ss</strong> is used.</li></ul><h3>Diving Into Thirds-Based Sorting</h3><ol><li><strong>Initial Distribution</strong>: The sorting process begins by distributing the contents of Stack A into three categories based on their rank — max, mid, and min thirds. This distribution helps in managing the complexity by focusing on smaller subsets of the stack.</li></ol><ul><li><strong>Max Thirds</strong> remain in Stack A, positioned at the bottom as they represent the largest elements.</li><li><strong>Mid Thirds</strong> are transferred to Stack B, specifically to the top of Stack B.</li><li><strong>Min Thirds</strong> are rotated within Stack B to move to the bottom, preparing them for the subsequent sorting phase.</li></ul><p><strong>2. Recursive Sorting</strong>: Each of these chunks is recursively sorted again by dividing them into 3 chunks. When sorting elements in stack B,</p><ul><li>Max Thirds are transferred to top of Stack A.</li><li>Mid Thirds are Transferred and rotated to bottom of Stack A.</li><li>Min Thirds remain in Stack B.</li></ul><p>The algorithm employs recursive functions that sort these smaller chunks and prepare them for eventual reintegration into Stack A.</p><p><strong>3. Integrating Back to Stack A</strong>: Stack is sorted to have elements in ascending order. Once sorted in Stack B, elements are strategically moved back to Stack A. This step also involves recursive sorting to ensure elements are placed in the correct order as they return to Stack A.</p><p><strong>4. Final Adjustments</strong>: The algorithm concludes with any necessary final adjustments in Stack A to ensure the entire stack is sorted in ascending order. This might involve additional sorting operations or simple rotations to align all elements properly.</p><h3>General Approach and Flow</h3><h4>The Main Event</h4><p>The <strong>main function</strong> is the quarterback, orchestrating the entire play. Here’s what happens:</p><ol><li><strong>Initialization:</strong> Sets up our stacks.</li><li><strong>Input Validation:</strong> Checks for invalid input.</li><li><strong>Stack Filling:</strong> Populates Stack A with integers from the command line and ranks them.</li><li><strong>Sorting:</strong> Calls the right function to work its magic.</li><li><strong>Clean-up:</strong> Frees up memory (because who likes a memory leak?).</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WhaqUB12JtVsMmOF0BA27w.png" /><figcaption>general flow of the program</figcaption></figure><h4>Building the Stack</h4><p>Before we get into the thick of code, let’s set the stage with the main characters:</p><ul><li><strong>s_rlist:</strong> Each node in this doubly linked list holds a piece of our puzzle—the integers we need to sort. It’s like each number is waiting in line for the roller coaster, linked arm in arm with the next.</li><li><strong>s_head:</strong> Think of this as the gatekeeper of our list. It knows how many are in line and who’s first.</li><li><strong>s_stacks:</strong> The mastermind struct, holding together both Stack A and Stack B, keeping track of every move.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/722/1*nOFTdH6QOLk-4OKhrSNx-A.png" /><figcaption>doubly linked list</figcaption></figure><p>The <strong>push_swap</strong> program focuses solely on sorting the <em>order</em> of numbers, rather than their actual values. To simplify comparisons during the sorting process, it’s helpful to map each number to its <em>rank</em> within the list.</p><p>For example, given the list:</p><p><strong>( 78, -12, 15, 300, 0 )</strong></p><p>We can map these values into their ranks:</p><p><strong>( 4, 1, 3, 5, 2 )</strong></p><p>This transformation allows the algorithm to work more efficiently, as comparisons are made based on the ranks rather than the original values.</p><p>We first Initialize the Stacks and fill Stack A with the elements passed via command-line arguments.</p><pre>if (!stacks_init(&amp;stacks)) <br>    return (ft_putstr_fd(&quot;Error\\n&quot;, 2), 1);<br>if (!fill_stacks(&amp;stacks, argc, argv)) { <br>    free_stacks(&amp;stacks); <br>    return (ft_putstr_fd(&quot;Error\\n&quot;, 2), 1); }</pre><h4>1. sort_from_left: Sorting From Small to Large</h4><p>This function kicks off the sorting process for Stack A:</p><ul><li><strong>Small Segments:</strong> If there are 5 or fewer elements, it calls specialized functions like sort_three_a or sort_five_a. (Because why reinvent the wheel?)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/656/1*Ck0RJNmhuXT7uAidNJ-kIQ.jpeg" /><figcaption>sort smaller segments efficiently</figcaption></figure><ul><li>Large Segments: For bigger groups, it delegates to thirds_right to divide and sort the stack further.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/393/1*mwdhbNb6fm3uUFHZ3teeSw.png" /><figcaption>sort_from_left function</figcaption></figure><h4>2. thirds_right</h4><p>This function is where the real action happens. For a given segment:</p><ul><li><strong>Divide into Thirds:</strong> Split the range of positions into three smaller chunks.</li><li><strong>Push &amp; Rotate:</strong></li><li>Smallest elements? Push them to Stack B.</li><li>Middle elements? Rotate Stack A to keep things neat.</li><li>Largest elements? They stay in Stack A for now.</li></ul><h4>3. sort_back_left:</h4><p>After splitting and sorting:</p><ul><li>This function merges the sorted thirds back into Stack A.</li><li>It uses rotations and reverse rotations (rr_times) to maintain order while reintegrating the elements.</li></ul><figure><img alt="42 push swap: sorting recursion to sort from stack A and stack B" src="https://cdn-images-1.medium.com/max/1024/1*rm6vS85tz5P1ilPZvhPSMg.png" /><figcaption>Recursion</figcaption></figure><h4>Example: Tackling a 20-Element Stack</h4><p>Imagine we’ve got 20 elements in Stack A, and we need to sort them back into Stack A:</p><ol><li><strong>Split into Thirds: Ranks</strong> 1–7, 8–14, and 15–20 are identified.</li><li><strong>Process the Thirds:</strong></li></ol><ul><li>check if elements are in 15–20 rank then rotate to stack A bottom.</li><li>If elements are in 8–14 rank then push to Stack B and finally push 1–7 rank elements to stack B and rotate to bottom.</li></ul><p><strong>3. Merge Back:</strong> Use sort_back_right to integrate the sorted thirds into a fully ordered Stack A.</p><h3>Benchmark? Crushed It. 💥</h3><p>To validate the project, the benchmark we were aiming for:</p><ul><li><strong>100 random numbers</strong>: Sort them in <strong>under 700 operations</strong>.</li><li><strong>500 random numbers</strong>: Keep it below <strong>5500 operations</strong>.</li></ul><p>Here’s how the above algorithm stacked up:</p><ul><li><strong>100 numbers</strong>: Sorted in <strong>~630 operations</strong>.</li><li><strong>500 numbers</strong>: Sorted in <strong>~4600 operations</strong>.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*PKS69bEcI9KNOrpkzj0jeA.gif" /></figure><h3>Challenges Faced:</h3><ol><li><strong>Managing Complexity: “How many moves are <em>too</em> many?” 🤔</strong></li></ol><ul><li><strong>The Challenge:</strong> My initial attempts were more “brute force” than “elegant solution.” I hit operation counts that were way over the benchmark.</li><li><strong>How I Fixed It:</strong> I had to rethink my approach, break down the problem into smaller chunks (hello, thirds-based sorting!), and optimize every move.</li></ul><p><strong>2. Debugging Rotations and Reverse Rotations: “Why is everything upside down?”</strong> 🙃</p><ul><li><strong>The Challenge:</strong> Rotations (ra, rb) and reverse rotations (rra, rrb) sounded simple... until my stacks started looking like scrambled eggs. Keeping track of how elements moved around in the stack was hard.</li><li><strong>How I Fixed It:</strong> I added print statements to track every operation and visualize the stack at every step.</li></ul><p><strong>3. Handling Edge Cases: “What even <em>is</em> sorted?” 🤯</strong></p><ul><li><strong>The Challenge:</strong> Edge cases like stacks with only two numbers, already-sorted stacks, or reverse-sorted stacks threw unexpected curveballs. My code initially failed on these cases, either doing extra operations or not sorting at all.</li><li><strong>How I Fixed It:</strong> I wrote specific conditions to handle these cases efficiently, avoiding unnecessary operations and reducing the overall count.</li></ul><p><strong>4. Optimization Paralysis: “Is this the best I can do?” 🤷‍♂️</strong></p><ul><li><strong>The Challenge:</strong> Even after meeting the benchmarks, I kept wondering if I could squeeze out a few more operations. The pursuit of perfection was never-ending, and I had to draw the line between “good enough” and over-engineering.</li><li><strong>How I Fixed It:</strong> I set milestones — once my algorithm consistently performed well below the benchmarks, I pushed the project for evaluation.</li></ul><h3>Mood Check:</h3><p>Throughout the project, I fluctuated between:</p><ul><li>“I don’t know where or how to start!”</li><li>“Oh this is interesting”</li><li><em>“I’m the sorting master!”</em> 💪</li><li><em>“Why isn’t this working?”</em> 😭</li><li><em>“Okay, it works… but it’s slooow.”</em> 🐢</li></ul><p>And finally, <em>“YES! IT PASSES ALL TESTS!”</em> 🎉</p><p>Here’s a link to check the <a href="https://github.com/Apoorva-S-Natesh/push_swap.git">code</a>.</p><p>While developing the sorting logic, I found invaluable insights in a blog post by Ulysse Gerkens on Medium, which outlines an efficient approach to the push_swap challenge in less than 4200 operations. This resource inspired some of the strategies I implemented in my own solution.</p><h4>Tools Used</h4><ul><li><a href="https://github.com/o-reo/push_swap_visualizer.git">Visualizer</a> for visualization.</li><li>For generating a large list of random numbers: <a href="https://www.calculatorsoup.com/calculators/statistics/random-number-generator.php">random number generator</a>.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=df3ac13c9188" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>