当前位置: 首页 > news >正文

数组/链表/【环形数组】实现 队列/栈/双端队列【移动语义应用】【自动扩缩】

队列与栈:从原理到实现的全面解析(含C++实战与核心知识点)

引言:操作受限的数据结构

在计算机科学中,队列(Queue)和栈(Stack)是两种经典的“操作受限”数据结构。与可随机访问的数组、链表不同,它们的核心特性体现在严格的操作规则上:

  • 栈(Stack):遵循“先进后出(LIFO,Last In First Out)”,仅允许在一端(栈顶)插入和删除元素。
  • 队列(Queue):遵循“先进先出(FIFO,First In First Out)”,仅允许在一端(队尾)插入元素,在另一端(队头)删除元素。

这两种结构看似简单,却是很多高级算法(如深度优先搜索DFS、广度优先搜索BFS)和系统设计(如缓存、任务调度)的基础。本文将从核心API出发,分别用链表和数组(含环形数组优化)实现它们,并深入解析实现中的C++关键技术点。

一、队列与栈的核心API

无论是队列还是栈,其核心操作都围绕“增、删、查、判空/大小”展开。以下是它们的模板API定义,后续实现需严格遵循这些接口。

1. 栈(Stack)的核心API

栈的操作集中在“栈顶”,核心API如下:

template <typename E>
class MyStack {
public:// 向栈顶插入元素(入栈),时间复杂度O(1)void push(const E& e);// 从栈顶删除元素(出栈),返回被删除元素,时间复杂度O(1)E pop();// 查看栈顶元素(不删除),时间复杂度O(1)E peek() const;// 返回栈中元素个数,时间复杂度O(1)int size() const;// 判断栈是否为空,时间复杂度O(1)bool isEmpty() const;
};

2. 队列(Queue)的核心API

队列的操作分为“队尾插入”和“队头删除”,核心API如下:

template <typename E>
class MyQueue {
public:// 向队尾插入元素(入队),时间复杂度O(1)void push(const E& e);// 从队头删除元素(出队),返回被删除元素,时间复杂度O(1)E pop();// 查看队头元素(不删除),时间复杂度O(1)E peek() const;// 返回队列中元素个数,时间复杂度O(1)int size() const;// 判断队列是否为空,时间复杂度O(1)bool isEmpty() const;
};

二、链表实现队列和栈

链表(本文以std::list为例)天然支持高效的头尾操作,因此实现队列和栈非常直观。

1. 链表实现栈

栈的“先进后出”特性适合用链表的尾部作为栈顶(链表尾部插入/删除为O(1)):

#include <list>
#include <iostream>
#include <stdexcept>template <typename E>
class MyLinkedStack {
private:std::list<E> list; // 底层链表容器public:// 入栈:向链表尾部插入元素void push(const E& e) {list.push_back(e);}// 出栈:从链表尾部删除元素并返回E pop() {if (isEmpty()) {throw std::runtime_error("Stack is empty");}E topVal = list.back(); // 获取尾部元素list.pop_back(); // 删除尾部元素return topVal;}// 查看栈顶元素:返回链表尾部元素E peek() const {if (isEmpty()) {throw std::runtime_error("Stack is empty");}return list.back();}// 元素个数:返回链表大小int size() const {return list.size();}// 判断为空:链表是否为空bool isEmpty() const {return list.empty();}
};// 测试示例
int main() {MyLinkedStack<int> stack;stack.push(1);stack.push(2);stack.push(3);// 出栈顺序:3 -> 2 -> 1(先进后出)while (!stack.isEmpty()) {std::cout << stack.pop() << " "; // 输出:3 2 1}return 0;
}

2. 链表实现队列

队列的“先进先出”特性适合用链表的尾部插入(队尾)、头部删除(队头):

#include <list>
#include <iostream>
#include <stdexcept>template <typename E>
class MyLinkedQueue {
private:std::list<E> list; // 底层链表容器public:// 入队:向链表尾部插入元素void push(const E& e) {list.push_back(e);}// 出队:从链表头部删除元素并返回E pop() {if (isEmpty()) {throw std::runtime_error("Queue is empty");}E frontVal = list.front(); // 获取头部元素list.pop_front(); // 删除头部元素return frontVal;}// 查看队头元素:返回链表头部元素E peek() const {if (isEmpty()) {throw std::runtime_error("Queue is empty");}return list.front();}// 元素个数:返回链表大小int size() const {return list.size();}// 判断为空:链表是否为空bool isEmpty() const {return list.empty();}
};// 测试示例
int main() {MyLinkedQueue<int> queue;queue.push(1);queue.push(2);queue.push(3);// 出队顺序:1 -> 2 -> 3(先进先出)while (!queue.isEmpty()) {std::cout << queue.pop() << " "; // 输出:1 2 3}return 0;
}

三、数组实现队列和栈

数组(或动态数组std::vector)的尾部操作是O(1),但头部操作(插入/删除)通常是O(n)(需搬移元素)。通过“环形数组”技巧,可将数组头部操作优化至O(1)。

1. 数组实现栈

栈仅需尾部操作,直接用动态数组的尾部作为栈顶即可:

#include <vector>
#include <stdexcept>template <typename E>
class MyArrayStack {
private:std::vector<E> arr; // 底层动态数组public:// 入栈:向数组尾部插入元素void push(const E& e) {arr.push_back(e);}// 出栈:从数组尾部删除元素并返回E pop() {if (isEmpty()) {throw std::runtime_error("Stack is empty");}E topVal = arr.back(); // 获取尾部元素arr.pop_back(); // 删除尾部元素return topVal;}// 查看栈顶元素:返回数组尾部元素E peek() const {if (isEmpty()) {throw std::runtime_error("Stack is empty");}return arr.back();}// 元素个数:返回数组大小int size() const {return arr.size();}// 判断为空:数组是否为空bool isEmpty() const {return arr.empty();}
};

2. 环形数组:优化数组头部操作的核心

普通数组头部操作效率低(O(n)),而环形数组通过“逻辑环形”设计,让头部操作(插入/删除)效率提升至O(1)。

(1)环形数组核心原理

环形数组通过两个指针startend标记有效元素范围,结合取模运算实现“逻辑环形”:

  • 区间定义:有效元素范围为左闭右开区间[start, end),即start指向第一个有效元素,end指向最后一个有效元素的下一个位置。
  • 取模运算:当startend超出数组边界时,通过% arr.size()让其“绕回”数组头部,形成逻辑环形。
  • 扩缩容:当元素满时自动扩容(翻倍),当元素数为容量1/4时自动缩容(减半),保证空间效率。
(2)环形数组完整实现(模板类)
#include <iostream>
#include <vector>
#include <stdexcept>template<typename T>
class CycleArray {
private:std::vector<T> arr;  // 底层存储容器int start;           // 指向第一个有效元素(左闭)int end;             // 指向最后一个有效元素的下一个位置(右开)int count;           // 当前有效元素数量// 扩缩容核心函数:将元素复制到新容量数组,重置start和endvoid resize(int newSize) {std::vector<T> newArr(newSize); // 创建新数组// 复制旧数组元素到新数组(线性排列)for (int i = 0; i < count; ++i) {newArr[i] = arr[(start + i) % arr.size()];}// 移动语义:高效转移新数组所有权(避免深拷贝)arr = std::move(newArr);// 重置指针:新数组中有效元素从0开始,end = countstart = 0;end = count;}public:// 委托构造函数:无参构造默认容量为1的环形数组CycleArray() : CycleArray(1) {}// explicit构造函数:禁止隐式类型转换,指定初始容量explicit CycleArray(int size) : arr(size), start(0), end(0), count(0) {}// 头部插入元素void addFirst(const T& val) {if (isFull()) {resize(arr.size() * 2); // 满则扩容(翻倍)}// start左移:若start=0,左移后绕回数组尾部start = (start - 1 + arr.size()) % arr.size();arr[start] = val;count++;}// 头部删除元素void removeFirst() {if (isEmpty()) {throw std::runtime_error("CycleArray is empty");}// 重置start位置元素为默认值(逻辑清空)arr[start] = T();// start右移:绕回逻辑start = (start + 1) % arr.size();count--;// 缩容:元素数为容量1/4且不为空时,缩容至1/2if (count > 0 && count == arr.size() / 4) {resize(arr.size() / 2);}}// 尾部插入元素void addLast(const T& val) {if (isFull()) {resize(arr.size() * 2); // 满则扩容}arr[end] = val; // end位置插入元素// end右移:绕回逻辑end = (end + 1) % arr.size();count++;}// 尾部删除元素void removeLast() {if (isEmpty()) {throw std::runtime_error("CycleArray is empty");}// end左移:获取最后一个有效元素位置end = (end - 1 + arr.size()) % arr.size();// 重置end位置元素为默认值(逻辑清空)arr[end] = T();count--;// 缩容逻辑同上if (count > 0 && count == arr.size() / 4) {resize(arr.size() / 2);}}// 获取头部元素T getFirst() const {if (isEmpty()) {throw std::runtime_error("CycleArray is empty");}return arr[start];}// 获取尾部元素T getLast() const {if (isEmpty()) {throw std::runtime_error("CycleArray is empty");}// end是右开,尾部元素为end-1位置return arr[(end - 1 + arr.size()) % arr.size()];}// 判断是否已满(元素数=容量)bool isFull() const {return count == arr.size();}// 获取元素数量int size() const {return count;}// 判断是否为空bool isEmpty() const {return count == 0;}
};
(3)关键C++知识点解析

环形数组实现中涉及多个核心C++特性,需重点理解:

① 模板类(Template Class)
template<typename T>
class CycleArray { ... };
  • 作用:让类支持任意数据类型(如intstring),实现代码复用。
  • 使用:实例化时指定类型,如CycleArray<int> arr(10)
② 委托构造函数(Delegating Constructor)
CycleArray() : CycleArray(1) {}
  • 作用:一个构造函数调用同类的另一个构造函数,避免代码重复。
  • 逻辑:无参构造时,委托CycleArray(1)初始化,默认容量为1。
③ explicit关键字
explicit CycleArray(int size) : arr(size), start(0), end(0), count(0) {}
  • 作用:禁止隐式类型转换,防止意外调用构造函数。
  • 示例CycleArray arr = 10; 会报错(隐式转换被禁止),必须显式调用 CycleArray arr(10);
④ 移动语义与std::move
arr = std::move(newArr); // 移动语义!
④ 移动语义与 std::move

移动语义是C++11引入的核心特性,用于高效转移资源所有权,避免深拷贝带来的性能损耗。在环形数组的resize函数中,std::move起到了关键作用:

为什么需要移动语义?
传统的赋值操作(如arr = newArr)会触发深拷贝,即复制整个数组内容,时间复杂度为O(n)。而移动语义通过转移资源所有权(如指针),仅需O(1)时间。

std::move的本质
std::move本身不移动任何东西,它只是将左值强制转换为右值引用(T&&),从而触发移动构造函数或移动赋值运算符。

环形数组中的应用
resize函数中:

  1. 先将旧数组元素复制到新数组(线性排列)。
  2. 通过std::move将新数组的资源所有权转移给arr,避免二次复制。

代码示例

// resize函数关键部分
std::vector<T> newArr(newSize); // 创建新数组
// 复制旧数组元素到新数组(线性排列)
for (int i = 0; i < count; ++i) {newArr[i] = arr[(start + i) % arr.size()];
}
// 移动语义:高效转移资源所有权
arr = std::move(newArr);

移动赋值运算符的简化实现

// std::vector的移动赋值运算符(简化版)
vector& operator=(vector&& other) noexcept {// 1. 释放当前资源delete[] data;// 2. 接管other的资源data = other.data;size = other.size;capacity = other.capacity;// 3. 清空other(防止析构时重复释放)other.data = nullptr;other.size = 0;other.capacity = 0;return *this;
}

性能对比

操作时间复杂度内存操作
传统赋值(深拷贝)O(n)复制所有元素
移动赋值(移动语义)O(1)转移指针,无需复制元素

3. 环形数组实现队列

基于环形数组的O(1)头尾操作,实现队列变得非常简单:

#include <stdexcept>template <typename E>
class MyArrayQueue {
private:CycleArray<E> arr; // 底层使用环形数组public:// 入队:调用环形数组的尾部插入void push(const E& e) {arr.addLast(e);}// 出队:调用环形数组的头部删除E pop() {if (isEmpty()) {throw std::runtime_error("Queue is empty");}E frontVal = arr.getFirst();arr.removeFirst();return frontVal;}// 查看队头元素E peek() const {if (isEmpty()) {throw std::runtime_error("Queue is empty");}return arr.getFirst();}// 获取队列大小int size() const {return arr.size();}// 判断队列是否为空bool isEmpty() const {return arr.isEmpty();}
};

4. 环形数组 vs 普通数组:操作复杂度对比

操作普通数组环形数组
尾部插入(push_back)O(1)O(1)
尾部删除(pop_back)O(1)O(1)
头部插入(push_front)O(n)O(1)
头部删除(pop_front)O(n)O(1)
随机访问(get(i))O(1)O(1)

结论:环形数组在保留随机访问能力的同时,将头部操作从O(n)优化到O(1),是实现队列和栈的理想选择。

四、总结与应用场景

1. 队列与栈的实现选择

实现方式适用场景优势
链表实现需要频繁增删,元素数量不稳定无需扩容,空间灵活
数组实现随机访问需求,元素数量较稳定缓存友好,访问效率高
环形数组实现需高效头尾操作,空间敏感头部操作O(1),自动扩缩容

2. 典型应用场景

  • 栈的应用

    • 函数调用栈(递归实现)
    • 表达式求值(后缀表达式计算)
    • 浏览器后退/前进功能
    • 括号匹配检查
  • 队列的应用

    • 广度优先搜索(BFS)
    • 任务调度(FIFO策略)
    • 消息队列(生产者-消费者模型)
    • 网络数据包处理

3. 关键C++知识点回顾

  • 模板编程(泛型实现)
  • 环形数组的取模运算(逻辑环形)
  • 移动语义与std::move(资源高效转移)
  • explicit构造函数(防止隐式转换)
  • 委托构造函数(代码复用)

五、思考题解答

为什么编程语言标准库(如C++的std::vector)不默认使用环形数组?

  1. 随机访问效率:普通数组的随机访问无需计算偏移量(直接arr[i]),而环形数组需通过(start + i) % capacity计算,存在额外开销。
  2. 缓存局部性:普通数组的连续内存布局更符合CPU缓存机制,环形数组可能因“绕回”导致缓存失效。
  3. 使用场景:标准库的vector更侧重通用场景(如随机访问、尾部操作),而环形数组更适合特定场景(如队列、双端队列)。

通过本文的实现和解析,你已掌握队列和栈的核心原理、多种实现方式及C++关键技术点。这些知识不仅是数据结构的基础,更是理解高级算法和系统设计的关键。

双端队列(Deque):灵活的头尾操作数据结构

双端队列(Double-Ended Queue,简称Deque)是队列的扩展,允许在队头和队尾同时进行插入和删除操作,突破了普通队列“先进先出”的严格限制,灵活性更高。本文将详细讲解双端队列的核心API、链表与数组实现方式,以及典型应用场景。

一、双端队列的核心API

双端队列的操作覆盖队头和队尾,核心API如下(模板类定义):

template<typename E>
class MyDeque {
public:// 从队头插入元素,时间复杂度 O(1)void addFirst(const E& e);// 从队尾插入元素,时间复杂度 O(1)void addLast(const E& e);// 从队头删除元素,返回被删除元素,时间复杂度 O(1)E removeFirst();// 从队尾删除元素,返回被删除元素,时间复杂度 O(1)E removeLast();// 查看队头元素(不删除),时间复杂度 O(1)E peekFirst() const;// 查看队尾元素(不删除),时间复杂度 O(1)E peekLast() const;// 返回元素个数,时间复杂度 O(1)int size() const;// 判断是否为空,时间复杂度 O(1)bool isEmpty() const;
};

与普通队列的区别

  • 普通队列仅支持addLast(队尾插入)和removeFirst(队头删除);
  • 双端队列额外支持addFirst(队头插入)和removeLast(队尾删除),操作更灵活,无需严格遵循FIFO。

二、链表实现双端队列

链表(如std::list)天然支持高效的头尾操作(插入/删除均为O(1)),是实现双端队列的理想选择。

实现代码

#include <list>
#include <stdexcept>
#include <iostream>template<typename E>
class MyListDeque {
private:std::list<E> list; // 底层链表容器,支持O(1)头尾操作public:// 队头插入元素void addFirst(const E& e) {list.push_front(e); // 链表头部插入}// 队尾插入元素void addLast(const E& e) {list.push_back(e); // 链表尾部插入}// 队头删除元素并返回E removeFirst() {if (isEmpty()) {throw std::runtime_error("Deque is empty");}E frontVal = list.front(); // 获取队头元素list.pop_front(); // 删除队头元素return frontVal;}// 队尾删除元素并返回E removeLast() {if (isEmpty()) {throw std::runtime_error("Deque is empty");}E lastVal = list.back(); // 获取队尾元素list.pop_back(); // 删除队尾元素return lastVal;}// 查看队头元素E peekFirst() const {if (isEmpty()) {throw std::runtime_error("Deque is empty");}return list.front();}// 查看队尾元素E peekLast() const {if (isEmpty()) {throw std::runtime_error("Deque is empty");}return list.back();}// 元素个数int size() const {return list.size();}// 判断为空bool isEmpty() const {return list.empty();}
};// 测试示例
int main() {MyListDeque<int> deque;deque.addFirst(1);    // 队头插入1 → [1]deque.addFirst(2);    // 队头插入2 → [2, 1]deque.addLast(3);     // 队尾插入3 → [2, 1, 3]deque.addLast(4);     // 队尾插入4 → [2, 1, 3, 4]std::cout << deque.removeFirst() << " "; // 删除队头2 → 输出2std::cout << deque.removeLast() << " ";  // 删除队尾4 → 输出4std::cout << deque.peekFirst() << " ";   // 查看队头 → 输出1std::cout << deque.peekLast() << " ";    // 查看队尾 → 输出3// 最终输出:2 4 1 3return 0;
}

实现解析

链表实现双端队列的核心是复用链表的头尾操作:

  • std::listpush_front/pop_front支持队头O(1)插入/删除;
  • push_back/pop_back支持队尾O(1)插入/删除;
  • 无需额外逻辑处理,直接映射API即可,实现简洁高效。

三、数组实现双端队列(基于环形数组)

普通数组的头尾操作效率低(O(n)),但环形数组通过“逻辑环形”设计,可将头尾操作优化至O(1),非常适合实现双端队列。

实现代码

基于前文实现的CycleArray(环形数组),直接复用其addFirst/addLast/removeFirst/removeLast方法:

#include <stdexcept>
// 引入环形数组类(前文实现的CycleArray)
#include "cycle_array.h" // 假设环形数组定义在该文件中template <typename E>
class MyArrayDeque {
private:CycleArray<E> arr; // 底层依赖环形数组public:// 队头插入元素void addFirst(const E& e) {arr.addFirst(e);}// 队尾插入元素void addLast(const E& e) {arr.addLast(e);}// 队头删除元素并返回E removeFirst() {if (isEmpty()) {throw std::runtime_error("Deque is empty");}E frontVal = arr.getFirst();arr.removeFirst();return frontVal;}// 队尾删除元素并返回E removeLast() {if (isEmpty()) {throw std::runtime_error("Deque is empty");}E lastVal = arr.getLast();arr.removeLast();return lastVal;}// 查看队头元素E peekFirst() const {if (isEmpty()) {throw std::runtime_error("Deque is empty");}return arr.getFirst();}// 查看队尾元素E peekLast() const {if (isEmpty()) {throw std::runtime_error("Deque is empty");}return arr.getLast();}// 元素个数int size() const {return arr.size();}// 判断为空bool isEmpty() const {return arr.isEmpty();}
};// 测试示例
int main() {MyArrayDeque<int> deque;deque.addLast(1);     // 队尾插入1 → [1]deque.addFirst(2);    // 队头插入2 → [2, 1]deque.addLast(3);     // 队尾插入3 → [2, 1, 3]deque.addFirst(4);    // 队头插入4 → [4, 2, 1, 3]std::cout << deque.removeLast() << " ";  // 删除队尾3 → 输出3std::cout << deque.removeFirst() << " "; // 删除队头4 → 输出4std::cout << deque.peekFirst() << " ";   // 查看队头 → 输出2std::cout << deque.peekLast() << " ";    // 查看队尾 → 输出1// 最终输出:3 4 2 1return 0;
}

实现解析

环形数组为双端队列提供了高效支持:

  • CycleArray::addFirst/removeFirst对应队头操作;
  • addLast/removeLast对应队尾操作;
  • 环形数组的自动扩缩容机制确保空间效率,避免溢出或浪费。

四、双端队列的优势与应用场景

双端队列结合了栈和队列的特性,在需要灵活头尾操作的场景中表现突出。

核心优势

  1. 操作灵活性:同时支持队头/队尾插入删除,突破FIFO限制;
  2. 效率高效性:链表和环形数组实现均支持O(1)时间复杂度的头尾操作;
  3. 多功能复用:可同时模拟栈(用队头/队尾作为栈顶)和队列(用队尾入、队头出)。

典型应用场景

  1. 滑动窗口问题
    如“滑动窗口最大值”问题,用双端队列存储窗口内元素的索引,队头始终为当前窗口最大值,通过队头删除过期元素、队尾删除小于新元素的元素,实现O(n)时间复杂度。

  2. 实现栈和队列

    • 用双端队列模拟栈:仅使用addLastremoveLast(队尾作为栈顶);
    • 用双端队列模拟队列:仅使用addLastremoveFirst(标准FIFO)。
  3. 缓存设计
    如LRU(最近最少使用)缓存的简化版,用双端队列存储访问记录,最近访问的元素移至队头,满时删除队尾元素。

  4. 数据流处理
    需同时处理首尾数据的场景(如实时日志分析、双向消息队列)。

五、链表实现 vs 数组实现对比

特性链表实现(MyListDeque)数组实现(MyArrayDeque)
时间复杂度(头尾操作)O(1)O(1)
空间效率无浪费(按需分配节点)可能有空闲空间(环形数组容量)
缓存友好性较差(节点内存不连续)较好(数组内存连续)
扩缩容成本低(插入/删除节点即可)中(需复制元素,依赖移动语义)
随机访问支持不支持(需遍历)支持(通过环形数组计算索引)

六、总结

双端队列是一种灵活高效的数据结构,核心价值在于队头和队尾的O(1)操作。通过链表实现可获得灵活的空间分配,通过环形数组实现可获得更好的缓存性能。其应用场景广泛,尤其在滑动窗口、缓存设计和模拟栈/队列混合操作中不可或缺。

关键C++知识点回顾

  • 模板类泛型实现(支持任意数据类型);
  • 链表std::list的头尾操作特性;
  • 环形数组的逻辑设计与取模运算;
  • 代码复用(基于环形数组快速实现双端队列)。

掌握双端队列的实现原理,不仅能提升对数据结构灵活性的理解,更能为复杂算法问题提供高效解决方案。

http://www.dtcms.com/a/285623.html

相关文章:

  • st-Gcn训练跳绳识别模型六:YOLOv8-Pose 和 ST-GCN 实现实时跳绳计数器应用
  • IDEA 2020.1版本起下载JDK
  • 当OT遇见IT:Apache IoTDB如何用“时序空间一体化“技术破解工业物联网数据孤岛困局?
  • 【每日算法】专题十三_队列 + 宽搜(bfs)
  • 四、CV_GoogLeNet
  • 代码训练营DAY35 第九章 动态规划part03
  • 【收集电脑信息】collect_info.sh
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 基于jieba实现词频统计
  • Kubernetes Pod深度理解
  • 【数据可视化-67】基于pyecharts的航空安全深度剖析:坠毁航班数据集可视化分析
  • 【问题解决】npm包下载速度慢
  • 【AI大模型学习路线】第三阶段之RAG与LangChain——第十八章(基于RAGAS的RAG的评估)RAG中的评估思路?
  • 把握流程节点,明确信息传递
  • C专题5:函数进阶和递归
  • 最小生成树算法详解
  • 2025外卖江湖:巨头争霸,谁主沉浮?
  • 洞见AI时代数据底座的思考——YashanDB亮相2025可信数据库发展大会
  • NIO网络通信基础
  • AndroidX中ComponentActivity与原生 Activity 的区别
  • 关于字符编辑器vi、vim版本的安装过程及其常用命令:
  • 从抓包GitHub Copilot认证请求,认识OAuth 2.0技术
  • web3 区块链技术与用
  • 基于深度学习的语音识别:从音频信号到文本转录
  • 开源的大语言模型(LLM)应用开发平台Dify
  • 如何用Python并发下载?深入解析concurrent.futures 与期物机制
  • 服务攻防-Java组件安全FastJson高版本JNDI不出网C3P0编码绕WAF写入文件CI链
  • ARM64高速缓存,内存属性及MAIR配置
  • 预测导管原位癌浸润性复发的深度学习:利用组织病理学图像和临床特征
  • Nand2Tetris(计算机系统要素)学习笔记 Project 3
  • sqli(1-8)