
网站的建设与管理自考,网站建设客户案例,wordpress商城插件收费,wordpress 简洁 模板奇牛Qt06 线性表容器——奇牛+gemini
1. 线性表与 Qt 容器概述
1.1 什么是线性表
线性表是 $n$ 个具有相同特性的数据元素的有限序列。其结构特点为:存在唯一的开始元素(没有前驱)和唯一的终端元素(没有后继)。
除…
奇牛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 禁用了拷贝构造和赋值运算符。
QVectorQWidget vec; // 错误!编译失败解决方案:存储对应类型的指针。
QVectorQWidget* vec; // 正确!指针允许赋值和拷贝2. 核心顺序容器
2.1 QVector —— 变长数组
QVectorT 在底层实现上对应线性表的顺序存储。
2.1.1 底层内存结构
从其底层数据结构(如图所示)来看:
QVectorT - [指针 d] - 指向 QVectorTypedDataT↳ [ 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。QVectorQString 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 容器支持嵌套,常用于表达矩阵或二维网格:
QVectorQVectorint matrix;// 填充 3x4 矩阵
for (int i = 0; i 3; i++)
{QVectorint 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的QListT 的底层实现并不是传统的链表。Qt6中的QList仅是QVector的别名,不再有实际意义上的QList。2.2.1 Qt5QList底层内存结构
从它的底层设计图可以看出:
QListT - [指针 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); —— 删除指定索引处的元素。QListQString 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 继承自 QListQString。它是专门用于处理字符串列表的子类,扩展了更多针对字符串的操作(如 join() 拼接、filter() 过滤等),比直接使用 QListQString 更具可读性和便利性。2.3 QLinkedList —— 纯正的双向链表
如果在底层需要一个真正的线性表链式存储结构,需使用 QLinkedListT。
2.3.1 底层内存结构
根据双向循环/带头结点的链表图:
QLinkedListData 维护着包含了 [ ref | size | flags ] 的头信息,
每个节点为 QLinkedListNodeT,内部拥有前驱指针 p、后继指针 n 以及实际的数据 T t。性能特点:不支持索引随机访问,查找特定元素的时间复杂度为 O(n)。但是,其在任意已知位置的插入和删除操作都是常数级时间,复杂度为 O(1)。
特有性质:迭代器直接指向表项实体本身。在执行插入或删除操作时,不会导致现有的其他迭代器失效。2.3.2 常用 API 与示例
注意:由于没有索引,QLinkedList 没有重载 [] 运算符,也没有 removeAt(int),只能通过迭代器或专用函数操作。void removeFirst(); —— 删除头节点。
void removeLast(); —— 删除尾节点。// 需要引入头文件 #include QLinkedList
QLinkedListQString 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 性能复杂度对比表容器类
用索引查找
中间任意位置插入
头部插入
尾部插入QLinkedListT
O(n)
O(1)
O(1)
O(1)QListT
O(1)
O(n)
Amort. O(1)
Amort. O(1)QVectorT
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),“栈底”是封闭的。
实现机制:QStackT 继承自 QVectorT,在顺序表的基础上限制并封装了栈的专属接口。
核心函数原型:void push(const T t); —— 元素压入栈顶。
T pop(); —— 弹出并返回栈顶元素。
T top(); —— 查看栈顶元素(不弹出)。QStackint stack;
stack.push(10); // 进栈
stack.push(20);
stack.push(30); // 此时栈顶为 30while (!stack.isEmpty())
{qDebug() stack.pop(); // 依次输出:30, 20, 10(后进先出)
}4.2 QQueue —— 队列
队列是一种先进先出(FIFO, First In First Out)的单向数据结构。数据只能从一个方向(队尾)进入,从另一个方向(队头)出去。
实现机制:QQueueT 继承自 QListT。
核心函数原型:void enqueue(const T t); —— 在队尾入队。
T dequeue(); —— 在队头出队并返回该元素。
T head(); —— 查看队头元素(不出队)。QQueueint queue;
queue.enqueue(10); // 入队
queue.enqueue(20);
queue.enqueue(30); // 队尾为 30,队头为 10while (!queue.isEmpty())
{qDebug() queue.dequeue(); // 依次输出:10, 20, 30(先进先出)
}5. 遍历实例:只读迭代器
在编写容器遍历代码时,应建立起区分“读写”与“只读”的良好习惯。只读迭代器的执行速度比读写迭代器更快,因为其底层不触发潜在的隐式共享深拷贝行为。
QVectorint data = {1, 2, 3, 4, 5};// 1. 读写遍历:需要修改内容时使用标准迭代器类型
for (auto it = data.begin(); it != data.end(); it++)
{*it = (*it) * 10; // 修改容器元素的值
}// 2. 只读遍历:仅做查询或打印时,必须坚持使用只读迭代器(性能最优)
// 迭代器类型为:QVectorT::const_iterator
for (auto it = data.constBegin(); it != data.constEnd(); it++)
{qDebug() *it; // 此时不允许进行 *it = xxx 的修改操作
}