简易STL实现 | Deque的实现

news2024/11/25 5:48:09

一种 在内存中存储元素的数据结构,它支持 在两端添加和删除元素(使用循环数组实现)

1、deque的特性(分段deque实现)

1、双端操作: deque支持在前端和后端执行快速的插入和删除操作

2、随机访问: 与list不同,deque允许在常量时间内 对元素进行随机访问。这是因为 deque的内部结构采用分段数组,而不是链表
myDeque[2] 和 myDeque[4] 的访问时间是常量的,不受deque的大小的影响
这是因为std::deque 使用了分段数组的结构,每个分段都是一个固定大小的数组,因此可以直接计算索引的位置,而不需要遍历整个容器

将数据存储 在多个较小的数组(块)中,而不是 单个连续的大数组。这样做的好处是 可以更有效地扩展容量,避免频繁的内存重新分配

#include <iostream>
#include <stdexcept>
#include <vector>

template <typename T>
class Deque {
public:
    Deque() : capacity(1), size(0), frontIndex(0), backIndex(0) {
        blocks.push_back(new T[blockSize]); // 每次新分配小块数组都是固定大小(都是blockSize,就是多少个的问题)
    }

    ~Deque() {
        clear();
        for (auto block : blocks) {
            delete[] block;
        }
    }

    void push_back(const T& e) {
        if (size == capacity) {
            resize();
        }
        blocks[backIndex / blockSize][backIndex % blockSize] = e; // 核心思路
        backIndex = (backIndex + 1) % capacity;
        size++;
    }

    void push_front(const T& e) {
        if (size == capacity) {
            resize();
        }
        frontIndex = (frontIndex - 1 + capacity) % capacity; // 思路一致,在不需要分块的代码的基础上(见后面)加一个在哪个块的计算
        blocks[frontIndex / blockSize][frontIndex % blockSize] = e;
        size++;
    }

    void pop_back() {
        if (size == 0) {
            throw std::out_of_range("Deque is empty");
        }
        size--;
        backIndex = (backIndex - 1 + capacity) % capacity;
    }

    void pop_front() {
        if (size == 0) {
            throw std::out_of_range("Deque is empty");
        }
        size--;
        frontIndex = (frontIndex + 1) % capacity;
    }

    void clear() {
        while (size > 0) {
            pop_back();
        }
    }

    size_t getSize() const {
        return size;
    }

    T& operator[](size_t index) {
        if (index >= size) {
            throw std::out_of_range("index out of range");
        }
        size_t realIndex = (frontIndex + index) % capacity;
        return blocks[realIndex / blockSize][realIndex % blockSize];
    }

    void printElement() const {
        if (size == 0) {
            std::cout << "empty";
        } else {
            size_t cur = frontIndex;
            for (size_t i = 0; i < size; i++) {
                std::cout << blocks[cur / blockSize][cur % blockSize] << " ";
                cur = (cur + 1) % capacity;
            }
        }
        std::cout << std::endl;
    }

private:
    static const size_t blockSize = 64;  // 每个块的大小
    std::vector<T*> blocks;  // 存储块的指针
    size_t capacity;  // 总容量(所有块的总和)
    size_t size;  // 当前元素数量
    size_t frontIndex;  // 队首索引
    size_t backIndex;  // 队尾索引

    void resize() { // 核心思路
        size_t newCapacity = 2 * capacity;
        size_t newBlockCount = (newCapacity + blockSize - 1) / blockSize;
        blocks.resize(newBlockCount);
        for (size_t i = capacity / blockSize; i < newBlockCount; i++) {
            blocks[i] = new T[blockSize];
        }

        // 重新排列元素
        if (frontIndex > backIndex) {
            size_t shift = newCapacity - capacity;
            for (size_t i = 0; i < backIndex; i++) {
                blocks[(i + shift) / blockSize][(i + shift) % blockSize] = blocks[i / blockSize][i % blockSize];
            }
            backIndex += shift;
        }

        capacity = newCapacity;
    }
};

块数组的引入:
使用 std::vector<T*> blocks 来存储每个块的指针。每个块的大小由 blockSize 决定。每次新分配小块数组都是固定大小(都是blockSize,就是多少个的问题)

push_back 和 push_front:(pop跟不是块数组一致)
当插入新元素时,确定插入位置是否需要跨块,如果需要,则计算正确的块索引和块内的偏移量(思路一致,在不需要分块的代码的基础上(见后面)加一个在哪个块的计算)

resize:
当当前容量不足时,resize 函数会将容量扩展到原来的两倍,并重新分配块
如果 frontIndex 在 backIndex 后面(即队列在数组中是环形排列的),则需要将后半部分元素移至新位置

void resize() { // 核心思路
        size_t newCapacity = 2 * capacity;
        size_t newBlockCount = (newCapacity + blockSize - 1) / blockSize;
        blocks.resize(newBlockCount);
        for (size_t i = capacity / blockSize; i < newBlockCount; i++) {
            blocks[i] = new T[blockSize];
        }

        // 重新排列元素
        if (frontIndex > backIndex) {
            size_t shift = newCapacity - capacity;
            for (size_t i = 0; i < backIndex; i++) {
                blocks[(i + shift) / blockSize][(i + shift) % blockSize] = blocks[i / blockSize][i % blockSize];
            }
            backIndex += shift;
        }

        capacity = newCapacity;
    }

分段数组:Deque 内部的元素存储在多个小块数组(即“blocks”)中,而不是单一的大数组中。每个块的大小是固定的(由 blockSize 定义)。
动态扩展:当 Deque 需要存储更多元素时,resize() 函数会动态增加存储空间
size_t newCapacity = 2 * capacity;:动态扩展时,通常会将容量加倍以减少未来的扩展次数
size_t newBlockCount = (newCapacity + blockSize - 1) / blockSize;:计算扩展后的块数量 newBlockCount。每个块的大小是 blockSize,因此总块数为 newCapacity 除以 blockSize。(newCapacity + blockSize - 1) 用来确保向上取整,这样可以容纳所有的新元素
blocks.resize(newBlockCount);:blocks 是存储块指针的 std::vector,通过 resize 将 blocks 的大小调整为新的块数 newBlockCount

for (size_t i = capacity / blockSize; i < newBlockCount; i++) {
    blocks[i] = new T[blockSize];
}

为新增的块分配内存。循环遍历 blocks 中的新块区域,为每个新块分配大小为 blockSize 的数组

if (frontIndex > backIndex) {
    size_t shift = newCapacity - capacity;
    for (size_t i = 0; i < backIndex; i++) {
        blocks[(i + shift) / blockSize][(i + shift) % blockSize] = blocks[i / blockSize][i % blockSize];
    }
    backIndex += shift;
}

检测队列是否环绕:frontIndex > backIndex 表示当前的队列元素在数组中是环绕排列的(即头部索引在尾部索引后面)
计算偏移量 shift:新旧容量之间的差值 shift 是新元素需要移动的步数
元素重新排列:将当前 blocks 中 backIndex 前的元素依次复制到新的位置。新的位置是在旧的位置上加上 shift 偏移后的索引。通过这个操作,确保环绕的部分正确地移动到新的空间中(将环绕的部分(即 backIndex 之前的部分)移动到新的存储空间的后半部分,以使队列不再环绕),保持 Deque 的正确顺序
更新 backIndex:将 backIndex 也相应地向前移动 shift 个位置,以反映它在新扩展后的块数组中的位置

访问和打印:
operator[] 访问时,通过计算得到正确的块索引和块内偏移量
printElement 函数遍历元素,并以正确的顺序打印出来

3、动态扩展: deque的大小可以动态调整,无需 事先分配固定大小的内存。这使得 deque 适用于需要动态增长和缩小的情况

4、内存局部性: deque的内部结构利用了多个缓冲区,有助于提高内存局部性,从而在某些情况下提供更好的性能(指程序在执行过程中访问的内存地址 具有一定的规律性,即在时间和空间上相邻的内存地址被频繁访问)
与单一的大数组相比,deque 的分段数组结构 使得每个缓冲区的大小固定且较小。这样,即使需要扩展,也不会频繁地移动大量内存数据,只需操作当前使用的缓冲区即可,这降低了缓存未命中的可能性
分段数组结构 使得最近访问的缓冲区和其中的元素很可能会被缓存,从而在访问这些数据时 具有较高的缓存命中率,这在需要频繁操作队列的前后端时尤其明显

2、deque 工作原理

backIndex指向的位置是当前末尾元素的下一个位置, 也就是还没有有效的数据(如果容量满了, 其会指向frontIndex的位置)
在这里插入图片描述

3、实现 deque(不分段)

#include <iostream>
#include <stdexcept>
#include <string>
#include <sstream>

template <typename T>
class Deque {
public:
    Deque() : elements(nullptr), capacity(0), size(0), frontIndex(0), backIndex(0) {}

    ~Deque() {
        clear();
        delete[] elements;
    }

    void push_back(const T& e) {
        if (size == capacity) {
            resize();
        }
        elements[backIndex] = e;
        backIndex = (backIndex + 1) % capacity;
        size++;
    }

    void push_front(const T& e) {
        if (size == capacity) {
            resize();
        }
        size_t newFrontIndex = (frontIndex - 1 + capacity) % capacity; // 为了防止数组索引为负, 使其指向尾端
        frontIndex = newFrontIndex;
        elements[frontIndex] = e;
        size++;
    }

    void pop_back() { // 不需要负责内存的释放,内存的释放由析构函数解决
        if (size == 0) {
            throw std::out_of_range("Deque is empty");
        }
        size--;
        backIndex = (backIndex - 1 + capacity) % capacity;
    }

    void pop_front() {
        if (size == 0) {
            throw std::out_of_range("Deque is empty");
        }
        size--;
        frontIndex = (frontIndex + 1) % capacity;
    }

    void clear() {
        while (size > 0) {
            pop_back(); // 可以复用
        }
    }

    size_t getSize() {
        return size;
    }

    T& operator[](int index) { // 参数必须是int,不然就有可能越界(size_t转换负值为未知正整数)
        if (index < 0 || index >= size) {
            throw std::out_of_range("index out of range");
        }
        return elements[(frontIndex + index) % capacity]; // 注意下标不是直接是index
    }

    void printElement() {
        if (size == 0) {
            std::cout << "empty";
        }
        size_t cur = frontIndex;
        for (size_t i = 0; i < size; i++) {
            std::cout << elements[cur] << " ";
            cur = (cur + 1) % capacity;
        }
        std::cout << std::endl;

    }
private:
    T* elements;
    size_t capacity;
    size_t size;
    size_t frontIndex;
    size_t backIndex;

    void resize() {
        size_t newCapacity = (capacity == 0) ? 1 : 2 * capacity;
        T* newElements = new T[newCapacity];
        size_t cur = frontIndex;
        for (size_t i = 0; i < size; i++) {
            newElements[i] = elements[cur];
            cur = (cur + 1) % capacity;
        }
        delete[] elements;
        elements = newElements;
        frontIndex = 0;
        backIndex = size;
        capacity = newCapacity;
    }


}; // 类定义后别忘了;

int main() {
    Deque<int> myDeque;
    int N;
    std::cin >> N;
    getchar();
    for (int i = 0; i < N; i++) {
        std::string line;
        std::getline(std::cin, line);
        std::istringstream iss(line);
        std::string command;
        iss >> command;
        if (command == "push_back") {
            int num;
            iss >> num;
            myDeque.push_back(num);
        }
        else if (command == "push_front") {
            int num;
            iss >> num;
            myDeque.push_front(num);
        }
        else if (command == "pop_back") { // pop要判断是否为空
            if (myDeque.getSize() == 0) {
                continue;
            }
            myDeque.pop_back();
        }
        else if (command == "pop_front") {
            if (myDeque.getSize() == 0) {
                continue;
            }
            myDeque.pop_front();
        }
        else if (command == "clear") {
            myDeque.clear();
        }
        else if (command == "size") {
            std::cout << myDeque.getSize() << std::endl;
        }
        else if (command == "get") {
            size_t pos;
            iss >> pos;
            std::cout << myDeque[pos] << std::endl;
        }
        else if (command == "print") {
            myDeque.printElement();
        }
    }

    return 0;
}

和 C++ STL标准库实现版本的区别:
1、标准库中的 std::deque(双端队列)通常是通过 多个连续存储区域(即一维数组)来实现的,而不是单一的连续数组, 这多个一维数组 连接起来形成了deque数组, 目前的实现采用的是循环数组, 缺点就是resize时要复制旧数组, 而官方的std::deque只需要再串联一个一维数组就可以了, 效率更高

2、内存管理: 在实际的 STL 中,内存管理通常会更加复杂和高效。STL 会使用分配器(allocator)来管理内存,而不是直接使用 new 和 delete。分配器允许用户提供自定义的内存管理策略

3、迭代器和算法支持: STL 提供了丰富的迭代器和算法支持,允许通用的方式操作容器
迭代器的支持
输入迭代器:只能进行读操作,且只能单向遍历

#include <iostream>
#include <iterator>
#include <numeric>

int main() {
    std::istream_iterator<int> input(std::cin), eof;
    int sum = std::accumulate(input, eof, 0);
    std::cout << "Sum: " << sum << std::endl;
    return 0;
}

输出迭代器:只能进行写操作,且只能单向遍历

#include <iostream>
#include <iterator>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::ostream_iterator<int> output(std::cout, " ");
    std::copy(vec.begin(), vec.end(), output);
    std::cout << std::endl;
    return 0;
}

前向迭代器:可以读写操作,并且可以多次遍历

#include <iostream>
#include <algorithm>
#include <forward_list>

int main() {
    std::forward_list<int> flist = {1, 2, 3, 2, 1};
    std::replace(flist.begin(), flist.end(), 2, 10);
    for (int n : flist) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

双向迭代器:可以前后遍历,例如 list 容器的迭代器

#include <iostream>
#include <algorithm>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};
    std::reverse(lst.begin(), lst.end());
    for (int n : lst) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

随机访问迭代器:支持常数时间内的随机访问操作,例如 vector 和 deque 容器的迭代器

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {5, 3, 9, 1, 7};
    std::sort(vec.begin(), vec.end());
    if (std::binary_search(vec.begin(), vec.end(), 3)) {
        std::cout << "Found!" << std::endl;
    }
    return 0;
}

算法的支持
排序算法:std::sort 可以对任何随机访问迭代器(如 vector、deque)容器进行排序
搜索算法:std::find 可以在线性时间内在任何容器中查找特定元素
修改算法:std::transform 可以对容器中的每个元素应用指定的函数并存储结果
复制算法:std::copy 可以将一个容器的内容复制到另一个容器中

4、性能优化: 实际的 STL 库通常会进行更多的性能优化,包括使用更复杂的数据结构、考虑缓存友好性等

5、标准兼容性: 实际的 STL 遵循 C++ 标准,确保在不同的平台和编译器上的一致行为。它还提供了许多其他功能,如分配器的自定义、迭代器标签(iterator tags)等

4、高频面试题

1、deque 和 vector 的主要区别是什么
内存分配:vector 使用单一连续的内存空间来存储元素,而 deque 使用多个分散的内存块。这意味着 deque 可以在两端进行高效的插入和删除操作,而 vector 只能在尾部高效地进行这些操作
插入效率:在 deque 的前端和后端插入或删除元素的操作效率很高,但在中间插入或删除元素效率较低,因为它需要移动多个内存块中的元素。而 vector 在尾部插入和删除元素高效(头部不行),但在前端或中间进行这些操作效率较低,因为可能需要移动大量元素
随机访问:vector 提供了更快的随机访问性能,因为它使用连续内存,而 deque 由于其分散的内存块,随机访问性能稍低
内存耗费:vector 在扩展时可能会有较大的内存重新分配成本,而 deque 因为是分块存储,所以通常不需要大量的内存重新分配

2、使用 deque 实现固定大小滑动窗口

#include <iostream>
#include <deque>

class SlidingWindow {
public:
    SlidingWindow(size_t windowSize) : maxSize(windowSize) {}

    void add(int value) {
        // 如果窗口已满,则删除最早的元素
        if (window.size() == maxSize) {
            window.pop_front();
        }
        // 添加新元素到窗口
        window.push_back(value);
    }

    double getAverage() const {
        if (window.empty()) return 0.0;
        
        double sum = 0;
        for (int value : window) {
            sum += value;
        }
        return sum / window.size();
    }

    void printWindow() const {
        for (int value : window) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }

private:
    std::deque<int> window;
    size_t maxSize;
};

3、在 deque 中使用 [] 操作符和 at() 方法有何区别
operator[] 提供对元素的无检查访问,这意味着如果使用超出范围的索引,它不会抛出异常,而是导致未定义行为
at() 方法提供边界检查的访问。如果指定的索引超出了 deque 的范围,at() 方法会抛出一个 std::out_of_range 异常。因此,at() 比 operator[] 更安全,但可能会略微降低性能
使用 at() 方法可以帮助调试中发现越界错误,而 operator[] 在确定不会越界时可以提供更好的性能

4、在 deque 的前端插入一个元素时,迭代器会发生什么

  1. std::deque 的行为
    std::deque 是一个双端队列,允许在两端快速插入和删除元素。与 std::vector 不同,std::deque 是分段存储的,因此在内部的实现中有多个内存块
    1)前端插入:
    在 std::deque 的前端插入元素时,通常不会导致整个容器的重新分配。但如果前端没有足够的空间(即最前面的块已满),std::deque 可能会在前端分配一个新的块
    由于 std::deque 可能需要移动或重新分配内部的内存块,因此所有的迭代器、引用和指针可能会失效
    2)后端插入:
    在 std::deque 的尾部插入元素时,情况与前端插入类似。如果后端没有足够的空间,可能需要分配新的块,从而导致迭代器、引用和指针失效
  2. std::vector 是一个动态数组,当在尾部插入元素时,其行为如下:
    1)尾部插入:
    如果 std::vector 的当前容量足够,那么插入新元素不会导致迭代器、引用和指针失效
    但是,如果插入新元素导致 std::vector 的容量不足,它会分配更大的内存块,并将现有的元素复制到新分配的内存中。这种情况下,所有指向旧元素的迭代器、引用和指针都会失效
    2)前端或中间插入:
    在 std::vector 的前端或中间插入元素时,即使没有触发容量重新分配,由于元素需要移动以为新元素腾出空间,指向被移动元素的迭代器、引用和指针也会失效

5、 解释 deque 的内部工作机制。它是如何实现 两端插入和删除操作的高效性的
std::deque 维护了一系列定长数组(称为块或缓冲区),并且有一个中央控制器(通常是指针数组)来管理这些块。每个块可以独立地增长和收缩,这意味着当在两端添加或删除元素时,只有相关联的块受到影响

在前端插入:如果第一个块有空间,则在该块的开始插入新元素。如果没有空间,deque 会分配一个新块 并将其链接到现有的块序列中
在后端插入:如果最后一个块有空间,则在该块的末尾插入新元素。如果没有空间,deque 会分配一个新块 并将其链接到现有的块序列中
在前端删除:移除第一个块的第一个元素。如果该块变空,则释放该块资源 并更新中央控制器
在后端删除:移除最后一个块的最后一个元素。如果该块变空,则释放该块资源 并更新中央控制器

6、deque 的构造函数

std::deque<int> dq; // 创建一个空的 deque

std::deque<int> dq(10, 5); // 10个元素,每个都是5

std::vector<int> vec{1, 2, 3, 4, 5};
std::deque<int> dq(vec.begin(), vec.end()); // 复制给定迭代器范围内的元素

std::deque<int> dq1(10, 5);
std::deque<int> dq2(dq1); // 拷贝构造函数

std::deque<int> dq1(10, 5);
std::deque<int> dq2(std::move(dq1)); // 移动构造函数

std::deque<int> dq{1, 2, 3, 4, 5}; // 使用一个初始化列表来创建

// 带有分配器的默认构造函数:创建一个空的 deque,但是使用自定义的分配器来分配内存
std::allocator<int> alloc;
std::deque<int, std::allocator<int>> dq(alloc);

std::allocator<int> alloc;
std::deque<int, std::allocator<int>> dq(10, 5, alloc); // 10个元素,每个都是5,使用自定义分配器

std::vector<int> vec{1, 2, 3, 4, 5};
std::allocator<int> alloc;
std::deque<int, std::allocator<int>> dq(vec.begin(), vec.end(), alloc);

std::deque<int> dq1(10, 5);
std::allocator<int> alloc;
std::deque<int, std::allocator<int>> dq2(dq1, alloc); // 拷贝dq1,使用自定义分配器

std::deque<int> dq1(10, 5);
std::allocator<int> alloc;
std::deque<int, std::allocator<int>> dq2(std::move(dq1), alloc); // 移动dq1,使用自定义分配器

std::allocator<int> alloc;
std::deque<int, std::allocator<int>> dq({1, 2, 3, 4, 5}, alloc);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2093014.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Colorfy v3.26 — 修改版,超过2000种图片涂色

Colorfy 是一款适用于安卓设备的涂色书应用&#xff0c;提供了超过 2000 种不同的图片供用户涂色&#xff0c;包括动物、花卉、城市风景等。每日更新新图&#xff0c;支持各种颜色和绘画工具&#xff0c;如水彩笔、铅笔、细线笔等。用户可以放大绘画区域进行精细绘画。此版本由…

Transformer-LSTM神经网络多输入单输出回归预测的MATLAB实现

在现代机器学习和深度学习领域&#xff0c;处理多维时序数据的需求越来越多。在这些应用场景中&#xff0c;我们常常面临需要同时处理多个输入变量&#xff0c;并预测一个输出变量的任务。这类问题通常被称为多输入单输出回归问题。为了有效地应对这类问题&#xff0c;结合 Tra…

在浮躁的时代,保持冷静和沉稳,心怀勇气

在大家阅读之前&#xff0c;我真诚地祝愿大家在浮躁的世界中找到属于自己的宁静与力量&#xff0c;让每一天都充满希望和快乐。 在这个快节奏、信息爆炸的时代&#xff0c;我们每个人都像是被卷入了一个永不停歇的漩涡&#xff0c;不断地被各种声音和画面拉扯着。有时候&#x…

一篇搞懂classpath,resources

Classpath&#xff08;类路径&#xff09;在Java开发中是一个非常重要的概念&#xff0c;它指定了Java虚拟机&#xff08;JVM&#xff09;和Java编译器寻找类文件和包的位置。具体来说&#xff0c;Classpath告诉Java执行环境在哪些目录下可以找到所要执行的Java程序所需要的类或…

如何打造基于Java SpringBoot和Vue的医院门诊智能预约平台?四步实现高效就医流程,整合MySQL数据库,优化用户体验。

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…

【#第三期实战营闯关作业##LMDeploy 量化部署进阶实践 】

今天学习了《LMDeploy 量化部署进阶实践》一课&#xff0c;&#xff0c;收获很大。以下是记录复现过程及截图&#xff1a; 创建一个名为lmdeploy的conda环境 创建成功后激活环境并安装0.5.3版本的lmdeploy及相关包。 相关包install成功 创建好的conda环境并启动InternLM2_5-7…

Java 入门指南:Java 并发编程 —— 线程安全问题与锁机制

线程安全的概念 线程安全可以简单理解为在多线程环境下&#xff0c;一个方法或实例在并发执行时能够保持数据的一致性和完整性&#xff0c;不会因为多个线程的交替执行而导致错误的结果。 线程不安全的原因 线程不安全通常由以下几个原因引起&#xff1a; 原子性问题&#x…

让自家的智能语音助手实现todo任务的添加

我家的树莓派在成为了“智能语音助手”后&#xff0c;经过rasa学习训练&#xff0c;已经可以帮忙查日期/时间&#xff0c;查天气预报&#xff0c;进行一些简单的闲聊。但是&#xff0c;我希望它的功能还可以再强大些&#xff0c;比如说&#xff0c;可以帮我记录todo任务。为了实…

ProtoBuf 详解

1、初识ProtoBuf 序列化的概念&#xff1a; 举个例子&#xff1a; 我们在打电话的过程中&#xff0c;两个人可以通过电话听到对方的声音&#xff0c;这个过程就涉及到了序列化和反序列化。 相同的例子还有&#xff1a;网络通信、数据持久化等 常见的实现方式&#xff1a;JS…

upload-labs靶场通关攻略

一。 1.将一个php文件重命名为jpg文件 2.将这个jpg文件上传然后抓包吧jpg改成php接着放行 3.鼠标右键图片在新建标签页打开图片后去蚁剑连接 二 1.上传jpg文件然后抓包把jpg改成php放行 2.图片右键新建标签页打开图片&#xff0c;复制图片地址去蚁剑连接 三 1.文件重命名为1…

四款AI工具写完毕业论文,这才是真正的写作神器!(含教程)

在当今信息爆炸的时代&#xff0c;AI写作工具的出现极大地提高了写作效率和质量。特别是在学术论文的撰写过程中&#xff0c;AI工具不仅能够帮助快速生成论文草稿&#xff0c;还能优化内容质量、进行查重和排版&#xff0c;提供写作建议和创意灵感&#xff0c;极大地减轻了学者…

ChatGPT的全面写作革命:我们迎来效率飞跃还是创造力危机?

近年来&#xff0c;随着人工智能技术的持续发展&#xff0c;类似于ChatGPT的AI工具开始变革我们的工作模式。从撰写访谈大纲到文章框架&#xff0c;再到设计标题&#xff0c;许多人已逐渐依赖AI的辅助功能。不过&#xff0c;最近一家媒体采取了更加大胆的尝试&#xff1a;完全交…

Python基础 3 - 函数及数据容器

文章目录 一、函数概念1、函数介绍2、函数的定义3、函数的调用4、函数说明文档5、函数嵌套调用6、变量作用域1&#xff09;局部变量2&#xff09;全局变量3&#xff09;声明全局变量 二、数据容器入门1、列表 (list)1) 列表的定义2) 调用列表元素3) 列表的方法4) 列表的特点5) …

债务重组的费用不菲,为什么现在越来越多的人还是愿意做债重?

债务重组&#xff0c;起初是专为优质企业客户量身打造的大额融资解决方案&#xff0c;周期紧凑&#xff0c;一个月见成效。但随着时代变迁&#xff0c;它悄然转型&#xff0c;成了负债繁重、网贷缠身者的“救星”。这类朋友通过债务重组&#xff0c;先是由专业机构垫付月供&…

论文阅读:VideoMamba: State Space Model for Efficient Video Understanding

论文地址&#xff1a;arxiv 摘要 为了解决视频理解中的局部冗余与全局依赖性的双重挑战。作者将 Mamba 模型应用于视频领域。所提出的 VideoMamba 克服了现有的 3D 卷积神经网络与视频 Transformer 的局限性。 经过广泛的评估提示了 VideoMamba 的能力&#xff1a; 在视觉领…

基于 Householder 变换的 qr 分解 算法与源码实现

1&#xff0c;算法描述 1.1 算法1 反射向量 计算 Householder 向量 给定 算法计算满足 v(1) 1.0 的 和 , 使得 是正交矩阵且 , 即&#xff0c;将m维向量 通过反射变换 反射至 轴上去。 1.2 算法2 QR 分解 Householder QR 分解 未完待补。。。。 2&#xff0c;源码…

什么是RS485总线?

1.什么是RS485总线&#xff1f; RS485 是一种通用的通信标准&#xff0c;广泛用于数据采集和控制应用中。 它的主要优点之一是它允许将多个 RS485 设备放在同一条总线上&#xff0c;这使得多个节点可以相互连接。 RS-485是美国电子工业协会&#xff08;EIA&#xff09;在1983年…

2024HarmonyOS应用开发者高级认证最新整理题库和答案(已收录182道 )

更新截止2024-08-27,完整题库一共182道题,足够覆盖90%考题,如有新题和遗漏我会持续补充 所有题目的选项都是打乱顺序的,记答案不要记序号 完整题库请在我的网盘下载或查看在线文档 完整题库在线文档预览 单选(已收录102道) 1 . 以下哪个装饰器用来表示并发共享对象。(B) A. @…

Windows服务器应急响应(下)

目录 介绍步骤 介绍 进程&#xff08;Process&#xff09;是计算机中的程序关于某数据集合上的一次运行活动&#xff0c;是系统进行资源分配和调度的基本单位&#xff0c;是操作系统结构的基础。在早期面向进程设计的计算机结构中&#xff0c;进程是程序的基本执行实体&#x…

基于FPGA的lz4解压缩仿真调试

1、简介 对于任意长度顺序呈现的输入数据流&#xff0c;通过对冗余byte的数据编码&#xff0c;完成数据压缩的问题。数据包格式 从数据包长度可知&#xff0c;最少需要5个字节才能压缩&#xff0c;否则压缩无意义&#xff0c;对于lz其他的介绍可以百度&#xff0c;本文只介绍…