list 的模拟实现

目录

1. list 的实现框架

2. push_back

3. 迭代器

4. constructor

4.1. default

4.2. fill

4.3. range

4.4. initializer list

5. insert

6. erase

7. clear 和 destructor

8. copy constructor

9. operator=

10. const_iterator

10.1. 普通人的处理方案

10.2. SGI-STL的处理方案

11. operator->


1. list 的实现框架

namespace Xq
{template <class T>struct list_node{T _data;list_node<T>* _left;list_node<T>* _right;// default list_node(const T& x = T()):_data(x), _prev(nullptr), _next(nullptr){}};// 带头双向循环链表template<class T>class list{private:typedef list_node<T> Node;public:private:Node* _head;  // 哨兵位头节点};
}

2. push_back

先搭个架子,跑起来再说,push_back 的实现很简单,如下:

// 构造新节点
Node* BuildNewNode(const T& val)
{Node* newnode = new Node(val);return newnode;
}// 尾插
void push_back(const T& val)
{// 1. 构造新节点Node* newnode = BuildNewNode(val);// 2. 找尾Node* tail = _head->_prev;// 3. 添加新节点tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;
}

测试代码如下: 

void Test1(void)
{Xq::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);
}

可是我们发现一个问题,这咋遍历呢? 难道说,像C语言一样,写个 Print 函数,这也太挫了,因此,我们用容器统一访问元素的方式,通过迭代器访问,如下:  

void Test1(void)
{Xq::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);Xq::list<int>::iterator it = lt.begin();while (it != lt.end()){std::cout << *it << " ";++it;}std::cout << std::endl;
}

那么这个迭代器如何实现呢? 

3. 迭代器

首先,在 string 和 vector 的模拟实现中,我们也使用过迭代器,但是它们两个的迭代器很特殊,因为它们存储的元素的地址是连续的,因此,它们的迭代器本质上就是原生指针,那 list 这里能不能也是原生指针呢?

答案:不可以,因为 list 底层的元素的地址可不是连续的,因此,不能是原生指针。

对于 string 和 vector, 因为存储的元素是连续存储的,故可以使用原生指针,同时,原生指针可以满足需求。

对于 list 来说,存储的元素的地址不是连续的,因此,不能使用原生指针,但是 list 又要求能够遍历元素,而元素就是一个一个的节点,即Node*,要遍历元素本质上就是要从当前节点去到下一个节点,既要重载 ++,-- 等操作,可是我们知道,指针属于内置类型,无法重载,因此只能将 Node* 封装到一个类中,这个类,人们起了一个名字,叫迭代器,通过重载迭代器这个类的 ++、-- 等操作,使得可以遍历节点,一般而言,这个自定义类型 (迭代器) 需要满足下面的操作:

加加 (++)、减减 (--)、解引用 (*)、访问成员 (->)、等于 (==)、不等于 (!=) 。

首先这个迭代器的框架如下:

template <class T>
struct list_iterator
{typedef list_node<T> Node;Node* _node;// 下面就是迭代器所要支持的操作:// 加加 (++)、减减 (--)、解引用 (*)、// 访问成员 (->)、等于 (==)、不等于 (!=)
};

 实现如下:

template <class T>struct list_iterator{typedef list_node<T> Node;Node* _node;list_iterator(Node* node = nullptr): _node(node) {}// 前置++, ++itlist_iterator& operator++(){_node = _node->_next;return *this;}// 后置++, it++list_iterator operator++(int){// 后置++返回调用前的状态, 因此这里需要构造一个临时对象, 故只能传值返回// a. 可以使用构造一个迭代器// list_iterator ret(_node);// b. 也可以使用拷贝构造, 在这里, 默认生成的拷贝构造就满足需求// 因为在list_iterator类中,不需要释放这些资源 (_node),默认的析构不会对内置做处理list_iterator ret(*this);_node = _node->_next;return ret;}// 前置--, --it;list_iterator& operator--(){_node = _node->_prev;return *this;}// 后置--, it--list_iterator operator--(int){// 与后置++一个思路, 但在这里使用构造函数list_iterator ret(_node);_node = _node->_prevc;return ret;}// *this != it;bool operator!=(const list_iterator& it){return _node != it._node;}// *this == it;bool operator==(const list_iterator& it){return _node == it._node;}// 返回对象的引用T& operator*(){return _node->_data;}// 返回对象的地址T* operator->(){// 复用 operator*return &(operator*());}};

有了上面,还不足以支持用迭代器遍历数据,容器 (list) 自身也需要提供 begin、end等一些列函数,begin 和 end 在 list 中的位置如下:

下面是一些 list::begin 和 list::end 的相关实现:

iterator begin()
{// 用第一个有效元素构造 begin 迭代器// 这里是单参数隐式类型转换// 先用节点构造迭代器, 在进行拷贝构造// 编译器优化为直接构造return _head->_next;   
}iterator end()
{// 用最后一个有效元素的下一个位置构造 end 迭代器// 本质就是 _head// 这里是单参数隐式类型转换// 先用节点构造迭代器, 在进行拷贝构造// 编译器优化为直接构造return _head;
}

有了上面的支持,此时我们就可以用迭代器遍历链表了,测试用例如下:

void Test1(void)
{Xq::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);Xq::list<int>::iterator it = lt.begin();while (it != lt.end()){std::cout << *it << " ";++it;}std::cout << std::endl;
}

现象如下:

上面是迭代器的初步实现的版本,后续还会有改进。

走到这里,转换一下思路,来实现一些简单的函数,诸如 insert,通过 insert 来实现 push_back,push_front。

4. constructor

4.1. default

// 构造一个哨兵位的头节点
void empty_initialize()
{_head = BuildNewNode(T());_head->_next = _head->_prev = _head;
}
// defalut
list()
{empty_initialize();
}

4.2. fill

list(size_t n, const T& val)
{// 构造哨兵位头节点empty_initialize();// 复用push_backwhile (n--){push_back(val);}
}// 避免和 range constructor 冲突
list(int n, const T& val)
{// 构造哨兵位头节点empty_initialize();// 复用push_backwhile (n--){push_back(val);}
}

4.3. range

template<class InputIterator>
list(InputIterator first, InputIterator last)
{// 构造哨兵位头节点empty_initialize();// 通过一段区间构造一个list对象while (first != last){push_back(*first);++first;}
}

4.4. initializer list

list(const std::initializer_list<T>& lt)
{// 构造哨兵位头节点empty_initialize();// 复用push_backfor (auto& it : lt)push_back(it);
}

5. insert

实现这种函数,建议通过画图来进行编写代码,这样会事半功倍的,即便出错了,也能通过画图和调试很快锁定错误。

iterator insert(iterator position, const T& val);

insert 在 position 位置之前插入特定节点,假如 position 是node3 的位置,先找到 position 位置之前的节点,在链入新节点,同时,返回值就是插入的新节点,如图所示:

 

 代码如下:

iterator insert(iterator pos, const T& val)
{// 得到pos位置的节点Node* cur = pos._node;// 找到pos节点之前的一个节点Node* cur_prev = cur->_prev;// 构造新节点Node* newnode = BuildNewNode(val);// 将新节点链入到list中cur_prev->_next = newnode;newnode->_prev = cur_prev;newnode->_next = cur;cur->_prev = newnode;// 返回新插入的节点return newnode;
}

有了insert,我们就可以通过 insert 实现 push_back 和 push_front。

push_back 是尾插,那么 position 就是在最后一个有效节点的下一个节点,最后一个有效节点的下一个节点不就是 end() 吗?如下所示:

void push_back(const T& val)
{insert(end(), val);
}

push_front是头插,那么 position 就是第一个有效节点,第一个有效节点不就是 begin() 吗?如下所示:

void push_front(const T& val)
{insert(begin(), val);
}

6. erase

iterator erase (iterator position);

erase 是删除 position 位置的特定节点,并返回删除节点的下一个节点, 假如 position 是 node2,如下所示:

 

代码如下: 

iterator erase(iterator pos)
{// pos 不能是哨兵位头节点assert(pos != end());// 找到要删除的节点Node* del = pos._node;// 找到删除节点的前一个节点Node* del_prev = del->_prev;// 找到删除节点的后一个节点Node* del_next = del->_next;// 将删除节点从list移除del_prev->_next = del_next;del_next->_prev = del_prev;// 释放删除节点, 此时pos就迭代器失效了delete del;// 隐式类型转换return del_next;
}

有了erase,pop_back,pop_front 就简单多了。

pop_back,删除最后一个有效节点,那么最后一个有效节点不就是哨兵位头节点的前一个节点吗?如下所示:

void pop_back()
{// 删除最后一个有效节点// 隐式类型转换erase(_head->_prev);
}

pop_front,删除第一个有效节点,那么第一个有效节点不就是哨兵位头节点的后一个节点吗?如下所示:

void pop_front()
{// 删除第一个有效节点// 隐式类型转换erase(_head->_next);
}

7. clear 和 destructor

clear 就是删除 list 的所有有效节点 (不包括哨兵位头节点),实现如下:

void clear(void)
{Node* del = _head->_next;if (del == _head) return;while (del != _head){Node* next = del->_next;erase(del);del = next;}
}

我们也可以用迭代器删除,如下:

void clear(void)
{iterator it = begin();while (it != end())erase(it++);
}

destructor 在有了 clear 就简单多了,本质上就是调用 clear 释放所有有效节点,并将 list 的哨兵位头节点释放掉即可,实现如下:

~list()
{clear();delete _head;_head = nullptr;
}

8. copy constructor

我们知道,如果我们没有显示定义copy constructor,那么编译器就会默认生成一份,默认生成的对内置类型和自定义类型都会处理,对内置类型按照浅拷贝的方式进行拷贝,对自定义类型会去调用它的copy constructor;对于list<T>来说,编译器默认生成的是不符合需求的的,它会带来两个问题:

  • 其中一个对象发生修改,另一个对象也会发生改变;
  • 当这两个对象生命周期结束时,会调用析构函数,同一空间被析构两次,进程crash。

其中一个对象发生修改,另一个对象也会发生改变,如下 demo : 

void Test4(void)
{Xq::list<int> lt{ 1, 2, 3, 4 };Xq::list<int> copy(lt);std::cout << "original lt:";for (auto it : lt)std::cout << it << " ";std::cout << "\n";std::cout << "copy modify: ";for (auto& it : copy)std::cout << (it *= 2) << " ";std::cout << "\n";std::cout << "new lt:";for (auto it : lt)std::cout << it << " ";std::cout << "\n";
}

lt {1, 2, 3, 4},通过 it 拷贝构造得到 copy, copy {1, 2, 3, 4},copy 遍历一遍,每个元素 *= 2,即 copy {2, 4, 6, 8},那么此时 lt 是什么呢?

预期:因为是浅拷贝,此时 lt 也会变为 {2, 4, 6, 8},之所以这里没有崩溃,是因为我将析构给屏蔽了 (即此时析构什么事也不做),现象如下:

符合预期,如果我此时恢复析构函数,那么当这两个对象生命周期结束时,先后调用析构函数,同一空间被析构两次,进程崩溃,现象如下:

因此,针对 list ,用户需要自身以深拷贝的方式实现拷贝构造,实现如下:

void swap(list<T>& tmp)
{// 交换两个哨兵位头节点// 即交换两个list对象std::swap(_head, tmp._head);
}template<class InputIterator>
list(InputIterator first, InputIterator last)
{// 构造哨兵位头节点empty_initialize();// 通过一段区间构造一个list对象while (first != last){push_back(*first);++first;}
}		list(const list<T>& copy)
{// 构造哨兵位头节点empty_initialize();// 通过 copy 构造 tmplist<T> tmp(copy.begin(), copy.end()); // 再和tmp交换哨兵位的头节点swap(tmp);// tmp 生命周期结束, 自动调用析构, 释放哨兵位的头节点
}

上面这套方案,是不是有点繁琐? 用户也可以采用下面的方案:

list(const list<T>& copy)
{// 构造哨兵位头节点empty_initialize();// 通过迭代器直接复用 push_backfor (const auto& it : copy){push_back(it);}
}

之所以上面可以,是因为有我们之前所做的努力,无非就是复用罢了。

9. operator=

同理,operator=是不是也存在着和拷贝构造同样的问题,因此我们也需要以深拷贝的方式实现operator=,实现如下:

// 在类里面可以不写模板参数, 
// 但是建议不管是类外还是类内都把模板参数加上
// void swap(list& copy)   // 不建议// 交换两个list的哨兵位的头节点
void swap(list<T>& copy)
{std::swap(_head, copy._head);
}// 利用传值传参的特性 --- 会进行拷贝构造
list<T>& operator=(list<T> copy)
{// 交换 copy 和 *this 两个list的头节点swap(copy);// 返回赋值后的list对象return *this;// copy出了函数作用域, 会自动调用析构函数, 释放资源
}

10. const_iterator

我们上面不是已经实现了一个迭代器吗,为什么还要有 const_iterator 呢? 

上面我们所实现的迭代器称之为普通迭代器,而在一些场景下,普通迭代器不能满足需求,比如下面这个例子:

void print_list(const Xq::list<int>& tmp)
{Xq::list<int>::iterator it = tmp.begin();while (it != tmp.end()){cout << *it << " ";++it;}cout << endl;
}void Test5(void)
{Xq::list<int> lt{ 1, 2, 3, 4 };print_list(lt);
}

因为我们目前没有提供 const 迭代器,上面的代码会编译报错,如下:

那么如何解决呢? 很简单,我们提供一个const迭代器就OK了,但是不同的人会有不同的处理,在这里有两种处理方案:普通人的处理方案和SGI-STL的处理方案,具体如下:

10.1. 普通人的处理方案

作为普通人的我 (🐂🐎),我想到的就是这种方案 (惭愧~~~)。

处理很简单,照着普通迭代器的模样,再写一份 const 迭代器就OK了,如下:

template <class T>
struct const_list_iterator
{typedef list_node<T> Node;Node* _node;const_list_iterator(Node* node = nullptr) : _node(node) {}// 前置++, ++itconst_list_iterator& operator++(){_node = _node->_next;return *this;}// 后置++, it++const_list_iterator operator++(int){const_list_iterator ret(*this);_node = _node->_next;return ret;}// 前置--, --it;const_list_iterator& operator--(){_node = _node->_prev;return *this;}// 后置--, it--const_list_iterator operator--(int){const_list_iterator ret(_node);_node = _node->_prevc;return ret;}bool operator!=(const const_list_iterator& it){return _node != it._node;}bool operator==(const const_list_iterator& it){return _node == it._node;}// 返回const 对象的引用const T& operator*(){return _node->_data;}// 返回对象的地址const T* operator->(){// 复用 operator*return &(operator*());}
};

上面是一份 const 迭代器,同时,容器自身也需要提供相应的begin和end,如下: 

const_iterator begin() const
{// 用第一个有效元素构造 begin 迭代器return _head->_next;
}const_iterator end() const
{// 用最后一个有效元素的下一个位置构造 end 迭代器// 本质就是 _headreturn _head;
}

可能会有人有这样的疑惑,我们以 begin 举例,如下: 

iterator begin()
{return _head->_next;   
}const_iterator begin() const
{return _head->_next;
}

这两个函数构成函数重载吗,答案,构成,为什么呢? 因为它们参数的类型不一样,因为每一个非静态的成员函数都有一个 this 指针。

  • 作为普通类型的 this 指针类型为:iterator* const this;
  • 作为const类型的 this 指针类型为: const const_iterator* const this。

因此,两个函数的this指针类型不一样,即参数类型不一样,故构成函数重载,end 同理。

这就是普通处理 const 迭代器的方案,粗暴的复用 (大量相同重复的逻辑),没有一点技术含量,非常挫,下面来看看高手怎么玩的呢?

10.2. SGI-STL的处理方案

就普通迭代器和const 迭代器,我们发现,这两个类除了类名不一样,其实核心点就在两个地方放生了改变,那两个地方呢?

一个就是 operator*()、另一个就是 operator->();

如果是普通迭代器的 operator* 和 operator->,如下:

// 返回普通对象的引用
T& operator*()
{return _node->_data;
}// 返回普通对象的地址  
T* operator->()
{// 复用 operator*return &(operator*());
}

如果是 const 迭代器的 operator* 和 operator->(),如下:

// 返回const 对象的引用
const T& operator*()
{return _node->_data;
}// 返回 const 对象的地址
const T* operator->()
{// 复用 operator*return &(operator*());
}

可以发现,它们的主要区别就在于返回值的类型不同罢了,因此,高手们就想到了用模板参数来解决这个问题,通过类模板参数,来进行泛型化,如下:

template <class T, class Ref, class Ptr>
struct list_iterator
{// 返回对象的 Ref (reference)Ref operator*(){return _node->_data;}// 返回对象的 Ptr (pointer)Ptr operator->(){// 复用 operator*return &(operator*());}
};

在 list 容器中,通过迭代器的类型决定 Ref 和 Ptr 是什么,进而决定这个 list_iterator 类模板实例化成什么具体的模板类,具体来说:

  • 如果是普通类型的迭代器,那么Ref 就是 T&,Ptr 就是 T*;
  • 如果是 const 类型的迭代器,那么 Ref 就是 const T&, Ptr 就是 const T*。

用代码来说,如下所示:

template<class T>
class list
{
public:// 如果你是普通迭代器, 那么Ref就是T&, Ptr就是T*typedef list_iterator<T, T&, T*> iterator;// 如果你是const迭代器, 那么Ref就是const T&, Ptr就是const T*    typedef list_iterator<T, const T&, const T*> const_iterator;;// ...
}

可以看到, iterator 和 const_iterator 这两个类,通过通过模板参数达到泛型化,进而解决了大量相符重复代码的问题,这就是高手的处理方案,很神奇。

我们也可以用下图来理解一下这个过程:

 ​​​​​​​

11. operator->

在这里需要强调一下operator->。

首先,为什么需要 operator-> 呢? -> 这个操作费我们是经常使用的,称之为访问成员操作符,在有些场景下,我们是需要通过 -> 访问成员的。

假如有这样的一个类型,如下:

struct A
{A(int a = 0, int b = 0) :_a(a), _b(b) {}int _a;int _b;
};

场景如下:

void Test6(void)
{Xq::list<A> lt{ { 1, 1 }, { 2, 2, }, { 3, 3, }, { 4, 4, } };for (const auto& it : lt){std::cout << *it << std::endl;}
}

如果这个类型 (struct A),没有实现 operator<<,那么这里就会编译报错,因为 A 是一个自定义类型,如下:

如果不想实现 operator<< ,那么这里就可以用 ->,如下:

void Test6(void)
{Xq::list<A> lt{ { 1, 1 }, { 2, 2, }, { 3, 3, }, { 4, 4, } };Xq::list<A>::iterator it = lt.begin();while (it != lt.end()){cout << it->_a << " " << it->_b << endl;++it;}cout << endl;
}

那么 operator-> 如何实现呢?

operator-> 返回的是当前数据的指针,因此,我们可以复用 operator*,如下:

template<class T, class Ref, class Ptr>
struct list_iterator
{// 返回数据的指针Ptr operator->(){return &(operator*());//等价于return &(_node->_data);}// ...
};

不过要在这里解释一下,这里的 -> 是如何调用的:

std::cout << it->_a << " " << it->_b << std::endl;
// 上面也可以这样写:
std::cout << it.operator->()->_a << " " << it.operator->()->_b << std::endl;// it->_a 实际上是 it->->_a, 第一个 -> 是调用operator->, 第二个 -> 才是访问成员属性
// it-> 相当于 operator->, 返回一个Ptr, 返回迭代器里面数据的地址 (T* 或者 const T*)
// Ptr->_a 或者 Ptr->_b, 因此说 it->_a 实际上是 it->->_a
// 但是语法为了可读性, 编译器进行了特殊处理, 省略了一个->, 即 it->_a

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

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

相关文章

运维的边缘计算

运维的边缘计算是指在靠近物或数据源头的一侧&#xff0c;采用网络、计算、存储、应用核心能力为一体的开放平台&#xff0c;进行运维管理和服务的计算模式。具体来说&#xff0c;边缘计算在运维领域的应用主要体现在以下几个方面&#xff1a; 超低时延&#xff1a;在传统的云…

数据库复习1

1.试述数据、数据库、数据库管理系统、数据库系统的概念 1.数据(Data): 数据是关于事物的符号表示或描述。它可以是任何事实、观察或者测量的结果&#xff0c;如数字、字符、声音、图像等。数据在没有上下文的情况下可能没有明确的意义。 2.数据库(Database): 数据库是一个持…

Linux——socket编程之tcp通信

前言 前面我们学习socket的udp通信&#xff0c;了解到了socket的概念与udp的实现方法&#xff0c;今天我们来学习一下面向连接的tcp通信。 一、tcp套接字创建 UDP和TCP都是通过套接字&#xff08;socket&#xff09;来实现通信的&#xff0c;因此TCP也得使用socket()接口创建…

时间复杂度_空间复杂度

时间复杂度_空间复杂度 1.算法效率 算法效率分析分为两种:第一种是时间效率&#xff0c;第二种是空间效率。 时间效率被称为时间复杂度&#xff0c;而空间效率被称作空间复杂度。时间复杂度主要衡量的是一个算法的运行速度&#xff0c;而空间复杂度主要衡量一个算法所需要的…

vue什么是服务端渲染(SSR)

Vue服务端渲染&#xff08;SSR&#xff09;在优化SEO方面有着显著的优势&#xff0c;因为它允许搜索引擎直接访问服务器渲染的页面&#xff0c;从而更容易解析页面内容。以下是一些关于如何使用Vue SSR优化SEO的建议&#xff1a; 为每个URL生成静态HTML&#xff1a;Vue SSR允许…

达摩院 2025届暑期实习 大模型算法

文章目录 写在前面一面/技术面 2024/4/7 晚上19:00-20:00二面/技术面 2024/4/23 早上11:15-12:15 写在前面 学校情况&#xff1a;211本中9硕&#xff0c;本硕都是计算机科班&#xff0c;但研究方向并不是NLP&#xff0c;而是图表示学习论文情况&#xff1a;1A(NeurIPS)1B(ICDM…

C#技巧之同步与异步

区别 首先&#xff0c;同步就是程序从上往下顺序执行&#xff0c;要执行完当前流程&#xff0c;才能往下个流程去。 而异步&#xff0c;则是启动当前流程以后&#xff0c;不需要等待流程完成&#xff0c;立刻就去执行下一个流程。 同步示例 创建一个窗体&#xff0c;往窗体里…

面试 Java 基础八股文十问十答第二十八期

面试 Java 基础八股文十问十答第二十八期 作者&#xff1a;程序员小白条&#xff0c;个人博客 相信看了本文后&#xff0c;对你的面试是有一定帮助的&#xff01;关注专栏后就能收到持续更新&#xff01; ⭐点赞⭐收藏⭐不迷路&#xff01;⭐ 1&#xff09;动态代理是什么&am…

2131 - 枚举-练习-涂国旗

2131 - 枚举-练习-涂国旗 c刷题 超能力编程 分析 枚举涂w的底边和涂b的底边即可 剩下的部分都涂r 数据范围这么小,暴力枚举&#xff0c;代码简单难度低。搜索什么的用不着啦&#xff01; 那么问题来了&#xff1a;怎么枚举呢&#xff1f; 我们只要枚举白与蓝、蓝与红的边界&…

【SQL基础】mysql中如何将日期时间类型转换为日期类型

在MySQL中&#xff0c;将DATETIME类型的数据转换为日期格式可以通过使用DATE()函数来实现。DATE()函数可以从DATETIME或TIMESTAMP类型的值中提取出日期部分。 以下是几种将DATETIME转换为日期格式的示例&#xff1a; 直接转换DATETIME列为日期&#xff1a; SELECT DATE(date…

Containerd方式部署K8s集群

1.1 Kubernetes基础环境部署 kubernetes有多种部署方式&#xff0c;目前主流的方式有kubeadm、minikube、二进制包 minikube&#xff1a;一个用于快速搭建单节点kubernetes的工具 kubeadm&#xff1a;一个用于快速搭建kubernetes集群的工具 二进制包 &#xff1a;从官网下载…

【DPU系列之】DPU中的ECPF概念是什么?全称是什么?(E CPF对标H CPF;embedded CPU function ownership)

ECPF&#xff1a;embedded CPU function ownership。 嵌入式CPU运转ownership。也叫DPU模式&#xff0c;是DPU工作运转3种模式之一&#xff0c;也是默认的模式。这里的嵌入式CPU指的是DPU上ARM CPU&#xff0c;表示网卡所有资源和功能被embedded CPU全权管理&#xff0c;行使所…

【动态规划】投资问题

本文利用markdown基于https://blog.csdn.net/qq_41926985/article/details/105627049重写,代码部分为本人编辑 代码要求 应用动态规划方法&#xff0c;求解投资问题&#xff0c;实现下面的例子。 #define MAX_N 4 //最大投资项目数目 #define MAX_M 5 //最大投资钱数(万元) /…

【机器视觉】yolo-world-opencvsharp-.net4.8 C# 窗体应用程序

这段代码是基于 OpenCvSharp, OpenVinoSharp 和 .NET Framework 4.8 的 Windows Forms 应用程序。其主要目的是加载和编译机器学习模型&#xff0c;对输入数据进行推理&#xff0c;并显示结果。 下面是该程序的主要功能和方法的详细总结&#xff1a; 初始化 OpenVINO 运行时核心…

基于Pytorch深度学习——卷积神经网络(卷积层/池化层/多输入多输出通道/填充和步幅/)

本文章来源于对李沐动手深度学习代码以及原理的理解&#xff0c;并且由于李沐老师的代码能力很强&#xff0c;以及视频中讲解代码的部分较少&#xff0c;所以这里将代码进行尽量逐行详细解释 并且由于pytorch的语法有些小伙伴可能并不熟悉&#xff0c;所以我们会采用逐行解释小…

【DPU系列之】如何通过带外口登录到DPU上的ARM服务器?(Bluefield2举例)

文章目录 1. 背景说明2. 详细操作步骤2.1 目标拓扑结构2.2 连接DPU带外口网线&#xff0c;并获取IP地址2.3 ssh登录到DPU 3. 进一步看看系统的一些信息3.1 CPU信息&#xff1a;8核A723.2 内存信息 16GB3.3 查看ibdev设备 3.4 使用小工具pcie2netdev查看信息3.5 查看PCIe设备信息…

python笔记:gensim进行LDA

理论部分&#xff1a;NLP 笔记&#xff1a;Latent Dirichlet Allocation &#xff08;介绍篇&#xff09;-CSDN博客 参考内容&#xff1a;DengYangyong/LDA_gensim: 用gensim训练LDA模型&#xff0c;进行新闻文本主题分析 (github.com) 1 导入库 import jieba,os,re from ge…

安卓手机APP开发__用媒体会话服务进行后台播放

安卓手机APP开发__媒体开发部分__用媒体会话服务进行后台播放 目录 概述 使用一个媒体会话服务 实现服务的生命周期 提供对媒体会话的读取 在配置文件中声明服务 概述 当APP不在前台时&#xff0c;经常希望能够播放媒体。例如&#xff0c;一个音乐播放器 在用户锁屏或者…

【云原生】Docker 的网络通信

Docker 的网络通信 1.Docker 容器网络通信的基本原理1.1 查看 Docker 容器网络1.2 宿主机与 Docker 容器建立网络通信的过程 2.使用命令查看 Docker 的网络配置信息3.Docker 的 4 种网络通信模式3.1 bridge 模式3.2 host 模式3.3 container 模式3.4 none 模式 4.容器间的通信4.…

Stream流操作

看到Stream流这个概念&#xff0c;我们很容易将其于IO流联系在一起&#xff0c;事实上&#xff0c;两者并没有什么关系&#xff0c;IO流是用于处理数据传输的&#xff0c;而Stream流则是用于操作集合的。 当然&#xff0c;为了方便我们区分&#xff0c;我们依旧在这里复习一下…