diff --git a/senior/3.数据结构.md b/senior/3.数据结构.md index 73b45e5..3a71006 100644 --- a/senior/3.数据结构.md +++ b/senior/3.数据结构.md @@ -802,17 +802,452 @@ private: - **树的高度**:`rank` 数组用于记录每个集合的树的高度(或深度)。通过保持较小的树总是连接到较大的树,减少了树的高度,从而提高了查找操作的效率。 - **避免不必要的深度**:如果不使用 `rank`,在多次合并中可能导致某棵树的高度迅速增加,使得查找操作的时间复杂度增加到 O(n)。 ## 树的孩子兄弟表示法 +树的孩子兄弟表示法是一种用于表示树形结构的数据结构。它将每个节点的孩子节点和兄弟节点分别用指针连接起来,从而避免了使用数组来存储孩子节点的方式。这种表示法特别适合于孩子数量不固定的树。 +在孩子兄弟表示法中,每个节点包含两个指针: +1. `firstChild`:指向该节点的第一个孩子。 +2. `nextSibling`:指向该节点的下一个兄弟。 + +下面是一个简单的 C++ 实现示例: + +```cpp +#include + +struct TreeNode { + int data; // 节点数据 + TreeNode* firstChild; // 指向第一个孩子 + TreeNode* nextSibling; // 指向下一个兄弟 + + // 构造函数 + TreeNode(int value) : data(value), firstChild(nullptr), nextSibling(nullptr) {} +}; + +// 插入孩子节点 +void addChild(TreeNode* parent, int childValue) { + TreeNode* newChild = new TreeNode(childValue); + if (parent->firstChild == nullptr) { + parent->firstChild = newChild; // 如果没有孩子,直接添加 + } else { + TreeNode* sibling = parent->firstChild; + while (sibling->nextSibling != nullptr) { + sibling = sibling->nextSibling; // 找到最后一个兄弟 + } + sibling->nextSibling = newChild; // 添加为最后一个兄弟 + } +} + +// 遍历树(前序遍历) +void traverse(TreeNode* node) { + if (node == nullptr) return; + std::cout << node->data << " "; // 访问当前节点 + traverse(node->firstChild); // 遍历第一个孩子 + traverse(node->nextSibling); // 遍历下一个兄弟 +} + +int main() { + TreeNode* root = new TreeNode(1); // 创建根节点 + addChild(root, 2); // 添加孩子 + addChild(root, 3); + addChild(root->firstChild, 4); // 添加孩子到节点2 + addChild(root->firstChild, 5); + addChild(root->firstChild->nextSibling, 6); // 添加孩子到节点3 + + std::cout << "Tree traversal: "; + traverse(root); // 遍历树 + std::cout << std::endl; + + // 释放内存(省略,实际使用中应注意内存管理) + + return 0; +} +``` + +### 代码说明: +1. **TreeNode 结构体**:定义了树的节点,包含数据、指向第一个孩子的指针和指向下一个兄弟的指针。 +2. **addChild 函数**:用于向父节点添加孩子节点。如果父节点没有孩子,则直接将新节点设置为第一个孩子;如果有孩子,则遍历兄弟节点,找到最后一个兄弟并将新节点添加为其下一个兄弟。 +3. **traverse 函数**:实现了树的前序遍历,先访问当前节点,然后递归访问第一个孩子和下一个兄弟。 +4. **main 函数**:创建树的根节点,添加一些孩子节点,并进行遍历输出。 + +这种表示法的优点是灵活性高,适合于孩子数量不固定的树结构。 # 特殊树 ## 二叉堆,树状数组 +二叉堆(Binary Heap)是一种特殊的完全二叉树,通常用于实现优先队列。它有两个主要的性质: +1. **结构性质**:二叉堆是一棵完全二叉树,意味着除了最后一层外,其他层的节点都是满的,最后一层的节点从左到右填充。 + +2. **堆性质**:在最大堆中,父节点的值总是大于或等于其子节点的值;在最小堆中,父节点的值总是小于或等于其子节点的值。这使得堆的根节点总是最大值(最大堆)或最小值(最小堆)。 + +### C++ 实现 + +以下是一个简单的最大堆的 C++ 实现: + +```cpp +#include +#include +#include + +class MaxHeap { +public: + MaxHeap() {} + + void insert(int value) { + heap.push_back(value); + siftUp(heap.size() - 1); + } + + int extractMax() { + if (heap.empty()) { + throw std::runtime_error("Heap is empty"); + } + int maxValue = heap[0]; + heap[0] = heap.back(); + heap.pop_back(); + siftDown(0); + return maxValue; + } + + int getMax() const { + if (heap.empty()) { + throw std::runtime_error("Heap is empty"); + } + return heap[0]; + } + + bool isEmpty() const { + return heap.empty(); + } + +private: + std::vector heap; + + void siftUp(int index) { + while (index > 0) { + int parentIndex = (index - 1) / 2; + if (heap[index] > heap[parentIndex]) { + std::swap(heap[index], heap[parentIndex]); + index = parentIndex; + } else { + break; + } + } + } + + void siftDown(int index) { + int size = heap.size(); + while (index < size) { + int leftChild = 2 * index + 1; + int rightChild = 2 * index + 2; + int largest = index; + + if (leftChild < size && heap[leftChild] > heap[largest]) { + largest = leftChild; + } + if (rightChild < size && heap[rightChild] > heap[largest]) { + largest = rightChild; + } + if (largest != index) { + std::swap(heap[index], heap[largest]); + index = largest; + } else { + break; + } + } + } +}; + +int main() { + MaxHeap maxHeap; + maxHeap.insert(10); + maxHeap.insert(20); + maxHeap.insert(5); + maxHeap.insert(30); + + std::cout << "Max value: " << maxHeap.getMax() << std::endl; // 输出 30 + std::cout << "Extracted max: " << maxHeap.extractMax() << std::endl; // 输出 30 + std::cout << "New max value: " << maxHeap.getMax() << std::endl; // 输出 20 + + return 0; +} +``` + +### 用途 + +1. **优先队列**:二叉堆常用于实现优先队列,允许快速插入和删除最大(或最小)元素。 + +2. **排序算法**:堆排序(Heap Sort)是一种基于二叉堆的排序算法,具有 O(n log n) 的时间复杂度。 + +3. **图算法**:在 Dijkstra 算法和 Prim 算法中,二叉堆用于高效地获取当前最小(或最大)边。 + +4. **调度算法**:在操作系统中,二叉堆可以用于任务调度,确保高优先级任务优先执行。 + +二叉堆因其高效的插入和删除操作而广泛应用于各种算法和数据结构中。 ## 线段树 +线段树是一种高效的数据结构,用于处理区间查询和更新。对于区间修改,常用懒惰标记(Lazy Propagation)来优化性能。 + +### 懒惰标记的基本思想 + +当我们需要对一个区间进行修改(如加一个值),而不是立即更新所有相关的节点,而是记录这个修改操作,以后再处理。这种方式可以大幅度减少不必要的更新。 + +### 实现步骤 + +1. **构建线段树**:首先构建线段树,每个节点存储该区间的值。 + +2. **修改操作**: + - 当进行区间更新时,检查当前节点的懒惰标记。 + - 如果当前节点有懒惰标记,先应用这个标记(更新当前节点值),然后将标记传递给子节点(如果有的话)。 + - 更新时,如果当前区间完全在要修改的区间内,直接标记当前节点,并返回。 + - 如果当前区间部分重叠,递归地更新左右子树。 + +3. **查询操作**: + - 在查询之前,检查并更新懒惰标记,以确保返回的值是最新的。 + +### 示例 + +假设我们要对区间 [l, r] 加一个值 `val`: +- 如果当前节点的区间完全包含 [l, r],则设置懒惰标记 `add` 为 `val`。 +- 如果只部分重叠,则递归更新子节点。 + +这样,懒惰标记使得我们能在 O(log n) 的时间复杂度内完成区间修改,而不是 O(n)。 +下面是一个使用 C++ 实现的线段树,包括区间更新和懒惰标记的处理,配有详细注释: + +```cpp +#include +#include +using namespace std; + +// 定义线段树类 +class SegmentTree { +private: + vector tree; // 线段树 + vector lazy; // 懒惰标记 + int n; // 数据长度 + + // 建立线段树 + void buildTree(const vector& arr, int node, int start, int end) { + if (start == end) { + // 如果是叶子节点,直接赋值 + tree[node] = arr[start]; + } else { + int mid = (start + end) / 2; + // 递归建立左右子树 + buildTree(arr, 2 * node + 1, start, mid); + buildTree(arr, 2 * node + 2, mid + 1, end); + // 合并左右子树的值 + tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; + } + } + + // 更新区间 + void updateRange(int l, int r, int val, int node, int start, int end) { + // 先处理懒惰标记 + if (lazy[node] != 0) { + // 应用懒惰标记 + tree[node] += (end - start + 1) * lazy[node]; + if (start != end) { + // 传递懒惰标记到子节点 + lazy[2 * node + 1] += lazy[node]; + lazy[2 * node + 2] += lazy[node]; + } + // 清除当前节点的懒惰标记 + lazy[node] = 0; + } + + // 如果当前区间完全不在更新范围内 + if (start > end || start > r || end < l) { + return; + } + + // 如果当前区间完全在更新范围内 + if (start >= l && end <= r) { + tree[node] += (end - start + 1) * val; + if (start != end) { + // 设置懒惰标记 + lazy[2 * node + 1] += val; + lazy[2 * node + 2] += val; + } + return; + } + + // 如果部分重叠,递归更新左右子树 + int mid = (start + end) / 2; + updateRange(l, r, val, 2 * node + 1, start, mid); + updateRange(l, r, val, 2 * node + 2, mid + 1, end); + // 更新当前节点值 + tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; + } + + // 查询区间和 + int queryRange(int l, int r, int node, int start, int end) { + // 处理懒惰标记 + if (lazy[node] != 0) { + tree[node] += (end - start + 1) * lazy[node]; + if (start != end) { + lazy[2 * node + 1] += lazy[node]; + lazy[2 * node + 2] += lazy[node]; + } + lazy[node] = 0; + } + + // 如果当前区间完全不在查询范围内 + if (start > end || start > r || end < l) { + return 0; // 返回0表示不影响和 + } + + // 如果当前区间完全在查询范围内 + if (start >= l && end <= r) { + return tree[node]; + } + + // 如果部分重叠,查询左右子树 + int mid = (start + end) / 2; + int leftSum = queryRange(l, r, 2 * node + 1, start, mid); + int rightSum = queryRange(l, r, 2 * node + 2, mid + 1, end); + return leftSum + rightSum; + } + +public: + // 构造函数 + SegmentTree(const vector& arr) { + n = arr.size(); + tree.resize(4 * n); + lazy.resize(4 * n, 0); + buildTree(arr, 0, 0, n - 1); + } + + // 对区间 [l, r] 进行加 val 的更新 + void update(int l, int r, int val) { + updateRange(l, r, val, 0, 0, n - 1); + } + + // 查询区间 [l, r] 的和 + int query(int l, int r) { + return queryRange(l, r, 0, 0, n - 1); + } +}; + +int main() { + vector arr = {1, 2, 3, 4, 5}; + SegmentTree segTree(arr); + + cout << "Sum of values in range [1, 3]: " << segTree.query(1, 3) << endl; // 输出 9 + segTree.update(1, 3, 3); // 对区间 [1, 3] 加 3 + cout << "After update, sum of values in range [1, 3]: " << segTree.query(1, 3) << endl; // 输出 18 + + return 0; +} +``` + +### 代码说明 + +1. **构造函数**:构建线段树,初始化 `tree` 和 `lazy` 数组。 + +2. **构建树**:`buildTree` 函数递归构建线段树的节点。 + +3. **更新区间**: + - `updateRange` 函数处理区间更新,包括懒惰标记的管理。 + +4. **查询区间**: + - `queryRange` 函数处理区间求和,并在需要时更新懒惰标记。 + +5. **主函数**:测试线段树的功能,包括初始化、查询和更新操作。 ## 字典树(Trie树) +字典树(Trie)是一种高效的字符串存储结构,常用于快速查找单词或前缀。它通过每个字符创建一个节点,所有共享前缀的单词会共享相同的路径。 + +### 字典树的基本操作: + +1. **插入**:从根节点开始,逐字符插入单词,如果字符不存在,就创建新的节点。 +2. **查找**:同样从根节点开始,逐字符查找。如果路径上的字符不存在,则表示该单词不在字典树中。 +3. **删除**:需要遍历字符,找到单词末尾的节点,并进行必要的清理。 +使用固定大小的数组实现字典树(Trie)是一种高效的内存管理方式,特别适用于字母表大小有限的情况。这里我们假设单词最长为30个字符,并使用一个二维数组 `trie[30][256]` 来表示字典树,其中第一维表示字符位置,第二维表示每个字符的孩子节点。 + +### 数组实现的 Trie 结构 + +我们可以用 `trie` 数组来存储每个节点的孩子节点索引,并使用一个标志来表示某个单词的结束。 + +### C++ 实现示例 + +以下是一个使用固定数组实现的 Trie 的示例: + +```cpp +#include +#include +using namespace std; + +class Trie { +private: + int trie[30][256] = {0}; // trie数组,初始为0 + bool isEnd[30] = {false}; // 用于标记单词结束 + int nodeCount; // 当前节点数 + +public: + Trie() { + nodeCount = 1; // 从1开始,因为0索引通常是根节点 + } + + void insert(const string& word) { + int currentNode = 0; // 从根节点开始 + for (char ch : word) { + int index = ch; // 将字符作为索引 + if (trie[currentNode][index] == 0) { + trie[currentNode][index] = nodeCount++; // 创建新节点 + } + currentNode = trie[currentNode][index]; + } + isEnd[currentNode] = true; // 标记单词结束 + } + + bool search(const string& word) { + int currentNode = 0; + for (char ch : word) { + int index = ch; + if (trie[currentNode][index] == 0) { + return false; // 找不到该单词 + } + currentNode = trie[currentNode][index]; + } + return isEnd[currentNode]; // 检查是否是单词结束 + } + + bool startsWith(const string& prefix) { + int currentNode = 0; + for (char ch : prefix) { + int index = ch; + if (trie[currentNode][index] == 0) { + return false; // 找不到前缀 + } + currentNode = trie[currentNode][index]; + } + return true; // 找到前缀 + } +}; + +int main() { + Trie trie; + trie.insert("hello"); + cout << trie.search("hello") << endl; // 输出 1 + cout << trie.startsWith("hell") << endl; // 输出 1 + cout << trie.search("world") << endl; // 输出 0 + return 0; +} +``` + +### 解释 + +1. **数组定义**: + - `trie[30][256]`:第一维表示字符的深度,第二维表示字符(ASCII值)。 + - `isEnd[30]`:标记每个节点是否是单词的结束。 + +2. **节点管理**: + - `nodeCount` 记录当前节点的数量,防止索引冲突。 + +3. **插入、查找与前缀匹配**:分别在 `insert`、`search` 和 `startsWith` 方法中实现。 + ## 笛卡尔树 + ## 平衡树(AVL, treap, splay) # 常见图