diff --git a/senior/3.数据结构.md b/senior/3.数据结构.md index 3a71006..d2be761 100644 --- a/senior/3.数据结构.md +++ b/senior/3.数据结构.md @@ -400,9 +400,11 @@ int main() { 该算法的时间复杂度为 \(O(n)\),因为每个元素最多被插入和移除队列一次。 ## 优先队列 + 优先队列(Priority Queue)是一种特殊的队列数据结构,其中每个元素都有一个优先级。在优先队列中,**出队操作会优先处理优先级最高的元素**,而不是像普通队列那样遵循“先进先出”(FIFO)的原则。 ### 特性 + 1. **插入元素**(Enqueue):可以将元素加入队列,通常是 O(log n) 时间复杂度。 2. **取出最大/最小元素**(Dequeue):每次从队列中取出优先级最高的元素,通常是 O(log n) 时间复杂度。 3. **查看最大/最小元素**:可以在 O(1) 时间内查看当前优先级最高的元素。 @@ -416,6 +418,7 @@ int main() { C++ 标准库提供了一个名为 `std::priority_queue` 的容器,可以直接用来实现优先队列。这个容器底层实现通常是**堆**(默认是最大堆),即每次访问的是最大元素。 #### 1. **默认最大堆实现** + ```cpp #include #include @@ -441,6 +444,7 @@ int main() { ``` #### 2. **最小堆的实现** + 默认情况下,`std::priority_queue` 是最大堆,要实现最小堆,可以通过**自定义比较函数**来实现。可以利用 `std::greater` 或使用 lambda 表达式。 ```cpp @@ -468,6 +472,7 @@ int main() { ``` #### 3. **自定义优先级的对象** + 如果队列中存放的是自定义对象,则需要为该对象实现比较函数。例如,假设有一个任务结构体,优先级根据任务的优先级数值决定: ```cpp @@ -503,15 +508,19 @@ int main() { 这里定义了 `operator<` 来决定优先级高低。默认情况下,`std::priority_queue` 会将优先级最高的任务(`priority` 最大)排在队首。 ### 底层实现 + `std::priority_queue` 底层使用的是**堆**(heap),常见实现是**二叉堆**。堆是一种二叉树的完全树结构,其中最大堆的父节点总是大于等于其子节点,最小堆的父节点总是小于等于其子节点。堆的性质使得插入和删除操作可以在 O(log n) 时间复杂度内完成。 --- ### 自己实现一个优先队列 + 这里有一个讲的不错的[b站视频](https://www.bilibili.com/video/BV1AF411G7cA/) - + + 如果不使用标准库,可以手动实现优先队列,最常见的方法是基于堆。以下是一个简单的基于数组的二叉堆实现的优先队列(最大堆): + ```cpp #include #include @@ -604,56 +613,65 @@ int main() { ``` ### 代码讲解: + 1. **`siftUp()` 和 `siftDown()`**:这两个函数是核心的调整堆结构的操作。`siftUp` 在插入新元素后执行,上浮新元素以保持最大堆结构。`siftDown` 在移除堆顶元素后执行,下沉新的堆顶以保持最大堆结构。 - 2. **插入操作**: + - 当调用 `push()` 函数时,将新元素添加到堆的末尾,并调用 `siftUp()` 函数调整堆的结构,确保最大堆的性质得到维护。 - 3. **删除操作**: + - 当调用 `pop()` 函数时,删除堆顶元素(最大值)。为了保持堆的连续性,使用最后一个元素替代堆顶,然后调用 `siftDown()` 来恢复最大堆性质。 - 4. **`top()` 函数**:返回当前堆中的最大元素,但不删除它。如果堆为空,则抛出异常。 - 5. **`empty()` 函数**:用于检查堆是否为空。 ### 示例输出: + ```text 最大值: 20 第二大值: 15 ``` ## ST表 + > [OI Wiki链接](https://oi-wiki.org/ds/sparse-table/) ST表(Sparse Table)是一种用于解决静态区间查询问题的数据结构,通常用于查询不可变数组中的区间最值(最小值、最大值、最大公约数等)。它的构建时间复杂度为 \(O(n \log n)\),查询时间复杂度为 \(O(1)\),非常适合处理多次查询的场景。ST表的原理基于“倍增法”,通过预处理使得每次查询时可以通过少量预处理信息快速得出结果。 ### 原理 + ST表的核心思想是将原数组分块,并记录每个块中的区间最值。具体来说,给定一个数组 `arr`,ST表预处理的是所有长度为 \(2^j\) 的区间的最值,其中 \(j\) 表示区间的大小为 \(2^j\)。 #### 数据结构 + ST表用一个二维数组 `st[i][j]` 表示: + - \(st[i][j]\) 表示从位置 \(i\) 开始,长度为 \(2^j\) 的区间中的最值,即区间 `[i, i + 2^j - 1]` 中的最小值(或最大值、其他运算)。 #### 预处理 + 预处理的过程如下: + 1. 初始化:长度为 \(2^0 = 1\) 的区间的最值就是数组本身。因此,\[ -st[i][0] = arr[i] \quad \text{对于每个} \ i \in [0, n-1]。 -\] + st[i][0] = arr[i] \quad \text{对于每个} \ i \in [0, n-1]。 + \] 2. 填充其他长度的区间: 对于 \(j \geq 1\),区间 `[i, i + 2^j - 1]` 可以通过合并两个长度为 \(2^{j-1}\) 的区间得到:\[ -st[i][j] = \min(st[i][j-1], st[i + 2^{j-1}][j-1])。 -\] + st[i][j] = \min(st[i][j-1], st[i + 2^{j-1}][j-1])。 + \] 其中,`min` 操作可以替换为其他操作(如 `max` 或 `gcd`),根据具体需求调整。 #### 查询 + 对于任意区间 `[L, R]`,我们可以通过分解成两个重叠区间的方式来计算区间最值: + 1. 找到满足 \(2^j \leq (R - L + 1)\) 的最大 \(j\),这个 \(j\) 可以通过查找预处理的 `log` 数组得到(常用二进制计算)。 2. 使用两个长度为 \(2^j\) 的区间覆盖 `[L, R]`:\[ -\min(st[L][j], st[R - 2^j + 1][j]) -\] + \min(st[L][j], st[R - 2^j + 1][j]) + \] 因为两个区间 `[L, L + 2^j - 1]` 和 `[R - 2^j + 1, R]` 会完全覆盖区间 `[L, R]`。 ### C++ 实现 + 以下是 ST 表求区间最小值的 C++ 实现: 下面是为这段 ST 表代码添加的详细注释: @@ -708,7 +726,7 @@ public: int main() { // 输入数组 vector arr = {1, 3, 2, 7, 9, 11, 3, 5, 8, 10}; - + // 创建 ST 表,并传入数组进行预处理 SparseTable st(arr); @@ -723,29 +741,32 @@ int main() { ``` ### 详细注释讲解: + 1. **数据结构:** + - `st` 是一个二维数组,存储每个区间的最小值。`st[i][j]` 存储的是从位置 `i` 开始,长度为 \(2^j\) 的区间的最小值。 - `log` 是一个辅助数组,用于存储每个数的对数值,便于快速确定区间大小。 - 2. **预处理:** + - `log` 数组用来预处理快速计算 \( \log_2 \) 值,避免在查询时重复计算对数。 - `st` 表的初始化从长度为 \(2^0 = 1\) 的区间开始,其值就是数组的原始元素,然后通过倍增法构建更长的区间。 - 3. **查询:** + - 查询区间 `[L, R]` 时,找到最大的 \( j \) 使得 \(2^j \leq (R - L + 1)\),并使用两个长度为 \(2^j\) 的区间覆盖原始区间 `[L, R]`。 - 两个区间分别为 `[L, L + 2^j - 1]` 和 `[R - 2^j + 1, R]`,查询这两个子区间的最小值。 - 4. **主要操作:** + - `log[i / 2] + 1` 用来快速得到当前区间的对数值,确保动态规划的填充和查询过程中的高效计算。 - `min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1])` 使用两个较小的区间合并得到更大的区间最值。 - 5. **时间复杂度:** + - **预处理**:\(O(n \log n)\),遍历每个长度的区间进行最小值计算。 - **查询**:\(O(1)\),每次查询只需要比较两个最小值即可。 # 集合与森林 ## 并查集 + 当然可以!下面是带有详细注释的 C++ 并查集代码,并说明 `rank` 的作用。 ```cpp @@ -758,7 +779,7 @@ public: UnionFind(int size) { parent.resize(size); // 动态数组存储每个节点的父节点 rank.resize(size, 1); // 动态数组存储每个节点的秩(树的高度) - + // 初始化每个节点的父节点为自己 for (int i = 0; i < size; i++) { parent[i] = i; @@ -801,10 +822,13 @@ private: - **树的高度**:`rank` 数组用于记录每个集合的树的高度(或深度)。通过保持较小的树总是连接到较大的树,减少了树的高度,从而提高了查找操作的效率。 - **避免不必要的深度**:如果不使用 `rank`,在多次合并中可能导致某棵树的高度迅速增加,使得查找操作的时间复杂度增加到 O(n)。 + ## 树的孩子兄弟表示法 + 树的孩子兄弟表示法是一种用于表示树形结构的数据结构。它将每个节点的孩子节点和兄弟节点分别用指针连接起来,从而避免了使用数组来存储孩子节点的方式。这种表示法特别适合于孩子数量不固定的树。 在孩子兄弟表示法中,每个节点包含两个指针: + 1. `firstChild`:指向该节点的第一个孩子。 2. `nextSibling`:指向该节点的下一个兄弟。 @@ -863,19 +887,21 @@ int main() { ``` ### 代码说明: + 1. **TreeNode 结构体**:定义了树的节点,包含数据、指向第一个孩子的指针和指向下一个兄弟的指针。 2. **addChild 函数**:用于向父节点添加孩子节点。如果父节点没有孩子,则直接将新节点设置为第一个孩子;如果有孩子,则遍历兄弟节点,找到最后一个兄弟并将新节点添加为其下一个兄弟。 3. **traverse 函数**:实现了树的前序遍历,先访问当前节点,然后递归访问第一个孩子和下一个兄弟。 4. **main 函数**:创建树的根节点,添加一些孩子节点,并进行遍历输出。 这种表示法的优点是灵活性高,适合于孩子数量不固定的树结构。 + # 特殊树 ## 二叉堆,树状数组 + 二叉堆(Binary Heap)是一种特殊的完全二叉树,通常用于实现优先队列。它有两个主要的性质: 1. **结构性质**:二叉堆是一棵完全二叉树,意味着除了最后一层外,其他层的节点都是满的,最后一层的节点从左到右填充。 - 2. **堆性质**:在最大堆中,父节点的值总是大于或等于其子节点的值;在最小堆中,父节点的值总是小于或等于其子节点的值。这使得堆的根节点总是最大值(最大堆)或最小值(最小堆)。 ### C++ 实现 @@ -974,15 +1000,14 @@ int main() { ### 用途 1. **优先队列**:二叉堆常用于实现优先队列,允许快速插入和删除最大(或最小)元素。 - 2. **排序算法**:堆排序(Heap Sort)是一种基于二叉堆的排序算法,具有 O(n log n) 的时间复杂度。 - 3. **图算法**:在 Dijkstra 算法和 Prim 算法中,二叉堆用于高效地获取当前最小(或最大)边。 - 4. **调度算法**:在操作系统中,二叉堆可以用于任务调度,确保高优先级任务优先执行。 二叉堆因其高效的插入和删除操作而广泛应用于各种算法和数据结构中。 + ## 线段树 + 线段树是一种高效的数据结构,用于处理区间查询和更新。对于区间修改,常用懒惰标记(Lazy Propagation)来优化性能。 ### 懒惰标记的基本思想 @@ -992,19 +1017,20 @@ int main() { ### 实现步骤 1. **构建线段树**:首先构建线段树,每个节点存储该区间的值。 - 2. **修改操作**: + - 当进行区间更新时,检查当前节点的懒惰标记。 - 如果当前节点有懒惰标记,先应用这个标记(更新当前节点值),然后将标记传递给子节点(如果有的话)。 - 更新时,如果当前区间完全在要修改的区间内,直接标记当前节点,并返回。 - 如果当前区间部分重叠,递归地更新左右子树。 - 3. **查询操作**: + - 在查询之前,检查并更新懒惰标记,以确保返回的值是最新的。 ### 示例 假设我们要对区间 [l, r] 加一个值 `val`: + - 如果当前节点的区间完全包含 [l, r],则设置懒惰标记 `add` 为 `val`。 - 如果只部分重叠,则递归更新子节点。 @@ -1141,15 +1167,13 @@ int main() { ### 代码说明 1. **构造函数**:构建线段树,初始化 `tree` 和 `lazy` 数组。 - 2. **构建树**:`buildTree` 函数递归构建线段树的节点。 - 3. **更新区间**: + - `updateRange` 函数处理区间更新,包括懒惰标记的管理。 - 4. **查询区间**: - - `queryRange` 函数处理区间求和,并在需要时更新懒惰标记。 + - `queryRange` 函数处理区间求和,并在需要时更新懒惰标记。 5. **主函数**:测试线段树的功能,包括初始化、查询和更新操作。 ## 字典树(Trie树) @@ -1161,7 +1185,7 @@ int main() { 1. **插入**:从根节点开始,逐字符插入单词,如果字符不存在,就创建新的节点。 2. **查找**:同样从根节点开始,逐字符查找。如果路径上的字符不存在,则表示该单词不在字典树中。 3. **删除**:需要遍历字符,找到单词末尾的节点,并进行必要的清理。 -使用固定大小的数组实现字典树(Trie)是一种高效的内存管理方式,特别适用于字母表大小有限的情况。这里我们假设单词最长为30个字符,并使用一个二维数组 `trie[30][256]` 来表示字典树,其中第一维表示字符位置,第二维表示每个字符的孩子节点。 + 使用固定大小的数组实现字典树(Trie)是一种高效的内存管理方式,特别适用于字母表大小有限的情况。这里我们假设单词最长为30个字符,并使用一个二维数组 `trie[30][256]` 来表示字典树,其中第一维表示字符位置,第二维表示每个字符的孩子节点。 ### 数组实现的 Trie 结构 @@ -1237,16 +1261,95 @@ int main() { ### 解释 1. **数组定义**: + - `trie[30][256]`:第一维表示字符的深度,第二维表示字符(ASCII值)。 - `isEnd[30]`:标记每个节点是否是单词的结束。 - 2. **节点管理**: - - `nodeCount` 记录当前节点的数量,防止索引冲突。 + - `nodeCount` 记录当前节点的数量,防止索引冲突。 3. **插入、查找与前缀匹配**:分别在 `insert`、`search` 和 `startsWith` 方法中实现。 ## 笛卡尔树 +### [维基百科用途解释](https://zh.wikipedia.org/wiki/%E7%AC%9B%E5%8D%A1%E5%B0%94%E6%A0%91) +![1729697900947](image/3.数据结构/1729697900947.png) + +笛卡尔树是一种基于树结构的数据结构,结合了二叉搜索树和堆的特性。它通常用于动态集合的操作,比如合并和查找。笛卡尔树的基本原理是: + +1. **树的结构**:对于每个节点,满足左子树的所有节点小于该节点,右子树的所有节点大于该节点(符合二叉搜索树的性质),同时每个节点的值也是该节点的优先级(符合堆的性质)。 + +2. **构建方法**:通过将元素依次插入,保持上述性质。插入时,如果新节点的优先级大于当前节点,则将其插入到当前节点的右子树,否则插入到左子树。 + +3. **合并操作**:可以通过递归方法实现合并两个笛卡尔树,比较根节点的优先级,并将较小的树作为左子树合并。 + +下面是一个简单的C++实现示例: + +```cpp +#include +using namespace std; + +struct Node { + int value; + int priority; + Node *left, *right; + + Node(int v) : value(v), priority(rand()), left(nullptr), right(nullptr) {} +}; + +Node* merge(Node* left, Node* right) { + if (!left) return right; + if (!right) return left; + + if (left->priority > right->priority) { + left->right = merge(left->right, right); + return left; + } else { + right->left = merge(left, right->left); + return right; + } +} + +void split(Node* root, int key, Node*& left, Node*& right) { + if (!root) { + left = right = nullptr; + return; + } + if (root->value < key) { + left = root; + split(root->right, key, left->right, right); + } else { + right = root; + split(root->left, key, left, right->left); + } +} + +void insert(Node*& root, int value) { + Node *newNode = new Node(value); + if (!root) { + root = newNode; + return; + } + if (root->priority > newNode->priority) { + split(root, value, root->left, root->right); + root = newNode; + root->left = root->left; + root->right = root->right; + } else { + insert(root->value < value ? root->right : root->left, value); + } +} + +// 使用示例 +int main() { + Node* root = nullptr; + insert(root, 10); + insert(root, 20); + insert(root, 15); + // 可以继续实现其他操作,如查找、删除等 + return 0; +} +``` +这个示例展示了如何插入节点并保持笛卡尔树的性质。可以根据需要扩展实现其他功能,比如查找和删除。 ## 平衡树(AVL, treap, splay) diff --git a/senior/image/3.数据结构/1729697900947.png b/senior/image/3.数据结构/1729697900947.png new file mode 100644 index 0000000..c0a44e0 Binary files /dev/null and b/senior/image/3.数据结构/1729697900947.png differ