奇牛Qt06 线性表容器——奇牛+gemini
奇牛Qt06 线性表容器——奇牛+gemini
1. 线性表与 Qt 容器概述
1.1 什么是线性表
线性表是 $n$ 个具有相同特性的数据元素的有限序列。其结构特点为:
- 存在唯一的开始元素(没有前驱)和唯一的终端元素(没有后继)。
- 除了第一个和最后一个数据元素之外,其它数据元素都有且仅有一个直接前驱和一个直接后继。
1.2 Qt 容器的设计与选型
Qt 对 C++ 标准模板库(STL)的容器进行了进一步优化和封装,构建了一套更符合 Qt 生态、支持隐式共享(写时复制)的容器类。
- 主流顺序容器:
QVector、QList、QLinkedList。 - 派生特殊容器:
QStack(栈)、QQueue(队列)。
1.3 存储数据的基本要求
存入 Qt 容器中的元素类型 T 必须能够进行赋值操作。即该类型必须提供:
- 默认构造函数(Default Constructor)
- 拷贝/复制构造函数(Copy Constructor)
- 赋值运算符重载(Assignment Operator)
支持的类型:基础数据类型(int, double 等)、标准 Qt 类型(QString, QDate, QTime 等)。
不可存储的类型:QObject 及其子类(如 QWidget),因为 QObject 禁用了拷贝构造和赋值运算符。
QVector<QWidget> vec; // 错误!编译失败
解决方案:存储对应类型的指针。
QVector<QWidget*> vec; // 正确!指针允许赋值和拷贝
2. 核心顺序容器
2.1 QVector —— 变长数组
QVector<T> 在底层实现上对应线性表的顺序存储。
2.1.1 底层内存结构
从其底层数据结构(如图所示)来看:
QVector<T> -> [指针 d] -> 指向 QVectorTypedData<T>
↳ [ ref | alloc | size | flags ] [ array[0] ] ... [ array[alloc-1] ]
- 连续内存:所有元素在物理内存中是连续相邻存储的。
- 管理开销:包含引用计数(
ref)、已分配空间(alloc)、当前元素个数(size)和标志位(flags)。 - 性能特点:支持 O(1) 随机访问。但在前部或中间位置插入/删除数据时,需要移动后续的所有元素,导致效率极低,时间复杂度为 O(n)。
- 适用场景:适用于需要频繁通过索引查询,而极少做中间插入、删除操作的业务。
2.1.2 常用 API 补充与示例
语法格式与函数原型:
void push_back(const T &value);—— 尾部插入。void push_front(const T &value);—— 头部插入。iterator insert(int i, const T &value);—— 在指定索引i处插入元素。void remove(int i);—— 删除指定索引i处的元素。T takeAt(int i);—— 删除指定索引i处的元素并将其返回。bool removeOne(const T &value);—— 删除第一个匹配到value的元素,成功返回true。int indexOf(const T &value, int from = 0) const;—— 从from开始查找元素,返回第一次匹配的索引。bool contains(const T &value) const;—— 判断是否包含某元素。int count() const;—— 返回元素总数,等价于size()。void replace(int i, const T &value);—— 将索引i处的元素替换为value。void swapItemsAt(int i, int j);—— 交换索引i和j的元素。void move(int from, int to);—— 将元素从索引from移动到索引to。
QVector<QString> data;
// 1. 插入操作
data << "100" << "200" << "300"; // 流式尾部插入
data.push_back("400"); // 尾部追加 -> ["100", "200", "300", "400"]
data.push_front("90"); // 头部插入 -> ["90", "100", "200", "300", "400"]
data.insert(2, "999"); // 指定位置插入 -> ["90", "100", "999", "200", "300", "400"]
// 2. 删除操作
data.remove(2); // 删除索引2的"999"
QString ret = data.takeAt(2); // 删除并返回索引2的"200",此时 ret = "200"
data.removeOne("400"); // 匹配并删除 "400"
// 3. 查询与修改
QString item = data[1]; // 通过下标访问
int index = data.indexOf("100"); // 获取索引
bool find = data.contains("100");// 是否包含
int total = data.count(); // 元素总数
data.replace(1, "Rock"); // 将索引1修改为 "Rock"
data[1] = "888"; // 下标法修改
data.swapItemsAt(0, 2); // 交换元素位置
data.move(0, 2); // 将0号元素移动到2号位置,其他顺移
2.1.3 二维容器嵌套
Qt 容器支持嵌套,常用于表达矩阵或二维网格:
QVector<QVector<int>> matrix;
// 填充 3x4 矩阵
for (int i = 0; i < 3; i++)
{
QVector<int> line;
for (int j = 0; j < 4; j++)
{
line.push_back(i * 4 + j);
}
matrix.push_back(line);
}
// 遍历输出
for (int i = 0; i < matrix.count(); i++) {
for (int j = 0; j < matrix[i].count(); j++)
{
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}
2.2 QList —— 指针数组(高效的多用途容器)
在 STL 中,std::list 底层通常是双向链表(如结构图所示,节点包含 data、pre 和 next 指针,支持快速的在任意位置 insert、pop 和 push)。
然而,Qt5的QList<T> 的底层实现并不是传统的链表。
Qt6中的
QList仅是QVector的别名,不再有实际意义上的QList。
2.2.1 Qt5QList底层内存结构
从它的底层设计图可以看出:
QList<T> -> [指针 d] -> 指向 QListData::Data
↳ [ ref | alloc | begin | end | flags ] [ void* array[0] ] ... [ void* array[alloc-1] ]
↓ ↓
[ 堆区 T ] [ 堆区 T ]
- 实现机制:
QList内部维护了一个指针数组(void* array[]),数组中的每个指针指向在堆(Heap)空间中独立分配的元素T。 - 优化特点:由于内部存储的是指针,在头部或尾部添加/删除元素时,只需要移动或扩容指针数组,不需要移动实际的复杂数据对象。因此,它兼具了类似
QVector的快速索引(O(1))和接近链表的两端高效插入性能。 - 使用原则:如果需要一个常规的顺序表,且不强制要求元素在内存中必须绝对连续存储,在 Qt 5 中应优先选择
QList。
2.2.2 常用 API 与示例
QList 包含与 QVector 高度相似的 API。其特有的头部操作函数原型为:
void prepend(const T &value);—— 头部插入(等价于push_front)。void removeAt(int i);—— 删除指定索引处的元素。
QList<QString> list;
list << "10" << "20";
list.append("30");
list.push_back("40"); // 尾部追加
list.push_front("9"); // 头部追加
list.prepend("8"); // 头部追加 -> ["8", "9", "10", "20", "30", "40"]
list.insert(2, "111"); // 中间插入
int idx = list.indexOf("10");
bool hasTen = list.contains("10");
QString deletedStr = list.takeAt(2); // 删除并返回
list.removeAt(0); // 删除0号元素
list.removeOne("30"); // 删除"30"
list[1] = "666";
list.replace(1, "999");
list.swapItemsAt(0, 2);
list.move(0, 2);
// 使用标准迭代器遍历
for (auto it = list.begin(); it != list.end(); it++)
{
qDebug() << *it;
}
补充:QStringList
QStringList继承自QList<QString>。它是专门用于处理字符串列表的子类,扩展了更多针对字符串的操作(如join()拼接、filter()过滤等),比直接使用QList<QString>更具可读性和便利性。
2.3 QLinkedList —— 纯正的双向链表
如果在底层需要一个真正的线性表链式存储结构,需使用 QLinkedList<T>。
2.3.1 底层内存结构
根据双向循环/带头结点的链表图:
QLinkedListData 维护着包含了 [ ref | size | flags ] 的头信息,
每个节点为 QLinkedListNode<T>,内部拥有前驱指针 p、后继指针 n 以及实际的数据 T t。
- 性能特点:不支持索引随机访问,查找特定元素的时间复杂度为 O(n)。但是,其在任意已知位置的插入和删除操作都是常数级时间,复杂度为 O(1)。
- 特有性质:迭代器直接指向表项实体本身。在执行插入或删除操作时,不会导致现有的其他迭代器失效。
2.3.2 常用 API 与示例
注意:由于没有索引,QLinkedList 没有重载 [] 运算符,也没有 removeAt(int),只能通过迭代器或专用函数操作。
void removeFirst();—— 删除头节点。void removeLast();—— 删除尾节点。
// 需要引入头文件 #include <QLinkedList>
QLinkedList<QString> linkedList;
linkedList << "100" << "200";
linkedList.append("300");
linkedList.push_back("400");
linkedList.push_front("90"); // ["90", "100", "200", "300", "400"]
// 必须使用迭代器遍历
for (auto it = linkedList.begin(); it != linkedList.end(); it++)
{
qDebug() << *it;
}
linkedList.removeFirst(); // 删除 "90"
linkedList.removeLast(); // 删除 "400"
linkedList.removeOne("300"); // 删除 "300"
3. 性能深度对比与选型原则
3.1 性能复杂度对比表
| 容器类 | 用索引查找 | 中间任意位置插入 | 头部插入 | 尾部插入 |
|---|---|---|---|---|
QLinkedList<T> |
O(n) | O(1) | O(1) | O(1) |
QList<T> |
O(1) | O(n) | Amort. O(1) | Amort. O(1) |
QVector<T> |
O(1) | O(n) | O(n) | Amort. O(1) |
什么是 Amort. O(1)(均摊常数时间)?
以
QVector尾部插入为例:如果触发了底层扩容(重新申请大内存、拷贝原数据),单次调用的时间复杂度是 O(n);但在多数情况下不需要扩容,调用复杂度为 O(1)。将扩容成本均摊到每一次常规插入中,平均时间复杂度仍然是 O(1)。
4. 衍生顺序容器补充
4.1 QStack —— 栈(堆栈)
栈是一种后进先出(LIFO, Last In First Out)的单端线性表。数据只能从“栈顶”进栈(Push)和出栈(Pop),“栈底”是封闭的。
实现机制:QStack<T> 继承自 QVector<T>,在顺序表的基础上限制并封装了栈的专属接口。
核心函数原型:
void push(const T &t);—— 元素压入栈顶。T pop();—— 弹出并返回栈顶元素。T& top();—— 查看栈顶元素(不弹出)。
QStack<int> stack;
stack.push(10); // 进栈
stack.push(20);
stack.push(30); // 此时栈顶为 30
while (!stack.isEmpty())
{
qDebug() << stack.pop(); // 依次输出:30, 20, 10(后进先出)
}
4.2 QQueue —— 队列
队列是一种先进先出(FIFO, First In First Out)的单向数据结构。数据只能从一个方向(队尾)进入,从另一个方向(队头)出去。
实现机制:QQueue<T> 继承自 QList<T>。
核心函数原型:
void enqueue(const T &t);—— 在队尾入队。T dequeue();—— 在队头出队并返回该元素。T& head();—— 查看队头元素(不出队)。
QQueue<int> queue;
queue.enqueue(10); // 入队
queue.enqueue(20);
queue.enqueue(30); // 队尾为 30,队头为 10
while (!queue.isEmpty())
{
qDebug() << queue.dequeue(); // 依次输出:10, 20, 30(先进先出)
}
5. 遍历实例:只读迭代器
在编写容器遍历代码时,应建立起区分“读写”与“只读”的良好习惯。只读迭代器的执行速度比读写迭代器更快,因为其底层不触发潜在的隐式共享深拷贝行为。
QVector<int> data = {1, 2, 3, 4, 5};
// 1. 读写遍历:需要修改内容时使用标准迭代器类型
for (auto it = data.begin(); it != data.end(); it++)
{
*it = (*it) * 10; // 修改容器元素的值
}
// 2. 只读遍历:仅做查询或打印时,必须坚持使用只读迭代器(性能最优)
// 迭代器类型为:QVector<T>::const_iterator
for (auto it = data.constBegin(); it != data.constEnd(); it++)
{
qDebug() << *it; // 此时不允许进行 *it = xxx 的修改操作
}

浙公网安备 33010602011771号