前言
本文为自学使用,欢迎大家补充
一、动态数组
- 动态数组 是一种可以在运行时自动改变大小(扩容或缩容)的数组数据结构。它提供了在序列末尾高效添加/删除元素的能力,并且支持随机访问(通过索引直接访问任何元素)。
C++动态数组:std::vector
std::vector是 C++ 标准模板库(STL)中提供的容器,它就是一个典型的动态数组。它管理着一个动态分配的连续内存空间,当新元素插入导致空间不足时,它会自动分配一个更大的新数组,并将原有元素移动过去。
1. 头文件
#include <vector>
2. 初始化
// 空向量
std::vector<int> vec1;
// 指定初始大小和初始值(可选,默认为0)
std::vector<int> vec2(5); // 5个元素,每个都是0
std::vector<int> vec3(5, 10); // 5个元素,每个都是10
// 使用初始化列表 (C++11及以上)
std::vector<int> vec4 = {1, 2, 3, 4, 5};
// 从数组或其他容器初始化
int arr[] = {1, 2, 3};
std::vector<int> vec5(arr, arr + 3); // 使用指针范围
std::vector<int> vec6(vec4.begin(), vec4.end()); // 使用迭代器范围
3. 元素访问
std::vector<int> v = {10, 20, 30};
// 1. 使用下标运算符 [] (不检查边界,访问越界行为未定义)
int a = v[1]; // 20
// 2. 使用 at() 成员函数 (检查边界,越界抛出 std::out_of_range 异常)
int b = v.at(1); // 20
// 3. 访问第一个和最后一个元素
int first = v.front(); // 10
int last = v.back(); // 30
// 4. 直接访问底层数组(C++11及以上)
int* data_ptr = v.data();
4. 容量相关
v = {1, 2, 3};
// 获取当前元素个数
size_t size = v.size(); // 3
// 获取当前分配的容量(能容纳的元素总数, >= size)
size_t cap = v.capacity(); // 可能为 3, 4, 或其他 >=3 的值
// 检查容器是否为空
bool isEmpty = v.empty(); // false
// 改变向量的大小(size),新增元素用默认值填充
v.resize(5); // v 变为 {1, 2, 3, 0, 0}
v.resize(2); // v 变为 {1, 2} (多余的元素被销毁)
// 改变向量的容量(capacity),通常用于预分配内存以避免多次扩容
v.reserve(100); // 提前分配至少100个元素的空间,size()不变
5. 修改内容(增删改)
// 1. 在末尾添加元素 (均摊时间复杂度 O(1))
v.push_back(40); // v: {1, 2, 3, 40}
// 2. 在末尾删除元素 (时间复杂度 O(1))
v.pop_back(); // v: {1, 2, 3}
// 3. 在指定位置(迭代器指定)之前插入元素 (时间复杂度 O(n))
auto it = v.begin() + 1; // 指向第二个元素 ‘2'
v.insert(it, 99); // v: {1, 99, 2, 3}
// 4. 删除指定位置(迭代器指定)或范围的元素 (时间复杂度 O(n))
it = v.begin() + 2; // 指向第三个元素 ‘2'
v.erase(it); // v: {1, 99, 3}
// 5. 清空所有元素
v.clear(); // v: {}
// 6. 交换两个向量的内容
std::vector<int> other = {7, 8, 9};
v.swap(other); // v 变为 {7, 8, 9}, other 变为 {}
6. 迭代器
用于遍历或指定位置。
std::vector<int> v = {1, 2, 3};
// 正向迭代器
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
// 使用 auto (C++11)
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
// 范围 for 循环 (C++11)
for (int num : v) {
std::cout << num << " ";
}
// 反向迭代器
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
std::cout << *rit << " "; // 输出: 3 2 1
}
Python动态数组:list
Python 的内置类型 list就是一个功能强大的动态数组。它的行为与 C++ 的 vector非常相似,但语法更加简洁和高级。
1. 初始化
# 空列表
list1 = []
# 使用初始化列表
list2 = [1, 2, 3, 4, 5]
# 使用 list() 构造函数
list3 = list() # 空列表
list4 = list(range(5)) # [0, 1, 2, 3, 4]
list5 = list(“hello”) # [‘h‘, ’e‘, ’l‘, ’l‘, ’o’] (从可迭代对象转换)
2. 元素访问
v = [10, 20, 30]
# 1. 使用下标索引 (支持负数索引,越界抛出 IndexError)
a = v[1] # 20
last = v[-1] # 30 (倒数第一个)
# 2. 切片 (Slice) - 获取子列表 [start:stop:step]
sub = v[1:3] # [20, 30] ( stop 索引不包含)
sub2 = v[::2] # [10, 30] (每隔一个取一个)
3. 容量相关
v = [1, 2, 3]
# 获取当前元素个数
length = len(v) # 3
# 检查列表是否为空
is_empty = not v # 或者 len(v) == 0
4. 修改内容(增删改)
v = [1, 2, 3]
# 1. 在末尾添加元素 (时间复杂度 O(1))
v.append(4) # v: [1, 2, 3, 4]
# 2. 扩展列表,将另一个可迭代对象的元素逐个添加到末尾 (时间复杂度 O(k))
v.extend([5, 6]) # v: [1, 2, 3, 4, 5, 6]
# 3. 在指定位置插入元素 (时间复杂度 O(n))
v.insert(1, 99) # 在索引1的位置插入99, v: [1, 99, 2, 3, 4, 5, 6]
# 4. 按值删除元素(删除第一个匹配项) (时间复杂度 O(n))
v.remove(99) # v: [1, 2, 3, 4, 5, 6]
# 5. 按索引删除并返回元素 (时间复杂度 O(n), pop() 是 O(1))
popped_val = v.pop(2) # 删除索引2的元素 ‘3‘, v: [1, 2, 4, 5, 6]
popped_last = v.pop() # 删除并返回最后一个元素 ‘6‘, v: [1, 2, 4, 5]
# 6. 清空所有元素
v.clear() # v: []
# 7. 按索引赋值修改
v = [1, 2, 3]
v[1] = 20 # v: [1, 20, 3]
# 8. 切片赋值 (可以替换、插入、删除)
v = [1, 2, 3, 4, 5]
v[1:3] = [20, 30, 40] # 替换,v: [1, 20, 30, 40, 4, 5]
v[1:1] = [99, 88] # 插入,v: [1, 99, 88, 20, 30, 40, 4, 5]
v[2:4] = [] # 删除,v: [1, 99, 30, 40, 4, 5]
5. 其他常用操作
v = [1, 2, 3, 2, 4]
# 查找元素第一次出现的索引 (找不到抛出 ValueError)
index = v.index(2) # 1
# 计算某个元素出现的次数
count = v.count(2) # 2
# 反转列表本身 (in-place)
v.reverse() # v: [4, 2, 3, 2, 1]
# 对列表本身进行排序 (in-place)
v.sort() # v: [1, 2, 2, 3, 4]
v.sort(reverse=True) # 降序排序
# 使用 sorted() 函数获取排序后的新列表,原列表不变
new_list = sorted(v, reverse=True)
# 列表推导式 (创建新列表的强大工具)
squares = [x**2 for x in v] # [1, 4, 4, 9, 16]
even_squares = [x**2 for x in v if x % 2 == 0] # [4, 4, 16]
6. 遍历
v = [‘a‘, ’b‘, ’c’]
# 直接遍历元素
for item in v:
print(item)
# 需要索引时使用 enumerate
for index, item in enumerate(v):
print(f“Index {index}: {item}”)
二、双端队列
双端队列是一种具有队列和栈性质的抽象数据类型,允许在队列的前端和后端都进行插入和删除操作。
| 特性 | C++ (std::deque) | Python (collections.deque) |
|---|---|---|
| 实现方式 | 动态分配的分块数组(通常) | 双向链表 |
| 随机访问 | 支持,通过 [ ]运算符,常数时间 O(1) | 支持,通过 [ ]运算符,但 O(n) 时间(需遍历) |
| 中间插入/删除 | 效率较低,线性时间 O(n) | 效率较低,线性时间 O(n) |
| 两端操作 | 高效,分摊常数时间 O(1) | 高效,常数时间 O(1) |
| 线程安全 | 否(需要手动加锁) | 是(其原子操作是线程安全的) |
| 功能扩展 | 是 STL 的一部分,算法丰富 | 专为两端操作设计,提供旋转等实用方法 |
C++双端队列:std::deque
std::deque(Double Ended QUEue)是 C++ 标准模板库中的一种容器适配器,包含在 <deque>头文件中。
1. 包含头文件和初始化
#include <deque>
#include <iostream>
using namespace std;
// 初始化
deque<int> dq; // 创建一个空的双端队列
deque<int> dq1 = {1, 2, 3, 4}; // 初始化列表
deque<int> dq2(5, 100); // 创建包含5个100的deque
deque<int> dq3(dq1.begin(), dq1.end()); // 通过迭代器范围初始化
2. 元素访问
dq = {10, 20, 30, 40};
// 通过索引访问(不进行边界检查)
cout << dq[1]; // 输出 20
// 使用 at() 访问(进行边界检查,越界则抛出 std::out_of_range 异常)
cout << dq.at(2); // 输出 30
// 访问首尾元素
cout << dq.front(); // 输出 10(第一个元素)
cout << dq.back(); // 输出 40(最后一个元素)
3. 修改操作(两端)
// 在尾部添加元素
dq.push_back(50); // dq: {10, 20, 30, 40, 50}
dq.emplace_back(60); // 更高效,直接在尾部构造元素
// 在头部添加元素
dq.push_front(0); // dq: {0, 10, 20, 30, 40, 50, 60}
dq.emplace_front(-10); // 直接在头部构造元素
// 删除尾部元素
dq.pop_back(); // 删除60,无返回值
// 删除头部元素
dq.pop_front(); // 删除-10,无返回值
4. 修改操作(中间)
// 插入元素(效率较低)
auto it = dq.begin() + 2; // 获取指向第三个元素的迭代器
dq.insert(it, 25); // 在第三个位置插入25
// dq: {0, 10, 25, 20, 30, 40, 50}
// 删除元素(效率较低)
it = dq.begin() + 3;
dq.erase(it); // 删除第四个元素(20)
// dq: {0, 10, 25, 30, 40, 50}
// 清空队列
dq.clear();
5. 容量操作
cout << dq.size(); // 返回元素数量
cout << dq.empty(); // 判断是否为空
dq.resize(10); // 将大小调整为10,新元素默认初始化
dq.shrink_to_fit(); // 请求减少容量以适应大小(非强制)
6. 迭代器
// 正向迭代
for (auto it = dq.begin(); it != dq.end(); ++it) {
cout << *it << " ";
}
// 反向迭代
for (auto rit = dq.rbegin(); rit != dq.rend(); ++rit) {
cout << *rit << " ";
}
// 范围for循环
for (auto &elem : dq) {
cout << elem << " ";
}
Python 中的双端队列:collections.deque
deque是 Python 标准库 collections模块中的一个类,专为高效的两端操作而设计。
1. 导入和初始化
from collections import deque
# 初始化
dq = deque() # 创建一个空的双端队列
dq1 = deque([1, 2, 3, 4]) # 从可迭代对象初始化
dq2 = deque('hello') # dq2: deque(['h', 'e', 'l', 'l', 'o'])
dq3 = deque(maxlen=5) # 创建一个最大长度为5的deque(满时自动丢弃另一端元素)
2. 元素访问
dq = deque([10, 20, 30, 40])
# 通过索引访问(相对较慢,因为需要遍历)
print(dq[1]) # 输出 20
# 访问首尾元素
print(dq[0]) # 输出 10(等同于 dq[0])
print(dq[-1]) # 输出 40(等同于 dq[-1])
3. 修改操作(两端)
# 在尾部添加元素
dq.append(50) # dq: deque([10, 20, 30, 40, 50])
dq.appendleft(0) # 在头部添加元素,dq: deque([0, 10, 20, 30, 40, 50])
# 扩展 deque(添加多个元素)
dq.extend([60, 70]) # 在尾部扩展,dq: deque([0, 10, 20, 30, 40, 50, 60, 70])
dq.extendleft([-10, -20]) # 在头部扩展(注意:会将可迭代对象逆序插入)
# 首先插入 -20,然后插入 -10
# dq: deque([-20, -10, 0, 10, 20, 30, 40, 50, 60, 70])
# 删除元素
popped_right = dq.pop() # 删除并返回尾部元素 (70)
popped_left = dq.popleft() # 删除并返回头部元素 (-20)
4. 其他实用操作
# 旋转(非常实用)
dq = deque([1, 2, 3, 4, 5])
dq.rotate(2) # 向右旋转2步(正数)
# dq: deque([4, 5, 1, 2, 3])
dq.rotate(-1) # 向左旋转1步(负数)
# dq: deque([5, 1, 2, 3, 4])
# 计数
count = dq.count(2) # 计算某个值出现的次数
# 查找和删除(注意:删除第一个匹配的值,若找不到则引发 ValueError)
dq.remove(3) # 删除值为3的第一个元素
# 清空队列
dq.clear()
# 复制队列
dq_copy = dq.copy()
5. 容量操作
print(len(dq)) # 返回元素数量
if not dq: # 判断是否为空
print("Deque is empty")
# 最大长度属性
limited_dq = deque(maxlen=3)
limited_dq.extend([1, 2, 3])
limited_dq.append(4) # 头部元素1被自动丢弃
# limited_dq: deque([2, 3, 4], maxlen=3)
print(limited_dq.maxlen) # 输出 3
三、双向链表
双向链表(Doubly Linked List)是一种常见的线性数据结构,每个节点(Node)除了存储数据外,还包含两个指针,分别指向前一个节点(prev)和后一个节点(next)。这使得遍历可以双向进行,但同时也增加了内存开销和操作的复杂性。
C++双向链表
在 C++ 中,标准模板库(STL)提供了 std::list容器,它是一个典型的双向链表的实现。
1. 头文件
#include <list>
2. 声明与初始化
// 空链表
std::list<int> myList;
// 初始化有 n 个元素,默认值为 0
std::list<int> myList(5);
// 初始化有 n 个元素,每个值都为 value
std::list<int> myList(5, 10); // {10, 10, 10, 10, 10}
// 用初始化列表
std::list<int> myList = {1, 2, 3, 4, 5};
// 用迭代器范围初始化(例如从数组或其他容器)
int arr[] = {1, 2, 3};
std::list<int> myList(std::begin(arr), std::end(arr));
3. 元素访问
注意:std::list不支持随机访问(如 operator[]或 .at()),只能通过迭代器进行顺序访问。
std::list<int> l = {10, 20, 30};
// 访问第一个和最后一个元素
std::cout << l.front() << std::endl; // 输出: 10
std::cout << l.back() << std::endl; // 输出: 30
// 使用迭代器进行遍历(正向)
for (std::list<int>::iterator it = l.begin(); it != l.end(); ++it) {
std::cout << *it << " ";
}
// C++11 起,使用 auto 更简洁
for (auto it = l.begin(); it != l.end(); ++it) {
std::cout << *it << " ";
}
// 或使用范围 for 循环
for (const auto &elem : l) {
std::cout << elem << " ";
}
4. 容量操作
std::list<int> l = {1, 2, 3};
std::cout << l.empty() << std::endl; // 检查是否为空,输出: 0 (false)
std::cout << l.size() << std::endl; // 获取元素个数,输出: 3
// l.max_size(); // 返回链表可容纳的最大元素数量(一个很大的数)
5. 修改器(增删改)
std::list<int> l;
// --- 添加元素 ---
l.push_back(40); // 在末尾添加: {40}
l.push_front(10); // 在开头添加: {10, 40}
l.insert(l.begin(), 5); // 在指定迭代器位置前插入: {5, 10, 40}
auto it = l.begin();
std::advance(it, 2); // 将迭代器 it 向后移动 2 位,指向 40
l.insert(it, 20); // 在 40 前插入 20: {5, 10, 20, 40}
// 可以插入多个相同值或一个区间
l.insert(l.begin(), 2, 0); // 在开头插入两个 0: {0, 0, 5, 10, 20, 40}
// --- 删除元素 ---
l.pop_back(); // 删除末尾元素: {0, 0, 5, 10, 20}
l.pop_front(); // 删除开头元素: {0, 5, 10, 20}
it = l.begin();
std::advance(it, 2); // it 指向 10
l.erase(it); // 删除指定位置的元素 (10): {0, 5, 20}
// l.erase(l.begin(), l.end()); // 删除区间内的所有元素,清空链表
l.remove(5); // 删除所有值为 5 的节点: {0, 20}
l.clear(); // 清空整个链表
// --- 其他操作 ---
std::list<int> l1 = {1, 3, 5};
std::list<int> l2 = {2, 4, 6};
l1.swap(l2); // 交换两个链表的内容
l1.merge(l2); // 合并两个已排序的链表(l2 会被清空)
l1.merge(l2, comp);// 使用比较函数 comp 合并
l1.sort(); // 对链表进行排序(因为不支持随机访问,STL 为 list 提供了专门的 sort 成员函数)
l1.sort(comp); // 使用比较函数 comp 排序
l1.reverse(); // 反转链表
l1.unique(); // 删除连续重复的元素(通常先排序再使用)
l1.unique(binary_pred); // 使用二元谓词判断重复
//拼接
lst.splice(pos_iter, other_list); // 将 other_list 全部元素移到 pos_iter 前
lst.splice(pos_iter, other_list, other_iter); // 移动 other_list 的单个元素
lst.splice(pos_iter, other_list, first_iter, last_iter); // 移动元素范围
//删除特定值
lst.remove(value); // 删除所有等于 value 的元素
lst.remove_if(predicate); // 删除满足谓词的元素
6. 迭代器
除了 begin(), end(),还有反向迭代器和常量迭代器。
std::list<int> l = {1, 2, 3};
// 反向遍历
for (std::list<int>::reverse_iterator rit = l.rbegin(); rit != l.rend(); ++rit) {
std::cout << *rit << " "; // 输出: 3 2 1
}
// 常量迭代器(用于避免修改元素)
for (std::list<int>::const_iterator cit = l.cbegin(); cit != l.cend(); ++cit) {
// *cit = 5; // 错误!不能修改
}
Python双向链表
Python 的标准库没有直接提供名为“双向链表”的数据结构。但是,有两种常见的替代方案:
使用 collections.deque(推荐):这是一个双向队列,底层通常使用双向链表实现,提供了高效的头尾操作。
好的,我们来详细探讨一下 C++ 和 Python 中的“静态数组”概念及其用法。
首先,需要明确一个关键点:“静态数组”的定义在两种语言中有根本性的不同。
- 在 C++ 中,“静态”通常指两件事:
- 内存分配方式:在编译期确定大小,在程序的栈(Stack)内存上分配。其生命周期与所在的作用域相同(例如,函数结束时自动销毁)。
- 关键字
static:用于修饰变量,使其生命周期变为整个程序运行期(在静态存储区分配),但作用域不变。
- 在 Python 中:语言本身并没有内置的、严格意义上的“静态数组”。我们通常用以下两种方式来模拟或实现类似功能:
array模块:提供了一个类似于数组的高效数据结构,用于存储同类型的基本数据类型(如整型、浮点型)。- 第三方库
NumPy:提供了强大、高性能的多维数组对象ndarray,这是科学计算领域的事实标准。
四、静态数组
C++静态数组
C++ 中的静态数组是语言内置的核心特性,其大小在编译时必须已知且固定。
1. 定义和初始化
// 1. 基本定义:类型 名字[大小];
int arr[5]; // 定义一个包含5个整数的数组,其值未初始化(通常是垃圾值)
// 2. 初始化列表:类型 名字[大小] = {值1, 值2, ...};
int arr1[5] = {1, 2, 3, 4, 5}; // 完全初始化
int arr2[5] = {1, 2, 3}; // 部分初始化,剩余元素被初始化为0
int arr3[] = {1, 2, 3, 4, 5}; // 编译器自动推断数组大小为5
// 3. 使用 static 关键字(改变生命周期)
void myFunction() {
static int staticArr[3] = {10, 20, 30}; // 在静态存储区分配,函数调用结束后依然存在
}
2. 访问元素
int arr[5] = {10, 20, 30, 40, 50};
// 1. 通过下标运算符 [] (从0开始)
int first = arr[0]; // 10
arr[2] = 100; // 将第三个元素改为100
// 2. 通过指针算术(数组名是首元素的地址)
int second = *(arr + 1); // 20,等价于 arr[1]
// 注意:C++ 不进行数组边界检查,访问 arr[5] 会导致未定义行为(程序可能崩溃)
3. 遍历数组
int arr[] = {1, 2, 3, 4, 5};
size_t length = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
// 1. for 循环(使用下标)
for (int i = 0; i < length; ++i) {
std::cout << arr[i] << " ";
}
// 2. 范围for循环 (C++11)
for (int element : arr) {
std::cout << element << " ";
}
// 3. 使用指针
for (int* ptr = arr; ptr < arr + length; ++ptr) {
std::cout << *ptr << " ";
}
4. 作为函数参数
数组会退化为指针,因此必须同时传递大小信息。
// 函数声明(三种等价形式)
void printArray(int* arr, size_t size);
void printArray(int arr[], size_t size); // 更清晰的写法
void printArray(int arr[5], size_t size); // 这里的[5]会被编译器忽略,没有意义
// 函数定义
void printArray(int arr[], size_t size) {
for (size_t i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
}
// 调用
int myArr[] = {1, 2, 3, 4, 5};
printArray(myArr, sizeof(myArr) / sizeof(myArr[0]));
5. 多维静态数组
// 定义一个 2x3 的二维数组
int matrix[2][3] = { {1, 2, 3},
{4, 5, 6} };
// 访问元素
int value = matrix[0][1]; // 2
// 遍历
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}
6. 优缺点
- 优点:极致的性能。在栈上分配,分配和释放速度快,无内存碎片。
- 缺点:
- 大小固定,无法动态改变。
- 作为函数参数传递时,会丢失大小信息(退化为指针)。
- 越界访问不安全,会导致未定义行为。
Python“静态数组”
Python 没有内置的静态数组,我们使用 array模块或 numpy库来实现类似功能。
1. 使用 array模块
array模块提供了一种高效的数组结构,存储相同类型的基本数值类型(如 int, float)。
用法:
import array
# 1. 创建数组:array.typecode(iterable)
# 常用类型码:'i'(有符号int),'f'(float),'d'(double)
arr = array.array('i', [1, 2, 3, 4, 5]) # 创建一个整型数组
# arr = array.array('i’) # 创建一个空数组
# 2. 访问元素(与列表类似,支持索引和切片)
print(arr[0]) # 输出: 1
print(arr[1:3]) # 输出: array('i', [2, 3])
# 3. 修改元素
arr[0] = 10
# 4. 添加元素(动态扩容,因此不是“静态”的)
arr.append(6) # 在末尾添加一个元素
arr.extend([7, 8]) # 在末尾扩展多个元素
# 5. 遍历数组
for num in arr:
print(num)
# 6. 常用方法
arr.insert(1, 99) # 在索引1处插入99
arr.pop() # 移除并返回最后一个元素
arr.remove(99) # 移除第一个出现的99
arr.tolist() # 将数组转换为普通列表
特点:
- 比
list更节省内存,因为元素类型固定。 - 支持动态扩容,行为上更像 C++ 的
std::vector,而不是静态数组。 - 只能存储基本数值类型,不能存储任意对象(如字符串、其他列表)。
2. 使用 NumPy库
NumPy的 ndarray是 Python 科学计算的基石,提供了真正的固定类型、多维的高性能数组。
用法:
import numpy as np
# 1. 创建数组
arr = np.array([1, 2, 3, 4, 5]) # 一维数组
matrix = np.array([[1, 2, 3], [4, 5, 6]]) # 二维数组
# 2. 创建具有特定大小和初始值的“静态”数组(更接近C++概念)
zeros = np.zeros(5) # 创建长度为5,全部为0的数组
ones = np.ones((2, 3)) # 创建2x3,全部为1的数组
full = np.full(4, 7) # 创建长度为4,全部为7的数组
empty = np.empty(3) # 创建长度为3的未初始化数组(速度快)
# 3. 访问和修改(功能极其强大)
print(arr[0]) # 索引
print(matrix[0, 1]) # 多维索引
print(arr[1:4]) # 切片
arr[0] = 10 # 修改
matrix[:, 1] = [0, 0] # 修改第二列的所有行
# 4. 数组属性
print(arr.shape) # 数组的形状 (5,)
print(arr.dtype) # 数组的数据类型 int32(或int64)
print(arr.size) # 数组元素总数 5
# 5. 数组操作(矢量化运算,无需循环)
arr_squared = arr ** 2 # 对每个元素平方
arr_sum = arr + np.array([10, 20, 30, 40, 50]) # 数组相加
# 6. 重塑数组大小(返回新视图,不改变数据总数)
new_arr = arr.reshape(1, 5) # 将一维数组重塑为1x5的二维数组
特点:
- 性能极高:底层由 C 实现,操作是矢量化的,避免了 Python 循环的开销。
- 功能丰富:支持复杂的切片、索引、广播、线性代数运算等。
- 大小固定?:
ndarray在创建后大小是固定的,reshape操作返回的是新视图而非修改原大小。要改变大小需使用np.resize或np.append(这些操作会创建新数组)。
五、元组
核心概念总结
| 特性 | Python 元组 | C++ 元组 (std::tuple) |
|---|---|---|
| 本质 | 内置的、不可变的序列类型 | 标准库模板,编译期确定的静态类型集合 |
| 可变性 | 不可变 | 元组本身不可变,但元素若为引用或指针则可间接修改 |
| 元素类型 | 可以不同,动态类型(但类型是运行时信息) | 可以不同,静态类型(类型在编译时确定) |
| 访问方式 | 索引 t[i], 切片 t[start:stop], 迭代, 解包 | std::get<I>(t)或 std::get<T>(t)(编译时索引) |
| 内存与性能 | 动态分配,运行时开销 | 编译期确定布局,通常无运行时开销(零成本抽象) |
| 主要用途 | 轻量级数据结构、多返回值、函数参数、字典键 | 多返回值、异构数据聚合、模拟多返回值、与库交互 |
C++ 元组 (std::tuple)
C++ 的 std::tuple是标准库 (<tuple>) 提供的一个类模板,用于将固定数量的、类型可能不同的元素打包成一个单一对象。它是一个编译期概念。
1. 包含头文件和创建元组
#include <tuple>
#include <string>
#include <iostream>
// 使用 std::make_tuple (自动推导类型)
auto myTuple = std::make_tuple(10, "Hello", 3.14); // tuple<int, const char*, double>
// 使用 std::tuple 构造函数 (显式指定类型)
std::tuple<int, std::string, double> myTuple2(10, "Hello", 3.14);
// 创建空元组
std::tuple<> emptyTuple;
// C++17 起支持的类模板参数推导
std::tuple deducedTuple(10, std::string("Hello"), 3.14); // tuple<int, std::string, double>
2. 访问元素
使用 std::get,参数是编译期常量的索引或类型。
auto myTuple = std::make_tuple(10, "Hello", 3.14);
// 通过索引访问(索引从0开始)
int intVal = std::get<0>(myTuple);
const char* strVal = std::get<1>(myTuple);
double doubleVal = std::get<2>(myTuple);
// 通过类型访问(类型必须唯一,否则编译错误)
int intVal2 = std::get<int>(myTuple);
// auto strVal2 = std::get<const char*>(myTuple);
// 注意:如果多个元素类型相同,不能用此法访问第二个
3. 元组解包(Structured Bindings - C++17)
这是 C++ 中类似 Python 解包的强大特性。
#include <iostream>
#include <tuple>
std::tuple<int, std::string, double> getData() {
return std::make_tuple(42, "Alice", 87.5);
}
int main() {
// 传统方式,使用 std::tie (C++11)
int id;
std::string name;
double score;
std::tie(id, name, score) = getData(); // std::tie 创建了一个引用元组
// 现代方式,使用结构化绑定 (C++17)
auto [id2, name2, score2] = getData(); // 直接声明并初始化变量
std::cout << id2 << ", " << name2 << ", " << score2 << std::endl;
return 0;
}
4. 其他常用操作
auto t1 = std::make_tuple(1, 2, 3);
auto t2 = std::make_tuple(1, 2, 3);
// 比较操作 (==, !=, <, <=, >, >=)
// 按字典序比较元素
if (t1 == t2) {
std::cout << "Tuples are equal" << std::endl;
}
// 获取元组大小(元素个数)
size_t size = std::tuple_size<decltype(t1)>::value; // C++11/14
// 或者 C++17 起
size_t size17 = std::tuple_size_v<decltype(t1)>; // 3
// 获取元素类型
// std::tuple_element<0, decltype(t1)>::type 是 int
// 连接元组 (C++11 之后需要自己实现或使用 boost.hana)
// auto t3 = std::tuple_cat(t1, t2); // 结果是 (1,2,3,1,2,3)
5. 常用场景
-
函数返回多个值:这是最经典的用法。
std::tuple<bool, std::string, int> parseInput(const std::string& input) { // ... 解析逻辑 if (success) { return std::make_tuple(true, resultString, resultCode); } else { return std::make_tuple(false, "Error", -1); } } -
需要将多个数据作为单一实体传递或存储时,但又不想为此专门定义一个
struct。 -
与标准库其他组件交互:例如
std::map::insert返回一个tuple<iterator, bool>。
Python 元组
Python 的元组是一个不可变、有序的序列,可以包含任意类型的对象。
1. 创建元组
# 使用圆括号(推荐)
t1 = () # 空元组
t2 = (1, ) # 单元素元组(逗号必不可少)
t3 = (1, "hello", 3.14) # 多元素元组
t4 = 1, "hello", 3.14 # 括号可以省略,这同样创建一个元组
# 使用 tuple() 构造函数
t5 = tuple() # 空元组
t6 = tuple([1, 2, 3]) # 从可迭代对象(如列表)转换
2. 访问元素
t = ('a', 'b', 'c', 'd')
# 索引访问(从0开始)
first = t[0] # 'a'
last = t[-1] # 'd' (负索引表示从后向前)
# 切片(返回一个新元组)
sub_t = t[1:3] # ('b', 'c')
# 迭代
for item in t:
print(item)
3. 元组解包(Unpacking)
这是 Python 元组非常强大和常用的特性。
t = (1, 2, 3)
a, b, c = t # a=1, b=2, c=3
# 在函数参数中使用*
def sum(a, b):
return a + b
args = (5, 10)
result = sum(*args) # 等价于 sum(5, 10)
# 用于交换变量
a, b = b, a # 无需临时变量
# 使用 * 收集多余元素
a, b, *rest = (1, 2, 3, 4, 5) # a=1, b=2, rest=[3,4,5]
a, *middle, b = (1, 2, 3, 4, 5) # a=1, middle=[2,3,4], b=5
4. 方法
由于不可变,元组的方法很少。
t = (1, 2, 2, 3)
# count(x) - 返回元素 x 在元组中出现的次数
count = t.count(2) # 2
# index(x) - 返回元素 x 在元组中第一次出现的索引
index = t.index(3) # 3
5. 常用场景
-
函数返回多个值:
return success, result, message -
作为字典的键:因为不可变且可哈希。
location_dict = {} point = (10, 20) # 一个坐标元组 location_dict[point] = "Home" -
格式化字符串:
print("Name: %s, Age: %d" % ("Alice", 25)) -
保护数据:防止意外修改序列内容。
六、无序关联容器
- 无序关联容器 基于哈希表实现,通过计算元素的哈希值来快速定位存储位置,从而实现平均情况下 O(1) 时间复杂度的查找、插入和删除操作。
- 它们的元素没有特定的顺序(与基于红黑树的有序容器如
std::map或std::set相对)。 - 性能可能在某些情况下(如哈希冲突严重时)退化为 O(n)。
C++无序关联容器
C++ 在 STL(标准模板库)中提供了 4 种主要的无序关联容器,定义在 <unordered_set>和 <unordered_map>头文件中。
1. std::unordered_set
- 含义: 一个唯一键的集合,每个元素的值就是键本身。
- 特性:
- 键是唯一的。
- 元素不可修改(修改会破坏哈希结构),但可以插入和删除。
- 查找效率极高。
主要用法:
#include <iostream>
#include <unordered_set>
int main() {
// 1. 初始化
std::unordered_set<int> mySet = {5, 2, 8, 2, 9}; // {5, 2, 8, 9} (重复的2被忽略)
// 2. 插入元素
mySet.insert(3);
mySet.emplace(1); // 原地构造,效率更高
// 3. 查找元素
auto it = mySet.find(2);
if (it != mySet.end()) {
std::cout << "Found: " << *it << std::endl;
}
// 4. 删除元素
mySet.erase(8); // 通过值删除
mySet.erase(it); // 通过迭代器删除
// 5. 遍历元素 (顺序是无序且不可预测的)
for (const int& num : mySet) {
std::cout << num << " ";
}
std::cout << std::endl;
// 6. 其他常用操作
std::cout << "Size: " << mySet.size() << std::endl;
std::cout << "Bucket count: " << mySet.bucket_count() << std::endl;
mySet.clear(); // 清空
return 0;
}
2. std::unordered_multiset
- 含义: 一个允许重复键的集合。
- 特性: 除了允许重复键,其他特性与
unordered_set相同。
主要用法:
#include <iostream>
#include <unordered_set>
int main() {
std::unordered_multiset<int> myMultiSet = {5, 2, 8, 2, 9}; // {5, 2, 8, 2, 9}
// 插入重复元素
myMultiSet.insert(2);
// 查找元素 2,会返回指向第一个匹配元素的迭代器
auto range = myMultiSet.equal_range(2); // 获取所有键为2的元素范围
for (auto it = range.first; it != range.second; ++it) {
std::cout << *it << " ";
}
// 输出: 2 2 2 (顺序依赖于实现,但所有2都会在一起)
return 0;
}
3. std::unordered_map
- 含义: 由键值对组成的集合,键是唯一的。
- 特性:
- 键是唯一的。
- 可以通过键快速查找、访问或插入对应的值。
主要用法:
#include <iostream>
#include <unordered_map>
#include <string>
int main() {
// 1. 初始化
std::unordered_map<std::string, int> myMap = {{"Alice", 25}, {"Bob", 30}};
// 2. 插入元素
myMap.insert({"Charlie", 28});
myMap["David"] = 22; // 最常用的插入或修改方式
// 3. 访问元素 (注意:使用 [] 访问不存在的键会创建它)
std::cout << "Bob‘s age: " << myMap["Bob"] << std::endl;
// 4. 修改元素
myMap["Alice"] = 26;
// 5. 查找元素 (推荐使用 find 而不是直接 [],避免意外创建)
auto it = myMap.find("Eve");
if (it == myMap.end()) {
std::cout << "Eve not found." << std::endl;
}
// 6. 遍历元素 (键值对是 std::pair<const Key, T>)
for (const auto& pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// C++17 结构化绑定
for (const auto& [key, value] : myMap) {
std::cout << key << ": " << value << std::endl;
}
// 7. 删除元素
myMap.erase("David");
return 0;
}
4. std::unordered_multimap
- 含义: 一个允许重复键的映射。
- 特性: 除了允许重复键,其他特性与
unordered_map相同。没有operator[],因为一个键可能对应多个值。
主要用法:
#include <iostream>
#include <unordered_map>
#include <string>
int main() {
std::unordered_multimap<std::string, std::string> myMultiMap;
// 插入元素 (允许重复键)
myMultiMap.insert({"fruit", "apple"});
myMultiMap.insert({"fruit", "banana"});
myMultiMap.insert({"animal", "dog"});
// 查找元素
auto range = myMultiMap.equal_range("fruit");
for (auto it = range.first; it != range.second; ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
// 输出:
// fruit: apple
// fruit: banana
return 0;
}
5. 高级用法:自定义哈希和比较函数
当键是自定义类型时,需要提供哈希函数和相等比较函数。
#include <iostream>
#include <unordered_set>
struct Person {
std::string name;
int age;
// 重载 == 运算符用于比较
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
};
// 自定义哈希函数 (可以定义为函数对象)
struct PersonHash {
std::size_t operator()(const Person& p) const {
// 结合 name 和 age 的哈希值
return std::hash<std::string>()(p.name) ^ (std::hash<int>()(p.age) << 1);
}
};
int main() {
// 使用自定义哈希和比较类型
std::unordered_set<Person, PersonHash> personSet;
personSet.insert({"Alice", 25});
personMap.insert({"Bob", 30});
// 如果 Person 没有重载 ==,则需要提供自定义比较类
// 作为模板的第三个参数,例如:
// std::unordered_set<Person, PersonHash, PersonEqual> personSet;
return 0;
}
Python无序关联容器
Python 内置了两种主要的无序关联容器:dict和 set。在 Python 3.6+ 中,dict的实现保证了插入顺序,但从语言定义上,它们仍然被称为“无序集合”,因为你不应依赖其顺序进行逻辑计算(尽管在实践中现在可以)。set仍然是无序的。
1. set
- 含义: 一个唯一且不可变对象的无序集合。
- 特性:
- 元素必须是可哈希的(通常是不可变类型,如数字、字符串、元组)。
- 元素是唯一的。
- 可变,可以增删元素。
主要用法:
# 1. 初始化
my_set = {5, 2, 8, 2, 9} # {8, 9, 2, 5} (重复的2被忽略,顺序随机)
my_set = set([5, 2, 8, 2, 9]) # 从列表创建
# 2. 添加元素
my_set.add(3)
my_set.update([1, 4, 6]) # 添加多个元素
# 3. 查找元素
# 集合没有“获取”操作,通常用 `in` 检查成员关系
if 2 in my_set:
print(“2 is in the set”)
# 4. 删除元素
my_set.remove(8) # 如果元素不存在,会引发 KeyError
my_set.discard(7) # 如果元素不存在,不会报错
popped_element = my_set.pop() # 随机移除并返回一个元素(因为无序)
# 5. 遍历元素 (顺序是无序且不可预测的)
for element in my_set:
print(element)
# 6. 其他常用操作
print(len(my_set)) # 大小
my_set.clear() # 清空
# 集合运算
other_set = {1, 2, 3, 10}
print(my_set | other_set) # 并集 union()
print(my_set & other_set) # 交集 intersection()
print(my_set - other_set) # 差集 difference()
2. dict
- 含义: 键值对映射。
- 特性:
- 键必须是唯一的、可哈希的。
- Python 3.7+ 官方保证,3.6+ 实现细节:字典会维护元素的插入顺序。
- 值可以是任意类型。
主要用法:
# 1. 初始化
my_dict = {"Alice": 25, "Bob": 30}
my_dict = dict(Alice=25, Bob=30)
# 2. 插入/更新元素
my_dict["Charlie"] = 28
my_dict.update({"David": 22, "Alice": 26}) # 更新Alice,插入David
# 3. 访问元素
print(my_dict["Bob"]) # 30
# 使用 get() 避免 KeyError
print(my_dict.get("Eve")) # 返回 None
print(my_dict.get("Eve", "N/A")) # 返回默认值 “N/A”
# 4. 删除元素
del my_dict["David"]
popped_value = my_dict.pop("Bob") # 移除并返回值
my_dict.popitem() # 移除并返回最后插入的键值对 (LIFO,因为有序)
# 5. 遍历元素
for key in my_dict: # 遍历键
print(key, my_dict[key])
for key, value in my_dict.items(): # 遍历键值对 (推荐)
print(key, value)
for value in my_dict.values(): # 遍历值
print(value)
# 6. 其他常用操作
print("Alice" in my_dict) # 检查键是否存在
print(len(my_dict)) # 获取元素数量
# 字典推导式
squares = {x: x*x for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
3. collections.defaultdict
- 含义:
dict的一个子类,它提供了默认值功能,避免键不存在时的KeyError。
from collections import defaultdict
# 初始化时提供一个默认工厂函数
word_count = defaultdict(int) # int() 的默认值是 0
for word in ['a', 'b', 'a', 'c']:
word_count[word] += 1 # 如果 word 不存在,会自动创建并赋值为0
print(word_count) # defaultdict(<class ‘int’>, {‘a’: 2, ‘b’: 1, ‘c’: 1})
# 使用 list 作为工厂
list_dict = defaultdict(list)
list_dict[“fruits”].append(“apple”)
七、优先队列
优先队列是一种抽象数据类型,它类似于常规的队列或栈,但每个元素都关联一个“优先级”。出队操作总是移除优先级最高(或最低)的元素,而不是像普通队列那样按先进先出的顺序。
其底层通常由 堆(Heap) 这种数据结构实现,因此插入和删除操作的时间复杂度均为 O(log n),获取最值元素的时间复杂度为 O(1)。
C++优先队列
在 C++ 中,优先队列是标准模板库 (STL) 中 queue头文件下的一个容器适配器 (std::priority_queue)。
1. 头文件
#include <queue>
2. 基本声明
std::priority_queue<T> pq; // 默认最大堆,优先级最高的(值最大的)在顶部
std::priority_queue<T, std::vector<T>, std::greater<T>> pq; // 最小堆,值最小的在顶部
T: 元素类型,如int,std::string等。Container: 底层容器,默认为std::vector<T>。你也可以使用std::deque<T>,但通常不需要。Compare: 比较函数/函数子,用于定义优先级顺序。默认为std::less<T>(最大堆)。
3. 主要用法和成员函数
| 方法 | 功能描述 | 时间复杂度 |
|---|---|---|
push(const T& value) | 将元素插入优先队列 | O(log n) |
emplace(Args&&... args) | 在队列中原地构造一个新元素(避免复制) | O(log n) |
pop() | 移除顶部元素(优先级最高的元素) | O(log n) |
top() | 返回顶部元素的常量引用(不移除它) | O(1) |
empty() | 检查队列是否为空 | O(1) |
size() | 返回队列中元素的数量 | O(1) |
注意:std::priority_queue没有 clear()方法。如果需要清空,可以用循环 pop或直接重新赋值一个空的队列。
// 清空队列的方法
pq = std::priority_queue<T>(); // 方法一:赋值为一个新的空队列
while (!pq.empty()) pq.pop(); // 方法二:循环弹出
4. 默认最大堆(降序)
#include <iostream>
#include <queue>
int main() {
std::priority_queue<int> pq; // 最大堆
pq.push(30);
pq.push(10);
pq.push(50);
pq.push(20);
while (!pq.empty()) {
// 输出顺序将是 50, 30, 20, 10
std::cout << pq.top() << " ";
pq.pop();
}
return 0;
}
5. 最小堆(升序)
#include <iostream>
#include <queue>
#include <vector>
#include <functional> // 对于 std::greater
int main() {
// 必须显式提供底层容器和比较器
std::priority_queue<int, std::vector<int>, std::greater<int>> pq;
pq.push(30);
pq.push(10);
pq.push(50);
pq.push(20);
while (!pq.empty()) {
// 输出顺序将是 10, 20, 30, 50
std::cout << pq.top() << " ";
pq.pop();
}
return 0;
}
6. 自定义比较器(存放自定义对象)
这是最灵活和强大的用法。你需要定义一个函数子(Functor)或使用 Lambda 表达式。
#include <iostream>
#include <queue>
#include <string>
// 自定义类
struct Task {
int priority;
std::string description;
Task(int p, const std::string& d) : priority(p), description(d) {}
};
// 方法一:自定义比较函数子(Functor)
// 我们希望优先级数字小的任务反而优先级高(最小堆)
struct CompareTask {
bool operator()(const Task& a, const Task& b) {
// 返回 true 表示 a 的优先级应该低于 b(即 a 应该在 b 之后)
return a.priority > b.priority;
}
};
int main() {
// 使用自定义比较器
std::priority_queue<Task, std::vector<Task>, CompareTask> pq;
pq.push(Task(3, "Low priority task"));
pq.push(Task(1, "High priority task"));
pq.push(Task(2, "Medium priority task"));
while (!pq.empty()) {
Task task = pq.top();
std::cout << task.priority << ": " << task.description << std::endl;
pq.pop();
}
// 输出:
// 1: High priority task
// 2: Medium priority task
// 3: Low priority task
return 0;
}
使用 Lambda 表达式作为比较器(C++11 及以上)
这种方法需要你在声明时指定比较器的类型,并传入 Lambda。
auto cmp = [](const Task& a, const Task& b) { return a.priority > b.priority; };
// 注意:decltype(cmp) 用于获取 Lambda 的类型
std::priority_queue<Task, std::vector<Task>, decltype(cmp)> pq(cmp);
Python优先队列
Python 的标准库提供了 heapq模块,但它实现的是最小堆。所有功能都是基于列表 (list) 的模块级函数。
1. 头文件(模块)
import heapq
2. 主要函数
| 函数 | 功能描述 | 时间复杂度 |
|---|---|---|
heapq.heappush(heap, item) | 将元素 item推入堆 heap | O(log n) |
heapq.heappop(heap) | 弹出并返回堆中的最小元素 | O(log n) |
heapq.heappushpop(heap, item) | 先 push item,再 pop 最小元素。比分开调用效率高 | O(log n) |
heapq.heapreplace(heap, item) | 先 pop 最小元素,再 push item。比分开调用效率高 | O(log n) |
heapq.heapify(x) | 将常规列表 x在原地转换为堆(最小堆) | O(n) |
heapq.nlargest(n, iterable) | 从可迭代对象中返回前 n个最大的元素 | O(log n) |
heapq.nsmallest(n, iterable) | 从可迭代对象中返回前 n个最小的元素 | O(log n) |
3. 基本最小堆用法
import heapq
# 创建一个空列表来表示堆
min_heap = []
data = [30, 10, 50, 20]
# 将数据推入堆
for item in data:
heapq.heappush(min_heap, item)
# 或者,也可以直接使用 heapify 转换列表
# heapq.heapify(data) # 此时 data 就变成了堆
# min_heap = data
while min_heap:
# 按顺序弹出最小元素:10, 20, 30, 50
smallest = heapq.heappop(min_heap)
print(smallest, end=' ')
4. 实现最大堆
Python 没有直接的最大堆。通用技巧是存入元素的负数,这样最小的负数就代表了原值中最大的数。
import heapq
# 实现最大堆:存入和取出时都对值取反
max_heap = []
data = [30, 10, 50, 20]
for item in data:
heapq.heappush(max_heap, -item) # 存入负值
while max_heap:
# 弹出负值的最小项(即原值的最大项),再取反得到原值
largest = -heapq.heappop(max_heap)
print(largest, end=' ') # 输出:50, 30, 20, 10
5. 存放元组(实现多级比较,非常常用)
这是 Python 中优先队列最强大的功能,常用于实现按优先级排序。元组按第一个元素进行比较,如果相同则比较第二个。
import heapq
# 模拟任务,格式:(优先级, 描述)
heap = []
heapq.heappush(heap, (3, "Low priority task"))
heapq.heappush(heap, (1, "High priority task"))
heapq.heappush(heap, (2, "Medium priority task"))
while heap:
priority, description = heapq.heappop(heap)
print(f"{priority}: {description}")
# 输出:
# 1: High priority task
# 2: Medium priority task
# 3: Low priority task
如果你想实现最大堆优先级,只需对优先级取负即可:
heapq.heappush(heap, (-priority, description))
6. 自定义类的比较
需要实现 __lt__(小于) 魔术方法,因为 heapq在比较时依赖于它。
import heapq
class Task:
def __init__(self, priority, description):
self.priority = priority
self.description = description
# 定义小于操作符,使类实例可以相互比较
# 这里定义成最小堆的逻辑:优先级数字小的任务更小
def __lt__(self, other):
return self.priority < other.priority
# 使用方法与普通类型一样
heap = []
heapq.heappush(heap, Task(3, "Low"))
heapq.heappush(heap, Task(1, "High"))
heapq.heappush(heap, Task(2, "Medium"))
while heap:
task = heapq.heappop(heap)
print(f"{task.priority}: {task.description}")
八、栈
栈(Stack)是一种遵循 LIFO(Last In, First Out,后进先出) 原则的线性数据结构。这意味着最后一个被添加的元素将会是第一个被移除的元素。其基本操作通常命名为:
- Push(压栈): 将一个元素添加到栈顶。
- Pop(出栈): 移除并返回栈顶的元素。
- Peek / Top(查看栈顶): 返回栈顶的元素但不移除它。
- isEmpty(判空): 检查栈是否为空。
C++栈
在 C++ 中,最标准、最常用的栈实现是标准模板库(STL)中的 std::stack。
1. 实现方式
#include <stack>
std::stack是一个容器适配器(Container Adapter)。这意味着它基于其他底层容器(如 std::deque, std::list, std::vector)来提供栈的接口。默认情况下,它的底层容器是 std::deque。
2. 定义与初始化
#include <iostream>
#include <stack>
#include <vector>
#include <list>
int main() {
// 1. 使用默认底层容器(deque)
std::stack<int> stack1;
// 2. 使用特定的底层容器(例如 vector)
std::stack<int, std::vector<int>> stack2;
// 3. 使用 list 作为底层容器
std::stack<int, std::list<int>> stack3;
// 4. 用一个已存在的容器初始化(C++11 起)
std::vector<int> vec = {1, 2, 3};
// 注意:这会按顺序 {1, 2, 3} 压栈,栈顶是 3
std::stack<int, std::vector<int>> stack4(vec);
return 0;
}
3. 核心操作
std::stack<int> s;
// Push - 压栈
s.push(10);
s.push(20);
s.push(30); // 栈现在为:顶 [30, 20, 10] 底
// Top - 查看栈顶元素(不删除)
std::cout << "Top element: " << s.top() << std::endl; // 输出 30
// Pop - 弹出栈顶元素(不返回它,仅是删除)
s.pop(); // 移除 30
std::cout << "Top after pop: " << s.top() << std::endl; // 输出 20
// Empty - 检查栈是否为空
if (s.empty()) {
std::cout << "Stack is empty" << std::endl;
} else {
std::cout << "Stack is not empty" << std::endl; // 会执行这条
}
// Size - 获取栈中元素个数
std::cout << "Stack size: " << s.size() << std::endl; // 输出 2
4. 重要特性与注意事项
pop()不返回元素:这是 C++ STL 设计的一个特点,为了保证异常安全。你需要先用top()获取元素,然后再pop()移除它。- 底层容器访问受限:你无法直接遍历
std::stack的所有元素(因为这会违反栈的 LIFO 原则)。如果需要遍历,你可能选错了数据结构,或者需要将栈元素弹出到另一个临时栈中。 - 性能:所有操作(push, pop, top, empty)的时间复杂度都是常数时间 O(1)。
Python栈
Python 没有专门的、独立的“栈”类。通常使用列表(list)来模拟栈的行为,或者使用 collections.deque来获得更高效的实现。
1. 使用 List 模拟栈
a. 实现方式
Python 的 list提供了 append()和 pop()方法,可以非常直观地模拟栈。
b. 用法详列
# 初始化一个空栈
stack = []
# Push - 压栈,使用 append()
stack.append(10)
stack.append(20)
stack.append(30) # 栈现在为:底 [10, 20, 30] 顶
print("Stack after pushes:", stack) # 输出 [10, 20, 30]
# Pop - 弹出栈顶元素(会返回该元素)
top_element = stack.pop()
print("Popped element:", top_element) # 输出 30
print("Stack after pop:", stack) # 输出 [10, 20]
# Peek / Top - 查看栈顶元素(不删除)
if stack: # 先判断栈是否非空,避免索引错误
print("Top element:", stack[-1]) # 输出 20
# Empty - 检查栈是否为空
if not stack:
print("Stack is empty")
else:
print("Stack is not empty") # 会执行这条
# Size - 获取栈中元素个数
print("Stack size:", len(stack)) # 输出 2
c. 重要特性与注意事项
- 性能问题:使用
list模拟栈时,append()和pop()在尾部操作的平均时间复杂度是 O(1)。但在尾部插入时,如果底层内存需要重新分配,可能会触发最坏情况 O(n),但摊销(amortized)后仍是 O(1)。 - 不要在头部操作:绝对不要使用
list.insert(0, item)或list.pop(0)来模拟栈,这些操作的时间复杂度是 O(n),效率极低。
2. 使用 collections.deque实现栈(推荐)
deque(双端队列)是一个双向链表,在两端进行添加或删除操作都具有 O(1) 的性能,是实现栈和队列的理想选择。
a. 用法
from collections import deque
# 初始化一个栈(deque)
stack = deque()
# Push - 压栈,使用 append()
stack.append(10)
stack.append(20)
stack.append(30)
# Pop - 弹出栈顶元素,使用 pop()
top_element = stack.pop() # 30
# Top - 查看栈顶元素
if stack:
print(stack[-1]) # 20
# 其他操作如判空、大小与 list 相同
print(len(stack) == 0) # 判空
print(len(stack)) # 大小
b. 为什么推荐 deque?
- 性能:
deque的append()和pop()操作保证是稳定的 O(1) 时间复杂度,没有list可能触发的内存重新分配问题。 - 明确意图:当你使用
deque并只在一端操作时,代码的阅读者能清晰地明白你在使用一个栈(或队列),而不是一个通用的列表。
九、队列
C++ 队列
C++ 标准库提供了 std::queue容器适配器,它通常基于 std::deque或 std::list实现,提供了严格的 FIFO(先进先出)接口。
1.核心头文件和类型
#include <queue> // 主要头文件
#include <deque> // 底层容器
#include <list> // 可选的底层容器
std::queue<int> q; // 默认基于 deque
std::queue<int, std::list<int>> q_list; // 指定基于 list
2. 详细用法列表
| 操作 | 函数 | 描述 | 时间复杂度 |
|---|---|---|---|
| 入队 | push(const T& value) | 将元素添加到队列末尾 | O(1) |
| 出队 | pop() | 移除队首元素。注意:不返回该元素! | O(1) |
| 访问队首 | front() | 返回队首元素的引用(可读可写) | O(1) |
| 访问队尾 | back() | 返回队尾元素的引用(可读可写) | O(1) |
| 判空 | empty() | 检查队列是否为空 | O(1) |
| 获取大小 | size() | 返回队列中元素的数量 | O(1) |
| 构造 | queue<T> q; | 构造一个空队列 | - |
| 构造 | queue<T> q(other_q); | 拷贝构造 | O(n) |
| 移动构造 | queue<T> q(move(other_q)); | 移动构造(C++11) | O(1) |
| 赋值 | q1 = q2 | 拷贝赋值 | O(n) |
| 移动赋值 | q1 = move(q2) | 移动赋值(C++11) | O(1) |
| 交换 | swap(q1, q2) | 交换两个队列的内容 | O(1) |
3. 重要注意事项
-
pop()不返回值:这是 C++std::queue与许多其他语言队列最大的区别。你必须先使用front()获取队首元素,然后再pop()将其移除,以避免元素在拷贝或抛出异常时丢失。std::queue<int> q; q.push(1); q.push(2); // 正确用法 int front_element = q.front(); // 获取队首元素 1 q.pop(); // 移除队首元素 // 错误!pop() 返回 void,不能这样用 // int element = q.pop(); -
底层容器:
std::queue是一个容器适配器。你可以指定第二个模板参数来改变其底层容器(必须提供back(),push_back(),pop_front(),empty(),size()等接口)。#include <list> std::queue<int, std::list<int>> q; // 基于 list 的队列 -
线程安全:
std::queue本身不是线程安全的。如果需要在多线程环境中使用,需要自行加锁(如std::mutex),或者使用像tbb::concurrent_queue这样的第三方并发容器。
4. 示例代码
#include <iostream>
#include <queue>
int main() {
std::queue<std::string> customer_queue;
// 入队
customer_queue.push("Alice");
customer_queue.push("Bob");
customer_queue.push("Charlie");
// 查看队首和队尾
std::cout << "Front: " << customer_queue.front() << std::endl; // Alice
std::cout << "Back: " << customer_queue.back() << std::endl; // Charlie
// 遍历并出队(遍历后会清空队列)
while (!customer_queue.empty()) {
std::cout << "Processing: " << customer_queue.front() << std::endl;
customer_queue.pop();
}
// 现在 size() 为 0
return 0;
}
Python队列
Python 的队列实现更加多样化,根据不同的使用场景,主要有以下三种:
1. collections.deque(双端队列)
这是最常用、最通用的队列实现,位于标准库的 collections模块。它是一个双向队列,在两端都能实现 O(1) 时间复杂度的添加和删除操作。
核心头文件
from collections import deque
详细用法列表
| 操作 | 方法 | 描述 | 时间复杂度 |
|---|---|---|---|
| 入队 (右) | append(item) | 将元素添加到队列右端(尾部) | O(1) |
| 出队 (左) | popleft() | 移除并返回队首(左端)元素 | O(1) |
| 入队 (左) | appendleft(item) | 将元素添加到队列左端(头部) | O(1) |
| 出队 (右) | pop() | 移除并返回队尾(右端)元素 | O(1) |
| 判空 | if not dq: | 借助 len()或直接判断布尔值 | O(1) |
| 获取大小 | len(dq) | 返回队列中元素的数量 | O(1) |
| 访问队首 | dq[0] | 访问但不移除队首元素 | O(1) |
| 访问队尾 | dq[-1] | 访问但不移除队尾元素 | O(1) |
| 构造 | dq = deque() | 构造一个空队列 | - |
| 构造 | dq = deque(iterable) | 从可迭代对象(如列表)构造队列 | O(n) |
| 扩展队列 | extend(iterable) | 在右侧批量添加元素 | O(k) |
| 扩展队列 | extendleft(iterable) | 在左侧批量添加元素(注意顺序) | O(k) |
| 旋转 | rotate(n=1) | 将队列向右旋转 n 步(负数为向左) | O(k) |
| 清除 | clear() | 移除所有元素 | O(n) |
示例代码
from collections import deque
# 创建队列
q = deque(['Alice', 'Bob'])
# 或者 q = deque(); q.append('Alice')
# 入队
q.append('Charlie') # 右端入队: deque(['Alice', 'Bob', 'Charlie'])
q.appendleft('Zoe') # 左端入队: deque(['Zoe', 'Alice', 'Bob', 'Charlie'])
# 访问
print(q[0]) # 队首: 'Zoe'
print(q[-1]) # 队尾: 'Charlie'
# 出队
front = q.popleft() # 移除并返回 'Zoe'
rear = q.pop() # 移除并返回 'Charlie'
# 遍历(不会清空队列)
for person in q:
print(person) # 现在队列里是 ['Alice', 'Bob']
2. queue.Queue(同步队列)
这个模块专门为多线程编程设计。它提供了线程安全的 FIFO 实现,支持基本的阻塞操作,用于在生产者和消费者线程之间安全地传递消息。
核心头文件
import queue
详细用法列表
| 操作 | 方法 | 描述 |
|---|---|---|
| 入队 | put(item, block=True, timeout=None) | 放入元素。可选阻塞和超时。 |
| 出队 | get(block=True, timeout=None) | 移除并返回一个元素。可选阻塞和超时。 |
| 判空 | empty() | 检查队列是否为空(不可靠,因多线程环境瞬息万变) |
| 获取大小 | qsize() | 返回队列的大致大小(同样不可靠) |
| 任务完成 | task_done() | 消费者线程告知队列一个任务已完成 |
| 等待完成 | join() | 阻塞主线程,直到队列中所有任务被处理完毕 |
示例代码 (多线程)
import queue
import threading
import time
def worker(q):
while True:
item = q.get() # 阻塞获取任务
print(f"Processing: {item}")
time.sleep(1)
q.task_done() # 告知任务完成
# 创建线程安全队列
task_queue = queue.Queue()
# 启动工作线程
threading.Thread(target=worker, args=(task_queue,), daemon=True).start()
# 主线程作为生产者
for i in range(5):
task_queue.put(f"Task {i}")
task_queue.join() # 等待所有任务完成
print("All tasks done!")
3. queue.LifoQueue和 queue.PriorityQueue
queue.LifoQueue:一个线程安全的后进先出队列(栈)。queue.PriorityQueue:一个线程安全的优先级队列。元素通常为(priority, data)元组,优先级数字越小越先出队。
5万+

被折叠的 条评论
为什么被折叠?



