队列 Queue

队列(Queue)是一种线性数据结构,它遵循“先进先出”(FIFO,First In First Out)原则,即第一个进入队列的元素将是第一个被移除的元素。队列的结构像排队的人群,先到的人先得到服务。 队列的特点: 先进先出(FIFO):队列的访问顺序遵循先进先出的原则,最先加入的元素最先被移除。 操作限制:队列只允许在一端进行插入操作(称为队尾),只允许在另一端进行删除操作(称为队头)。 存储结构:队列可以使用数组或链表来实现。 队列的常见操作: enqueue(入队):将元素加入到队列的尾部。 dequeue(出队):从队列的头部移除元素,并返回该元素的值。 peek(或 front):查看队列头部的元素,但不移除它。 isEmpty:检查队列是否为空。 size:返回队列中元素的个数。 队列的应用: 任务调度:操作系统使用队列来管理进程调度。队列用于存放等待执行的任务,系统会按照先进先出的顺序执行这些任务。 打印队列:在打印机中,打印任务通常以队列的形式管理。先发出的打印任务先被处理。 消息队列:在计算机网络和多线程编程中,消息队列常用来管理不同任务或进程间的通信。消息按照先进先出的顺序进行传递。 广度优先搜索(BFS):广度优先搜索算法(BFS)使用队列来遍历图或树的节点。 队列的示例(C++): #include <iostream> #include <queue> // 引入queue头文件 using namespace std; int main() { queue<int> q; // 创建一个空队列 // enqueue操作:入队 q.push(10); // 入队10 q.push(20); // 入队20 q.push(30); // 入队30 // peek操作:查看队头元素 cout << "队头元素: " << q.front() << endl; // 输出: 10 // dequeue操作:出队 q.pop(); // 出队10 // 打印队头元素 cout << "出队后, 队头元素: " << q.front() << endl; // 输出: 20 // isEmpty操作:检查队列是否为空 if (!q.empty()) { cout << "队列不是空的!" << endl; // 输出: 队列不是空的! } // size操作:获取队列的大小 cout << "队列的大小: " << q.size() << endl; // 输出: 2 return 0; } 代码解析: push:将元素添加到队列的尾部。 front:查看队列头部元素的值。 pop:从队列的头部移除元素。 empty:检查队列是否为空。 size:返回队列中的元素个数。 队列的优缺点: 优点: ...

二月 18, 2025 · 林墨瀚

二叉树 Binary Tree

二叉树是一种每个节点最多有两个子节点的树形数据结构。它是一种广泛应用于计算机科学中的数据结构,常用于表达层次结构、实现查找、排序、表达式求值等任务。 二叉树的基本定义: 节点:二叉树中的每个元素,通常包含数据和指向其子节点的指针。 根节点(Root):二叉树的第一个节点,是树的起点。 子节点(Children):每个节点可以有最多两个子节点,分别是左子节点和右子节点。 叶节点(Leaf):没有任何子节点的节点,也叫终端节点。 父节点(Parent):指向某一节点的上级节点。 深度(Depth):从根节点到该节点的路径长度。 高度(Height):从该节点到最远叶子节点的路径长度。 二叉树的基本类型 满二叉树(Full Binary Tree): 每个节点要么没有子节点,要么有两个 子节点。 即,除了叶节点外,其他每个节点都有两个子节点。 完全二叉树(Complete Binary Tree): 除了最后一层外,每一层的节点都尽可能多,且最后一层的节点都集中在左侧。 平衡二叉树(Balanced Binary Tree): 任意节点的左右子树的高度差不超过 1。 例如,AVL树和红黑树是常见的平衡二叉树。 二叉搜索树(Binary Search Tree, BST): 对于树中的每一个节点,左子树的所有节点的值小于该节点的值,右子树的所有节点的值大于该节点的值。 二叉搜索树提供了高效的查找、插入和删除操作。 赫夫曼树(Huffman Tree): 一种带权路径长度最短的二叉树,常用于数据压缩算法中。 二叉树的基本操作 二叉树的操作主要包括遍历、插入、删除、查找等。下面介绍常见的操作: 1. 遍历(Traversal) 遍历是指按照一定的顺序访问二叉树的每个节点。常见的遍历方式有: 前序遍历(Pre-order Traversal):访问根节点,然后递归遍历左子树和右子树。 顺序:根 → 左 → 右 示例:A, B, D, E, C, F 中序遍历(In-order Traversal):递归遍历左子树,然后访问根节点,最后递归遍历右子树。 顺序:左 → 根 → 右 示例:D, B, E, A, F, C 后 序遍历(Post-order Traversal):递归遍历左子树和右子树,然后访问根节点。 顺序:左 → 右 → 根 示例:D, E, B, F, C, A 层次遍历(Level-order Traversal):逐层遍历树的节点,通常使用队列实现。 顺序:从根节点开始,逐层访问每个节点。 示例:A, B, C, D, E, F 2. 插入(Insertion) 二叉搜索树的插入:在二叉搜索树中插入节点时,根据节点的值与当前节点的值进行比较,决定将新节点插入到左子树还是右子树,直到找到合适的位置。 3. 删除(Deletion) 删除节点:删除二叉树中的节点时,需要考虑三种情况: 没有子节点(叶节点):直接删除。 有一个子节点:用子节点替代被删除的节点。 有两个子节点:通常用右子树中的最小节点或左子树中的最大节点来替代被删除的节点,然后递归删除替代节点。 4. 查找(Search) 二叉搜索树的查找:从根节点开始,根据查找的值与当前节点的值比较,决定是向左子树还是右子树查找,直到找到该节点或遍历完整棵树。 二叉树的应用: * / \ + + / \ / \ a b c d 表达式树:用于表示数学表达式。在计算机科学中,表达式树是二叉树的一种应用,其中每个叶子节点代表操作数,每个非叶子节点代表操作符。 例如:(a + b) * (c + d) 可以表示为以上代码块。 二叉查找树的应用: 高效查找:在二叉查找树中,查找操作的平均时间复杂度是 O(log n),比线性查找 O(n) 更高效。 排序:通过中序遍历二叉搜索树可以得到一个有序序列,从而实现排序。 堆(Heap): 二叉堆是一种特殊的二叉树,常用于实现优先队列。二叉堆有两种:最大堆和最小堆。最大堆的父节点的值大于等于子节点的值,最小堆则相反。 二叉树的平衡:AVL 树、红黑树等自平衡二叉树可以用于实现高效的查找、插入和删除操作,广泛应用于数据库索引、内存管理等领域。 二叉树的优缺点: 优点: ...

二月 18, 2025 · 林墨瀚

哈希表 Hash Table

哈希表(Hash Table)是一种用于存储键值对(key-value pairs)的数据结构,它可以实现非常高效的查找、插入和删除操作。哈希表通过哈希函数将键映射到哈希表的索引位置,从而能在常数时间 O(1) 内完成查找、插入和删除操作。 哈希表的基本原理 哈希函数:哈希表通过哈希函数将键(key)映射到哈希表中的一个特定位置(即索引)。哈希函数的目标是尽可能将不同的键映射到不同的索引,以减少冲突。 哈希函数的一种常见实现方法是:hash(key) = key % table_size,这里的 table_size 是哈希表的大小。 冲突(Collision):不同的键可能通过哈希函数计算后得到相同的哈希值,从而映射到同一个位置,这时就发生了冲突。解决冲突的常见方法有: 链式地址法(Separate Chaining):每个哈希表的槽位(bucket)存储一个链表,冲突的元素存储在同一个链表中。 开放定址法(Open Addressing):当发生冲突时,哈希表会尝试在表中寻找其他空位,直到找到一个空槽。 负载因子(Load Factor):负载因子是哈希表中的元素数量与哈希表大小的比值,用来衡量哈希表的填充程度。负载因子过高可能导致哈希冲突增多,从而降低性能。通常当负载因子超过某个阈值时,会对哈希表进行扩容(rehash)。 扩容与再哈希(Rehashing):当哈希表的负载因子超过设定的阈值时,可以通过增加哈希表的大小并重新计算每个元素的哈希值来减少冲突,从而提高查找效率。 哈希表的基本操作 插入(Insert): 使用哈希函数计算键的哈希值,并将其映射到对应的槽位。 如果槽位已被占用,则根据解决冲突的方法(链式或开放定址法)处理冲突。 查找(Search): 使用哈希函数计算键的哈希值,然后检查该槽位是否存在对应的值。如果有冲突,则需要继续检查链表或其他空槽。 删除(Delete): 使用哈希函数找到元素的位置,并删除它。如果存在冲突(如链式法),需要相应地删除链表中的元素。 扩容(Rehashing): 当哈希表的负载因子过高时,哈希表的大小需要扩大,通常是扩展为原来大小的两倍,然后重新计算每个元素的哈希值并重新插入。 哈希表的优缺点 优点: 快速查找:在理想情况下,哈希表的查找、插入和删除操作的时间复杂度是 O(1),即常数时间操作。 高效存储:由于哈希表通过哈希函数直接映射到槽位,因此可以在大规模数据存储时提供高效的访问。 缺点: 哈希冲突:当多个键映射到同一个位置时,会发生哈希冲突。虽然可以通过链式或开放定址法解决冲突,但会影响操作效率。 不适合排序:哈希表中的元素没有顺序,不能直接支持按键排序。 空间开销:哈希表为了避免过多冲突,通常需要比实际存储元素更多的空间。这可能导致内存浪费。 哈希表的应用 快速查找:哈希表用于实现快速查找功能,广泛应用于数据库索引、缓存系统等。 去重:通过将元素作为键存储,哈希表可以方便地去除重复元素。例如,在处理大数据时,通过哈希表去除重复记录。 计数统计:哈希表可以用于统计每个元素出现的次数,广泛应用于词频统计、数据分析等。 实现集合操作:哈希表常常用于实现集合(set)的操作,如交集、并集、差集等。 哈希表的示例实现(C++) #include <iostream> #include <list> #include <vector> using namespace std; class HashTable { private: vector<list<int>> table; // 哈希表的桶(使用链表处理冲突) int size; // 哈希表的大小 int hashFunction(int key) { return key % size; // 哈希函数(取模) } public: HashTable(int s) { size = s; table.resize(s); // 初始化哈希表 } // 插入操作 void insert(int key) { int index = hashFunction(key); // 计算哈希值 table[index].push_back(key); // 在对应的槽位插入键 } // 查找操作 bool search(int key) { int index = hashFunction(key); for (int element : table[index]) { if (element == key) { return true; // 找到元素 } } return false; // 未找到元素 } // 删除操作 void remove(int key) { int index = hashFunction(key); table[index].remove(key); // 从链表中删除元素 } // 打印哈希表 void display() { for (int i = 0; i < size; i++) { cout << "Bucket " << i << ": "; for (int element : table[i]) { cout << element << " "; } cout << endl; } } }; int main() { HashTable ht(7); // 创建一个大小为7的哈希表 ht.insert(10); ht.insert(20); ht.insert(15); ht.insert(7); ht.insert(30); ht.display(); // 显示哈希表 cout << "Search 15: " << (ht.search(15) ? "Found" : "Not Found") << endl; cout << "Search 25: " << (ht.search(25) ? "Found" : "Not Found") << endl; ht.remove(15); cout << "After deleting 15:" << endl; ht.display(); // 删除元素后显示哈希表 return 0; } 代码解析 HashTable 类:包含了哈希表的基本操作,如插入、查找、删除和显示哈希表。 insert():通过哈希函数计算插入元素的槽位,然后将元素插入到该槽位的链表中。 search():通过哈希函数找到槽位后,遍历链表检查是否有该元素。 remove():通过哈希函数找到槽位,并从该槽位的链表中删除元素。 display():显示哈希表的所有槽位及其存储的元素。 总结 哈希表是一种高效的键值对存储数据结构,能够提供常数时间复杂度的查找、插入和删除操作。它广泛应用于需要高效查找、去重和计数的场景。虽然哈希表在实际应用中需要解决哈希冲突问题,但通过合适的冲突解决方法,可以保证其高效性。 ...

二月 18, 2025 · 林墨瀚

链表 Linked List

链表是一种线性数据结构,它由多个节点(Node)组成,每个节点包含两部分: 数据部分:存储实际的数据。 指针部分:存储指向下一个节点的地址。 链表的结构特点是节点在内存中不一定是连续存储的,而是通过指针将一个个节点连接起来。每个节点可以通过指针找到下一个节点,形成一个链式结构。 链表的类型: 单向链表(Singly Linked List):每个节点只包含指向下一个节点的指针,最后一个节点的指针指向 null。 双向链表(Doubly Linked List):每个节点包含两个指针,分别指向前一个节点和下一个节点,便于从两端遍历。 循环链表(Circular Linked List):链表的最后一个节点的指针指向第一个节点,形成一个环。 链表的特点: 动态大小:链表大小可以动态调整,元素可以随时添加或删除,且不需要提前指定大小。 插入和删除操作高效:链表在任何位置的插入和删除都比数组更高效,因为只需要改变指针的指向即可。 访问不方便:链表的元素不是连续存储的,访问某个特定元素时需要从头遍历链表,效率较低。 链表的缺点: 随机访问差:在链表中,访问某个元素需要从头节点开始逐个遍历,无法像数组那样通过索引直接访问,效率较低。 额外内存开销:每个节点都需要额外的指针空间,增加了内存的消耗。 复杂的实现和维护:链表的插入、删除、遍历等操作涉及到指针操作,相比数组实现较复杂。 链表的操作: 插入操作:在链表的任意位置插入一个新节点,需要更新指针的连接。 删除操作:删除链表中的某个节点时,更新指针,使其跳过被删除的节点。 遍历操作:从头节点开始,顺序访问链表中的所有节点。 查找操作:遍历链表查找特定元素(通常时间复杂度是 O(n))。 示例(C++): #include <iostream> using namespace std; // 定义链表节点结构 struct Node { int data; // 存储数据 Node* next; // 指向下一个节点的指针 }; // 插入节点 void insert(Node*& head, int value) { Node* newNode = new Node(); // 创建新节点 newNode->data = value; // 设置数据 newNode->next = head; // 新节点指向当前头节点 head = newNode; // 更新头节点 } // 打印链表 void printList(Node* head) { Node* temp = head; while (temp != nullptr) { cout << temp->data << " "; temp = temp->next; } cout << endl; } int main() { Node* head = nullptr; // 空链表 // 插入元素 insert(head, 10); insert(head, 20); insert(head, 30); // 打印链表 printList(head); // 输出: 30 20 10 return 0; } 在这个例子中,我们定义了一个链表节点 Node,每个节点包含一个 data 数据部分和一个 next 指针,指向下一个节点。通过 insert 函数将元素插入链表头部,并通过 printList 函数遍历链表输出元素。 ...

二月 18, 2025 · 林墨瀚

数组 array

数组的特点: 固定大小:数组在创建时必须指定大小,一旦定义,大小不能更改。这意味着,如果需要存储更多的数据,必须创建一个新的数组并复制数据。 元素类型相同:数组中的所有元素必须是相同类型的数据(如整数、字符、浮点数等)。 按索引访问:数组的每个元素都有一个唯一的索引,索引从0开始,允许直接访问任何元素。 连续内存:数组的元素在内存中是连续存储的,这使得数组具有较快的访问速度。 数组的缺点: 固定大小:数组一旦创建,其大小是固定的,无法动态扩展。如果数据量超出了原先设定的大小,必须重新分配更大的数组并复制数据,效率较低。 浪费内存:如果你事先定义的数组大小过大,可能会浪费内存空间;如果大小过小,可能会导致无法存储所有数据。 插入和删除效率低:数组中的元素是连续存储的,因此在数组中间插入或删除元素时,需要移动大量的数据,效率较低。 示例(C++): #include <iostream> using namespace std; int main() { // 声明并初始化一个包含5个整数的数组 int arr[5] = {1, 2, 3, 4, 5}; // 访问数组元素 cout << "数组的第一个元素: " << arr[0] << endl; // 输出 1 // 修改数组元素 arr[2] = 10; // 将数组中第三个元素修改为 10 // 输出修改后的数组元素 cout << "修改后的第三个元素: " << arr[2] << endl; // 输出 10 return 0; } 数组的操作: 访问元素:通过索引访问数组中的元素。例如,arr[3] 访问数组 arr 的第四个元素(索引从0开始)。 修改元素:通过指定索引修改数组中的元素。例如,arr[1] = 20 将数组中第二个元素的值改为20。 遍历数组:通过循环遍历数组中的每个元素。例如,使用 for 循环来访问所有元素。 for (int i = 0; i < 5; i++) { cout << arr[i] << " "; } 数组初始化:在声明时,你可以通过花括号 {} 初始化数组,或者使用循环动态赋值。 数组的应用: 存储一组相同类型的数据:最常见的应用是存储同类型的数据集合,例如学生的成绩、多个传感器的测量值等。 例:存储班级学生的数学成绩。 int scores[30]; // 存储30个学生的成绩 实现栈和队列:通过数组可以实现栈(后进先出)和队列(先进先出)的数据结构。 例:使用数组来实现一个队列: int queue[100]; int front = -1, rear = -1; 数据处理和分析:在数据科学和计算机科学中,数组广泛用于存储和处理数据。 例:用数组存储每日气温数据,进行温度变化分析。 排序和查找:数组是许多经典排序和查找算法(如冒泡排序、二分查找)的基础数据结构。 例:使用数组进行排序: for (int i = 0; i < 5; i++) { for (int j = 0; j < 5 - i - 1; j++) { if (arr[j] > arr[j + 1]) { swap(arr[j], arr[j + 1]); } } } 图像处理:在图像处理中,像素值通常以二维数组的形式存储,用于表示图像中的颜色和亮度。 例:一个图像的RGB像素值可以用二维数组表示。 int image[100][100][3]; // 100x100像素的图像,每个像素有RGB三个值

二月 18, 2025 · 林墨瀚

栈 Stack

栈(Stack)是一种线性数据结构,它遵循“后进先出”(LIFO,Last In First Out)原则,即最后加入的元素最先被取出。栈通常用于实现需要“反向”操作的算法和数据结构。 栈的特点: 后进先出(LIFO):栈的访问顺序遵循后进先出的原则,最后插入的元素最先被移除。 操作限制:栈只允许在一端进行插入和删除操作,这一端通常被称为栈顶(Top)。 动态增长:栈可以根据需要动态地扩展或收缩(对于动态数组实现的栈)。 存储结构:栈可以使用数组或链表来实现。 栈的常见操作: push:将一个元素添加到栈顶。 pop:移除栈顶元素,并返回该元素的值。 peek(或 top):查看栈顶元素的值,但不移除它。 isEmpty:检查栈是否为空。 size:返回栈中元素的个数。 栈的应用: 递归调用的实现:计算机的函数调用使用栈来管理函数的调用和返回。当一个函数调用时,它的返回地址和局部变量会被压入栈中,函数执行完毕后从栈中弹出返回地址。 表达式求值:栈常用于表达式的求值和解析,特别是在中缀表达式、后缀表达式和前缀表达式的转换和计算中。 深度优先搜索(DFS):栈用于深度优先搜索(DFS)算法,通过栈记录搜索路径,实现图的遍历。 回溯算法:在回溯算法中,栈用于记录当前的选择和路径,便于回退到上一步状态。 栈的示例(C++): #include <iostream> #include <stack> // 引入stack头文件 using namespace std; int main() { stack<int> st; // 创建一个空栈 // push操作:压栈 st.push(10); // 压入10 st.push(20); // 压入20 st.push(30); // 压入30 // peek操作:查看栈顶元素 cout << "栈顶元素: " << st.top() << endl; // 输出: 30 // pop操作:弹出栈顶元素 st.pop(); // 弹出30 // 打印栈顶元素 cout << "弹出栈顶元素后, 栈顶元素: " << st.top() << endl; // 输出: 20 // isEmpty操作:检查栈是否为空 if (!st.empty()) { cout << "栈不是空的!" << endl; // 输出: 栈不是空的! } // size操作:获取栈的大小 cout << "栈的大小: " << st.size() << endl; // 输出: 2 return 0; } 代码解析: push:将元素压入栈中。 top:查看栈顶元素的值。 pop:移除栈顶元素。 empty:检查栈是否为空。 size:返回栈的元素数量。 栈的优缺点: 优点: 时间效率高:栈的插入和删除操作通常为 O(1) 时间复杂度,因此在许多需要频繁插入和删除操作的场景中,栈非常高效。 简单的操作:栈的操作非常简单,只需要知道栈顶元素,并根据栈顶进行插入或删除操作。 缺点: 访问受限:栈只允许访问栈顶的元素,不能直接访问栈中的其他元素,因此它并不适用于需要随机访问元素的场景。 空间限制:栈的大小是有限的,如果栈中的元素太多,可能会发生栈溢出(尤其是递归调用时,栈空间可能会用尽)。 栈的应用实例: 括号匹配:栈可以用于检测表达式中的括号是否匹配,特别是在编译器中,栈用于解析代码中的括号、花括号、方括号等。 Undo(撤销)操作:在许多应用程序中,撤销功能通常是通过栈来实现的,每次执行操作时,将当前状态压入栈中,撤销时从栈中弹出之前的状态。 表达式求值:栈用于处理中缀、前缀、后缀表达式的转换和计算。 总结: 栈是一种常见的线性数据结构,具有后进先出的特点,广泛应用于各种计算机科学和算法问题中。它的简单性和高效性使其成为实现许多算法和数据结构的基础。在需要跟踪状态或执行回溯操作的场景中,栈是非常有用的工具。 ...

二月 18, 2025 · 林墨瀚