<?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[The Coding Handbook - Medium]]></title>
        <description><![CDATA[Building the biggest and most comprehensive computer science handbook on Medium - Medium]]></description>
        <link>https://medium.com/the-coding-handbook?source=rss----e35ccb162ae1---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>The Coding Handbook - Medium</title>
            <link>https://medium.com/the-coding-handbook?source=rss----e35ccb162ae1---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 11:41:54 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/the-coding-handbook" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[A Detailed Guide to Multi-Layer Perceptrons (MLPs)]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/the-coding-handbook/a-detailed-guide-to-multi-layer-perceptrons-mlps-46ca3cef9665?source=rss----e35ccb162ae1---4"><img src="https://cdn-images-1.medium.com/max/875/0*wxtens_Y4bBiqple" width="875"></a></p><p class="medium-feed-snippet">From a Single Neuron to Deep Neural Networks</p><p class="medium-feed-link"><a href="https://medium.com/the-coding-handbook/a-detailed-guide-to-multi-layer-perceptrons-mlps-46ca3cef9665?source=rss----e35ccb162ae1---4">Continue reading on The Coding Handbook »</a></p></div>]]></description>
            <link>https://medium.com/the-coding-handbook/a-detailed-guide-to-multi-layer-perceptrons-mlps-46ca3cef9665?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/46ca3cef9665</guid>
            <category><![CDATA[neural-networks]]></category>
            <category><![CDATA[computer-science]]></category>
            <category><![CDATA[pytorch]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Sun, 25 Oct 2026 22:54:48 GMT</pubDate>
            <atom:updated>2026-10-25T22:54:49.860Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[An Introduction to Python’s Object-Oriented Programming (OOP)]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/the-coding-handbook/an-introduction-to-pythons-object-oriented-programming-oop-6a771bce291a?source=rss----e35ccb162ae1---4"><img src="https://cdn-images-1.medium.com/max/600/1*RAs0XBZR72mDu1ZWunlpeQ.jpeg" width="600"></a></p><p class="medium-feed-snippet">In the world of Python, Object-Oriented Programming (OOP) is a paradigm that bundles code into &#x201C;objects.&#x201D; Many people get confused when&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/the-coding-handbook/an-introduction-to-pythons-object-oriented-programming-oop-6a771bce291a?source=rss----e35ccb162ae1---4">Continue reading on The Coding Handbook »</a></p></div>]]></description>
            <link>https://medium.com/the-coding-handbook/an-introduction-to-pythons-object-oriented-programming-oop-6a771bce291a?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/6a771bce291a</guid>
            <category><![CDATA[python-programming]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[object-oriented]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 15 Sep 2026 19:08:23 GMT</pubDate>
            <atom:updated>2026-09-15T19:09:00.134Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[The Most Complete PyTorch Tutorial You’ve Seen]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/the-coding-handbook/the-most-complete-pytorch-tutorial-youve-seen-10be216bee67?source=rss----e35ccb162ae1---4"><img src="https://cdn-images-1.medium.com/max/2600/1*guBazoPcQYMSnyGApR1Vrw.jpeg" width="5890"></a></p><p class="medium-feed-snippet">Everything you need to know to build, train, and scale deep learning models.</p><p class="medium-feed-link"><a href="https://medium.com/the-coding-handbook/the-most-complete-pytorch-tutorial-youve-seen-10be216bee67?source=rss----e35ccb162ae1---4">Continue reading on The Coding Handbook »</a></p></div>]]></description>
            <link>https://medium.com/the-coding-handbook/the-most-complete-pytorch-tutorial-youve-seen-10be216bee67?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/10be216bee67</guid>
            <category><![CDATA[pytorch]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:56:54 GMT</pubDate>
            <atom:updated>2026-09-08T21:58:52.136Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Introduction to Python logging]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/the-coding-handbook/introduction-to-python-logging-1b21c479cf61?source=rss----e35ccb162ae1---4"><img src="https://cdn-images-1.medium.com/max/600/1*dpkwotYfj2RrRhyPWmyu5g.jpeg" width="600"></a></p><p class="medium-feed-snippet">In the early days of learning Python, print() is usually our go-to debugging tool. Whether we&apos;re checking the value of a variable&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/the-coding-handbook/introduction-to-python-logging-1b21c479cf61?source=rss----e35ccb162ae1---4">Continue reading on The Coding Handbook »</a></p></div>]]></description>
            <link>https://medium.com/the-coding-handbook/introduction-to-python-logging-1b21c479cf61?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/1b21c479cf61</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[computer-science]]></category>
            <category><![CDATA[logging]]></category>
            <category><![CDATA[coding]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:55:57 GMT</pubDate>
            <atom:updated>2026-09-08T21:55:58.893Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Tutorial to NumPy]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/the-coding-handbook/tutorial-to-numpy-ee9156060e29?source=rss----e35ccb162ae1---4"><img src="https://cdn-images-1.medium.com/max/875/0*kSAw0WVjJqQdpHwU.jpeg" width="875"></a></p><p class="medium-feed-snippet">This article is a tutorial on Python&#x2019;s NumPy. Because nowadays, using Python essentially means you can&#x2019;t bypass NumPy, no matter what&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/the-coding-handbook/tutorial-to-numpy-ee9156060e29?source=rss----e35ccb162ae1---4">Continue reading on The Coding Handbook »</a></p></div>]]></description>
            <link>https://medium.com/the-coding-handbook/tutorial-to-numpy-ee9156060e29?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/ee9156060e29</guid>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[numpy]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:55:35 GMT</pubDate>
            <atom:updated>2026-09-08T21:55:36.497Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Binary Search Tree and Red-Black Tree]]></title>
            <link>https://medium.com/the-coding-handbook/binary-search-tree-and-red-black-tree-7512b337a863?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/7512b337a863</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[computer-science]]></category>
            <category><![CDATA[data-structure-algorithm]]></category>
            <category><![CDATA[coding]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:55:09 GMT</pubDate>
            <atom:updated>2026-09-08T21:55:10.343Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*YIdnOfEWE4b-7pUT.png" /><figcaption>2026.08.11 Colmar, France</figcaption></figure><h4>Why Do We Need Binary Search Trees or Red-Black Trees?</h4><p>In software engineering and system design, we frequently face a core challenge: how can we efficiently store, search, and dynamically modify data?</p><p><strong>Array</strong>: If the data is unsorted, finding a specific value requires scanning from beginning to end, giving a time complexity of O(n) — very slow. Even if we sort the array (allowing <a href="https://medium.com/@acamvproducingstudio/understanding-time-and-space-complexity-of-algorithm-658378aec525?sharedUserId=acamvproducingstudio">binary search</a>), inserting or deleting an entry still requires shifting every subsequent element forward or backward, so the modification complexity remains O(n). For example, here is what inserting new data into an array looks like:</p><pre>#include &lt;stdio.h&gt;<br><br>// Insert a new value into a sorted array<br>void insertSortedArray(int arr[], int *size, int capacity, int value) {<br>    int i;<br><br>    // [Boundary check] Can&#39;t insert if the array is already full — avoid writing out of bounds<br>    if (*size &gt;= capacity) {<br>        printf(&quot;Array is full, cannot insert new value %d\n&quot;, value);<br>        return;<br>    }<br><br>    // [Performance bottleneck] To find the correct position and make room,<br>    // every element larger than value must be shifted one slot to the right (worst case: O(n))<br>    for (i = *size - 1; (i &gt;= 0 &amp;&amp; arr[i] &gt; value); i--) {<br>        arr[i + 1] = arr[i]; // shift right<br>    }<br>    <br>    // Place the new value<br>    arr[i + 1] = value;<br>    (*size)++;<br>}</pre><p>Even if we already know where to insert, if the array holds 1 million entries and you want to insert at the very front, this loop still has to perform 1 million assignment operations.</p><p><a href="https://medium.com/@acamvproducingstudio/c-programming-linked-list-51e875e24c0a?sharedUserId=acamvproducingstudio"><strong>Linked List</strong></a>: To solve the array’s “mass relocation” problem, we might think of a linked list instead. Inserting a new node into a linked list is extremely fast (O(1)) — but only once you’ve already found the insertion point. Because a linked list’s memory layout is non-contiguous, it can’t use array-style binary search; it can only walk from the head one node at a time, so “finding the position” still costs O(n).</p><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdlib.h&gt;<br><br>typedef struct Node {<br>    int data;<br>    struct Node* next;<br>} Node;<br><br>// Find the correct position in a sorted linked list and insert<br>void insertSortedList(Node** head, Node* newNode) {<br>    Node dummy;<br>    dummy.next = *head;<br>    Node* current = &amp;dummy;<br><br>    // [Performance bottleneck] Can&#39;t jump to the middle — must compare nodes one by one<br>    // Finding the insertion point costs as much as O(n)<br>    while (current-&gt;next != NULL &amp;&amp; current-&gt;next-&gt;data &lt; newNode-&gt;data) {<br>        current = current-&gt;next; // walk from head to tail<br>    }<br>    <br>    // Inserting the node itself only requires updating two pointers (O(1)),<br>    // but finding the position already cost O(n)<br>    newNode-&gt;next = current-&gt;next;<br>    current-&gt;next = newNode;<br>    *head = dummy.next;<br>}</pre><p><strong>Hash Table</strong>: Lookup and insertion are extremely fast (O(1) on average), but it has one fatal weakness: the data is unordered. When we need to find the “maximum,” the “minimum,” or perform a “range query” (e.g., find all IDs between 100 and 200), a hash table is completely useless.</p><p>The <strong>Binary Search Tree (BST)</strong> and its evolved form, the <strong>Red-Black Tree (RBT)</strong>, were created to solve exactly this problem. They keep insertion, deletion, and search all at the fast O(log n) time complexity, while still preserving the ordering of the data. This makes them the go-to data structure for operating system schedulers, database indexes, and even state management in low-level communication protocols.</p><h4>Terminology of a tree</h4><p>Before diving into how BSTs and RBTs work, let’s cover some terminology used for “trees” in computer science. Picture a tree growing upside down (root at the top, leaves at the bottom):</p><ul><li><strong>Node</strong>: Each individual data point in the tree.</li><li><strong>Root</strong>: The single, unique topmost node of the entire tree.</li><li><strong>Child / Parent</strong>: A node extending downward from another node is called that node’s “child”; conversely, the node one level up is its “parent.” In a binary tree, each parent can have at most two children (a left child and a right child).</li><li><strong>Uncle Node</strong>: A sibling of the parent node — i.e., another child of the “grandparent” node two levels up. This term becomes important later when determining how a red-black tree should rebalance itself.</li><li><strong>Leaf</strong>: A node at the bottom level with no children.</li><li><strong>Subtree</strong>: Taking any node in the tree as a new root, that node together with all of its descendants forms a “subtree.”</li><li><strong>NIL Node (empty node)</strong>: In the definition of a red-black tree, whenever a node lacks a left or right subtree, we imagine an invisible empty node (NIL) attached there, and treat it as a leaf.</li></ul><p>Figure 1 shows an example of a binary tree, which we’ll use to get familiar with tree structure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/0*A6e8Yg6DrPz1Wwm_" /><figcaption>Figure 1: A binary tree (image source: <a href="https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8F%89%E6%A0%91">Wikipedia</a>)</figcaption></figure><p>The root of Figure 1 is node 2. Taking the left half as an example: node 7 is the left child of the top node 2, meaning the top node 2 is the parent of node 7. Going further down, node 7 is itself a parent — it has its own left child, 2, and right child, 6. The leaf nodes are the bottommost 2, 5, 11, and 4.</p><p>With these fundamentals in place, we can now move into the world of binary search trees.</p><h4>Binary Search Tree (BST)</h4><p>A binary search tree is a data structure based on the “divide and conquer” idea, designed so that both “search” and “insertion” are equally fast.</p><p>The rules of a BST are very simple — each node has at most two children (left and right), and the following rules must be strictly followed:</p><ol><li><strong>Smaller on the left</strong>: every node in the left subtree must have a value less than its parent’s value.</li><li><strong>Larger on the right</strong>: every node in the right subtree must have a value greater than its parent’s value.</li><li>Both the left and right subtrees must themselves be binary search trees.</li></ol><p>In other words, a newly inserted value is compared against existing nodes to decide whether it belongs on the left or the right of that node. When an equal value is encountered, there are two common approaches: if duplicates aren’t allowed, the new value is simply discarded; if duplicates are allowed, they are consistently placed on either the left or the right subtree.</p><p>Figure 2 shows a standard binary search tree.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/0*t3jQkUDrQFmqCvF1" /><figcaption>Figure 2: A standard binary search tree (image source: <a href="https://zh.wikipedia.org/zh-tw/%E4%BA%8C%E5%85%83%E6%90%9C%E5%B0%8B%E6%A8%B9">Wikipedia</a>)</figcaption></figure><p>Note that this figure looks almost identical to Figure 1 — but a binary search tree is not the same thing as a binary tree. More precisely, a binary tree is the broader category, requiring only that each node have at most two children, while a binary search tree is a binary tree with an additional ordering rule layered on top. So every binary search tree is a binary tree, but the reverse isn’t true.</p><p>To get a concrete feel for how a BST combines the strengths of “array-style binary search” and “linked-list-style dynamic insertion,” let’s look directly at its C implementation.</p><p>First, we need to define the tree’s node structure. It’s very similar to the linked list node from earlier, except the single next pointer is replaced with two branches, left and right, forming what&#39;s called a &quot;two-way branch&quot;:</p><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdlib.h&gt;<br><br>// Define the BST node structure<br>typedef struct TreeNode {<br>    int data;<br>    struct TreeNode* left;<br>    struct TreeNode* right;<br>} TreeNode;<br><br>// Helper function to create a new node<br>TreeNode* createNode(int value) {<br>    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));<br>    newNode-&gt;data = value;<br>    newNode-&gt;left = NULL;<br>    newNode-&gt;right = NULL;<br>    return newNode;<br>}</pre><p>Here, typedef struct defines a custom type named TreeNode with three members:</p><ul><li>int data: stores the node&#39;s actual value.</li><li>struct TreeNode* left: a pointer recording the memory address of the left child.</li><li>struct TreeNode* right: a pointer recording the memory address of the right child.</li></ul><p>createNode is the function that creates a new node:</p><ul><li>malloc(sizeof(TreeNode)): dynamically requests a block of memory just large enough to hold one TreeNode.</li><li>newNode-&gt;data = value;: stores the passed-in value into that memory&#39;s data field.</li><li>newNode-&gt;left = NULL; and newNode-&gt;right = NULL;: default both child pointers to NULL, indicating that this newly created node currently has no children attached (i.e., it&#39;s a leaf for now).</li><li>Finally, it returns a pointer to this new node, so calling code (such as the insertion function) can attach it to the existing tree.</li></ul><p>Next comes the core “insertion” logic. Because we strictly follow the “smaller on the left, larger on the right” rule, finding the insertion point no longer requires walking “from head to tail (O(n))” as with a linked list — instead, every comparison lets us eliminate one entire side of the current node’s subtree:</p><pre>// Insert a new node into a BST (recursive)<br>TreeNode* insertBST(TreeNode* root, int value) {<br>    // 1. If the tree is empty, or we&#39;ve reached the bottom (a NIL node),<br>    //    this is where the new node belongs!<br>    if (root == NULL) {<br>        return createNode(value);<br>    }<br><br>    // 2. If the value to insert is &quot;less than&quot; the current node, go left<br>    if (value &lt; root-&gt;data) {<br>        root-&gt;left = insertBST(root-&gt;left, value);<br>    }<br>    // 3. If the value to insert is &quot;greater than&quot; the current node, go right<br>    else if (value &gt; root-&gt;data) {<br>        root-&gt;right = insertBST(root-&gt;right, value);<br>    }<br>    <br>    // 4. Return the (possibly) unchanged node pointer to re-link the tree<br>    return root;<br>}</pre><p>In a BST, since all nodes are linked together via pointers, any node already present in the tree never needs to change its actual position in memory — completely sidestepping the array’s “mass relocation” performance bottleneck.</p><p>We mentioned at the start of this article that a hash table’s fatal flaw is that its data is unordered. So how do we retrieve data stored in a BST in sorted order?</p><p>This is thanks to the BST’s strict “smaller on the left, larger on the right” property. We simply use a traversal method called “in-order traversal” — following the order “visit the left subtree → read the current node → visit the right subtree” — and the data naturally comes out from smallest to largest! The code is simple:</p><pre>// In-order traversal: prints BST data from smallest to largest<br>void inorderTraversal(TreeNode* root) {<br>    if (root != NULL) {<br>        inorderTraversal(root-&gt;left);  // 1. Go as far left as possible (find the smallest)<br>        printf(&quot;%d &quot;, root-&gt;data);     // 2. Process the current node (print it)<br>        inorderTraversal(root-&gt;right); // 3. Go right (find larger values)<br>    }<br>}</pre><p>What we just described, however, is the ideal case of random insertion, where the BST ends up nicely balanced and the search complexity is O(log n) (equivalent to binary search).</p><p>But suppose the data you insert is already sorted (say, 1, 2, 3, 4, 5). Because of the “larger on the right” rule, the tree will keep growing entirely to the right, eventually degenerating into a single-direction linked list. At that point, search complexity collapses to the worst case, O(n).</p><h4>Red-Black Tree (RBT)</h4><p>To prevent a BST from degenerating into a linked list, computer scientists invented the red-black tree. Let’s first introduce what it is, then look at where it’s used.</p><p>A red-black tree builds on the basic BST by adding a “color” attribute to every node, governed by five rules:</p><ol><li>Every node is either black or red.</li><li>The root node, at the very top of the tree, must be black.</li><li>All empty nodes (i.e., the NIL nodes at the bottom) are considered black.</li><li>If a node is red, both of its children must be black (in other words, two consecutive red nodes can never occur).</li><li>Starting from any node, every path down to a NIL leaf must pass through exactly the same number of black nodes.</li></ol><p>Figure 3 shows an example of a red-black tree.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/0*6eXtUJu6Q6iFj7GO" /><figcaption>Figure 3: A red-black tree (<a href="https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91">image source: Wikipedia</a>)</figcaption></figure><p>The key to understanding how a red-black tree balances itself lies in how Rule 4 (no consecutive red nodes) and Rule 5 (equal black-node counts) constrain each other. Let’s see how these two rules bound the height of the tree:</p><p><strong>1. The shortest possible path (all black):</strong></p><p>Per Rule 5, every path from a given node down to any NIL leaf must pass through the same number of black nodes. Say this fixed number is N. If a particular path contains no red nodes at all, its total length is simply N. This is the shortest path possible in a red-black tree.</p><p><strong>2. The longest possible path (alternating red-black):</strong></p><p>If we deliberately wanted to stretch one side of the tree out (i.e., make it grow lopsided, approaching a linked list), the only way to do so is to squeeze red nodes in between black ones. But per Rule 4, red nodes can’t be consecutive. So the most we can possibly cram in follows a “black-red-black-red-black…” alternating pattern. In this extreme case, the number of red nodes on the path can be at most equal to the number of black nodes (N). So the longest possible path has a total length of at most N (black) + N (red) = 2N.</p><p>This rule guarantees that the longest path in the tree is never more than twice the length of the shortest path. In other words, it isn’t 100% perfectly balanced, but it’s “balanced enough” to guarantee O(log n) search time even in the worst case.</p><p>At this point you might be wondering: if a red-black tree still follows the BST’s “smaller on the left, larger on the right” rule, wouldn’t inserting 1, 2, 3, 4, 5 in sequence still make it grow into a one-sided linked list toward the right?</p><p>This is precisely where the elegance of the red-black tree algorithm lies. It doesn’t just passively check the five rules above — it actively “rebalances” every time a node is inserted or deleted.</p><p>In a red-black tree, every newly inserted node is, by default, marked “red.” If inserting it breaks one of the rules, the red-black tree kicks off two mechanisms: <strong>recoloring</strong> and <strong>rotation</strong>.</p><p>Let’s walk through inserting 1, 2, 3 in sequence:</p><ul><li><strong>Insert 1</strong>: As the root, Rule 2 kicks in, forcing it to be black. State: [Black 1].</li><li><strong>Insert 2</strong>: Greater than 1, so it goes to the right, defaulting to red. State: [Black 1] → right child [Red 2]. No violation.</li><li><strong>Insert 3 (conflict!)</strong>: Greater than 2, so it goes to the right, defaulting to red. State becomes [Black 1] → right [Red 2] → right [Red 3]. Now we have two consecutive red nodes, violating Rule 4. See Figure 4.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/782/0*Gc7ZBSS6ABE_d9p_.png" /><figcaption>Figure 4: Two consecutive red nodes violating Rule 4</figcaption></figure><p>When a rule is violated, a red-black tree has two possible strategies: “recolor,” or “rotate + recolor.” It always tries recoloring first, and only falls back to “rotate + recolor” if recoloring won’t work. Let’s look at exactly what this means.</p><p>In Figure 4 we have three nodes, and to avoid violating Rule 4 we need to change some node colors. But per Rule 2, the root must stay black, so only the other two nodes are eligible for recoloring.</p><p>However, per Rule 5, the number of black nodes on the path from any node down to a NIL must be identical. Whether we recolor both remaining nodes to black, or just one of them, we end up violating Rule 5 — because going right from the root would then pass through more black nodes than going left. Figure 5 illustrates this, with the black-node count to each NIL labeled underneath. You can clearly see Rule 5 is violated.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*0ncWLbm19_6RavkT.png" /><figcaption><em>Figure 5: Simple recoloring violates Rule 5</em></figcaption></figure><p>So plain recoloring can’t resolve the conflict here, which means the red-black tree switches to rotation plus recoloring. Rotation comes in two flavors — “left rotation” and “right rotation” — and which one to use depends on whether the rule-violating nodes are clustered mostly on the right (use left rotation) or the left (use right rotation). In the Figure 4 example, the violating nodes are on the right, so a left rotation is used. The result after the left rotation is shown in Figure 6:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*3fFTMp98svHJglmD.png" /><figcaption><em>Figure 6: Rotating the structure from Figure 4</em></figcaption></figure><p>Next, per Rule 2, the root must be recolored black. Then, between nodes 1 and 3, one of them must change color — does node 1 turn red, or does node 3 turn black? The answer is that node 1 turns red. As mentioned earlier, the overall logic of a red-black tree is that the black-node count on every path from any node to every leaf must stay equal. So turning a node black usually means you’d need to re-check the entire tree to make sure every path’s black count increased by one accordingly — but coloring a node red, when it doesn’t otherwise violate any rule, avoids that entirely. The corrected result is shown in Figure 7:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/806/0*ChFXAmt53lCsPFDd.png" /><figcaption>Figure 7: Recoloring after rotation</figcaption></figure><p>Now suppose we insert a new node, 4. This again breaks Rule 4, so the red-black tree first checks whether recoloring alone can fix it. As Figure 8 shows, in this case we can indeed fix things by simply recoloring two nodes to black.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/855/0*_HIwk5o5pkOap-O0.png" /><figcaption><em>Figure 8: Balance achieved through recoloring alone</em></figcaption></figure><p>This brings us to the condition for when a red-black tree only needs “recoloring” versus when it needs “rotation plus recoloring”: if the newly inserted node’s uncle is black, rotation plus recoloring is required. If the uncle is red, recoloring alone is sufficient.</p><p>Following this logic, you can try working out what happens if we then insert 5 — see the result in Figure 9.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/367/0*Yg0W4sDyO-20fNiA.png" /><figcaption><em>Figure 9: The result of inserting 1, 2, 3, 4, 5 in sequence</em></figcaption></figure><p>Under the red-black tree’s strict oversight, its two automatic correction mechanisms — recoloring and rotation — working in alternation, forcibly fold what would have been a straight line into a well-balanced, symmetric branching structure. This is the core value of the red-black tree. You might now worry: doesn’t this automatic correction mechanism slow down insertion?</p><p>The answer is: almost not at all. Both recoloring and rotation run in O(1) (constant time), because at the low level, the actual operations involved are extremely lightweight:</p><ul><li><strong>What recoloring really is</strong>: at the code level, a node’s color is typically just an integer or an enum. “Recoloring” is nothing more than modifying a variable’s value in memory (e.g., node-&gt;color = BLACK;). This is a direct memory assignment — whether the tree has a hundred nodes or a million, this line of code always takes the same amount of time, hence O(1).</li><li><strong>What rotation really is</strong>: rotation might sound like uprooting and relocating the whole tree, but it never actually moves any node’s physical position in memory. All a rotation does is “re-link pointers.” Take a left rotation as an example — we only need to update the left, right, and parent pointers among three nodes (the parent, the node itself, and its right child). That&#39;s roughly six pointer updates, requiring no loops or recursive traversal — a fixed number of steps, hence extremely fast O(1).</li></ul><p>Finding the insertion point costs O(log n), but the structural fix-up after insertion (rotation and recoloring) only costs O(1) up to at most O(log n) pointer operations. Put together, the overall insertion complexity remains solidly at O(log n). Let’s look at the implementation:</p><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdlib.h&gt;<br><br>// Define the color enum<br>typedef enum { RED, BLACK } Color;<br><br>// Define the red-black tree node structure<br>typedef struct Node {<br>    int data;<br>    Color color;<br>    struct Node *left, *right, *parent;<br>} Node;<br><br>// ---------------------------------------------------------<br>// [Core Operation 1] Left Rotation - Time complexity O(1)<br>// Purpose: restructure the tree purely by re-linking pointers,<br>// without moving any data in memory<br>// ---------------------------------------------------------<br>void leftRotate(Node **root, Node *x) {<br>    Node *y = x-&gt;right; // y is x&#39;s right child, about to be promoted<br>    <br>    // Step 1: hand y&#39;s left subtree over to x as its new right subtree<br>    x-&gt;right = y-&gt;left;<br>    if (y-&gt;left != NULL) {<br>        y-&gt;left-&gt;parent = x;<br>    }<br>    <br>    // Step 2: promote y into x&#39;s original position (update x&#39;s parent&#39;s pointer)<br>    y-&gt;parent = x-&gt;parent;<br>    if (x-&gt;parent == NULL) {<br>        *root = y; // x was the root; now y becomes the root<br>    } else if (x == x-&gt;parent-&gt;left) {<br>        x-&gt;parent-&gt;left = y;<br>    } else {<br>        x-&gt;parent-&gt;right = y;<br>    }<br>    <br>    // Step 3: push x down to become y&#39;s left child<br>    y-&gt;left = x;<br>    x-&gt;parent = y;<br>}<br><br>// ---------------------------------------------------------<br>// [Core Operation 2] Right Rotation - Time complexity O(1)<br>// Purpose: fully symmetric to left rotation; used to level out<br>// the tree&#39;s height when the left side is too heavy<br>// ---------------------------------------------------------<br>void rightRotate(Node **root, Node *x) {<br>    Node *y = x-&gt;left; // y is x&#39;s left child, about to be promoted<br>    <br>    // Step 1: hand y&#39;s right subtree over to x as its new left subtree<br>    x-&gt;left = y-&gt;right;<br>    if (y-&gt;right != NULL) {<br>        y-&gt;right-&gt;parent = x;<br>    }<br>    <br>    // Step 2: promote y into x&#39;s original position (update x&#39;s parent&#39;s pointer)<br>    y-&gt;parent = x-&gt;parent;<br>    if (x-&gt;parent == NULL) {<br>        *root = y; // x was the root; now y becomes the root<br>    } else if (x == x-&gt;parent-&gt;right) {<br>        x-&gt;parent-&gt;right = y;<br>    } else {<br>        x-&gt;parent-&gt;left = y;<br>    }<br>    <br>    // Step 3: push x down to become y&#39;s right child<br>    y-&gt;right = x;<br>    x-&gt;parent = y;<br>}<br><br>// ---------------------------------------------------------<br>// [Core Operation 3] Insert Fixup<br>// Purpose: when consecutive red nodes are found, decide whether<br>// to &quot;recolor&quot; or &quot;rotate&quot;<br>// ---------------------------------------------------------<br>void insertFixup(Node **root, Node *z) {<br>    // While z&#39;s parent is red, Rule 4 is violated (no consecutive red nodes) — fix it<br>    while (z-&gt;parent != NULL &amp;&amp; z-&gt;parent-&gt;color == RED) {<br>        <br>        // Case where z&#39;s parent is the grandparent&#39;s LEFT child<br>        // (the mirror-symmetric case, parent as right child, is handled analogously)<br>        if (z-&gt;parent == z-&gt;parent-&gt;parent-&gt;left) {<br>            Node *uncle = z-&gt;parent-&gt;parent-&gt;right; // find the uncle node<br>            <br>            // Case 1: uncle is red -&gt; [Recolor]<br>            if (uncle != NULL &amp;&amp; uncle-&gt;color == RED) {<br>                z-&gt;parent-&gt;color = BLACK;<br>                uncle-&gt;color = BLACK;<br>                z-&gt;parent-&gt;parent-&gt;color = RED; // grandparent turns red<br>                z = z-&gt;parent-&gt;parent;          // move focus up to grandparent and continue checking<br>            } <br>            // Case 2: uncle is black (or NIL) -&gt; [Rotate + Recolor]<br>            else {<br>                // If z happens to be a right child (forming a &quot;&gt;&quot; shape),<br>                // rotate left first to straighten it out<br>                if (z == z-&gt;parent-&gt;right) {<br>                    z = z-&gt;parent;<br>                    leftRotate(root, z);<br>                }<br>                // Recolor and right-rotate the grandparent (resolving the &quot;/&quot; shape)<br>                z-&gt;parent-&gt;color = BLACK;<br>                z-&gt;parent-&gt;parent-&gt;color = RED;<br>                rightRotate(root, z-&gt;parent-&gt;parent); // right rotation to level things out<br>            }<br>        } else {<br>            // (Mirror-symmetric logic for the case where the parent is the right child, omitted for brevity)<br>        }<br>    }<br>    // Rule 2: no matter what, force the root to always be black<br>    (*root)-&gt;color = BLACK;<br>}</pre><p>Red-black trees have many real-world applications, the most classic example being the Linux operating system’s process scheduler (CFS). In modern Linux, the core component responsible for deciding “which process gets to run on the CPU next” is called the <strong>Completely Fair Scheduler (CFS)</strong>. CFS tracks a “virtual runtime” (vruntime) for every running process. To achieve fairness, the system always picks the process with the smallest vruntime (i.e., the one that has run the least and most urgently needs CPU time) to run next. At any given moment, countless processes are starting, pausing, and finishing, so the scheduler needs to perform &quot;insert new process,&quot; &quot;remove finished process,&quot; and &quot;find the minimum&quot; extremely frequently. With an array or linked list, searching or inserting costs O(n), which would grind the system to a halt. With a hash table, insertion is fast, but hash tables are unordered, so instantly finding the &quot;minimum&quot; is impossible. This is exactly where a red-black tree shines: processes are stored in the tree keyed by vruntime, so the bottom-left-most leaf is always the process with the smallest vruntime. Finding the minimum, inserting a new process, and removing an old one can all be done in O(log n).</p><p>Additionally, <strong>C++’s </strong><strong>std::map and </strong><strong>std::set</strong>: these two containers in the C++ Standard Template Library (STL) are required to always keep their data sorted, with worst-case operation complexity strictly bounded at O(log n). As a result, most C++ compilers implement them under the hood using a red-black tree. We&#39;ll go into more detail on this once we get to the C++ portion of this series.</p><p>In summary, I hope this article has given readers a solid foundational understanding of binary search trees and red-black trees.</p><p><a href="https://medium.com/@acamvproducingstudio/welcome-to-ac-studio-read-this-first-77a38848daaa">Welcome to Ac Studio! Read This First</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7512b337a863" width="1" height="1" alt=""><hr><p><a href="https://medium.com/the-coding-handbook/binary-search-tree-and-red-black-tree-7512b337a863">Binary Search Tree and Red-Black Tree</a> was originally published in <a href="https://medium.com/the-coding-handbook">The Coding Handbook</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[C Programming — Queue]]></title>
            <link>https://medium.com/the-coding-handbook/c-programming-queue-f27463730c1b?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/f27463730c1b</guid>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[computer-science]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:54:50 GMT</pubDate>
            <atom:updated>2026-09-08T21:54:52.075Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*3K8reHWdChUfnpsV" /><figcaption>Photo by <a href="https://unsplash.com/@stteee">Stephen H</a> on <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>After introducing <a href="https://medium.com/@acamvproducingstudio/c-programming-stack-419eaba2226e?sharedUserId=acamvproducingstudio">Stack in the previous article</a>, in this article we introduce the Queue.</p><p>A Queue is a data structure that follows the First-In-First-Out (FIFO) rule — basically just like waiting in line to buy a drink: whoever gets in line first leaves first.</p><p>No matter how a Queue is implemented, it always comes down to the following operations:</p><ul><li><strong>Enqueue</strong>: Add a new data to the rear of the Queue.</li><li><strong>Dequeue</strong>: Remove the data from the front of the Queue.</li><li><strong>isEmpty</strong>: Check whether the Queue has no data left.</li><li><strong>isFull</strong>: Check whether the Queue has reached its capacity limit; this is especially common in fixed-capacity array based queue implementations. <a href="https://medium.com/@acamvproducingstudio/c-programming-stack-419eaba2226e?sharedUserId=acamvproducingstudio">Linked lists</a> based queue usually don’t have a pre-set fixed capacity, we will explain this more clearly later.</li><li><strong>Peek / Front</strong>: Look at the data currently at the front of the line without removing it.</li></ul><p>In C, there are generally two ways to implement a queue:</p><ul><li><strong>Array-based Queue</strong>: Implemented using an array. Its advantage is that the implementation is simple, and memory allocation is contiguous. The downside is that the capacity is usually fixed upon initiation; if more data needs to be accommodated, a larger array typically has to be created separately and the data moved over. Also, without the Circular Queue technique (introduced later in this article), space can be wasted.</li><li><strong>Linked List-based Queue</strong>: Implemented using a linked list. The advantage is that it can grow dynamically with the amount of data, without needing to pre-set a fixed capacity — as long as the system still has enough memory, more data can keep being added. The downside is that it’s more complex to implement.</li></ul><h4>Implementing a Queue with an Array</h4><p>Here’s how an Array-based Queue can be implemented in C:</p><pre>#define MAX_SIZE 5 // Define the maximum capacity of the queue as 5<br><br>typedef struct {<br>    int items[MAX_SIZE]; // The &quot;container&quot; used to hold data (array)<br>    int front;           // Records which position in the array holds the &quot;frontmost&quot; data<br>    int rear;            // Records which position in the array holds the &quot;rearmost&quot; data<br>} Queue;</pre><p>front and rear are not themselves data stored in the Queue, they are index values used to record the positions of data. The array&#39;s slots are numbered starting from 0 (0, 1, 2, 3, 4). We need front to remember &quot;which slot holds the next piece of data ready to leave,&quot; and rear to remember &quot;which slot holds the most recently added data.&quot;</p><p>Initializing the Queue looks like this:</p><pre>void initQueue(Queue *q) {<br>    q-&gt;front = -1;<br>    q-&gt;rear = -1;<br>}</pre><p>During initialization we set both front and rear to -1. Because array indexing in C starts at 0, if we set the initial value to 0, the program would mistakenly think slot 0 already contains data. Setting it to -1 is a common design convention in this kind of implementation, used to indicate that there is currently no valid data position. Since array indices start at 0, -1 can never be a valid index, so it conveniently serves to represent an empty Queue.</p><p>This might still feel a bit abstract, so here’s a step-by-step example of how front and rear move.</p><p>Suppose we have a Queue with MAX_SIZE 5, set up as above:</p><pre>Index:   0    1    2    3    4<br>Array: [   ][   ][   ][   ][   ]</pre><p>Since there’s nothing in it yet, as described above, we set both front and rear to -1.</p><p>Now suppose A is added to the Queue.</p><pre>Index:   0    1    2    3    4<br>Array: [ A ][   ][   ][   ][   ]<br>         ↑<br>    front, rear</pre><p>Since there’s only A in the line, front (the next piece of data ready to leave) is A&#39;s index, 0. rear (the most recently added data) is also index 0.</p><p>Now suppose B and C also enter the Queue in order.</p><pre>Index:   0    1    2    3    4<br>Array: [ A ][ B ][ C ][   ][   ]<br>         ↑         ↑<br>       front      rear</pre><p>At this point A is still the next piece of data ready to leave (remember, the Queue’s characteristic is FIFO, so A must leave first), so front stays at 0. The most recently added data is C, so rear is 2. This is the rule governing how front / rear change.</p><p><strong>Checking whether the Queue is empty (isEmpty) or full (isFull)</strong></p><p>In this linear-array Queue implementation, we can use front == -1 to indicate that the Queue is empty.</p><p>At initialization, both front and rear are -1, meaning the Queue has no data yet. When the last piece of data is removed, we reset both front and rear back to -1 inside dequeue(), so the Queue returns to the empty state (the code implementation follows below).</p><pre>bool isEmpty(Queue *q) {<br>    return q-&gt;front == -1; // Check whether front is -1 to determine if the queue is empty<br>}</pre><p>Checking for fullness is a matter of checking whether rear has reached the last slot:</p><pre>bool isFull(Queue *q) {<br>    return q-&gt;rear == MAX_SIZE - 1;<br>}</pre><p>With initialization and state-checking covered, the remaining core operations are Enqueue, Dequeue, and Peek.</p><p><strong>Enqueue</strong></p><p>When new data needs to be added to the queue, we first check whether the queue is already full (isFull). If there&#39;s still room, we move rear forward by one slot and place the data there. One thing to note in particular: if this is the &quot;first&quot; piece of data being added, we must also advance front from -1 to 0 at the same time, so that we know where the front of the line is.</p><pre>void enqueue(Queue *q, int value) {<br>    if (isFull(q)) {<br>        printf(&quot;Queue is Full, cannot add %d\n&quot;, value);<br>        return;<br>    }<br>    <br>    // If this is the first item, set front to 0<br>    if (q-&gt;front == -1) {<br>        q-&gt;front = 0;<br>    }<br>    <br>    q-&gt;rear++;                   // Move rear forward by one slot<br>    q-&gt;items[q-&gt;rear] = value;   // Place the data in the slot pointed to by rear<br>    printf(&quot;Successfully added %d\n&quot;, value);<br>}</pre><p><strong>Dequeue</strong></p><p>When someone leaves the line, we likewise first check whether the queue is empty (isEmpty). If there is data, we take out the data at the front index, then move front forward by one slot, meaning the person who was originally second in line is now first. Note that the data at the slot that was just dequeued still physically exists there — only the front index position has changed.</p><pre>int dequeue(Queue *q) {<br>    if (isEmpty(q)) {<br>        printf(&quot;Queue is Empty, cannot remove data\n&quot;);<br>        return -1; // Return -1 to indicate an error or no data<br>    }<br>    <br>    int item = q-&gt;items[q-&gt;front]; // Take out the frontmost data<br>    q-&gt;front++;                    // Move front forward by one slot<br>    <br>    // If the queue becomes empty after removal, reset both front and rear to -1<br>    if (q-&gt;front &gt; q-&gt;rear) {<br>        q-&gt;front = -1;<br>        q-&gt;rear = -1;<br>    }<br>    <br>    return item;<br>}</pre><p><strong>Peek / Front</strong></p><p>The purpose of peek is to look at the data pointed to by front without removing it from the Queue. This is the simplest operation. We just need to confirm the queue isn&#39;t empty, then return the data pointed to by front, without needing to change the values of front or rear.</p><pre>int peek(Queue *q) {<br>    if (isEmpty(q)) {<br>        printf(&quot;The queue is currently empty\n&quot;);<br>        return -1;<br>    }<br>    return q-&gt;items[q-&gt;front];<br>}</pre><p>This function exists because sometimes we just want to check “what’s the next task or next piece of data,” so we can peek at it and then write a conditional branch based on it.</p><p><strong>The Drawback of the Linear Array Queue: False Overflow</strong></p><p>If you look closely at the implementation logic above, you’ll notice a serious problem: Suppose our MAX_SIZE is 5. We add 5 items (at this point rear = 4, the queue is full), then remove 3 items (at this point front = 3). At this point, even though the first 3 slots of the array (0, 1, 2) have already been used and freed up, because rear is still 4, calling isFull() again would still return true. This wastes the space at the front, making it impossible to add new data. This situation — where space is clearly available at the front of the array, but no more data can be added because rear has already reached the end — is usually called <strong>False Overflow</strong>.</p><p>To solve this problem, we can use the Circular Queue technique.</p><p><strong>Circular Queue</strong></p><p>The concept behind a circular queue is simple: “connect the head and tail of the array to form a circle.” When rear reaches the last slot of the array, as long as there&#39;s still space at the next position, it wraps back around to slot 0 to continue placing data.</p><p>One difference between a circular queue and a linear queue is that the relative size of front and rear can no longer be directly used to determine whether the Queue is empty or full. For example, front might be greater than rear, yet the Queue may still contain data.</p><p>In C, the most common way to achieve this “wrap-around” effect is to use the modulo operator (%). For example, when rear reaches index 4, the next step, (4 + 1) % 5 = 0, perfectly computes that the next position is slot 0.</p><p><strong>Modifying the Checks and Operations for a Circular Queue</strong></p><p>To convert the original linear queue into a circular queue, besides letting front and rear move cyclically, we also need to redesign how we determine whether the Queue is &quot;empty&quot; or &quot;full.&quot;</p><p>Here we continue using the same design as the linear queue before it: front == -1 indicates the Queue is empty — at initialization, both front and rear are -1; when the last item is removed, both are also reset back to -1, returning the Queue to the empty state.</p><p>As long as the next position after rear would collide with front, that means the queue is full:</p><pre>bool isCircularFull(Queue *q) {<br>    return (q-&gt;rear + 1) % MAX_SIZE == q-&gt;front;<br>}</pre><p>Let’s walk through an example. Suppose the queue currently already contains 4 items:</p><pre>Index:   0    1    2    3    4<br>Array: [ A ][ B ][ C ][ D ][   ]<br>         ↑              ↑<br>      front            rear</pre><p>A, B, C, D are placed at indices 0–3, so front is 0 and rear is 3. At this point, the next position after rear is:</p><pre>(3 + 1) % 5 = 4</pre><p>And front is 0. Since 4 ≠ 0, the Queue is not yet full, and one more item, E, can still be added:</p><pre>Index:   0    1    2    3    4<br>Array: [ A ][ B ][ C ][ D ][ E ]<br>         ↑                   ↑<br>       front                rear</pre><p>Now rear is 4, and the next position after rear becomes:</p><pre>(4 + 1) % 5 = 0</pre><p>And front is also exactly 0, so:</p><pre>(q-&gt;rear + 1) % MAX_SIZE == q-&gt;front</pre><p>holds true, and the Queue is determined to be full.</p><p>Here’s the full code for a circular queue:</p><pre>// Check whether the circular queue is empty<br>bool isCircularEmpty(Queue *q) {<br>    return q-&gt;front == -1;<br>}<br><br>// Check whether the circular queue is full<br>bool isCircularFull(Queue *q) {<br>    return (q-&gt;rear + 1) % MAX_SIZE == q-&gt;front;<br>}<br><br>// Circular queue enqueue<br>void circularEnqueue(Queue *q, int value) {<br>    if (isCircularFull(q)) {<br>        printf(&quot;Queue is full, cannot add %d\n&quot;, value);<br>        return;<br>    }<br><br>    // If this is the first item, initialize front<br>    if (isCircularEmpty(q)) {<br>        q-&gt;front = 0;<br>        q-&gt;rear = 0;<br>    } else {<br>        // Use % to achieve the wrap-around effect<br>        q-&gt;rear = (q-&gt;rear + 1) % MAX_SIZE;<br>    }<br><br>    q-&gt;items[q-&gt;rear] = value;<br>    printf(&quot;Successfully added %d\n&quot;, value);<br>}<br><br>// Circular queue dequeue<br>int circularDequeue(Queue *q) {<br>    if (isCircularEmpty(q)) {<br>        printf(&quot;Queue is empty!\n&quot;);<br>        return -1;<br>    }<br><br>    int item = q-&gt;items[q-&gt;front];<br><br>    // If front and rear point to the same position,<br>    // it means the queue becomes empty after removing this item<br>    if (q-&gt;front == q-&gt;rear) {<br>        q-&gt;front = -1;<br>        q-&gt;rear = -1;<br>    } else {<br>        // Use % to achieve the wrap-around effect<br>        q-&gt;front = (q-&gt;front + 1) % MAX_SIZE;<br>    }<br><br>    return item;<br>}</pre><p><strong>Implementing a Queue with a Linked List</strong></p><p>In C, we first need to define the linked list’s “Node,” and then define a queue structure that contains front and rear pointers.</p><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdlib.h&gt;<br>#include &lt;stdbool.h&gt;<br><br>// Define the linked list node<br>typedef struct Node {<br>    int data;           // The stored data<br>    struct Node *next;  // Pointer to the next node<br>} Node;<br><br>// Define the queue, containing pointers to the front and rear<br>typedef struct {<br>    Node *front;<br>    Node *rear;<br>} LinkedListQueue;</pre><p><strong>Initialization and Checking for Emptiness</strong></p><p>In the linked-list version, initialization is very straightforward — just point both front and rear to NULL. Likewise, as long as front is NULL, the queue is considered completely empty. Here, there&#39;s usually no need to implement an isFull() like there is for a fixed-size array queue, because the queue has no pre-set fixed capacity; each time data is added, a new node can be created via dynamic memory allocation. However, this doesn&#39;t mean the capacity is unlimited — if the system doesn&#39;t have enough available memory, malloc() can still fail, in which case no more data can be added.</p><pre>// Initialize the queue<br>void initLLQueue(LinkedListQueue *q) {<br>    q-&gt;front = NULL;<br>    q-&gt;rear = NULL;<br>}<br><br>// Check whether the queue is empty<br>bool isLLEmpty(LinkedListQueue *q) {<br>    return q-&gt;front == NULL;<br>}</pre><p><strong>Enqueue</strong></p><p>When new data needs to be added, we dynamically allocate (malloc) a block of memory for the new node. If the queue is empty, this new node becomes both front and rear; if the queue already has data, we attach the new node after the current rear, then update rear to point to this new node.</p><pre>void llEnqueue(LinkedListQueue *q, int value) {<br>    // Dynamically allocate memory for the new node<br>    Node *newNode = malloc(sizeof(Node));<br>    if (newNode == NULL) {<br>        printf(&quot;Memory allocation failed!\n&quot;);<br>        return;<br>    }<br>    newNode-&gt;data = value;<br>    newNode-&gt;next = NULL; // Since it&#39;s placed at the very end, next points to NULL<br><br>    // If the queue is empty, both front and rear point to the new node<br>    if (isLLEmpty(q)) {<br>        q-&gt;front = newNode;<br>        q-&gt;rear = newNode;<br>    } else {<br>        // Attach the new node after the current rear, and update the rear pointer<br>        q-&gt;rear-&gt;next = newNode;<br>        q-&gt;rear = newNode;<br>    }<br>    printf(&quot;Successfully added %d\n&quot;, value);<br>}</pre><p><strong>Dequeue</strong></p><p>When removing data, we first temporarily store the node pointed to by front, take the data out of it, and then move front forward to the next node (front-&gt;next). Something worth flagging here: in C, <a href="https://medium.com/@acamvproducingstudio/c-programming-dynamic-memory-allocation-86221e811379?sharedUserId=acamvproducingstudio">dynamically allocated memory</a> always has to be freed manually (free), or it will cause a memory leak. Additionally, if the queue becomes empty after removing the node, we also need to remember to set rear back to NULL.</p><pre>int llDequeue(LinkedListQueue *q) {<br>    if (isLLEmpty(q)) {<br>        printf(&quot;Queue is empty, cannot remove data\n&quot;);<br>        return -1;<br>    }<br><br>    Node *temp = q-&gt;front;      // Temporarily store the node to be removed<br>    int item = temp-&gt;data;      // Extract the data<br><br>    q-&gt;front = q-&gt;front-&gt;next;  // Move front forward to the next node<br><br>    // If the queue becomes empty after removal, set rear to NULL as well<br>    if (q-&gt;front == NULL) {<br>        q-&gt;rear = NULL;<br>    }<br><br>    free(temp); // Free the memory<br>    return item;<br>}</pre><p><strong>Peek / Front</strong></p><p>This operation is largely the same as the array version — just confirm the queue isn’t empty, then directly return the data in the front node.</p><pre>int llPeek(LinkedListQueue *q) {<br>    if (isLLEmpty(q)) {<br>        printf(&quot;The queue is currently empty\n&quot;);<br>        return -1;<br>    }<br>    return q-&gt;front-&gt;data;<br>}</pre><p><strong>Conclusion</strong></p><p>To sum up, this article covered the core FIFO concept of the Queue, and implemented two mainstream versions:</p><ul><li><strong>Array-based Queue</strong>: Uses an array to store data, with simple memory allocation and good cache locality — suitable for cases with a clear upper bound on data volume, or where a fixed capacity is acceptable.</li><li><strong>Linked List-based Queue</strong>: Uses dynamic memory allocation, without needing to pre-specify a fixed capacity — suitable for cases where the amount of data varies significantly and is hard to estimate in advance; however, each node requires extra space for a pointer, and nodes aren’t necessarily contiguous in memory.</li></ul><p>So, after learning about Queues, where are they actually used in real-world software development? Here are two examples:</p><p><strong>Operating system task scheduling</strong>: When multiple programs or jobs are waiting to be processed by the CPU, the Queue is one of the common data structures used in OS scheduling. For example, in Round-Robin scheduling (a scheduling method where each job takes turns using the CPU for a fixed time slice), a Queue can be used to manage jobs waiting for the CPU in order, letting each job take turns getting a small slice of CPU time. Another classic application of Queue is print spooling — under FIFO scheduling, jobs are typically processed in the order they were submitted, so a job submitted earlier gets processed first.</p><p><strong>Graph algorithms</strong>: There’s a very famous algorithm in graph theory called Breadth-First Search (BFS), whose core relies on a Queue for its implementation. When we’re navigating a maze or analyzing relationships in a social network, BFS uses the Queue to ensure we explore nodes “from near to far, one layer at a time.”</p><p>The concept of Queue is simple, and it’s an important foundation for getting started in computer science. Once you’ve mastered it, learning more advanced material later becomes much easier.</p><p><a href="https://medium.com/@acamvproducingstudio/welcome-to-ac-studio-read-this-first-77a38848daaa">Welcome to Ac Studio! Read This First</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f27463730c1b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/the-coding-handbook/c-programming-queue-f27463730c1b">C Programming — Queue</a> was originally published in <a href="https://medium.com/the-coding-handbook">The Coding Handbook</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[C Programming — Stack]]></title>
            <link>https://medium.com/the-coding-handbook/c-programming-stack-419eaba2226e?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/419eaba2226e</guid>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[computer-science]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:54:33 GMT</pubDate>
            <atom:updated>2026-09-08T21:54:35.090Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*tNo4IQ-6QfCOauDH.jpeg" /><figcaption>2026.06.10 Passau, Germany</figcaption></figure><p>In the article introducing <a href="https://medium.com/@acamvproducingstudio/c-programming-dynamic-memory-allocation-86221e811379">dynamic memory allocation</a>, we explained Stack and Heap; they are different regions of memory space and are used at different times.</p><p>This article will introduce the Stack, but what we are introducing here is the Stack in data structures. It is a concept we define that regulates how data is stored, rather than a physically existing block of memory like the one we introduced in dynamic memory allocation.</p><h3>Stack</h3><p>A Stack is a data structure that follows the LIFO (Last-In, First-Out) principle, somewhat like a can of Pringles. When we put data in, it must be inserted from the top, and when we take data out, it must also be removed from the very top. Generally, when implementing it in C, we often define the following basic operations:</p><ul><li><strong>Push</strong>: Add data to the top of the Stack.</li><li><strong>Pop</strong>: Remove and return the data at the top of the Stack.</li><li><strong>Peek/Top</strong>: Return the data at the top of the Stack, but do not remove it.</li><li><strong>isEmpty</strong>: Check if there is still data inside the Stack.</li></ul><p>For beginners, the design of a Stack might seem quite useless. Why would I design a data structure that only allows me to put data on top, take data from the top, and doesn’t even let me freely modify values in the middle? The answer is that in certain scenarios, this seemingly restrictive setup is actually a protection mechanism.</p><p>For example, when you design the “Undo” and “Redo” functions in drawing software, you want the system to “retrace its steps” to prevent your brain from crashing while coding by writing code that extracts intermediate processes. Or, take the “Back” and “Forward” buttons in a browser — you wouldn’t want to suddenly jump to a completely different page. In these cases, the Stack demonstrates its functionality and serves as a protection mechanism.</p><p>In C, to build a Stack using an array, we need two things: an array data to hold the data, and a pointer (index) top to remember where the data is currently stacked up to (for instance, top = 2 means the data is currently stacked from 0 to 2). When we initialize a Stack, since it is still empty, top = -1. We can package them into a struct.</p><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdbool.h&gt; // To use bool, true, false<br><br>#define MAX_SIZE 5   // For teaching purposes, we first define that the Stack can hold at most 5 elements<br><br>// Define the Stack structure<br>typedef struct {<br>    int data[MAX_SIZE]; // The actual &quot;container&quot; used to store data<br>    int top;            // The &quot;pointer&quot; that always points to the topmost data (stores the array index)<br>} Stack;</pre><p>You might notice that we added typedef in front of the struct and wrote Stack outside the final brace. In C, the function of typedef is to &quot;define a new data type.&quot; If we only wrote the original struct Stack { ... };, every time we wanted to create a Stack variable in the future, we would have to write: struct Stack myStack;.</p><p>But by adding typedef, we are essentially telling the compiler: &quot;Treat this structure as a new custom type named Stack.&quot; In the future, we simply and concisely write: Stack myStack;. This saves the trouble of typing struct every time and makes the code look intuitive.</p><p>Furthermore, note that the definition of this type is written at the very top, outside our main function main. This is because you will write the main tasks inside the main function, and then write the previously mentioned functions like pop, push, etc., separately. Since these functions also need to pass the Stack as a parameter, you must define this data type first so that this Stack becomes universally accessible (globally defined), and the compiler won&#39;t throw an error when it encounters pop or push.</p><p>Next, let’s write some common Stack functions. Notice that the parameters passed into these functions are pointers pointing to the Stack, rather than passing the Stack itself directly. There are two main reasons behind this:</p><ol><li>If the Stack is passed directly, C will copy the entire Stack from the main function and pass it to the function. So the function gets a clone. Even if it pushes data into the clone, the clone will be freed when the function ends (refer back to dynamic memory allocation), resulting in absolutely no real changes. However, if we pass a pointer, the function can find the memory address of the Stack and push the data into it, thus actually modifying the values.</li><li>Suppose passing the entire Stack instead of its pointer; this would also waste a massive amount of performance. If the Stack stores a thousand integers, it is far more efficient to pass an 8-byte pointer instead.</li></ol><p>So in practice, the scenario for using push will look like the following:</p><pre>#include &lt;stdio.h&gt;<br><br>// Define the Stack specifications<br>typedef struct {<br>    int data[10];<br>    int top;<br>} Stack;<br><br>// Here we first define what the push function &quot;looks like&quot;<br>// Its first parameter is Stack *s, meaning it needs a &quot;house number (address)&quot;<br>void push(Stack *s, int value) {<br>    /* It&#39;s okay if you don&#39;t understand what push is doing; it will be explained below in the article<br>    */    <br>    s-&gt;top++;<br>    s-&gt;data[s-&gt;top] = value;<br>}<br><br>int main() {<br>    // 1. Declare a local variable my_stack inside main<br>    Stack my_stack;<br>    my_stack.top = -1; // Initialization<br><br>    // 2. Key point: when calling the function, add &amp; in front of the variable<br>    // This way, what is passed in is the &quot;address&quot; of my_stack<br>    push(&amp;my_stack, 10); <br>    push(&amp;my_stack, 20);<br><br>    // Verify that the original was indeed modified<br>    printf(&quot;The top of the Stack is now: %d\n&quot;, my_stack.data[my_stack.top]);<br><br>    return 0;<br>}</pre><p>After seeing the main architecture, we can look at the implementation details of each function. First are push (putting data into the Stack) and pop (taking data out of the Stack).</p><p>To put data in using push, we only need two steps: first move the pointer, then insert the data. In the code below, s is a pointer pointing to the Stack, and -&gt; is the Arrow Operator in C. Its function is to access members inside a structure. Basically, s-&gt;top is equivalent to (*s).top (dereference the pointer first, then access the member).</p><pre>void push(Stack *s, int value) {<br>    // Check before acting: if it&#39;s full, you can&#39;t stuff anymore in!<br>    if (isFull(s)) {<br>        printf(&quot;Error: Stack is full (Overflow)!\n&quot;);<br>        return;<br>    }<br>    <br>    s-&gt;top++;                // Step 1: Move the top pointer up one slot (from -1 to 0)<br>    s-&gt;data[s-&gt;top] = value; // Step 2: Put the new data into the slot top is currently pointing at<br>    printf(&quot;Successfully pushed: %d\n&quot;, value);<br>}</pre><p>Finally, there’s the Pop function to take data out. There is a counter-intuitive design here: we actually don’t need to truly “delete” the number in the array.</p><pre>int pop(Stack *s) {<br>    // Check before acting: if it&#39;s empty, don&#39;t take anything<br>    if (isEmpty(s)) {<br>        printf(&quot;Error: Stack is empty (Underflow)!\n&quot;);<br>        return -1; // Return an error code<br>    }<br>    <br>    int poppedValue = s-&gt;data[s-&gt;top]; // Step 1: First take out and store the top data<br>    s-&gt;top--;                          // Step 2: Move the top pointer down one slot<br>    return poppedValue;                // Step 3: Give the newly extracted data to the caller<br>}</pre><p>After we subtract 1 from top, although the number in that slot remains in memory, it is already &quot;invisible&quot; to the Stack. The next time we do a Push, the new data will naturally overwrite it. Doing this saves the performance cost of clearing the array to zero.</p><p>When dealing with Stacks, two very common errors are Stack Overflow and Stack Underflow.</p><ul><li><strong>Stack Overflow:</strong> The box is already full, but you continue to Push things into it. The reasons it occurs are usually (1) the static array size is set too small, or (2) the program falls into an infinite loop, constantly calling itself, leading to the exhaustion of internal Stack space.</li><li><strong>Stack Underflow:</strong> The box is empty, but you Pop things from it. The program will try to grab data that simply doesn’t exist, resulting in a logical program error. This is also why we must check if the Stack is empty before doing a Pop.</li></ul><p>Next are isEmpty and isFull, which are used to check if the Stack is empty or full, respectively. The way to determine this is to see if the position of top is at the very bottom or at the maximum capacity.</p><pre>// Check if the Stack is empty<br>bool isEmpty(Stack *s) {<br>    return s-&gt;top == -1; // If top is still at -1, it means it is empty<br>}<br><br>// Check if the Stack is full<br>bool isFull(Stack *s) {<br>    return s-&gt;top == MAX_SIZE - 1; // The index of the last slot in the array is MAX_SIZE - 1<br>}</pre><h3>Monotonic Stack</h3><p>A Monotonic Stack is not a new data type, but rather a processing strategy that utilizes a Stack to maintain “monotonicity” (increasing or decreasing). There are two types of Monotonic Stacks: the elements inside the Stack remain either monotonically increasing or monotonically decreasing. Therefore, it is specifically used to solve problems like “finding the next greater/smaller element,” which can significantly optimize time complexity.</p><p>Let’s use a problem directly as an example. Suppose a problem gives an array and asks us to find, for each element, the element to its right that is smaller than it; otherwise, return the element itself. For example, for [8, 4, 6, 2, 3], the answer is [4, 2, 2, 2, 3]. This is a scenario where a Monotonic Stack is very suitable.</p><p>We can imagine a Monotonic Stack as a “to-do list of numbers that haven’t found their answers yet.” In this example, the Stack’s task is to help each number find its “next smaller value.” If it hasn’t been found yet, this number (or its index) has to wait in line in the Stack. Once a new incoming number is smaller than it, it can leave the Stack and get its answer.</p><p>We prepare an empty Stack that stores indices (Index), because indices allow us to accurately fill the values into the answer array.</p><ul><li><strong>Encounter 8 (Index 0):</strong> The Stack is empty, 8 goes in and sits first. Stack: [0] (corresponding value 8)</li><li><strong>Encounter 4 (Index 1):</strong> We find that 4 is smaller than the 8 at the top of the Stack! Action: 8 has found its savior. We kick index 0 out of the Stack, and record ans[0] = 4. Now the Stack is empty, so 4 goes in to line up. Stack: [1] (corresponding value 4)</li><li><strong>Encounter 6 (Index 2):</strong> 6 is greater than 4. This does not meet our condition of finding a “smaller value.” Action: 6 also goes in to line up. The Stack maintains a state of increasing from bottom to top. Stack: [1, 2] (corresponding values 4, 6)</li><li><strong>Encounter 2 (Index 3):</strong> Here comes the key point! 2 is smaller than the 6 at the top of the Stack, and also smaller than the 4 below it. Action 1: 6 has found its savior (which is 2). Index 2 is out, record ans[2] = 2. Action 2: 4 has also found its savior (also 2). Index 1 is out, record ans[1] = 2. Now the Stack is empty again, 2 goes in to line up. Stack: [3] (corresponding value 2)</li><li><strong>Encounter 3 (Index 4):</strong> 3 is greater than 2. Action: 3 goes in to line up. Stack: [3, 4] (corresponding values 2, 3)</li></ul><p>Finally, the remaining indices 3 and 4 in the Stack indicate that there are no smaller numbers to their right. According to the problem requirements, we make their answers equal to themselves.</p><p>Now that we are clear about the whole operational logic, let’s actually translate this “finding a savior” process into C language code. We will continue to use the previously written Stack structure and the push, pop, and isEmpty functions.</p><h4>C Implementation of Monotonic Stack</h4><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdbool.h&gt;<br><br>// Assuming the previously defined Stack structure, push, pop, and isEmpty have all been written<br>// ... (Basic Stack functions omitted here) ...<br><br>void findNextSmaller(int arr[], int size) {<br>    Stack my_stack;<br>    my_stack.top = -1; // Initialize Stack<br>    <br>    int ans[size];     // Array used to store the final answers<br>    <br>    // Iterate through all numbers in the array<br>    for (int i = 0; i &lt; size; i++) {<br>        // Core logic: While the Stack is not empty, and &quot;the new incoming number&quot; is smaller than &quot;the number corresponding to the index at the top of the Stack&quot;<br>        while (!isEmpty(&amp;my_stack) &amp;&amp; arr[i] &lt; arr[my_stack.data[my_stack.top]]) {<br>            // This means the number at the top of the Stack has found its savior!<br>            int top_index = pop(&amp;my_stack); // Kick it out of the to-do list<br>            ans[top_index] = arr[i];        // Record its next smaller value<br>        }<br>        // After processing, the current number (its index) itself also needs to go into the Stack to line up and wait for its savior<br>        push(&amp;my_stack, i);<br>    }<br>    <br>    // The array has been fully traversed, but there might still be leftover indices in the Stack that haven&#39;t found a savior<br>    while (!isEmpty(&amp;my_stack)) {<br>        int leftover_index = pop(&amp;my_stack);<br>        ans[leftover_index] = arr[leftover_index]; // According to the problem, if there&#39;s no smaller value on the right, set it to itself<br>    }<br>    <br>    // Print the final answers to verify the result<br>    printf(&quot;Original array: &quot;);<br>    for (int i = 0; i &lt; size; i++) printf(&quot;%d &quot;, arr[i]);<br>    <br>    printf(&quot;\nAnswer array: &quot;);<br>    for (int i = 0; i &lt; size; i++) printf(&quot;%d &quot;, ans[i]);<br>    printf(&quot;\n&quot;);<br>}<br><br>int main() {<br>    int arr[] = {8, 4, 6, 2, 3};<br>    int size = sizeof(arr) / sizeof(arr[0]);<br>    <br>    findNextSmaller(arr, size);<br>    <br>    return 0;<br>}</pre><h4>Why use a Monotonic Stack?</h4><p>You might think: “Couldn’t I just use two for loops—one to look at the current number, and another to scan to the right to find a smaller number—to calculate the answer?&quot;</p><p>Thinking this way is also correct, but if the array length is 100,000, in the worst-case scenario using a double for loop (e.g., the array is already sorted from smallest to largest), the time complexity would be<strong> O(N²)</strong>. The program would need to execute up to 10 billion times, which will absolutely result in a Time Limit Exceeded error.</p><p>However, using a Monotonic Stack, observing the code above closely reveals: each element (index) will be pushed in at most once, and popped out at most once. Therefore, even though there is a while loop nested inside, the overall total number of operations is still proportional to the array length. Its time complexity is significantly reduced to O(N). This is the value of the Monotonic Stack in this scenario.</p><ul><li><a href="https://medium.com/@acamvproducingstudio/c-programming-pointers-in-c-c585edc6d3dd">C Programming — Pointers in C</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-linked-list-51e875e24c0a">C Programming — Linked List</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-dynamic-memory-allocation-86221e811379">C Programming — Dynamic Memory Allocation</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-double-pointers-and-function-pointers-a99c2687fa3a">C Programming — Double Pointers and Function Pointers</a></li><li><a href="https://medium.com/@acamvproducingstudio/understanding-time-and-space-complexity-of-algorithm-658378aec525">Understanding Time and Space Complexity of Algorithm</a></li><li><a href="https://medium.com/@acamvproducingstudio/welcome-to-ac-studio-read-this-first-77a38848daaa">Welcome to Ac Studio! Read This First</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=419eaba2226e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/the-coding-handbook/c-programming-stack-419eaba2226e">C Programming — Stack</a> was originally published in <a href="https://medium.com/the-coding-handbook">The Coding Handbook</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Understanding Time and Space Complexity of Algorithm]]></title>
            <link>https://medium.com/the-coding-handbook/understanding-time-and-space-complexity-of-algorithm-658378aec525?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/658378aec525</guid>
            <category><![CDATA[computer-science]]></category>
            <category><![CDATA[programming-basic]]></category>
            <category><![CDATA[time-complexity]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:53:58 GMT</pubDate>
            <atom:updated>2026-09-08T21:53:59.355Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*hq47yzccM6Fwo44R.png" /><figcaption>2026.04.30 Munich, Germany</figcaption></figure><p>Imagine you are a newly hired software engineer. Your boss asks you to write a feature to check if there are any repeat customers in today’s orders. So you take out the first order and compare it with the remaining orders; then take out the second order and continue to compare it with the remaining orders, and so on. You used 100 pieces of data for testing, and it worked perfectly. Thus, the code was officially deployed online. As a result, when 10,000 orders flooded in during the long weekend, your website crashed immediately.</p><p>The problem is not that your code has a bug, but that you underestimated the “growth trend of the time required for program execution when data grows.” Assuming there are 10 pieces of data, this snippet of code will probably execute about 100 comparisons. But when the data increases by 1,000 times to 10,000, the number of program executions becomes 100,000,000 times.</p><p>This is why learning complexity is important. Since everyone’s computer performance is different, we need an objective standard to evaluate how terrifying a program’s resource consumption will become when the input data volume approaches infinity.</p><p>To solve this problem, computer scientists introduced Big O Notation.</p><p>Big O Notation focuses on the “growth trend.” For example, when we say the time complexity of an algorithm is O(n), it means that as the data volume n increases, the execution time will increase linearly.</p><p>Big O operates on two important principles:</p><ol><li><strong>Ignore constant coefficients:</strong> If your program needs a loop to run through the data twice, strictly speaking, it should be O(2n). However, as n increases, the impact of multiplying by two is very small because we care about the rate of growth. Therefore, we directly omit the factor of two and write it as O(n).</li><li><strong>Keep only the highest order term:</strong> Similarly, suppose the number of times a program executes is n² + 5n + 1000. When n is only 10, the trailing 5n + 1000 has a significant impact. But when n becomes extremely large, the trailing 5n and 1000 have almost no effect. Since we care about extremely large n when evaluating complexity, we only look at the highest order term that truly makes an impact, writing it as O(n²).</li></ol><p>Whether it is time or space complexity, we use Big O Notation to represent it. It indicates how many operation steps (time) and memory (space) will be consumed as the data volume n grows.</p><p>Going back to the opening example of comparing orders, let’s calculate its time complexity. Assuming there are n pieces of data, you first take the first piece of data and compare it with the remaining n-1 pieces. Then you take the second piece and compare it with the remaining n-2 pieces, and so on. Therefore, the total number of executions is: (n-1)+(n-2)+…+1=n(n-1)/2.</p><p>This is equivalent to (1/2)n²-(1/2)n. Next, we apply the simplification principles we just learned by ignoring the coefficient 1/2 and the lower order term, resulting in a time complexity of O(n²).</p><p>Now that we’ve looked at time complexity, what about the space complexity of this algorithm? In this example, we are simply comparing values back and forth within the original orders; we didn’t create a massive array or database for this task. In other words, we can complete this job with a simple double loop, requiring only two basic variables, i and j. This means that even if the data volume surges, we still just use two loops, and the extra variables needed remain a fixed quantity. This indicates a space complexity of O(1).</p><p>So, how do we solve the problem of excessively high time complexity? We could create an empty “seen customers list.” For every order read, we check the list to see if it repeats; if not, we save this order into it (we will cover the technical details of this in an article introducing hash tables). This way, you only need to search once, and the time complexity becomes O(n). However, because you created a new list, your space complexity also becomes O(n). This is a fascinating aspect of algorithms, as you are trading space for time.</p><p>Let’s look at more examples to familiarize ourselves with time and space complexity.</p><p>Suppose you want to find a specific number in an increasing sequence. You can choose to write a for loop to go through all the numbers to find the target, or you can opt for Binary Search. These two approaches will result in quite different complexities.</p><p>First, suppose you use a for loop:</p><pre>def linear_search(arr, target):<br>    # n is the length of the array<br>    for i from 0 to len(arr) - 1:<br>        if arr[i] == target:<br>            return i  # Found it, return the position<br>    return -1         # Not found after finishing, return -1</pre><p>In terms of time complexity, you might get incredibly lucky and find the number at the very first position of the array, which is O(1); or the number might appear at the very last position, which is O(n). Generally speaking, we usually consider the worst-case scenario (based on system reliability considerations), so we say its time complexity is O(n). As for space complexity, since you only need a fixed number of variables, it is O(1).</p><p>Suppose you use Binary Search:</p><pre>def binary_search(arr, target):<br>    left = 0<br>    right = len(arr) - 1<br>    <br>    while left &lt;= right:<br>        mid = (left + right) // 2  # Find the midpoint<br>        <br>        if arr[mid] == target:<br>            return mid            # Exact hit on the midpoint<br>        elif arr[mid] &lt; target:<br>            left = mid + 1        # Target is larger, discard the left half<br>        else:<br>            right = mid - 1       # Target is smaller, discard the right half<br>            <br>    return -1                     # Not found after finishing, return -1</pre><p>The best case is that you hit the number you’re looking for on the very first cut, O(1). The worst case is finding it on the very last cut. Since every time you search, you divide the data volume by 2, the remaining data after the first execution is n/2, then n/4, then n/8…, all the way down to n/(2^k)=1. Taking the logarithm gives k=log n, meaning you only need to execute a maximum of log n operations, making the time complexity O(log n). The space complexity remains O(1) because you only use three variables (left, right, mid) to track positions, which won&#39;t increase memory consumption as the data grows.</p><p>Let’s look at sorting examples next. We just witnessed how blazingly fast binary search is, but its prerequisite is that the data must already be sorted. What if the current data is not sorted yet? How do we organize it? And how do we estimate the complexity of these sorting methods? Let’s see how to calculate their complexity (their implementation details will be covered in a future article regarding sorting).</p><p>By intuition, we’d likely think of the following two methods:</p><p><strong>Bubble Sort</strong>: Imagine a row of people of varying heights, and you need to arrange them from shortest to tallest. The dumbest but most intuitive method is this: starting from the left, compare adjacent people in pairs. If the left person is taller than the right, tell them to swap places. Keep comparing all the way to the far right. At this point, the “tallest person” will definitely have been swapped to the rightmost position. Next, ignoring the last person who is already in place, repeat the same action for the remaining people in front. As shown in Figure (1).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*W1-Ltp_sNwQGFl6J.png" /><figcaption>Figure (1): Bubble Sort</figcaption></figure><ul><li><strong>Time Complexity:</strong> Because we have to run through n people, and each time perform roughly n pairwise comparisons. So the calculation is (n-1) + (n-2) + (n-3) + … + 1, which according to the arithmetic progression sum formula is (1/2)n²-(1/2)n. Based on the simplification rules earlier, we get O(n²).</li><li><strong>Space Complexity:</strong> O(1), because everyone is just swapping places in situ, requiring no extra space.</li></ul><p><strong>Insertion Sort</strong>: Recall how you organize your hand of cards when playing poker? You pick up the first card, then the second card; if the second is smaller than the first, you insert it in front of the first. When you pick up the third card, you find the correct position to “insert” it among the already sorted cards in your hand. As shown in Figure (2).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*co9qXvQ_zlfl9yIn.png" /><figcaption><em>Figure (2): Insertion Sort</em></figcaption></figure><ul><li><strong>Time Complexity:</strong> In the worst-case scenario (e.g., the cards happen to be in reverse order, from largest to smallest), every time you draw a new card, you have to compare it with all the previous cards once, so it remains O(n²).</li><li><strong>Space Complexity:</strong> O(1), which can also be done in place.</li></ul><p>As you can see, the efficiency of the previous two methods is quite limited. To address this, computer scientists applied the concept used in binary search: cutting in half. This strategy is called Divide and Conquer.</p><p><strong>Merge Sort</strong>: If sorting 10,000 pieces of data is sluggish, wouldn’t sorting 5,000 pieces be much faster? The logic behind merge sort is: keep “cutting the large data in half” until each small group contains only 1 piece of data (since 1 piece of data is inherently sorted). Then, merge these small groups “in pairs” back into larger sorted groups, combining them all the way back to the original size, as shown in Figure (3).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/713/0*_DAC168LXmh3uEfE.png" /><figcaption>Figure (3): Merge Sort</figcaption></figure><ul><li><strong>Time Complexity:</strong> We can visualize the whole process as an inverted tree. Because we split it in half every time, the data volume goes from n -&gt; n/2 -&gt; n/4 all the way down to 1. The total number of levels for this breakdown and merging will be log n levels. In each level, whether preparing to split or merging numbers in pairs, we have to look at all n numbers. Since there are log n levels in total, and each level processes n times, multiplying them yields a time complexity of O(nlog n).</li><li><strong>Space Complexity:</strong> Suppose you want to merge [3,8] and [2,9] into [2,3,8,9]. You cannot swap them directly in the array, otherwise you will overwrite numbers that haven&#39;t been compared yet (for instance, if you want to swap A and B, you cannot simply write A=B, because when you later write B=A, A has already been overwritten). Therefore, during the merge process, you have to create a temporary array of size n, place the sorted numbers in sequentially, and finally copy them back to the original array. Since the temporary space you create is of size n, the space complexity is O(n).</li></ul><p>Having read this far, you should have a solid grasp of calculating time and space complexity. Now let’s review the intuition behind these complexities:</p><ul><li><strong>O(1)</strong>: This is the most ideal scenario because the complexity doesn’t change even as the data volume surges. Examples include reading a value at a specific array index (like arr[5]) or determining if a number is odd or even. No matter how large the data gets, the required time remains constant.</li><li><strong>O(log n):</strong> As the data volume increases, the time will also increase, but nowhere near as fast as the data grows. An example is Binary Search. When the data volume explodes exponentially, the number of operations crawls up slowly like a turtle.</li><li><strong>O(n)</strong>: As the data increases, the time grows steadily in direct proportion. Examples include using a for loop to traverse an entire array to find a number (Linear Search), or calculating the sum of all numbers in an array.</li><li><strong>O(nlog n)</strong>: This is usually the limit for “optimal sorting algorithms.” You can picture it like this: you first quickly and efficiently divide a massive pile of data into several small groups (this part is log n), and then process each piece of data (this part is n). While slightly slower than O(n), it remains a very robust and predictable range when dealing with colossal amounts of data. An example is Merge Sort.</li><li><strong>O(n²)</strong>: This is a rather tragic complexity because the workload skyrockets as the data volume increases. Examples include Bubble Sort, Insertion Sort, or the disastrous code we mentioned at the start that “uses a double loop to check for duplicate orders.”</li></ul><p>Of course, linear search, binary search, bubble sort, insertion sort, and merge sort do not encompass all search or sorting algorithms. The goal of this article is to build intuition — so that when you encounter an algorithm, you can estimate its underlying complexity. In practice, there’s a whole basket of these algorithms, which we will introduce separately in upcoming articles dedicated to searching and sorting.</p><ul><li><a href="https://medium.com/@acamvproducingstudio/c-programming-pointers-in-c-c585edc6d3dd">C Programming — Pointers in C</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-linked-list-51e875e24c0a">C Programming — Linked List</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-dynamic-memory-allocation-86221e811379">C Programming — Dynamic Memory Allocation</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-double-pointers-and-function-pointers-a99c2687fa3a">C Programming — Double Pointers and Function Pointers</a></li><li><a href="https://medium.com/@acamvproducingstudio/welcome-to-ac-studio-read-this-first-77a38848daaa">Welcome to Ac Studio! Read This First</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=658378aec525" width="1" height="1" alt=""><hr><p><a href="https://medium.com/the-coding-handbook/understanding-time-and-space-complexity-of-algorithm-658378aec525">Understanding Time and Space Complexity of Algorithm</a> was originally published in <a href="https://medium.com/the-coding-handbook">The Coding Handbook</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[C Programming — Double Pointers and Function Pointers]]></title>
            <link>https://medium.com/the-coding-handbook/c-programming-double-pointers-and-function-pointers-a99c2687fa3a?source=rss----e35ccb162ae1---4</link>
            <guid isPermaLink="false">https://medium.com/p/a99c2687fa3a</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[programming-basic]]></category>
            <category><![CDATA[computer-science]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Ac Studio]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 21:53:12 GMT</pubDate>
            <atom:updated>2026-09-08T21:53:14.028Z</atom:updated>
            <content:encoded><![CDATA[<h3>C Programming — Double Pointers and Function Pointers</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*XdPz-UzcbbATMOWC.png" /><figcaption>2026.04.18 Berghausen, Germany</figcaption></figure><h3>Prerequisites</h3><p>The prerequisite knowledge required for this article includes: (1). <a href="https://medium.com/@acamvproducingstudio/c-programming-pointers-in-c-c585edc6d3dd">Pointers</a> (2). <a href="https://medium.com/@acamvproducingstudio/c-programming-dynamic-memory-allocation-86221e811379">Dynamic Memory Allocation</a></p><p>In our introductory article on pointers, we covered the most basic concepts. A pointer variable can store the address of another variable, and we can then read the value at that address, as shown below:</p><pre>int number = 100;   // Declare a normal variable with a value of 100<br>int *ptr;           // Declare a pointer variable ptr<br><br>ptr = &amp;number;      // Store the &quot;address&quot; of number into ptr<br><br>// Now ptr stores an address (e.g., 0x101), and *ptr evaluates to 100<br>printf(&quot;Address is: %p\n&quot;, ptr);    // Prints the address<br>printf(&quot;Content is: %d\n&quot;, *ptr);   // Goes to the address and prints 100</pre><p>In this article, we are going to take a step further and introduce <strong>Double Pointers</strong> and <strong>Function Pointers</strong>.</p><h3>Double Pointers</h3><p>We can intuitively imagine that if the first pointer stores the address of a variable, a double pointer likely stores the address of the first pointer. Therefore, a double pointer is also known as a <strong>Pointer to Pointer</strong>. So, how do we define a double pointer?</p><p>We know that a pointer to an integer is int *ptr. The way we define a pointer is by adding a * after the type we want to point to. Therefore, a pointer pointing to an integer pointer can be written as int **ptr.</p><p>Dereferencing works the same way. When we write *ptr, we tell the computer to read the content stored at the address ptr. Thus, **ptr represents dereferencing twice. The first dereference reads the value inside the pointer it points to (which is the address of an integer), and the second dereference reads the actual value of that variable, as shown below:</p><pre>int val = 500;        // Original variable<br>int *ptr = &amp;val;      // First-level pointer: points to the address of val<br>int **pptr = &amp;ptr;    // Second-level pointer: points to the address of ptr<br><br>printf(&quot;Value of val: %d\n&quot;, val);<br>printf(&quot;Accessed via *ptr: %d\n&quot;, *ptr);<br>printf(&quot;Accessed via **pptr: %d\n&quot;, **pptr); // Dereferenced twice: finds ptr first, then val<br><br>printf(&quot;\nChain reaction of addresses:\n&quot;);<br>printf(&quot;Address stored in ptr (address of val): %p\n&quot;, ptr);<br>printf(&quot;Address stored in pptr (address of ptr): %p\n&quot;, pptr);</pre><h3>Triple Pointers</h3><p>Similarly, we can define a triple pointer, which is a pointer to a double pointer. If a double pointer is an address of an address, a triple pointer is the address of an address of an address. The logic for definition and dereferencing is exactly the same. int ***ppp means that the variable ppp is used to store the memory address of an int **type variable.</p><p>Dereferencing ***ppp follows the preceding logic:</p><ul><li>*ppp goes to the address stored in ppp and finds a double pointer.</li><li>**ppp goes to the double pointer&#39;s address and grabs a normal pointer.</li><li>***ppp grabs the actual variable.</li></ul><pre>int number = 100;<br>int *ptr = &amp;number;    // First-level pointer<br>int **pptr = &amp;ptr;     // Second-level pointer<br>int ***ppptr = &amp;pptr;  // Third-level pointer<br><br>printf(&quot;ppptr stores the address of pptr: %p\n&quot;, ppptr);<br>printf(&quot;*ppptr gets the content stored in pptr: %p\n&quot;, *ppptr);<br>printf(&quot;**ppptr gets the content stored in ptr: %p\n&quot;, **ppptr);<br>printf(&quot;***ppptr gets the final value: %d\n&quot;, ***ppptr); // Prints 100</pre><p>This logic can be extended to even more levels of pointers, but in practice, you usually won’t use more than this, otherwise, the code becomes very difficult to maintain.</p><p><strong>Test Your Understanding:</strong> Use the following questions to see if you have grasped multiple pointers (answers are at the very end).</p><pre>int a = 10;<br>int b = 20;<br>int *p = &amp;a;      // p points to a<br>int **pp = &amp;p;    // pp points to p<br>int ***ppp = &amp;pp; // ppp points to pp</pre><ol><li>What will be printed if you print **ppp?</li><li>If you execute *pp = &amp;b;, what will be the result of calling *p?</li><li>Following the previous question, if you execute ***ppp = 99;, which variable&#39;s value (a or b) will be changed to 99?</li></ol><h3>Practical Use Cases of Double Pointers</h3><p>Double pointers might look fancy, but they aren’t just for showing off; they have highly practical applications. A common scenario for using double pointers is handling <strong>arrays of strings</strong>.</p><p>In C, there is no built-in string data type. A string in C is essentially a continuous array of characters, with a \0 appended at the end as a termination marker. Therefore, when we write char* str = &quot;Hello&quot;;, the computer actually stores H-e-l-l-o-\0 in a block of memory, and lets the pointer str point to the address of the first letter, &#39;H&#39;. As long as the program knows where it starts, it can read sequentially down the memory until it hits \0, signaling the end of the string. Thus, &quot;a string&quot; in the eyes of C is simply a &quot;character pointer char *&quot;.</p><p>Understanding this, if we want to create a “list full of strings” — meaning a collection of multiple char * (e.g., [&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;])—it is actually an &quot;array of pointers&quot;. When this &quot;array containing pointers&quot; is passed as a parameter to a function, it naturally decays into a pointer to a pointer, which is a double pointer char **.</p><p>The most classic example is how the main function in C receives external arguments:</p><pre>// Here, argv is a double pointer<br>// It points to an array, and every element in that array is a string (char *)<br>int main(int argc, char **argv) {<br>    <br>    printf(&quot;You entered a total of %d arguments\n&quot;, argc);<br>    <br>    for (int i = 0; i &lt; argc; i++) {<br>        // argv[i] is equivalent to *(argv + i), which fetches the i-th string pointer in the array<br>        printf(&quot;Argument %d: %s\n&quot;, i, argv[i]);<br>    }<br>    <br>    return 0;<br>}</pre><p>This way of writing the main function is C&#39;s standard mechanism for receiving command-line arguments. Simply put, when we run a program in a terminal or command prompt, we don&#39;t just start it; we can also pass settings or data to it at launch. To catch this data coming from the outside, we need the argc and argv parameters.</p><p>Let’s break this down with a concrete example. Suppose we compile this code into an executable named my_program. We then enter the following command in the terminal to execute it:</p><p>./my_program Apple Banana Cherry</p><p>At this point, the operating system automatically splits the space-separated text, packages them up, and sends them into the main function:</p><ol><li><strong>argc (Argument Count):</strong> The first parameter of the main function, its goal is to count the total number of arguments. In this example, there are four items: ./my_program, Apple, Banana, and Cherry, so the value of argc will be 4 (the program&#39;s own name counts as well).</li><li><strong>argv:</strong> The second parameter, char argv, is the double pointer we just learned about. It&#39;s an array filled with pointers pointing to each string.</li></ol><p>When you hit Enter, the operating system (like Linux or Windows) steps in and does the following:</p><ul><li>Splits the text string.</li><li>Finds a place in memory to store the strings ./my_program, Apple, Banana, Cherry (each of them is a char *).</li><li>Creates a real array of pointers to hold the addresses of these 4 strings: char *system_array[5] = {&quot;./my_program&quot;, &quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;, NULL};</li></ul><p>Next, the operating system is ready to call your main function and pass this system_array in. However, in C, when you pass an array as a parameter to a function, C does not copy the entire array over (that would waste too much memory). It only passes <strong>&quot;the memory address of the first element of this array&quot;</strong>. This phenomenon is known as <strong>Array Decay</strong>. That is to say:</p><ul><li>system_array is an array.</li><li>Every element inside the array is a char * (string pointer).</li><li>When it is passed into the main function, C only passes the address of the first element.</li><li>What is the type of the address of a char * variable? It is a pointer pointing to a char *, which is char **.</li></ul><p>This is why the original “array of strings” naturally decays into a “double pointer char **argv&quot; the moment it crosses the function boundary.</p><h3>Function Pointers</h3><p>Besides pointing to variables, pointers can also point to functions, known as <strong>Function Pointers</strong>. Just like variables, functions also have their memory addresses.</p><p>In C, compiled functions are stored in the code segment of memory. The function name itself is actually a pointer pointing to the starting memory address of that function. A function pointer is a type of variable used to store this memory address, allowing us to dynamically decide which function to call at runtime.</p><p>In other words, since the function name itself is a memory location, how do we declare a variable to store the function’s location? When we declare a normal integer pointer, we write: int *ptr;. This tells the compiler: ptr is a pointer, and the content it points to is an int.</p><p>But for functions, just knowing it is a function is not enough. The compiler must know:</p><ol><li>What data type will this function return?</li><li>What types of parameters does it need to receive?</li></ol><p>Therefore, the standard syntax for declaring a function pointer is as follows:</p><pre>return_type (*pointer_variable_name)(parameter_type1, parameter_type2...);</pre><p>Note that the parentheses around (*pointer_variable_name) are crucial. For example, if you write: int (*ptr)(int, int); This is a function pointer pointing to a function that returns an int. But if you write: int *ptr(int, int); This means you are declaring a normal function that receives two int parameters and returns an integer pointer int *. The meanings of the two are completely different.</p><p>Let’s look at an example to implement a function pointer. Suppose we have a simple addition function:</p><pre>int add(int a, int b) {<br>    return a + b;<br>}</pre><p>The signature of this function is “receives two ints, and returns an int&quot;. Let&#39;s declare a function pointer that matches this signature and store the address of add into it:</p><pre>// 1. Declare the function pointer<br>int (*func_ptr)(int, int);<br><br>// 2. Assign the address of the add function to func_ptr<br>func_ptr = add;</pre><p>Once the function is stored in the pointer, we can execute the function through this pointer.</p><pre>// Call the function via the function pointer, passing in parameters 3 and 5<br>int result = func_ptr(3, 5); <br><br>printf(&quot;The result is: %d\n&quot;, result); // Output: The result is: 8</pre><p>You might feel this is pointless — why not just write add(3,5) directly instead of taking a long detour using a function pointer? Let&#39;s look at a more complex example. Suppose we want to design a calculator that can do both addition and subtraction. We can dynamically let the user decide whether to use addition or subtraction at runtime.</p><pre>#include &lt;stdio.h&gt;<br><br>// Define two functions with identical signatures<br>int add(int a, int b) {<br>    return a + b;<br>}<br><br>int subtract(int a, int b) {<br>    return a - b;<br>}<br><br>int main() {<br>    // Declare a function pointer<br>    int (*operation)(int, int);<br>    <br>    int choice = 1; // Assuming the user inputs 1 (for addition), or 2 for subtraction<br>    int x = 10, y = 5;<br><br>    // &quot;Dynamically&quot; change where the pointer points based on user choice<br>    if (choice == 1) {<br>        operation = add;      // Point to addition<br>    } else {<br>        operation = subtract; // Point to subtraction<br>    }<br><br>    // We don&#39;t need to write if-else blocks here to call the function<br>    // Just call operation directly, and it will execute the function that was just assigned!<br>    int result = operation(x, y);<br>    printf(&quot;Calculation result: %d\n&quot;, result);<br><br>    return 0;<br>}</pre><h3>Advanced Usage: Callback Functions</h3><p>Let’s look at a more advanced use of function pointers.</p><p>Suppose we have an array of integers, and we want to print out specific numbers from it. Initially, we want to print out all the even numbers, so we write this function:</p><pre>void print_evens(int arr[], int size) {<br>    for (int i = 0; i &lt; size; i++) {<br>        if (arr[i] % 2 == 0) { // Check if it is an even number<br>            printf(&quot;%d &quot;, arr[i]);<br>        }<br>    }<br>    printf(&quot;\n&quot;);<br>}</pre><p>Then we want to change it to find all numbers greater than 50. This means we have to write another condition for checking if it’s greater than 50, eventually turning our code into a messy pile. However, we can elegantly solve this problem using function pointers.</p><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdbool.h&gt;<br><br>// This is our &quot;Strategy 1&quot;: Check if it&#39;s an even number<br>bool is_even(int num) {<br>    return num % 2 == 0;<br>}<br><br>// This is our &quot;Strategy 2&quot;: Check if it&#39;s greater than 50<br>bool is_greater_than_50(int num) {<br>    return num &gt; 50;<br>}<br><br>// This function only handles the &quot;framework&quot;; it doesn&#39;t know what the specific filtering rule is.<br>// It requires you to pass in a function pointer (Callback) that &quot;receives an int, returns a bool&quot;.<br>void filter_and_print(int arr[], int size, bool (*condition)(int)) {<br>    for (int i = 0; i &lt; size; i++) {<br>        // Here we &quot;call back&quot; the function passed in; it decides whether to print or not<br>        if (condition(arr[i])) { <br>            printf(&quot;%d &quot;, arr[i]);<br>        }<br>    }<br>    printf(&quot;\n&quot;);<br>}<br><br>int main() {<br>    int my_array[] = {12, 55, 8, 99, 42, 3};<br>    int size = 6;<br><br>    printf(&quot;Print even numbers: &quot;);<br>    filter_and_print(my_array, size, is_even); // Pass in Strategy 1<br><br>    printf(&quot;Print numbers greater than 50: &quot;);<br>    filter_and_print(my_array, size, is_greater_than_50); // Pass in Strategy 2<br><br>    return 0;<br>}</pre><p>This technique is called a <strong>Callback Function (Callback)</strong>.</p><p>Next, let’s look at the ultimate boss of Callbacks in C — the built-in quicksort function (qsort). This function is defined in &lt;stdlib.h&gt;, and its signature looks like this:</p><pre>void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));</pre><p>This is a sorting machine. It knows how to sort things at top speed (using the QuickSort algorithm), but it doesn’t know what kind of things you are passing in (because it uses the universal void * pointer, which could be integers, floats, or even strings), and it certainly doesn&#39;t know which one is bigger or smaller.</p><p>Therefore, it forces you to pass in a fourth parameter: int (*compar)(const void *, const void *). This is a Callback function pointer used to tell qsort the rules for comparing two elements.</p><pre>#include &lt;stdio.h&gt;<br>#include &lt;stdlib.h&gt;<br><br>// This is the Callback function we will pass to qsort<br>// qsort dictates that the comparison function must return an int and receive two const void * parameters<br>int compare_ints(const void *a, const void *b) {<br>    // 1. Because we know what is passed in are integers, we first cast void * back to int *<br>    int *ptr_a = (int *)a;<br>    int *ptr_b = (int *)b;<br>    <br>    // 2. Extract the values inside<br>    int val_a = *ptr_a;<br>    int val_b = *ptr_b;<br>    <br>    // 3. Tell qsort which is bigger<br>    // Rule: if a &lt; b return negative; if a == b return 0; if a &gt; b return positive<br>    return val_a - val_b; <br>}<br><br>int main() {<br>    int data[] = {88, 56, 100, 2, 25};<br>    int size = 5;<br><br>    // Call qsort: pass in the array, the number of items, the size of each element, and our comparison rule (Callback)<br>    qsort(data, size, sizeof(int), compare_ints);<br><br>    // Print the result<br>    printf(&quot;After sorting: &quot;);<br>    for (int i = 0; i &lt; size; i++) {<br>        printf(&quot;%d &quot;, data[i]);<br>    }<br>    printf(&quot;\n&quot;); // Output: 2 25 56 88 100<br><br>    return 0;<br>}</pre><p>Many people become highly proficient at using qsort in their school programming classes, yet fail to realize that function pointers are quietly working behind the scenes.</p><h3>Answers to the Questions</h3><ol><li>The address of variable a. If changed to ***ppp, it would print 10.</li><li>*pp is exactly equal to p. Therefore, *pp = &amp;b is equivalent to changing the target that p points to from a to b.</li><li>Variable b, because the previous sub-question already changed p&#39;s arrow to point to b.</li></ol><ul><li><a href="https://medium.com/@acamvproducingstudio/c-programming-pointers-in-c-c585edc6d3dd">C Programming — Pointers in C</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-linked-list-51e875e24c0a">C Programming — Linked List</a></li><li><a href="https://medium.com/@acamvproducingstudio/c-programming-dynamic-memory-allocation-86221e811379">C Programming — Dynamic Memory Allocation</a></li><li><a href="https://medium.com/@acamvproducingstudio/understanding-time-and-space-complexity-of-algorithm-658378aec525">Understanding Time and Space Complexity of Algorithm</a></li><li><a href="https://medium.com/@acamvproducingstudio/welcome-to-ac-studio-read-this-first-77a38848daaa">Welcome to Ac Studio! Read This First</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a99c2687fa3a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/the-coding-handbook/c-programming-double-pointers-and-function-pointers-a99c2687fa3a">C Programming — Double Pointers and Function Pointers</a> was originally published in <a href="https://medium.com/the-coding-handbook">The Coding Handbook</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>