STL List and Vector 总结

Vector

深浅拷贝

深浅拷贝是什么

  • 浅拷贝
    • 浅拷贝是复制对象的值,包括指针变量的地址,而不是指针所指向的实际数据
    • 原始对象与复制对象共享相同的内存地址,如果修改其中一个对象的数据,则另一个对象的数据也会被修改,因为两者指向同一片内存地址空间
    • 如果数据改动或者存储地址变化时,会导致同一块内存被多次释放,从而导致内存泄漏或者程序崩溃
  • 深拷贝
    • 赋值对象的值以及指针变量所指向的实际数据
    • 原始对象和复制对象拥有各自独立的内存空间,互不影响
      浅拷贝实验

实现原理

vector是一个动态数组容器,提供动态大小的数组功能,允许快速的随机访问以及高效的添加、删除和插入操作。

  • 数据结构
    • 指向数据的指针:指向存储元素的动态数组
    • 当前大小:当前存储有效元素的数量
    • 容量:当前分配的内存空间,可以容纳的最大元素数量
  • 动态内存分配
    • 当需要更多空间时,vector会自动分配内存块,并将现有元素复制到新内存中
    • 增加容量
      • vs下是1.5倍增长
      • g++下是2.0倍增长
    • reserve只负责开辟空间,resize则是开辟空间的同时对其初始化

自我实现

核心功能实现:扩容和尾插

cpp 复制代码
#include <iostream>

template <typename T>
class SimpleVector {
private:
    T* data;       // 指向数据的指针
    size_t size;   // 当前元素个数
    size_t capacity; // 容量

    // 扩容函数
    void resizeIfNeeded() {
        if (size >= capacity) {
            size_t newCapacity = capacity == 0 ? 1 : 2 * capacity;
            T* newData = new T[newCapacity];
            for (size_t i = 0; i < size; ++i) {
                newData[i] = std::move(data[i]);
            }
            delete[] data;
            data = newData;
            capacity = newCapacity;
        }
    }

public:
    // 构造函数
    SimpleVector() : data(nullptr), size(0), capacity(0) {}

    // 析构函数
    ~SimpleVector() {
        delete[] data;
    }

    // 尾部添加元素
    void push_back(const T& value) {
        resizeIfNeeded();
        data[size++] = value;
    }

    // 获取大小
    size_t getSize() const {
        return size;
    }

    // 获取容量
    size_t getCapacity() const {
        return capacity;
    }

    // 访问元素
    T& operator[](size_t index) {
        return data[index];
    }

    // 访问元素(const)
    const T& operator[](size_t index) const {
        return data[index];
    }
};

int main() {
    SimpleVector<int> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);

    for (size_t i = 0; i < vec.getSize(); ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;

    std::cout << "Size: " << vec.getSize() << std::endl;
    std::cout << "Capacity: " << vec.getCapacity() << std::endl;

    SimpleVector<char> vChar;
    vChar.push_back('m');

    std::cout<<vChar[0]<<std::endl;

    return 0;
}

List

自我实现

List插入(头插、尾插、指定位置插入)

cpp 复制代码
template <typename T>
class List {
private:
    Node<T>* head;
    Node<T>* tail;
    size_t size;

public:
    // 构造函数
    List() : head(nullptr), tail(nullptr), size(0) {}

    // 析构函数
    ~List() {
        clear();
    }

    // 清空链表
    void clear() {
        while (head != nullptr) {
            Node<T>* temp = head;
            head = head->next;
            delete temp;
        }
        tail = nullptr;
        size = 0;
    }

    // 获取大小
    size_t getSize() const {
        return size;
    }

    // 插入元素
    void insert(size_t position, const T& value) {
        if (position > size) {
            throw std::out_of_range("Position out of range");
        }

        Node<T>* newNode = new Node<T>(value);
        if (position == 0) {
            // 插入到头部
            newNode->next = head;
            if (head != nullptr) {
                head->prev = newNode;
            }
            head = newNode;
            if (tail == nullptr) {
                tail = newNode;
            }
        } else if (position == size) {
            // 插入到尾部
            newNode->prev = tail;
            if (tail != nullptr) {
                tail->next = newNode;
            }
            tail = newNode;
            if (head == nullptr) {
                head = newNode;
            }
        } else {
            // 插入到中间
            Node<T>* current = head;
            for (size_t i = 0; i < position; ++i) {
                current = current->next;
            }
            newNode->next = current;
            newNode->prev = current->prev;
            current->prev->next = newNode;
            current->prev = newNode;
        }
        ++size;
    }

    // 打印链表
    void print() const {
        Node<T>* current = head;
        while (current != nullptr) {
            std::cout << current->data << " ";
            current = current->next;
        }
        std::cout << std::endl;
    }
};

细节问题分析

List和vector的区别

  • vector是一个动态数组 ,支持动态数组,支持快速的随机访问。时间复杂度为O(1),如果是在中间插入或者删除元素时候,需要移动大量元素,此时的时间复杂度为O(n)。Vector适用于频繁随机访问的场景
  • List则是一个双向链表插入和删除非常高效,时间复杂度为O(1),但是随机访问元素的时候需要遍历,事件复杂度O(N)。List适用于频繁中间插入或删除元素的场景

迭代器失效

List迭代器不会失效的原因(不绝对)

  • List的底层为双向链表结构,插入和删除元素只需要调整相邻节点指针,不会影响其他节点和节点的指针,所以其他迭代器保持有效

  • 只有指向被删除节点的迭代器才会失效,而指向其他节点的迭代器不会受到影响
    Vector迭代器失效

  • 失效场景

    • 扩容时迭代器失效,因为底层数组的内存地址发生了变化,指向旧地址的迭代器不再有效
    • 中间插入或者删除元素时,指定位置插入或者删除元素都可能会导致迭代器失效,因为这些元素的位置发生了变化

数组或链表实现队列结构

数组实现队列:通过环形缓冲区避免数据移动,提高效率。

cpp 复制代码
#include <iostream>
#include <stdexcept>

template <typename T>
class ArrayQueue {
private:
    T* data;
    size_t capacity;
    int front;
    int rear;
    size_t count;

public:
    // 构造函数
    ArrayQueue(size_t capacity) : capacity(capacity), front(0), rear(-1), count(0) {
        data = new T[capacity];
    }

    // 析构函数
    ~ArrayQueue() {
        delete[] data;
    }

    // 入队操作
    void enqueue(const T& value) {
        if (count == capacity) {
            throw std::overflow_error("Queue overflow");
        }
        rear = (rear + 1) % capacity;
        data[rear] = value;
        ++count;
    }

    // 出队操作
    void dequeue() {
        if (count == 0) {
            throw std::underflow_error("Queue underflow");
        }
        front = (front + 1) % capacity;
        --count;
    }

    // 获取队列前端元素
    T& peek() {
        if (count == 0) {
            throw std::underflow_error("Queue is empty");
        }
        return data[front];
    }

    // 判断队列是否为空
    bool isEmpty() const {
        return count == 0;
    }

    // 获取队列的大小
    size_t size() const {
        return count;
    }
};

// 测试代码
int main() {
    ArrayQueue<int> queue(10);
    queue.enqueue(1);
    queue.enqueue(2);
    queue.enqueue(3);

    std::cout << "Front element: " << queue.peek() << std::endl;

    queue.dequeue();
    std::cout << "Front element after dequeue: " << queue.peek() << std::endl;

    std::cout << "Queue size: " << queue.size() << std::endl;

    return 0;
}

使用链表实现队列:通过链表的尾部插入以及头删操作实现入队和出队

cpp 复制代码
#include <iostream>
#include <stdexcept>

template <typename T>
struct Node {
    T data;
    Node* next;

    Node(const T& value) : data(value), next(nullptr) {}
};

template <typename T>
class LinkedListQueue {
private:
    Node<T>* front;
    Node<T>* rear;
    size_t count;

public:
    // 构造函数
    LinkedListQueue() : front(nullptr), rear(nullptr), count(0) {}

    // 析构函数
    ~LinkedListQueue() {
        while (front != nullptr) {
            dequeue();
        }
    }

    // 入队操作
    void enqueue(const T& value) {
        Node<T>* newNode = new Node<T>(value);
        if (rear) {
            rear->next = newNode;
        } else {
            front = newNode;
        }
        rear = newNode;
        ++count;
    }

    // 出队操作
    void dequeue() {
        if (front == nullptr) {
            throw std::underflow_error("Queue underflow");
        }
        Node<T>* temp = front;
        front = front->next;
        if (front == nullptr) {
            rear = nullptr;
        }
        delete temp;
        --count;
    }

    // 获取队列前端元素
    T& peek() {
        if (front == nullptr) {
            throw std::underflow_error("Queue is empty");
        }
        return front->data;
    }

    // 判断队列是否为空
    bool isEmpty() const {
        return front == nullptr;
    }

    // 获取队列的大小
    size_t size() const {
        return count;
    }
};

// 测试代码
int main() {
    LinkedListQueue<int> queue;
    queue.enqueue(1);
    queue.enqueue(2);
    queue.enqueue(3);

    std::cout << "Front element: " << queue.peek() << std::endl;

    queue.dequeue();
    std::cout << "Front element after dequeue: " << queue.peek() << std::endl;

    std::cout << "Queue size: " << queue.size() << std::endl;

    return 0;
}

数组或链表实现栈结构

**链表实现栈:**通过链表节点的插入和删除操作来实现入栈和出栈操作

cpp 复制代码
#include <iostream>
#include <stdexcept>

template <typename T>
struct Node {
    T data;
    Node* next;

    Node(const T& value) : data(value), next(nullptr) {}
};

template <typename T>
class LinkedListStack {
private:
    Node<T>* top;
    size_t count;

public:
    // 构造函数
    LinkedListStack() : top(nullptr), count(0) {}

    // 析构函数
    ~LinkedListStack() {
        while (top != nullptr) {
            pop();
        }
    }

    // 入栈操作
    void push(const T& value) {
        Node<T>* newNode = new Node<T>(value);
        newNode->next = top;
        top = newNode;
        ++count;
    }

    // 出栈操作
    void pop() {
        if (top == nullptr) {
            throw std::underflow_error("Stack underflow");
        }
        Node<T>* temp = top;
        top = top->next;
        delete temp;
        --count;
    }

    // 获取栈顶元素
    T& peek() {
        if (top == nullptr) {
            throw std::underflow_error("Stack is empty");
        }
        return top->data;
    }

    // 判断栈是否为空
    bool isEmpty() const {
        return top == nullptr;
    }

    // 获取栈的大小
    size_t size() const {
        return count;
    }
};

// 测试代码
int main() {
    LinkedListStack<int> stack;
    stack.push(1);
    stack.push(2);
    stack.push(3);

    std::cout << "Top element: " << stack.peek() << std::endl;

    stack.pop();
    std::cout << "Top element after pop: " << stack.peek() << std::endl;

    std::cout << "Stack size: " << stack.size() << std::endl;

    return 0;
}

**数组实现栈:**栈顶指针指向当前元素位置,入栈和出栈都在栈顶进行即可

cpp 复制代码
#include <iostream>
#include <stdexcept>

template <typename T>
class ArrayStack {
private:
    T* data;
    size_t capacity;
    int top;

public:
    // 构造函数
    ArrayStack(size_t capacity) : capacity(capacity), top(-1) {
        data = new T[capacity];
    }

    // 析构函数
    ~ArrayStack() {
        delete[] data;
    }

    // 入栈操作
    void push(const T& value) {
        if (top == capacity - 1) {
            throw std::overflow_error("Stack overflow");
        }
        data[++top] = value;
    }

    // 出栈操作
    void pop() {
        if (top == -1) {
            throw std::underflow_error("Stack underflow");
        }
        --top;
    }

    // 获取栈顶元素
    T& peek() {
        if (top == -1) {
            throw std::underflow_error("Stack is empty");
        }
        return data[top];
    }

    // 判断栈是否为空
    bool isEmpty() const {
        return top == -1;
    }

    // 获取栈的大小
    size_t size() const {
        return top + 1;
    }
};

// 测试代码
int main() {
    ArrayStack<int> stack(10);
    stack.push(1);
    stack.push(2);
    stack.push(3);

    std::cout << "Top element: " << stack.peek() << std::endl;

    stack.pop();
    std::cout << "Top element after pop: " << stack.peek() << std::endl;

    std::cout << "Stack size: " << stack.size() << std::endl;

    return 0;
}

Vector扩容机制及其时间复杂度

push_back 时间复杂度分析

  • 无需扩容时:此时只需要将新元素放入到现有元素的后面,所以时间复杂度为O(1)
  • 需要扩容时
    • 分配新的内存块;现有元素从旧内存块复制到新内存块中;释放旧内存块;添加新元素
    • 时间复杂度:O(n)
  • 摊销分析
    • 假设初始容量为 1,每次扩容时容量加倍。

    • 第一次扩容时,需要复制 1 个元素,操作耗时 O(1)O(1)O(1)。

    • 第二次扩容时,需要复制 2 个元素,操作耗时 O(2)O(2)O(2)。

    • 第三次扩容时,需要复制 4 个元素,操作耗时 O(4)O(4)O(4)。

    • 依此类推,第 kkk 次扩容时,需要复制 2k−12^{k-1}2k−1 个元素,操作耗时 O(2k−1)O(2^{k-1})O(2k−1)。

    • 假设总共有 nnn 个元素,vector 扩容的次数为 log⁡n\log nlogn 次,总的扩容复制操作耗时为: 1+2+4+⋯+2log⁡n−1=2log⁡n−1=n−11 + 2 + 4 + \cdots + 2^{\log n - 1} = 2^{\log n} - 1 = n - 11+2+4+⋯+2logn−1=2logn−1=n−1

      将这些耗时均摊到每次 push_back 操作上,得到每次操作的平均时间复杂度为 O(1)

Vector插入操作比List慢的原因

  • Vector的插入操作可能存在需要移动大量元素的情况,时间复杂度是O(n)
  • List的插入操作则只需要调整相邻节点的指针,时间复杂度O(1)
相关推荐
Ajiang28247353041 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空1 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’6 小时前
C++ list (链表)容器
c++·链表·list
----云烟----6 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024066 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic7 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it7 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康7 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神7 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式