<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Meer-Matze的小站</title><description>一个基于 Astro 的静态博客</description><link>https://meer-matze.github.io/</link><language>zh_CN</language><item><title>二叉树基类实现</title><link>https://meer-matze.github.io/posts/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%9F%BA%E7%B1%BB%E5%AE%9E%E7%8E%B0/</link><guid isPermaLink="true">https://meer-matze.github.io/posts/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%9F%BA%E7%B1%BB%E5%AE%9E%E7%8E%B0/</guid><description>有关二叉树的基类实现，包含基本的二叉树结构和常用的操作方法和迭代器实现。</description><pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;二叉树基类&lt;/h1&gt;
&lt;p&gt;在&lt;a href=&quot;/posts/%E4%BA%8C%E5%8F%89%E6%A0%91/&quot;&gt;总述&lt;/a&gt;中，我们介绍了二叉树的基本概念和一些常见的二叉树类型。在本篇博客中，我们将实现一个二叉树的基类，包含基本的二叉树结构和常用的操作方法，以及迭代器实现。
:::warning
&lt;strong&gt;注意&lt;/strong&gt;：本博客中的代码实现仅供参考，实际应用中可能需要根据具体需求进行调整和优化。
而且，我并没有很严格的按照c++的规范和习惯，所以可能会有一些不太规范的地方，请读者自行斟酌。
:::&lt;/p&gt;
&lt;h2&gt;二叉树代码结构&lt;/h2&gt;
&lt;p&gt;显然的，二叉树的基本结构是一个节点类和一个二叉树类。节点类包含节点的值、左子树和右子树的指针，而二叉树类包含根节点和一些操作方法。
这些构成了二叉树最基本的框架，我们可以在此基础上实现各种二叉树类型和操作方法。
当然，在基类中，我们只需要实现一些通用的操作方法，如&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;s&gt;删除节点&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;查找节点&lt;/li&gt;
&lt;li&gt;迭代器
:::tip
&lt;strong&gt;插入&lt;/strong&gt;和&lt;strong&gt;平衡&lt;/strong&gt;等操作通常是特定于某些二叉树类型的（如二叉搜索树、AVL树等），因此我们可以将这些方法声明为纯虚函数~~（但是我偷懒直接不声明了）~~，让具体的二叉树类型来实现它们。
:::
要注意的是，为了&lt;strong&gt;可拓展性&lt;/strong&gt;，我们将二叉树类设计为一个基类，具体的二叉树类型（如二叉搜索树、平衡二叉树等）可以继承这个基类并实现特定的功能。
那么，&lt;strong&gt;模板&lt;/strong&gt;的使用就必不可少。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二叉树的具体构成&lt;/h2&gt;
&lt;h3&gt;二叉树节点类&lt;/h3&gt;
&lt;p&gt;一个二叉树节点类通常包含以下成员：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;：节点储存的数据，可以是任意类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;left_child&lt;/code&gt;：指向左子树的指针。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;right_child&lt;/code&gt;：指向右子树的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造函数：用于初始化节点，而且禁止无参构造函数，以确保每个节点都有一个有效的数据值。&lt;/li&gt;
&lt;li&gt;析构函数：用于释放节点占用的资源，要递归地删除左子树和右子树，以避免内存泄漏。&lt;/li&gt;
&lt;li&gt;移动和拷贝函数：事实上，二叉树节点通常不需要实现移动和拷贝函数，因为它们的生命周期由二叉树类管理，且节点之间的关系通过指针维护。于是，我们应当&lt;strong&gt;禁止二叉树节点的拷贝和移动操作&lt;/strong&gt;，以避免不必要的资源管理复杂性和潜在的错误。&lt;/li&gt;
&lt;li&gt;operator==：实现比较两个节点是否相等，可以用于查找节点等操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二叉树类&lt;/h3&gt;
&lt;p&gt;一个二叉树类通常包含以下成员：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;root&lt;/code&gt;：指向二叉树根节点的指针。&lt;/li&gt;
&lt;li&gt;迭代器：用于遍历二叉树的迭代器，可以实现前序、中序、后序等不同的遍历方式。
:::note[为什么迭代器要放在二叉树类中？]
因为迭代器需要访问二叉树的内部结构（如根节点和子树），将迭代器作为二叉树类的成员可以更方便地实现遍历功能。
:::
和以下方法：&lt;/li&gt;
&lt;li&gt;构造函数：用于初始化二叉树，通常会将根节点初始化为&lt;code&gt;nullptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;析构函数：用于释放二叉树占用的资源，要递归地删除根节点及其子树，以避免内存泄漏。&lt;/li&gt;
&lt;li&gt;查找节点：实现一个方法来查找二叉树中的节点，可以根据节点的值进行查找。&lt;/li&gt;
&lt;li&gt;&lt;s&gt;删除节点&lt;/s&gt;：相当复杂，懒得实现了，后续有机会再更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了让二叉树类可以直接访问节点类的&lt;code&gt;protected&lt;/code&gt;成员，我们可以将节点类定义为二叉树类的&lt;strong&gt;嵌套类&lt;/strong&gt;，或者将节点类的成员声明为&lt;code&gt;public&lt;/code&gt;，而我直接将二叉树类设置为节点类的&lt;strong&gt;友元类&lt;/strong&gt;，这样二叉树类就可以访问节点类的&lt;code&gt;protected&lt;/code&gt;成员了。&lt;/p&gt;
&lt;h3&gt;二叉树迭代器&lt;/h3&gt;
&lt;p&gt;二叉树迭代器通常包含以下成员：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;current&lt;/code&gt;：指向当前节点的指针。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stack&lt;/code&gt;：由于二叉树的遍历往往是中序遍历，而又不能使用函数栈，所以我们需要一个栈来存储访问过的节点，以便在遍历过程中回退到父节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造函数：用于初始化迭代器，通常会将&lt;code&gt;current&lt;/code&gt;初始化为二叉树的根节点，并将所有左子树的节点压入栈中，以准备进行中序遍历。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator++&lt;/code&gt;：实现迭代器的前缀递增操作，用于移动到下一个节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator*&lt;/code&gt;：实现迭代器的解引用操作，用于访问当前节点的数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator==&lt;/code&gt;和&lt;code&gt;operator!=&lt;/code&gt;：实现迭代器的比较操作，用于判断两个迭代器是否指向同一个节点。
:::tip
迭代器的实现可以根据需要进行调整，例如可以实现前序遍历、后序遍历等不同的遍历方式，或者实现双向迭代器等更复杂的功能。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;具体代码实现&lt;/h1&gt;
&lt;p&gt;下面是一个简单的二叉树基类的代码实现，我会进行逐段解释。&lt;/p&gt;
&lt;h2&gt;二叉树节点类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#ifndef BINARY_TREE_HPP
#define BINARY_TREE_HPP
#include &amp;lt;stack&amp;gt;

namespace myStruct {
    template&amp;lt;typename Data, class DerivedNode&amp;gt;
    class treeNode 
    {
        using Node = treeNode&amp;lt;Data, DerivedNode&amp;gt;;
        friend class BinaryTree&amp;lt;Data, DerivedNode&amp;gt;;
    protected:
        Data data;
        DerivedNode *left_child;
        DerivedNode *right_child;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，首先是防止头文件被多次包含的预处理指令&lt;code&gt;#ifndef&lt;/code&gt;和&lt;code&gt;#define&lt;/code&gt;（最后有&lt;code&gt;#endif&lt;/code&gt;），如果想用&lt;code&gt;#pragma once&lt;/code&gt;也行但不推荐。
接着，我们定义了一个命名空间&lt;code&gt;myStruct&lt;/code&gt;，以避免命名冲突，和提供更好的代码组织。
然后，我们定义了一个模板类&lt;code&gt;treeNode&lt;/code&gt;，它接受两个模板参数：&lt;code&gt;Data&lt;/code&gt;和&lt;code&gt;DerivedNode&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Data&lt;/code&gt;表示节点中存储的数据类型，可以是任意类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DerivedNode&lt;/code&gt;表示&lt;strong&gt;派生节点的类型&lt;/strong&gt;，这样我们可以在基类中使用&lt;code&gt;DerivedNode&lt;/code&gt;来表示具体的节点类型，提供更好的可拓展性。这样做有如下好处：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可拓展性&lt;/strong&gt;：如果不使用&lt;code&gt;DerivedNode&lt;/code&gt;，我们只能在基类中使用&lt;code&gt;treeNode&lt;/code&gt;来表示节点，这样在派生类中就无法使用更具体的节点类型了。而使用&lt;code&gt;DerivedNode&lt;/code&gt;后，我们可以在派生类中定义一个具体的节点类型，并将其作为模板参数传递给基类，这样在基类中就可以使用这个具体的节点类型了。&lt;s&gt;偷懒小技巧&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRTP&lt;/strong&gt;：避免了使用虚函数，让编译器能够更好地优化代码，因为编译器可以根据&lt;code&gt;DerivedNode&lt;/code&gt;的具体类型来生成更高效的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接着，我们使用&lt;code&gt;using Node = treeNode&amp;lt;Data, DerivedNode&amp;gt;;&lt;/code&gt;来定义一个类型别名&lt;code&gt;Node&lt;/code&gt;，这样我们在后续的代码中就可以直接使用&lt;code&gt;Node&lt;/code&gt;来表示节点类型了。
最后，我们将&lt;code&gt;BinaryTree&amp;lt;Data, DerivedNode&amp;gt;&lt;/code&gt;声明为&lt;code&gt;treeNode&lt;/code&gt;的友元类，这样&lt;code&gt;BinaryTree&lt;/code&gt;就可以访问&lt;code&gt;treeNode&lt;/code&gt;的&lt;code&gt;protected&lt;/code&gt;成员了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    public:
        treeNode() = delete;// 禁止默认构造，必须有初始数据
        explicit treeNode(Data data) : data(std::move(data)), left_child(nullptr), right_child(nullptr) {}

        // 显式禁止拷贝和移动，树节点通常通过指针管理，避免意外拷贝导致内存管理混乱
        treeNode(const treeNode&amp;amp;) = delete;
        treeNode&amp;amp; operator=(const treeNode&amp;amp;) = delete;
        treeNode(treeNode&amp;amp;&amp;amp;) = delete;
        treeNode&amp;amp; operator=(treeNode&amp;amp;&amp;amp;) = delete;

        virtual ~treeNode(){
            delete left_child;
            delete right_child;
        }
    };
    template &amp;lt;typename Data, class DerivedNode&amp;gt;
    bool operator==(const DerivedNode &amp;amp;lhs, const DerivedNode &amp;amp;rhs)
    {
        return lhs.data == rhs.data;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，我们定义了&lt;code&gt;treeNode&lt;/code&gt;类的构造函数、析构函数和一些特殊成员函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;treeNode() = delete;&lt;/code&gt;：禁止默认构造函数，要求每个节点必须有一个有效的数据值。&lt;/li&gt;
&lt;li&gt;被&lt;code&gt;delete&lt;/code&gt;的四个函数：禁止拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。因为二叉树节点通常通过指针管理，禁止这些函数可以避免意外的拷贝或移动操作导致内存管理混乱。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virtual ~treeNode()&lt;/code&gt;：定义一个虚析构函数，递归地删除左子树和右子树，以避免内存泄漏。使用&lt;code&gt;virtual&lt;/code&gt;关键字确保在派生类中正确调用析构函数，释放资源。&lt;/li&gt;
&lt;li&gt;&lt;code&gt; treeNode(Data data)&lt;/code&gt;：定义一个显式的构造函数，接受一个&lt;code&gt;Data&lt;/code&gt;类型的参数来初始化节点的数据，并将左右子树指针初始化为&lt;code&gt;nullptr&lt;/code&gt;。&lt;code&gt;explicit&lt;/code&gt;关键字用于防止隐式类型转换，确保构造函数只能通过显式调用来使用。&lt;code&gt;std::move(data)&lt;/code&gt;用于将传入的数据移动到节点中，避免不必要的复制，提高性能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator==&lt;/code&gt;：实现比较两个节点是否相等，比较的是节点中的数据值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二叉树类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    template&amp;lt;class Node&amp;gt;
    class BinaryTree
    {
    protected:
        Node *root;
    public:
        BinaryTree() : root(nullptr) {}
        virtual ~BinaryTree() { delete root; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，&lt;code&gt;class Node&lt;/code&gt;是一个模板参数，表示二叉树中节点的类型，这样我们可以在派生类中定义一个具体的节点类型，并将其作为模板参数传递给基类。这样有利于提高代码的可拓展性。
在这之后，由于我们要实现查找操作，所以最好先把迭代器实现了，这样我们就可以使用迭代器来遍历二叉树并查找节点了。&lt;/p&gt;
&lt;h2&gt;二叉树迭代器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;        class InorderIterator
        {
        protected:
            Node *current;
            std::stack&amp;lt;Node*&amp;gt; node_stack;
            void pushLeft(Node *node) {
                while (node) {
                    node_stack.push(node);
                    node = node-&amp;gt;left_child;
                }
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，我们定义了一个嵌套类&lt;code&gt;InorderIterator&lt;/code&gt;，用于实现二叉树的中序遍历。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Node *current;&lt;/code&gt;：指向当前节点的指针。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::stack&amp;lt;Node*&amp;gt; node_stack;&lt;/code&gt;：一个栈，用于存储访问过的节点，以便在遍历过程中回退到父节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void pushLeft(Node *node)&lt;/code&gt;：一个辅助函数，用于将当前节点及其所有左子树的节点压入栈中，以准备进行中序遍历。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;        public:
            InorderIterator(Node *root) : current(root) {
                pushLeft(current);
                operator++(); // 初始化时将current指向第一个节点
            }
            ~InorderIterator() = default;
            auto&amp;amp; operator*(){
                return current-&amp;gt;data;
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，我们定义了&lt;code&gt;InorderIterator&lt;/code&gt;的构造函数、析构函数和解引用操作符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InorderIterator(Node *root)&lt;/code&gt;：构造函数，接受一个指向二叉树根节点的指针，并调用&lt;code&gt;pushLeft&lt;/code&gt;函数将根节点及其所有左子树的节点压入栈中，以准备进行中序遍历。然后调用&lt;code&gt;operator++()&lt;/code&gt;将&lt;code&gt;current&lt;/code&gt;指向第一个节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~InorderIterator() = default;&lt;/code&gt;：默认析构函数，因为我们没有需要特殊的资源管理。不要想着删除节点，因为节点的生命周期由二叉树类管理，迭代器不负责删除节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto&amp;amp; operator*()&lt;/code&gt;：解引用操作符，返回当前节点的数据的引用，以便可以直接访问和修改节点的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;            // 前置 ++ (++it)
            InorderIterator&amp;amp; operator++()
            {
                if (node_stack.empty())
                {
                    current = nullptr;
                    return *this;
                }
                current = node_stack.top();
                node_stack.pop();
                if (current-&amp;gt;right_child)
                {
                    pushLeft(current-&amp;gt;right_child);
                }
                return *this;
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，我们实现了迭代器的前置递增操作符&lt;code&gt;operator++()&lt;/code&gt;，用于移动到下一个节点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先检查栈是否为空，如果为空，说明已经遍历完所有节点，将&lt;code&gt;current&lt;/code&gt;设置为&lt;code&gt;nullptr&lt;/code&gt;并返回。&lt;/li&gt;
&lt;li&gt;否则，从栈顶弹出一个节点，将其赋值给&lt;code&gt;current&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;然后检查&lt;code&gt;current&lt;/code&gt;的右子树是否存在，如果存在，调用&lt;code&gt;pushLeft&lt;/code&gt;函数将右子树的节点压入栈中，以准备继续进行中序遍历。&lt;/li&gt;
&lt;li&gt;最后返回当前迭代器对象，以支持链式调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;InorderIterator&amp;amp; operator=(const InorderIterator&amp;amp; other)
            {
                if (this != &amp;amp;other)
                {
                    node_stack = other.node_stack; // 复制栈
                    current = other.current; // 复制当前节点指针
                }
                return *this;
            }

            // 判断是否相等，用于循环结束判断
            bool operator==(const InorderIterator&amp;amp; other) const
            {
                return current == other.current;
            }

            bool operator!=(const InorderIterator&amp;amp; other) const
            {
                return !(*this == other);
            }

        };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，我们实现了迭代器的拷贝赋值操作符和比较操作符。并且到达了迭代器类的结尾。但迭代器还没结束，还有&lt;code&gt;begin&lt;/code&gt;和&lt;code&gt;end&lt;/code&gt;方法，这两个方法在二叉树类中实现，毕竟迭代器本身怎么能获取&lt;code&gt;begin&lt;/code&gt;和&lt;code&gt;end&lt;/code&gt;呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        InorderIterator begin() {
            return InorderIterator(root);
        }

        InorderIterator end() {
            return InorderIterator(nullptr);
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，我们实现了二叉树类的&lt;code&gt;begin()&lt;/code&gt;和&lt;code&gt;end()&lt;/code&gt;方法，用于返回迭代器的起始位置和结束位置。
:::tip
有没有注意到没有&lt;code&gt;const_iterator&lt;/code&gt;?这是因为一旦要写&lt;code&gt;const_iterator&lt;/code&gt;，为了不重复代码~~（直接重写，力大砖飞）~~，就要用到&lt;code&gt;template &amp;lt;bool IsConst&amp;gt;&lt;/code&gt;模板参数来区分&lt;code&gt;const_iterator&lt;/code&gt;和&lt;code&gt;iterator&lt;/code&gt;，属于高阶用法了，但这是&lt;code&gt;STL&lt;/code&gt;中的常见做法。
请注意，&lt;code&gt;const_iterator&lt;/code&gt;是有必要的，不然在处理&lt;code&gt;const&lt;/code&gt;元素时就无法使用迭代器了，所以后续有机会再更新&lt;code&gt;const_iterator&lt;/code&gt;的实现。
:::&lt;/p&gt;
&lt;h2&gt;二叉树类的查找方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;        Node* find(const Data&amp;amp; value) {
            for (auto it = begin(); it != end(); ++it) {
                if (*it == value) 
                    return it.current; // 返回指向找到节点的指针
            }
            return nullptr; // 如果未找到，返回nullptr
        }
    };
} // namespace myStruct
#endif // BINARY_TREE_HPP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一段中，我们通过迭代器遍历二叉树来实现查找方法&lt;code&gt;find&lt;/code&gt;。已经是宝宝巴士级别了，就不过多说明了。&lt;/p&gt;
&lt;h1&gt;结语&lt;/h1&gt;
&lt;p&gt;二叉树的基类实现是一个重要的基础，提供了二叉树的基本结构和常用的操作方法。通过使用模板和迭代器，我们可以实现一个灵活且可拓展的二叉树类，适用于各种不同类型的二叉树。至于下一步，那就是&lt;strong&gt;AVL树&lt;/strong&gt;了，具体实现请待下回分解。&lt;s&gt;不得不说，AVL树真的很麻烦啊&lt;/s&gt;&lt;/p&gt;
</content:encoded></item><item><title>字典树</title><link>https://meer-matze.github.io/posts/%E5%AD%97%E5%85%B8%E6%A0%91/</link><guid isPermaLink="true">https://meer-matze.github.io/posts/%E5%AD%97%E5%85%B8%E6%A0%91/</guid><description>字典树（Trie）是一种树形数据结构，用于高效地存储和检索字符串集合。它的主要特点是通过公共前缀来组织字符串，从而节省空间并提高查询效率。每个节点代表一个字符，路径从根节点到某个节点表示一个字符串。字典树常用于自动补全、拼写检查和前缀匹配等应用场景。</description><pubDate>Sun, 05 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;字典树（Trie）&lt;/h1&gt;
&lt;p&gt;字典树（Trie）是一种树形数据结构，通过公共前缀来组织字符串集合。这种结构能有效节省存储空间，并使自动补全、拼写检查等前缀匹配操作变得异常高效。&lt;/p&gt;
&lt;h2&gt;字典树的原理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://oi-wiki.org/string/images/trie1.png&quot; alt=&quot;字典树示例&quot; /&gt;
如图所示（图源：&lt;a href=&quot;https://oi-wiki.org/string/trie/&quot;&gt;OI Wiki&lt;/a&gt;），字典树的核心特点如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;节点不存储字符&lt;/strong&gt;：字符通常存储在&lt;strong&gt;边&lt;/strong&gt;上，或者由节点在父节点中的位置决定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享前缀&lt;/strong&gt;：路径从根节点到某个节点表示一个字符串。例如，路径 &lt;code&gt;c -&amp;gt; a -&amp;gt; a&lt;/code&gt; 表示字符串 &lt;code&gt;&quot;caa&quot;&lt;/code&gt;，而 &lt;code&gt;&quot;caa&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;cab&quot;&lt;/code&gt;共享了前缀 &lt;code&gt;&quot;ca&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标记结尾&lt;/strong&gt;：并非所有节点都代表字符串的终点，通常需要一个布尔值（如 &lt;code&gt;is_end_of_word&lt;/code&gt;）来标记该节点是否构成了一个完整的单词。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;字典树的实现&lt;/h1&gt;
&lt;h2&gt;字典树的节点数据结构&lt;/h2&gt;
&lt;p&gt;字典树的节点一般包含以下几个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;children&lt;/code&gt;：一个字典或数组，用于存储子节点，&lt;code&gt;key&lt;/code&gt;为字符，&lt;code&gt;value&lt;/code&gt;为对应的子节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_end_of_word&lt;/code&gt;：一个布尔值，表示当前节点是否是一个完整字符串的结尾。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;：&lt;em&gt;可选&lt;/em&gt; 额外的数据字段，可以存储与字符串相关的信息，例如动态规划中的状态值。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;struct TrieNode {
    std::unordered_map&amp;lt;char, TrieNode*&amp;gt; children; // 存储子节点
    bool is_end_of_word; // 是否是一个完整字符串的结尾
    int data; // 额外的数据字段

    TrieNode() : is_end_of_word(false), data(0) {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字典树的插入操作&lt;/h2&gt;
&lt;p&gt;插入一个字符串到字典树中，首先从根节点开始，依次检查字符串的每个字符。如果当前字符在子节点中不存在，则创建一个新的节点。最后一个字符处理完后，将最后一个节点的 &lt;code&gt;is_end_of_word&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;，表示这是一个完整字符串的结尾。
:::tip
根节点通常不存储任何字符，它只是一个起点。它的下一个节点才存储第一个字符。这样设计可以简化插入和查询操作。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Trie {
    TrieNode* root;

    Trie() {
        root = new TrieNode();
    }

    void insert(const std::string&amp;amp; word) {
        TrieNode* current = root;
        for (char c : word) {
            if (current-&amp;gt;children.find(c) == current-&amp;gt;children.end()) {
                current-&amp;gt;children[c] = new TrieNode();
            }
            current = current-&amp;gt;children[c];
        }
        current-&amp;gt;is_end_of_word = true; // 标记字符串结尾
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字典树的查询操作&lt;/h2&gt;
&lt;p&gt;查询一个字符串是否存在于字典树中，首先从根节点开始，依次检查字符串的每个字符。如果当前字符在子节点中不存在，则说明字符串不存在于字典树中。如果所有字符都存在，并且最后一个节点的 &lt;code&gt;is_end_of_word&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，则说明字符串存在于字典树中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Trie {
    \\...（前略）
    bool search(const std::string&amp;amp; word) {
        TrieNode* current = root;
        for (char c : word) {
            if (current-&amp;gt;children.find(c) == current-&amp;gt;children.end()) {
                return false; // 字符不存在，字符串不存在
            }
            current = current-&amp;gt;children[c];
        }
        return current-&amp;gt;is_end_of_word; // 检查是否是完整字符串的结尾
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字典树的删除操作&lt;/h2&gt;
&lt;p&gt;删除一个字符串从字典树中，首先需要检查字符串是否存在于字典树中。如果存在，则需要沿着路径删除对应的节点。删除操作需要注意，如果一个节点的子节点不为空，则不能删除该节点，因为它可能是其他字符串的前缀。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Trie {
    \\...（前略）
    bool remove(const std::string&amp;amp; word) {
        return remove_helper(root, word, 0);
    }
private:
    //index：当前处理字符串的第index个字符
    //返回值：是否需要删除当前节点
    bool remove_helper(TrieNode* current, const std::string&amp;amp; word, int index) {
        if (index == word.size()) {
            if (!current-&amp;gt;is_end_of_word) {
                return false; // 字符串不存在
            }
            current-&amp;gt;is_end_of_word = false; // 取消字符串结尾标记
            return current-&amp;gt;children.empty(); // 如果没有子节点，可以删除当前节点
        }

        char c = word[index];
        if (current-&amp;gt;children.find(c) == current-&amp;gt;children.end()) {
            return false; // 字符不存在，字符串不存在
        }

        bool should_delete_child = remove_helper(current-&amp;gt;children[c], word, index + 1);
        if (should_delete_child) {
            delete current-&amp;gt;children[c]; // 删除子节点
            current-&amp;gt;children.erase(c); // 从子节点中移除
            return current-&amp;gt;children.empty() &amp;amp;&amp;amp; !current-&amp;gt;is_end_of_word; // 如果当前节点没有子节点且不是字符串结尾，可以删除当前节点
        }

        return false; // 不需要删除当前节点
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字典树的合并&lt;/h2&gt;
&lt;p&gt;暂时没写到这种题目，后续再补充。&lt;/p&gt;
&lt;h1&gt;字典树的应用&lt;/h1&gt;
&lt;p&gt;字典树在许多应用场景中非常有用，以下是一些常见的应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动补全&lt;/strong&gt;：字典树可以用于实现自动补全功能。当用户输入一个前缀时，字典树可以快速找到所有以该前缀开头的字符串，从而提供建议。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拼写检查&lt;/strong&gt;：字典树可以用于实现拼写检查功能。当用户输入一个单词时，字典树可以快速检查该单词是否存在于字典树中，从而判断拼写是否正确。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前缀匹配&lt;/strong&gt;：字典树可以用于实现前缀匹配功能。当用户输入一个前缀时，字典树可以快速找到所有以该前缀开头的字符串，从而提供相关信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字符串统计&lt;/strong&gt;：字典树可以用于统计字符串集合中的字符串数量。例如，可以在每个节点中维护一个计数器，记录以该节点为前缀的字符串数量，从而快速统计以某个前缀开头的字符串数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字符串排序&lt;/strong&gt;：字典树可以用于对字符串集合进行排序。通过深度优先搜索字典树，可以按照字典序输出所有字符串，从而实现字符串排序功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;字典树（Trie）是一种高效的树形数据结构，适用于存储和检索字符串集合。它通过共享前缀来节省空间，并提供快速的查询、插入和删除操作。字典树在自动补全、拼写检查、前缀匹配等应用场景中非常有用，是处理字符串相关问题的重要工具。&lt;s&gt;不好用，不如哈希表&lt;/s&gt;&lt;/p&gt;
</content:encoded></item><item><title>等效电源的综合例题</title><link>https://meer-matze.github.io/posts/%E7%AD%89%E6%95%88%E7%94%B5%E6%BA%90%E7%9A%84%E7%BB%BC%E5%90%88%E4%BE%8B%E9%A2%98/</link><guid isPermaLink="true">https://meer-matze.github.io/posts/%E7%AD%89%E6%95%88%E7%94%B5%E6%BA%90%E7%9A%84%E7%BB%BC%E5%90%88%E4%BE%8B%E9%A2%98/</guid><description>收录了一些我不会的等效电路例题，供以后复习使用。</description><pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;例1&lt;/h1&gt;
&lt;p&gt;图示电路中，$N$为线性含源电阻网络。已知$i_1 = 2\text{A}$时，$i_2 = \frac{1}{3}\text{A}$；当$R$增加$10\Omega$时，$i_1 = 1.5\text{A}$，$i_2 = 0.5\text{A}$。求：当$R$减少$10\Omega$时，$i_2$的值。
&lt;img src=&quot;./1.webp&quot; alt=&quot;电路图&quot; /&gt;
解：
&lt;img src=&quot;./2.webp&quot; alt=&quot;解题步骤&quot; /&gt;
不妨将除$\Delta R$以外的元件等效为一个电压源和一个内阻的串联组合。于是有：
$$i_1 = \frac{U_{oc}}{R_i+\Delta R}$$
代入数据可得：
$$
\begin{cases}
2\text{A} = \dfrac{U_{oc}}{R_i}\quad &amp;amp;\leftarrow \Delta R = 0 \
1.5\text{A} = \dfrac{U_{oc}}{R_i + 10\Omega} &amp;amp;\leftarrow \Delta R = 10\Omega
\end{cases}
$$
解出
$$
\begin{cases}
U_{oc} = 60\text{V} \
R_i = 30\Omega
\end{cases}
$$
当$R$减少$10\Omega$时，$\Delta R = -10\Omega$，则
$$
i_1 = \frac{U_{oc}}{R_i + \Delta R} = \frac{60\text{V}}{30\Omega - 10\Omega} = 3\text{A}
$$
根据叠加原理，将$\Delta R$和$R+N$分别看成两个独立的激励源（$\Delta R$可以等效为一个电流源），$i_2$的值为两者的叠加，不妨设两者单独作用时$i_2$的值分别为$i_{\Delta R}$和$i_{N}$，而线性电路中，各部分满足线性关系，故$i_{\Delta R} = k i_1$，则
$$
i_2 = k i_1 + i_{\Delta R}
$$
易得
$$
k = -\frac{1}{3}, \quad i_{\Delta R} = 1 \text{A}
$$
于是
$$
i_2\Big|&lt;em&gt;{\Delta R = -10\Omega} = k i_1 + i&lt;/em&gt;{\Delta R}\Big|_{i_1=3\text{A}} = -\frac{1}{3} \times 3\text{A} + 1\text{A} = 0
$$&lt;/p&gt;
&lt;h1&gt;例2&lt;/h1&gt;
&lt;p&gt;电路如图(a)所示，已知当 $R=2\Omega$ 时，$I_1 = 5\text{A}$，$I_2 = 4\text{A}$。求当 $R=4\Omega$ 时 $I_1$ 和 $I_2$ 的值。
&lt;img src=&quot;3.webp&quot; alt=&quot;电路图abcde&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解：应用戴维南定理求 $I_1$。由图 (b) 有
$$
\begin{cases}
U_s = 5 \Omega I\
I_s = I + I + 3.5I = 5.5I
\end{cases}
$$
等效电阻
$$
R_i = \frac{U_s}{I_s} = \frac{10}{11}\Omega
$$
又由已知条件得
$$
U_{oc} = (R_i + 2\Omega) \times I_1 = \frac{160}{11}\text{V}
$$
简化后的电路如图 (c) 所示。
所以当 $R=4\Omega$ 时
$$
I_1 = \frac{U_{oc}}{R + R_i} = \frac{\frac{160}{11}\text{V}}{(4 + \frac{10}{11})\Omega} = \frac{80}{27}\text{A}
$$&lt;/p&gt;
&lt;p&gt;将 $I_1$ 用电流源来置换，用叠加定理分析置换后的电路，即将 $I_2$ 分解成
$$
I_2 = I_2&apos; + I_2&apos;&apos;
$$
其中 $I_2&apos;$ 为电流源 $I_1$ 单独作用时的解答，如图 (d) 所示；$I_2&apos;&apos;$ 是其余电源共同作用时的解答，如图 (e) 所示。由图 (d) 可得：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
\text{KVL: }\quad 5\Omega I_2&apos; + 5\Omega I&apos; = 0 \
\text{KCL: }\quad -I_1 + 3.5I&apos; - I_2&apos; + I&apos; = 0
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;联立解得
$$
I_2&apos; = -\frac{2}{11}I_1
$$
因此，电流 $I_2$ 可以写成：$I_2 = I_2&apos; + I_2&apos;&apos; = -\frac{2}{11}I_1 + I_2&apos;&apos;$
由已知条件得
$$
4\text{A} = -\frac{2}{11} \times 5\text{A} + I_2&apos;&apos; \implies I_2&apos;&apos; = \frac{54}{11}\text{A}
$$
所以，当 $R=4\Omega$ 时，
$$
I_2 = -\frac{2}{11} \times \frac{80}{27}\text{A} + \frac{54}{11}\text{A}=\frac{118}{27}\text{A}
$$&lt;/p&gt;
</content:encoded></item><item><title>KMP</title><link>https://meer-matze.github.io/posts/kmp/</link><guid isPermaLink="true">https://meer-matze.github.io/posts/kmp/</guid><description>KMP算法的解释和实现</description><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前缀表&lt;/h1&gt;
&lt;p&gt;KMP算法的核心是构建一个前缀表（也称为失配函数），用于在匹配过程中快速跳过已经匹配的部分。前缀表记录了模式串中每个位置之前的最长相等前后缀（正序）的长度。&lt;/p&gt;
&lt;h2&gt;前缀表的作用&lt;/h2&gt;
&lt;p&gt;前缀表的作用是在匹配过程中，当发生不匹配时，能够快速地跳过已经匹配的部分，避免重复比较，从而提高匹配效率。&lt;/p&gt;
&lt;p&gt;简单来说，当模式串中的某个位置发生不匹配时，由于在这之前的部分已经匹配成功，如果可以利用已经匹配的部分的信息来跳过一些字符，就可以减少比较的次数。&lt;/p&gt;
&lt;p&gt;而前缀表通过记录模式串中每个位置之前的最长相等前后缀的长度，使得在发生不匹配时，可以直接跳过已经匹配的部分，继续进行比较，从而实现线性时间复杂度的字符串匹配。&lt;/p&gt;
&lt;h2&gt;前缀表的性质&lt;/h2&gt;
&lt;p&gt;前缀表具有以下性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前缀表的值表示模式串中每个位置之前的最长相等前后缀的长度。&lt;/li&gt;
&lt;li&gt;最小循环节的长度可以通过前缀表的最后一个值来计算，公式为：&lt;code&gt;周期长度 = 模式串长度 - 前缀表最后一个值&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;前缀表的值可以用来判断模式串是否具有周期性，如果模式串长度是周期长度的整数倍，则说明模式串具有周期性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::note
&lt;strong&gt;最小循环节&lt;/strong&gt;是指一个字符串的最短子串，使得该字符串可以作为这个子串重复若干次构成的字符串的子串。
例如，字符串 &lt;code&gt;cabcabca&lt;/code&gt; 的最小循环节是 &lt;code&gt;abc&lt;/code&gt;，因为 &lt;code&gt;abc&lt;/code&gt; 重复三次构成了 &lt;code&gt;abcabcabc&lt;/code&gt;，而 &lt;code&gt;cabcabca&lt;/code&gt;是其的一个字串。
:::&lt;/p&gt;
&lt;h2&gt;前缀表的使用&lt;/h2&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;模式串：&lt;code&gt;A B A B C&lt;/code&gt;
前缀表：&lt;code&gt;0 0 1 2 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当匹配到 &lt;code&gt;ABAB...&lt;/code&gt; 但第五位不是 &lt;code&gt;C&lt;/code&gt; 时，已知前四位 &lt;code&gt;ABAB&lt;/code&gt; 匹配，查表可知其最长相等前后缀长度为 2（即 &lt;code&gt;AB&lt;/code&gt;）。此时不需要从头开始，而是将模式串滑动，让前缀的 &lt;code&gt;AB&lt;/code&gt; 对齐到原后缀的 &lt;code&gt;AB&lt;/code&gt; 位置，直接从第三位继续比较.&lt;/p&gt;
&lt;h2&gt;构建前缀表&lt;/h2&gt;
&lt;p&gt;如果暴力地构建前缀表，时间复杂度为$O(n^2)$，不过我们可以通过动态规划的思想在$O(n)$的时间内构建前缀表。&lt;/p&gt;
&lt;p&gt;对于模式串中的每个位置，我们可以利用前一个位置的前缀表值来快速计算当前的位置的前缀表值。&lt;/p&gt;
&lt;p&gt;对于模式串中的每个位置 &lt;code&gt;i&lt;/code&gt; 与前缀表 &lt;code&gt;prefix[i]&lt;/code&gt;，会出现以下三种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;匹配成功&lt;/strong&gt;：如果第 &lt;code&gt;i&lt;/code&gt; 个字符与前一个位置最长相等前后缀的&lt;strong&gt;后一个字符&lt;/strong&gt;相等，则当前最长相等前后缀长度直接加 1，即 &lt;code&gt;prefix[i] = prefix[i - 1] + 1&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失配回退&lt;/strong&gt;：如果不相等且前一个位置的前缀长度不为 0，说明当前字符虽然没能延长之前的相等前后缀，但可能与之前的&lt;strong&gt;更短的相等前后缀&lt;/strong&gt;组成新的匹配。此时我们需要利用前缀表，将查找范围回退到“前一个相等前后缀”的位置继续尝试。
:::tip
相当于&lt;strong&gt;将第 &lt;code&gt;i&lt;/code&gt; 个字符与前一个位置的最长相等前后缀进行拼接，再求拼接后字符串的最长相等前后缀长度&lt;/strong&gt;。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;完全不匹配&lt;/strong&gt;：如果回退到开头（前一个位置的前缀长度为 0）依然不匹配，则当前位置的前缀表值为 0。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下是构建前缀表的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化一个数组&lt;code&gt;prefix&lt;/code&gt;，长度与模式串相同，初始值全为0。&lt;/li&gt;
&lt;li&gt;使用两个指针&lt;code&gt;i&lt;/code&gt;和&lt;code&gt;j&lt;/code&gt;，其中&lt;code&gt;i&lt;/code&gt;从1开始，&lt;code&gt;j&lt;/code&gt;从0开始。&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;i&lt;/code&gt;小于模式串的长度时：
&lt;ul&gt;
&lt;li&gt;如果模式串的第&lt;code&gt;i&lt;/code&gt;个字符与第&lt;code&gt;j&lt;/code&gt;个字符相等，则将&lt;code&gt;prefix[i]&lt;/code&gt;设置为&lt;code&gt;j + 1&lt;/code&gt;，然后将&lt;code&gt;i&lt;/code&gt;和&lt;code&gt;j&lt;/code&gt;都向右移动一位。&lt;/li&gt;
&lt;li&gt;如果不相等且&lt;code&gt;j&lt;/code&gt;不为0，则将&lt;code&gt;j&lt;/code&gt;设置为&lt;code&gt;prefix[j - 1]&lt;/code&gt;，继续比较。&lt;/li&gt;
&lt;li&gt;如果不相等且&lt;code&gt;j&lt;/code&gt;为0，则将&lt;code&gt;prefix[i]&lt;/code&gt;设置为0，并将&lt;code&gt;i&lt;/code&gt;向右移动一位。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最终得到的&lt;code&gt;prefix&lt;/code&gt;数组即为前缀表。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;KMP算法实现&lt;/h1&gt;
&lt;p&gt;以下是KMP算法的实现步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建模式串的前缀表。&lt;/li&gt;
&lt;li&gt;使用两个指针&lt;code&gt;i&lt;/code&gt;和&lt;code&gt;j&lt;/code&gt;，其中&lt;code&gt;i&lt;/code&gt;用于遍历文本串，&lt;code&gt;j&lt;/code&gt;用于遍历模式串。&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;i&lt;/code&gt;小于文本串的长度时：
&lt;ul&gt;
&lt;li&gt;如果文本串的第&lt;code&gt;i&lt;/code&gt;个字符与模式串的第&lt;code&gt;j&lt;/code&gt;个字符相等，则将&lt;code&gt;i&lt;/code&gt;和&lt;code&gt;j&lt;/code&gt;都向右移动一位。&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;j&lt;/code&gt;等于模式串的长度，说明找到了一个匹配，记录匹配的位置，并将&lt;code&gt;j&lt;/code&gt;设置为前缀表中&lt;code&gt;j - 1&lt;/code&gt;的位置继续匹配。&lt;/li&gt;
&lt;li&gt;如果不相等且&lt;code&gt;j&lt;/code&gt;不为0，则将&lt;code&gt;j&lt;/code&gt;设置为前缀表中&lt;code&gt;j - 1&lt;/code&gt;的位置，继续比较。&lt;/li&gt;
&lt;li&gt;如果不相等且&lt;code&gt;j&lt;/code&gt;为0，则将&lt;code&gt;i&lt;/code&gt;向右移动一位。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;代码示例&lt;/h2&gt;
&lt;p&gt;以下是KMP算法的C++实现示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;string&amp;gt;

// 全局变量：主串（待搜索的字符串）和模式串（要查找的子串）
std::string str;      // 主串
std::string sub_str;  // 模式串

// 前缀表 prefix[i] 表示模式串 s[0..i-1] 的最长前缀后缀长度
// 这有助于在匹配过程中快速跳过不匹配的部分
std::vector&amp;lt;int&amp;gt; initFrefix(const std::string&amp;amp; s, size_t length)
{
    std::vector&amp;lt;int&amp;gt; prefix(length, 0);  // 初始化前缀表，所有元素为 0
    for (int i = 1; i &amp;lt; length; i++)  // 从第二个字符开始遍历模式串
    {
        int j = prefix[i - 1];  // j 表示当前考虑的前缀长度
        // 当字符不匹配时，回退 j，直到找到匹配或 j 为 0
        while (j &amp;gt; 0 &amp;amp;&amp;amp; s[i] != s[j])
            j = prefix[j - 1];
        // 如果字符匹配，前缀长度加 1
        if (s[i] == s[j])
            j++;
        prefix[i] = j;  // 设置前缀表的值
    }
    return prefix;  // 返回计算好的前缀表
}

int main()
{
    std::cin &amp;gt;&amp;gt; str &amp;gt;&amp;gt; sub_str;
    const size_t sub_str_length = sub_str.length(), str_length = str.length();

    // 计算模式串的前缀表，用于后续匹配
    std::vector&amp;lt;int&amp;gt; prefix = initFrefix(sub_str, sub_str_length);

    // KMP 匹配过程
    // i：主串的当前索引
    // j：模式串的当前匹配长度
    int i = 0, j = 0;
    while (i &amp;lt; str_length)  // 遍历主串
    {
        if (str[i] == sub_str[j])  // 当前字符匹配
        {
            i++;  // 主串索引前进
            j++;  // 匹配长度增加
            if (j == sub_str_length)  // 完全匹配模式串
            {
                // 输出匹配的起始位置（1-based索引）
                std::cout &amp;lt;&amp;lt; i - j + 1 &amp;lt;&amp;lt; std::endl;
                // 继续查找下一个匹配，使用前缀表回退 j
                j = prefix[j - 1];
            }
        }
        else  // 当前字符不匹配
        {
            if (j &amp;gt; 0)  // 如果有匹配长度，回退 j
                j = prefix[j - 1];
            else  // 如果 j 为 0，主串索引前进
                i++;
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;结论&lt;/h1&gt;
&lt;p&gt;KMP算法通过构建前缀表，能够在字符串匹配过程中实现线性时间复杂度，避免了暴力匹配中可能出现的重复比较。它在文本搜索、字符串处理等领域有广泛的应用，是一种高效的字符串匹配算法。&lt;/p&gt;
</content:encoded></item><item><title>ST表</title><link>https://meer-matze.github.io/posts/st%E8%A1%A8/</link><guid isPermaLink="true">https://meer-matze.github.io/posts/st%E8%A1%A8/</guid><description>ST表的使用心得</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近写洛谷&lt;a href=&quot;https://www.luogu.com.cn/problem/P7167&quot;&gt;P7167&lt;/a&gt;的时候，用单调栈发现TLE了，然后学习了ST表，想着记录一些ST的使用方法。&lt;/p&gt;
&lt;h1&gt;ST表简介&lt;/h1&gt;
&lt;p&gt;ST 表（Sparse Table，稀疏表）是一种用于在&lt;strong&gt;静态&lt;/strong&gt;数组上进行快速查询的数据结构，特别适用于 idempotent operations（幂等操作），如最小值、最大值、最大公约数等。ST 表的构建时间复杂度为 $O(n \log n)$，查询时间复杂度为 $O(1)$，空间复杂度为 $O(n \log n)$。&lt;/p&gt;
&lt;h1&gt;ST表的使用场景&lt;/h1&gt;
&lt;p&gt;ST表往往用于可以动态规划且需要频繁区间查询的场景。&lt;/p&gt;
&lt;h2&gt;区间查询&lt;/h2&gt;
&lt;p&gt;ST表最常见的使用场景是区间查询，特别是当需要频繁查询某个区间的最小值、最大值或其他幂等操作时。由于ST表可以在$O(1)$时间内返回查询结果，因此非常适合处理大量查询的情况。&lt;/p&gt;
&lt;h2&gt;状态转移&lt;/h2&gt;
&lt;p&gt;在某些动态规划问题中，可能不能实现幂等操作，这时，ST表可以通过倍增来优化状态转移过程，达到快速查询的效果。例如P7167中需要频繁查看圆盘的容量并累加，这时可以使用ST表来快速查询从某个圆盘开始一定范围内的总容量。
其实此时已经跟ST表关系不大了，主要是用ST表实现倍增。&lt;/p&gt;
&lt;h1&gt;ST表的构建和查询&lt;/h1&gt;
&lt;h2&gt;构建&lt;/h2&gt;
&lt;p&gt;构建ST表的过程主要包括以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化：创建一个二维数组 &lt;code&gt;st&lt;/code&gt;，其中 &lt;code&gt;st[i][j]&lt;/code&gt; 表示以 &lt;code&gt;i&lt;/code&gt; 为起点，长度为 $2^j$ 的区间的查询结果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;填充：使用动态规划的方式填充 &lt;code&gt;st&lt;/code&gt; 数组，利用之前计算的结果来构建更长的区间。
状态转移方程通常为：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;st[i][j] = operation(st[i][j - 1], st[i + 2^(j - 1)][j - 1])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;operation&lt;/code&gt; 是你需要进行的幂等操作，如最小值或最大值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;查询&lt;/h2&gt;
&lt;p&gt;查询过程非常简单，给定一个区间 &lt;code&gt;[L, R]&lt;/code&gt;，首先计算区间长度 &lt;code&gt;k = floor(log2(R - L + 1))&lt;/code&gt;，然后使用预先计算好的 &lt;code&gt;st&lt;/code&gt; 数组来返回查询结果，通常是通过合并两个重叠区间的结果或倍增合并不重叠区间来得到最终答案。
例如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若满足幂等操作，可以直接返回 &lt;code&gt;operation(st[L][k], st[R - 2^k + 1][k])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若不满足幂等操作，可以通过倍增的方式合并多个区间的结果来得到最终答案。&lt;pre&gt;&lt;code&gt;result = initial_value;
for(int k = floor(log2(R - L + 1)); k &amp;gt;= 0; k--){
    if ((R - L + 1) &amp;gt;= 2^k){
     result = operation(result, st[L][k]);
     L += 2^k;
     // 这里不一定要有R，因为不满足幂等操作，所以R只是用来代表终止条件
     // L的位置转移不一定是连续的，所以需要根据实际情况来调整L的位置
    }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;案例&lt;/h1&gt;
&lt;h2&gt;gcd查询&lt;/h2&gt;
&lt;p&gt;假设我们需要在一个数组中频繁查询某个区间的最大公约数（gcd）。我们可以使用ST表来预处理数组，使得每次查询都能在$O(1)$时间内返回结果。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建ST表：对于每个元素，初始化 &lt;code&gt;st[i][0]&lt;/code&gt; 为该元素的值。然后使用动态规划填充 &lt;code&gt;st&lt;/code&gt; 数组，根据状态转移方程计算更长区间的gcd。&lt;/li&gt;
&lt;li&gt;查询：对于每个查询区间 &lt;code&gt;[L, R]&lt;/code&gt;，计算 &lt;code&gt;k = floor(log2(R - L + 1))&lt;/code&gt;，然后返回 &lt;code&gt;gcd(st[L][k], st[R - 2^k + 1][k])&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是一个简单的示例代码片段，展示了如何构建和查询ST表以进行gcd查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;cmath&amp;gt; // 包含 std::log2
#include &amp;lt;numeric&amp;gt; // 包含 std::gcd (C++17)
using namespace std;
const int MAXN = 100005; // 稍微开大一点防止越界
int st[MAXN][20]; // 假设最大数组长度为100000，20是因为20 &amp;gt; log2(100000) 
// st[L][k] 表示从 L 开始，长度为 2^k 的区间的 gcd 结果，即覆盖 [L, L + 2^k - 1]

void buildST(const vector&amp;lt;int&amp;gt;&amp;amp; arr, int n) {
    for (int i = 0; i &amp;lt; n; i++) {
        st[i][0] = arr[i];
    }
    for (int j = 1; (1 &amp;lt;&amp;lt; j) &amp;lt;= n; j++) {
        for (int i = 0; i + (1 &amp;lt;&amp;lt; j) - 1 &amp;lt; n; i++) {
            st[i][j] = std::gcd(st[i][j - 1], st[i + (1 &amp;lt;&amp;lt; (j - 1))][j - 1]);
        }
    }
}

int query(int L, int R) {
    int k = std::log2(R - L + 1);

    // st[R - (1 &amp;lt;&amp;lt; k) + 1][k] 表示以 R 结尾，长度为 2^k 的区间的 gcd 结果，即覆盖 [R - 2^k + 1, R]
    // 因为 gcd 是幂等操作（重叠区间不影响结果），合并两部分即可覆盖整个区间 [L, R]
    return std::gcd(st[L][k], st[R - (1 &amp;lt;&amp;lt; k) + 1][k]);
}

int main() {
    int n, q;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; q;
    vector&amp;lt;int&amp;gt; arr(n);
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; arr[i];
    }
    buildST(arr, n);
    while (q--) {
        int L, R;
        cin &amp;gt;&amp;gt; L &amp;gt;&amp;gt; R;
        cout &amp;lt;&amp;lt; query(L, R) &amp;lt;&amp;lt; &quot;\n&quot;;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P7167&lt;/h2&gt;
&lt;p&gt;在洛谷P7167中，我们需要处理大量的查询，查询内容涉及到圆盘的容量和累加。
通过使用ST表，我们可以快速查询从某个圆盘开始一定范围内的总容量，从而显著提高查询效率。具体实现时，可以将圆盘的容量预处理到ST表中，然后根据查询需求进行快速查询和累加。&lt;/p&gt;
&lt;p&gt;下面是一个示例代码片段，展示了如何使用ST表来处理P7167中的查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;

const int MAXN = 100005;
int D[MAXN], C[MAXN];   // D[i]：第 i 个圆盘的直径，C[i]：第 i 个圆盘的容量
int nxt[MAXN][20];      // nxt[i][j] (next)：表示从圆盘 i 溢出后，向下跳跃 2^j 次最终到达的圆盘编号
int sum[MAXN][20];      // sum[i][j]：从圆盘 i 溢出后，向下跳跃 2^j 次路径上所有圆盘的容量累加和
int stk[MAXN], top = 0; // 单调栈及栈顶索引，用于寻找下方第一个直径更大的圆盘。

int main() {
    
    int n, q;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; q;
    
    for (int i = 1; i &amp;lt;= n; i++) {
        cin &amp;gt;&amp;gt; D[i] &amp;gt;&amp;gt; C[i];
    }
    
    // 单调栈，寻找每个圆盘水流溢出后的下一个目标圆盘，作为倍增表的第一列，并记录第一列的容量累加和
    for (int i = 1; i &amp;lt;= n; i++) {
        while (top &amp;gt; 0 &amp;amp;&amp;amp; D[stk[top]] &amp;lt; D[i]) {
            nxt[stk[top]][0] = i;
            sum[stk[top]][0] = C[stk[top]];
            top--;
        }
        stk[++top] = i;
    }
    
    // 剩下的圆盘没有下方满足条件的圆盘，最终流向水池(虚设为0)
    while (top &amp;gt; 0) {
        nxt[stk[top]][0] = 0;
        sum[stk[top]][0] = C[stk[top]];
        top--;
    }
    
    // 构建倍增表 (ST表的变种体现)
    for (int j = 1; j &amp;lt;= 18; j++) {
        for (int i = 1; i &amp;lt;= n; i++) {
            nxt[i][j] = nxt[nxt[i][j - 1]][j - 1];
            sum[i][j] = sum[i][j - 1] + sum[nxt[i][j - 1]][j - 1];
        }
    }
    
    // 处理查询
    while (q--) {
        int R, V;
        cin &amp;gt;&amp;gt; R &amp;gt;&amp;gt; V;
        int curr = R;
        
        // 倍增跳跃寻找水流的终点
        for (int j = 18; j &amp;gt;= 0; j--) {
            if (nxt[curr][j] != 0 &amp;amp;&amp;amp; sum[curr][j] &amp;lt; V) {
                V -= sum[curr][j];
                curr = nxt[curr][j];
            }
        }
        
        // 经过最大的跳跃后，如果剩余水容量仍超出当前盘的容量，说明会最终流入水池
        if (V &amp;lt;= C[curr]) cout &amp;lt;&amp;lt; curr &amp;lt;&amp;lt; &apos;\n&apos;;
        else cout &amp;lt;&amp;lt; 0 &amp;lt;&amp;lt; &apos;\n&apos;;
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;结论&lt;/h1&gt;
&lt;p&gt;ST表是一种非常高效的数据结构，适用于需要频繁进行区间查询的场景。通过合理地构建和使用ST表，可以显著提高查询效率，尤其是在处理大量查询时。对于需要快速查询区间最小值、最大值或其他幂等操作的问题，ST表是一个非常值得考虑的选择。&lt;/p&gt;
</content:encoded></item><item><title>二叉树</title><link>https://meer-matze.github.io/posts/%E4%BA%8C%E5%8F%89%E6%A0%91/</link><guid isPermaLink="true">https://meer-matze.github.io/posts/%E4%BA%8C%E5%8F%89%E6%A0%91/</guid><description>二叉树总体介绍，定义、性质、分类、存储结构、遍历、构建、迭代器等内容。</description><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;二叉树的定义&lt;/h1&gt;
&lt;p&gt;二叉树是一种树形数据结构，其中每个节点最多有两个子节点，分别称为左子节点和右子节点。二叉树具有以下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个节点最多有两个子节点。&lt;/li&gt;
&lt;li&gt;每个节点都有一个父节点，除了根节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;二叉树的性质&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;二叉树的深度为 $d$，则二叉树的最大节点数为 $2^d - 1$&lt;/li&gt;
&lt;li&gt;二叉树的节点数为 $n$，则二叉树的深度至少为 $\log_2(n + 1)$&lt;/li&gt;
&lt;li&gt;二叉树的节点数为 $n$，则二叉树的叶子节点数为 $\left\lceil \frac{n}{2} \right\rceil$&lt;/li&gt;
&lt;li&gt;二叉树的节点数为 $n$，则二叉树的非叶子节点数为 $\left\lfloor \frac{n}{2} \right\rfloor$&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;二叉树的分类&lt;/h1&gt;
&lt;h2&gt;满二叉树&lt;/h2&gt;
&lt;p&gt;如果二叉树中除了叶子结点，每个结点的度都为 2，则此二叉树称为满二叉树。&lt;/p&gt;
&lt;h2&gt;完全二叉树&lt;/h2&gt;
&lt;p&gt;如果二叉树中除了最后一层，其他各层都被完全填充，&lt;strong&gt;并且最后一层的节点都靠左对齐&lt;/strong&gt;，则此二叉树称为完全二叉树。
因此，完全二叉树特别适合用数组来存储，因为它的节点可以按照层序遍历的顺序存储在数组中，且没有空洞。
完全二叉树的性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;完全二叉树的深度为 $d$，则完全二叉树的节点数在 $2^{d-1}$ 到 $2^d - 1$ 之间。&lt;/li&gt;
&lt;li&gt;完全二叉树的节点数为 $n$，则完全二叉树的深度为 $\lceil \log_2(n + 1) \rceil$&lt;/li&gt;
&lt;li&gt;完全二叉树的节点数为 $n$，则完全二叉树的叶子节点数为 $\left\lceil \frac{n}{2} \right\rceil$&lt;/li&gt;
&lt;li&gt;完全二叉树的节点数为 $n$，则完全二叉树的非叶子节点数为 $\left\lfloor \frac{n}{2} \right\rfloor$&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二叉搜索树&lt;/h2&gt;
&lt;p&gt;二叉搜索树（Binary Search Tree，BST）是一种特殊的二叉树，其中每个节点的值都满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;左子树中所有节点的值都小于该节点的值。&lt;/li&gt;
&lt;li&gt;右子树中所有节点的值都大于该节点的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;二叉搜索树的性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;二叉搜索树的中序遍历结果是一个有序的序列。&lt;/li&gt;
&lt;li&gt;二叉搜索树的平均时间复杂度为 $O(\log n)$，最坏时间复杂度为 $O(n)$，取决于树的平衡程度。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;平衡二叉树&lt;/h3&gt;
&lt;p&gt;平衡二叉树是一种特殊的二叉搜索树，其中每个节点的左右子树的高度差不超过1。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;平衡因子&lt;/strong&gt;：对于二叉树中的每个节点，计算其左子树的高度与右子树的高度之差，称为该节点的平衡因子。平衡二叉树结点的平衡因子的值只能是-1、0或1。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最小不平衡二叉树&lt;/strong&gt;：距离插入结点最近的，且平衡因子的绝对值大于1的结点为根的子树，称为最小不平衡树。&lt;/p&gt;
&lt;p&gt;对于不配平衡二叉树，可以通过旋转操作来恢复平衡。常见的旋转操作包括：旋转操作包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;右旋（Right Rotation）&lt;/strong&gt;：当某个节点的左子树过高时，进行单右旋转。指将根节点的左侧往右拉，原先的左子节点变成新的父节点，并把多余的右子节点出让，给已经降级的根节点当左子节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;左旋（Left Rotation）&lt;/strong&gt;：当某个节点的右子树过高时，进行单左旋转。指将根节点的右侧往左拉，原先的右子节点变成新的父节点，并把多余的左子节点出让，给已经降级的根节点当右子节点&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;AVL树&lt;/h4&gt;
&lt;p&gt;AVL树是一种自平衡二叉搜索树，其中每个节点的平衡因子只能是-1、0或1。当插入或删除节点导致某个节点的平衡因子变为-2或2时，需要进行旋转操作来恢复平衡。&lt;/p&gt;
&lt;h3&gt;红黑树&lt;/h3&gt;
&lt;p&gt;红黑树是一种二叉搜索树，但在每个结点上增加一个存储位表示结点颜色（Red或Black）。
通过限制着色方式，红黑树确保没有一条路径会比其他路径长出两倍，从而接近平衡。
因而，红黑树是相对接近平衡的二叉树，并不是一个完美平衡二叉查找树。&lt;/p&gt;
&lt;p&gt;红黑树的性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个节点要么是红色，要么是黑色。&lt;/li&gt;
&lt;li&gt;根节点是黑色。&lt;/li&gt;
&lt;li&gt;每个叶子节点（NIL节点）是黑色。&lt;/li&gt;
&lt;li&gt;如果一个节点是红色的，则它的两个子节点都是黑色的。&lt;/li&gt;
&lt;li&gt;从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;红黑树的插入和删除操作需要通过旋转和重新着色来维护红黑树的性质，以确保树的平衡。&lt;/p&gt;
&lt;p&gt;红黑树不要求严格高度平衡，而是要求从任一节点到叶子（NIL）的每条路径黑高相同，从而保证整体近似平衡。&lt;/p&gt;
&lt;h4&gt;红黑树插入时的旋转与着色过程&lt;/h4&gt;
&lt;p&gt;插入新节点时，先按二叉搜索树规则插入，并将新节点标记为红色。之后根据父节点、叔叔节点、祖父节点的颜色和位置关系进行修复。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤1：普通BST插入&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到插入位置，将新节点作为叶子插入。&lt;/li&gt;
&lt;li&gt;新节点初始颜色设为红色（这样不会立即破坏黑高一致性）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;步骤2：检查是否违反性质&lt;/strong&gt;
只有当“父节点也是红色”时才会违反性质4（红节点不能有红孩子），此时需要修复。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤3：按Case修复（以父节点是祖父左孩子为例）&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Case 1（叔叔为红）：
父节点和叔叔节点都染黑，祖父节点染红，然后将“当前节点”上移到祖父，继续向上检查。&lt;/li&gt;
&lt;li&gt;Case 2（叔叔为黑，且当前节点是内侧）：
例如父是左孩子、当前是父的右孩子。先对父节点做一次左旋，把它转成Case 3的外侧结构。&lt;/li&gt;
&lt;li&gt;Case 3（叔叔为黑，且当前节点是外侧）：
例如父是左孩子、当前是父的左孩子。先将父染黑、祖父染红，再对祖父做一次右旋，修复完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当父节点是祖父的右孩子时，处理完全对称：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Case 1仍是变色上移。&lt;/li&gt;
&lt;li&gt;Case 2先右旋父节点。&lt;/li&gt;
&lt;li&gt;Case 3再左旋祖父节点并配合变色。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;步骤4：收尾&lt;/strong&gt;
无论中间如何旋转或变色，最后都要把根节点染成黑色。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;记忆口诀&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;叔红只变色，上移看祖父。&lt;/li&gt;
&lt;li&gt;叔黑先折线变直线（Case 2转Case 3）。&lt;/li&gt;
&lt;li&gt;叔黑直线就旋祖父并交换父祖颜色。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;红黑树和AVL树的比较&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;平衡性：AVL树比红黑树更严格地保持平衡，因此AVL树的查询性能通常优于红黑树。然而，红黑树在插入和删除操作时更快，因为它允许更大的不平衡。&lt;/li&gt;
&lt;li&gt;插入和删除：AVL树在插入和删除节点时可能需要多次旋转来保持平衡，而红黑树通常只需要一次旋转。&lt;/li&gt;
&lt;li&gt;内存使用：AVL树需要存储每个节点的平衡因子，而红黑树只需要存储每个节点的颜色，因此红黑树在内存使用上更为高效。&lt;/li&gt;
&lt;li&gt;应用场景：AVL树适用于查询频繁且插入和删除较少的场景，而红黑树适用于插入和删除频繁的场景，如操作系统中的进程调度和数据库索引。&lt;/li&gt;
&lt;li&gt;编写复杂度：AVL树的实现相对复杂，因为需要处理更多的旋转情况，而红黑树的实现相对简单。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;二叉树的存储结构&lt;/h1&gt;
&lt;h2&gt;顺序存储&lt;/h2&gt;
&lt;p&gt;顺序存储通常使用数组表示二叉树，常见于完全二叉树（如堆）。&lt;/p&gt;
&lt;p&gt;若根节点下标从0开始，则有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下标为 &lt;code&gt;i&lt;/code&gt; 的节点，其左孩子下标为 &lt;code&gt;2i + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;下标为 &lt;code&gt;i&lt;/code&gt; 的节点，其右孩子下标为 &lt;code&gt;2i + 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;下标为 &lt;code&gt;i&lt;/code&gt; 的节点，其父节点下标为 &lt;code&gt;(i - 1) &amp;gt;&amp;gt; 1&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果根节点下标从1开始，则对应公式为：左孩子 &lt;code&gt;2i&lt;/code&gt;，右孩子 &lt;code&gt;2i + 1&lt;/code&gt;，父节点 &lt;code&gt;i // 2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按下标可 $O(1)$ 访问父子节点。&lt;/li&gt;
&lt;li&gt;实现简单，缓存局部性好。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对非完全二叉树会出现大量空位，浪费空间。&lt;/li&gt;
&lt;li&gt;插入、删除中间节点不灵活。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;链式存储&lt;/h2&gt;
&lt;p&gt;链式存储通过节点对象保存数据和左右指针，最常见定义是：&lt;code&gt;val&lt;/code&gt;、&lt;code&gt;left&lt;/code&gt;、&lt;code&gt;right&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;适合任意形态的二叉树，不会因为空位浪费大量空间。&lt;/li&gt;
&lt;li&gt;插入、删除节点更灵活。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个节点有额外指针开销。&lt;/li&gt;
&lt;li&gt;访问父节点不方便（若需要可增加 &lt;code&gt;parent&lt;/code&gt; 指针）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
工程中若树结构经常变化，通常优先使用链式存储。
:::&lt;/p&gt;
&lt;h1&gt;二叉树的遍历&lt;/h1&gt;
&lt;h2&gt;前序遍历&lt;/h2&gt;
&lt;p&gt;访问顺序：根 -&amp;gt; 左 -&amp;gt; 右。&lt;/p&gt;
&lt;p&gt;常见用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;复制一棵树。&lt;/li&gt;
&lt;li&gt;序列化树结构（可配合空节点标记）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归：先访问根，再递归左子树和右子树。&lt;/li&gt;
&lt;li&gt;迭代：用栈，先压右子节点，再压左子节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;中序遍历&lt;/h2&gt;
&lt;p&gt;访问顺序：左 -&amp;gt; 根 -&amp;gt; 右。&lt;/p&gt;
&lt;p&gt;核心性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对二叉搜索树，中序结果是升序序列。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归：左子树完成后访问根，再访问右子树。&lt;/li&gt;
&lt;li&gt;迭代：一路向左入栈，弹出访问后转向右子树。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;后序遍历&lt;/h2&gt;
&lt;p&gt;访问顺序：左 -&amp;gt; 右 -&amp;gt; 根。&lt;/p&gt;
&lt;p&gt;常见用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;删除树（先删子节点再删父节点）。&lt;/li&gt;
&lt;li&gt;计算树高、子树汇总信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归：左右子树都处理完成后再访问根。&lt;/li&gt;
&lt;li&gt;迭代：可使用双栈，或“访问标记法”单栈实现。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;层序遍历&lt;/h2&gt;
&lt;p&gt;访问顺序：按层从上到下、从左到右。&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用队列。&lt;/li&gt;
&lt;li&gt;根节点入队。&lt;/li&gt;
&lt;li&gt;每次出队一个节点并访问，再将其左右孩子入队。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;常见用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求树的最大宽度。&lt;/li&gt;
&lt;li&gt;按层打印、锯齿形遍历、最短层数问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;二叉树的构建&lt;/h1&gt;
&lt;p&gt;常见构建方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;已知前序 + 中序：可唯一确定一棵二叉树（节点值互异时）。&lt;/li&gt;
&lt;li&gt;已知后序 + 中序：同样可唯一确定一棵二叉树（节点值互异时）。&lt;/li&gt;
&lt;li&gt;已知层序 + 空节点标记：可按队列逐层恢复树。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;构建的关键在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过中序划分左右子树边界。&lt;/li&gt;
&lt;li&gt;在前序或后序中定位当前子树根节点。&lt;/li&gt;
&lt;li&gt;递归处理左右区间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;时间复杂度通常为 $O(n)$（配合哈希表记录中序下标）。&lt;/p&gt;
&lt;h1&gt;二叉树迭代器&lt;/h1&gt;
&lt;p&gt;二叉树迭代器是把“遍历过程状态化”的一种封装，使外部能像遍历数组一样逐个取节点。&lt;/p&gt;
&lt;p&gt;以BST中序迭代器为例（升序输出）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化时把根到最左路径全部压栈。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next()&lt;/code&gt;：弹出栈顶作为当前最小值；若其有右子树，则将右子树的最左路径继续压栈。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasNext()&lt;/code&gt;：判断栈是否为空。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;复杂度：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单次 &lt;code&gt;next()&lt;/code&gt; 均摊为 $O(1)$。&lt;/li&gt;
&lt;li&gt;额外空间为 $O(h)$，其中 $h$ 是树高。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>新启航</title><link>https://meer-matze.github.io/posts/start/</link><guid isPermaLink="true">https://meer-matze.github.io/posts/start/</guid><description>a fresh new start</description><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;一个全新的开始！&lt;/h1&gt;
&lt;p&gt;在这里我可能会&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写一些数学笔记&lt;/li&gt;
&lt;li&gt;算法练习&lt;/li&gt;
&lt;li&gt;吐槽&lt;/li&gt;
&lt;li&gt;有感而发&lt;/li&gt;
&lt;li&gt;各种各样&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;s&gt;希望不会半途而废&lt;/s&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>