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,一经查实,立即删除!

相关文章

数据库复习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;而空间复杂度主要衡量一个算法所需要的…

C#技巧之同步与异步

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

2131 - 枚举-练习-涂国旗

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

【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…

【云原生】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;我们依旧在这里复习一下…

长期找 AI 专家,邀请参加线上聊天直播

诚邀 AI 专家参加线上聊天&#xff0c;成为嘉宾。 分享前沿观点、探讨科技和生活 除节假日外&#xff0c;每周举办在线聊天直播 根据话题和自愿形式结合&#xff0c;每期 2~3 位嘉宾 成为嘉宾&#xff0c;见下&#xff1a;

ADS软件(PathWave 先进设计系统软件)分享与安装

ADS软件的简介 ADS软件&#xff08;Advanced Design System&#xff09;主要用于射频&#xff08;RF&#xff09;、微波&#xff08;Microwave&#xff09;和毫米波&#xff08;Millimeter-wave&#xff09;电路的设计、仿真和分析。它提供了一套强大的工具和功能&#xff0c;…

Angular进阶-NVM管理Node.js实现不同版本Angular环境切换

一、NVM介绍 1. NVM简介 Node Version Manager&#xff08;NVM&#xff09;是一个用于管理多个Node.js版本的工具。它允许用户在同一台机器上安装和使用多个Node.js版本&#xff0c;非常适合需要同时进行多个项目的开发者。NVM是开源的&#xff0c;支持MacOS、Windows和Linux…

【解决】docker一键部署报错

项目场景见&#xff1a;【记录】Springboot项目集成docker实现一键部署-CSDN博客 问题&#xff1a; 1.docker images 有tag为none的镜像存在。 2.有同事反馈&#xff0c;第一次启动docker-compose up -d 项目无法正常启动。后续正常。 原因&#xff1a; 1.服务中指定了镜像m…

Jackson-jr 对比 Jackson

关于Jackson-jr 对比 Jackson 的内容&#xff0c;有人在做了一张下面的图。 简单点来说就 Jackson-jr 是Jackson 的轻量级应用&#xff0c;因为我们在很多时候都用不到 Jackson 的很多复杂功能。 对很多应用来说&#xff0c;我们可能只需要使用简单的 JSON 读写即可。 如我们…

【Linux网络】网络文件共享

目录 一、存储类型 二、FTP文件传输协议 2.1 FTP工作原理 2.2 FTP用户类型 2.3 FTP软件使用 2.3.1 服务端软件vsftpd 2.3.2 客户端软件ftp 2.4 FTP的应用 2.4.1 修改端口号 2.4.2 匿名用户的权限 2.4.3 传输速率 三、NFS 3.1 工作原理 3.2 NFS软件介绍 3.3 NFS配…

企业级数据治理学习总结

1. 水在前面 “数据治理”绝对是吹过的牛里面最高大上的题目了&#xff0c;本来想直接以《企业级数据治理》为题来水的&#xff0c;码字前又跑去图书馆借了几本书&#xff0c;翻了几页才发现自己连半桶水都提不起&#xff0c;撑死只能在小屁孩跟前吹吹牛。 好吧&#xff0c;实在…