奇牛Qt06 线性表容器——奇牛+gemini

奇牛Qt06 线性表容器——奇牛+gemini

1. 线性表与 Qt 容器概述

1.1 什么是线性表

线性表是 $n$ 个具有相同特性的数据元素的有限序列。其结构特点为:

  • 存在唯一的开始元素(没有前驱)和唯一的终端元素(没有后继)。
  • 除了第一个和最后一个数据元素之外,其它数据元素都有且仅有一个直接前驱和一个直接后继。

1.2 Qt 容器的设计与选型

Qt 对 C++ 标准模板库(STL)的容器进行了进一步优化和封装,构建了一套更符合 Qt 生态、支持隐式共享(写时复制)的容器类。

  • 主流顺序容器QVectorQListQLinkedList
  • 派生特殊容器QStack(栈)、QQueue(队列)。

1.3 存储数据的基本要求

存入 Qt 容器中的元素类型 T 必须能够进行赋值操作。即该类型必须提供:

  1. 默认构造函数(Default Constructor)
  2. 拷贝/复制构造函数(Copy Constructor)
  3. 赋值运算符重载(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); —— 交换索引 ij 的元素。
  • 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 底层通常是双向链表(如结构图所示,节点包含 dataprenext 指针,支持快速的在任意位置 insertpoppush)。

然而,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 的修改操作
}
posted @ 2026-07-12 13:39  学习笔记草稿存放账号  阅读(5)  评论(0)    收藏  举报