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
扩容的次数为 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)