阅读量:0
Vector
深浅拷贝
深浅拷贝是什么
- 浅拷贝
- 浅拷贝是复制对象的值,包括指针变量的地址,而不是指针所指向的实际数据
- 原始对象与复制对象共享相同的内存地址,如果修改其中一个对象的数据,则另一个对象的数据也会被修改,因为两者指向同一片内存地址空间
- 如果数据改动或者存储地址变化时,会导致同一块内存被多次释放,从而导致内存泄漏或者程序崩溃
- 深拷贝
- 赋值对象的值以及指针变量所指向的实际数据
- 原始对象和复制对象拥有各自独立的内存空间,互不影响
浅拷贝实验
实现原理
vector是一个动态数组容器,提供动态大小的数组功能,允许快速的随机访问以及高效的添加、删除和插入操作。
- 数据结构
- 指向数据的指针:指向存储元素的动态数组
- 当前大小:当前存储有效元素的数量
- 容量:当前分配的内存空间,可以容纳的最大元素数量
- 动态内存分配
- 当需要更多空间时,vector会自动分配内存块,并将现有元素复制到新内存中
- 增加容量
- vs下是1.5倍增长
- g++下是2.0倍增长
- reserve只负责开辟空间,resize则是开辟空间的同时对其初始化
自我实现
核心功能实现:扩容和尾插
#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插入(头插、尾插、指定位置插入)
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迭代器失效
- 失效场景
- 扩容时迭代器失效,因为底层数组的内存地址发生了变化,指向旧地址的迭代器不再有效
- 中间插入或者删除元素时,指定位置插入或者删除元素都可能会导致迭代器失效,因为这些元素的位置发生了变化
数组或链表实现队列结构
数组实现队列:通过环形缓冲区避免数据移动,提高效率。
#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; }
使用链表实现队列:通过链表的尾部插入以及头删操作实现入队和出队
#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; }
数组或链表实现栈结构
链表实现栈:通过链表节点的插入和删除操作来实现入栈和出栈操作
#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; }
数组实现栈:栈顶指针指向当前元素位置,入栈和出栈都在栈顶进行即可
#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
扩容的次数为 logn\log nlogn 次,总的扩容复制操作耗时为: 1+2+4+⋯+2logn−1=2logn−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)