STL之list模拟实现

news2024/11/20 2:31:18

list常识

  1. list迭代器不支持【】,所以不支持随机访问。也不支持>、<,没有意义,因为iterator是地址,地址并不连续。
    重要的说三遍:
    list不支持随机访问,因为没有重写[]。
    list不支持随机访问。
    list不支持随机访问。
  2. list底层是双向循环链表,头插尾插都方便,但是vector只有尾插方便(不扩容时)。

list模拟实现

首先,list的底层是每个节点,所以需要:Node类,后才是list类。因为涉及利用迭代器去构造类,所以还需要迭代器类。

0.准备工作:需要的三个类解析

Node、ListIterator、list

  1. Node:节点类,list是循环链表,需要双指针,所以给需要前后指针,而存值类型需要是泛型,所以类外需要模板。
  • 缺省构造函数:省写全缺省型。
      这里初始化值为默认值,且两个指针指向空。
    多用初始化参数列表,参数列表这只初始化,不赋值。

初始化参数列表好处:
比如有const类型数据和引用类型数据,const类型成员变量,只能初始化不能赋值
引用类型只能初始化,不能改变,且不能在默认构造函数里,需要专门的构造函数去做初始化,但是也只能在初始化列表中初始化。
效率高

struct ListNode
    {
        ListNode(const T& val = T())
        :_val(val)
		, _prev(nullptr)
		, _next(nullptr)
		{}

        ListNode<T>* _pPre;

        ListNode<T>* _pNext;

        T _val;

    };
  1. Iterator:迭代器类,它的本质是指向Node的地址,所以私有成员是Node节点指针,后续解引用或++、–都需要通过该指针指向成员的prev或next来获取地址。此外,迭代器需要实现基本功能++、–、!=、=、等等。list不能随机访问,所以用迭代器方便访问且非常必要。让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。
  • 普通迭代器构造函数:
      接收一个节点指针,缺省型构造函数。
  • const类型迭代器构造函数:
      这也是为什么类外需要三个模板参数,因为只是返回值类型不同,因为比如Operator*,operator++、operator–这些没有参数,迭代器部分重载函数没有参数,C++重载需要参数类型不同,而这里只有返回值类型不同。所以为了减少代码冗余,我们把cosnt类型和普通类型的类、指针、引用都用模板,这样就可以重载了,不必为了const类型重写相似度极大的迭代器代码了,在list上,调用时候,会决定这些的类型。
template<class T, class Ref, class Ptr>
struct ListIterator

    {

        typedef ListNode<T>* PNode;

        typedef ListIterator<T, Ref, Ptr> Self;

    public:

        ListIterator(PNode pNode = nullptr)ListIterator(const Self& l);

            T& operator*();

            T* operator->();

            Self& operator++();

            Self operator++(int);

            Self& operator--();

        Self& operator--(int);

        bool operator!=(const Self& l)bool operator==(const Self& l)private:
        PNode _pNode;
    };

ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。

1. 迭代器的构造函数和拷贝构造函数

  迭代器的构造函数有两种。

  1. 普通构造函数:成员变量是指向节点的指针,所以我们利用节点的指针,可以初始化一个迭代器对象。

直接利用参数的节点指针,初始化自己的成员变量节点的指针值。

  1. 拷贝构造函数,我们利用迭代器对象,也可以拷贝构造迭代器。
 ListIterator(PNode pNode = nullptr)
            :_pNode(pNode)
        {}
 ListIterator(const Self& l) 
            : _pNode(l._pNode)
        {}

2. 迭代器类的重载函数

和T或T&相关的返回值类型的函数,建议先看完list的const迭代器,这里的写法没有考虑const迭代器类型。
0. 回忆:几个遗忘概念

  1. this的值是对象的地址,*this得到迭代器这个类的对象。
  2. ++、–我们要的是迭代器对象本身,所以返回*this或后置时返回迭代器对象temp即可。
  3. **typedef ListIterator<T, Ref, Ptr> Self; **这样写是说list将来内部会用到Ref、Ptr,而我们的ListIterator中存的值可能是这三种,这里改为Self,就理解成是一个迭代器类型即可。
  4. return & 和 return 普通,return &不是返回引用类型,而返回引用类型只能从函数类型上做声明
  1. 重载解引用:operator*
      对迭代器解引用,我们一般的用法像:*it = 2,所以需要能改变节点的值,所以用引用接收,而返回的是this->_pNode->val。
    函数类型是引用,使得直接能修改
    函数类型是引用,使得直接能修改
    函数类型是引用,使得直接能修改
T& operator*()
        {
            return _pNode->_val;
        }
  1. 重载->:
      当节点T中存的是对象,当使用it->,即:迭代器->,需要能拿到节点中存的对象,其实就是val。所以需要:先解引用,得到节点,节点是个对象,我们访问对象的成员,需要用对象的地址,所以再取地址&。
T* operator->()
        {
            return &*this;
        }
  1. 重载前置++、后置++,区别是括号中有没有int,后置有int。
    不论是前置还是后置,都需要返回迭代器,所以前置是*this,迭代器对象本身。Self表示这个迭代器类。此外,用&类型接收效率高。
      前置++:
Self& operator++(int)
        {
            Self tmp(*this);    // 这里的前置++,返回了一个*this解引用,是个迭代器,
            _pNode = _pNode->_pNext;
            return tmp;
        }

  后置++:利用拷贝构造,以迭代器对象*this拷贝tmp即可。
后置++我们返回的是tmp,不能用&,因为tmp生命周期只在函数体内,而使用普通类型Self则使得函数再返回一个临时拷贝,对return中的tmp再拷贝一次,简言之,return tmp搭配引用,是错误的

Self operator++(int)
        {
            Self tmp(*this);    // 这里的前置++,返回了一个*this解引用,是个迭代器,
            _pNode = _pNode->_pNext;
            return tmp;
        }
  1. 重载–:道理如同++,且注意后置–不可以用引用接收。
 Self& operator--(int)
        {
            Self tmp(*this);
            _pNode = _pNode->_pPre;
            return tmp;
        }
Self& operator--()
        {
            _pNode = _pNode->_pNext;
            return *this;
        }
  1. 重载!=和==:
    迭代器相不相等,不看迭代器本身,而是内部包含的节点是否相同,而看内部节点相同否,需要看:它的成员变量,节点指针指向的地址是不是相同,相同说明记录的是同一个节点。
bool operator!=(const Self& l)
        {
            return _pNode != l._pNode;
        }
 bool operator==(const Self& l)
        {
            return _pNode == l._pNode;
        }

list类部分:

1. 构造函数:

  1. 普通构造函数:造个头节点即可。
      
  list()
  {
  	_head = new node; //申请一个头结点
	_head->_next = _head; //头结点的后继指针指向自己
	_head->_prev = _head; //头结点的前驱指针指向自己
  }
  1. 拷贝构造函数:
list(const list<T>& l)
        {
            _pHead = new Node;
            _pHead->_pPre = _pHead;
            _pHead->_pNext = _pHead;
            list<T> tmp(l.begin(), l.end());
            swap(tmp);
        }

  用迭代器拷贝构造函数拷贝即可。现代写法做交换。
3. 迭代器的拷贝构造函数
先要一个头节点,再从头到尾,插值。

template <class Iterator>
        list(Iterator first, Iterator last)
        {
            _pHead = new Node;
            _pHead->_pPre = _pHead;
            _pHead->_pNext = _pHead;
            while (first != last)
            {
                push_back(*first);
                ++first;
            }
        }

4. Swap:

void swap(list<T>& l)
        {
            PNode tmp = _pHead;
            _pHead = l._pHead;
            l._pHead = tmp;
        }

这个函数非常重要,在现代写法中灵活利用,省写很多,通过参数(不要用引用类型),参数会调用拷贝构造函数,此swap在list中要交换两个节点,而这里面因为参数list类型的L是局部变量,最终会调用L的析构函数,而不会调用*this的析构函数,所以这两个指向的节点只要交换就行。

CRUD:

注意CRUD时的所有插入操作,都不需要考虑扩容,因为list的底层是链表节点。

  • erase():通过迭代器拿节点,然后删除链表节点。
  • insert():利用insert()可以实现各种插入操作。list和vector的insert都是对迭代器操作。
 iterator insert(iterator pos, const T& val)
        {
            //assert(pos._pNode); //检测pos的合法性 不能在头位置之前插入
            PNode pNewNode = new Node(val);
            
            PNode pCur = pos._pNode;
            
            // 先将新节点插入

            pNewNode->_pPre = pCur->_pPre;

            pNewNode->_pNext = pCur;

            pNewNode->_pPre->_pNext = pNewNode;

            pCur->_pPre = pNewNode;

            return iterator(pNewNode);

        }

5. 普通迭代器

  关于迭代器类函数begin和end的返回值,也再次提醒规律,只有全局对象,或者说是成员变量,才配用引用返回(函数类型是引用,确实我们也看到了在迭代器类中才用了返回,且是*this)。
  返回迭代器即可,迭代器初始化用节点地址。因为return的值是临时变量,所以我们不能用引用类型。

 iterator begin()
        {
            return iterator(_pHead->_pNext);
        }

        // list的end()其实是头节点,这里不存值。
        // 不用引用,因为返回的是临时变量。不是类成员变量,只有类成员变量,才有资格。如:*this等
        iterator end()
        {
            return iterator(_pHead);
        }

6. const迭代器相关

  list中通过实例化来使用迭代器类,迭代器类根据不同实例化给的值去区分现在内部的一些模板参数值是啥。所以迭代器类使用三个模板参数,后面两个是引用Ref和指针Ptr。使得const迭代器不能修改,只能读。list中用const_iterator,则iterator中的参数Ref和Ptr会实例化为const T&和const T*。注意:我们迭代器中定了参数,函数返回值类型就用参数即可。
请添加图片描述
自从,我们改了iterator的模板参数,增加了两个,就也得修改原来的一些返回值类型:
T& 变成Ref,T*变为Ptr。这样就使得list那边调用过来时候自动实例化使得能区分。
此外,别迷糊:内部重命名的Self是迭代器类本身,只有++、–时才使用,做这些返回,而模板参数只涉及内部存储值,和解引用、->相关。
此外,回忆operator*的返回值类型是T&,因为在iterator内部,还要对存储值本身要做修改,所以我们换T&为Ref后,Ref有两种情况,当是普通引用类型时候可读可写,而为const 引用类型时只可读。
请添加图片描述

7. 遇到的错误:

==ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。
====ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。
==ListNode和Iterator必须是struct或public类型,不然过程中你在list通过节点类和迭代器类不能调用里面的private成员变量。

完整代码

list2.h

#include<iostream>
#include<assert.h>
using namespace std;

namespace lz
{
    // List的节点类

    template<class T>

    struct ListNode
    {

        ListNode(const T& val = T())
            :_val(val)
            , _pPre(nullptr)
            , _pNext(nullptr)
        {
        }

        ListNode<T>* _pPre;

        ListNode<T>* _pNext;

        T _val;

    };


    // 迭代器类

    template<class T, class Ref, class Ptr>

    struct ListIterator
    {

        typedef ListNode<T>* PNode;

        typedef ListIterator<T, Ref, Ptr> Self; // 这里代表的是:迭代器类型,且内部可以存这三种,看看视频吧

    public:
        // 构造函数 :以节点的指针拷贝,即以节点地址拷贝。
        ListIterator(PNode pNode = nullptr)
            :_pNode(pNode)
        {}

        // 以迭代器拷贝:加const为了保护,这个用的更多
        ListIterator(const Self& l) 
            : _pNode(l._pNode)
        {
            //cout << "以迭代器类型拷贝,后置++中试试" << endl;
        }
        // 而下面的*this,是*this是个迭代器,走的是第二个

        // *返回值类型T& 节点的地址,不用Ref,因为const类型迭代器不可以解引用 所以
        // 这里我忘了:我们要修改一个值,就传这个变量本身即可,而我们用&接收,就能修改这个值。
        // 这里*,要的是迭代器的节点的成员,不用写this,直接能拿到。
        //T& operator
        Ref operator*()
        {
            return _pNode->_val;
        }
        
        
        // *this才是迭代器。
        // ->:迭代器的->是为了访问list成员类型是类类型时,我们需要拿到对象的地址,利用对象地址去拿值
        // 所以*this是内部存的对象,而&取地址,得到对象地址,有对象地址就能直接访问对象成员了,然而编译器会把两个->合并为一个
        
        //T* operator->()
        Ptr operator->()
        {
            return &*this;
        }

        // 前置++  迭代器++后,我们需要的权限是访问和改变list中节点,
        // 迭代器本质是存着节点的地址,所以对它解引用,得到节点本身。
        // 且返回引用效率高,且我们要操作的是对象本身 通过迭代器可以改变值,所以需要返回引用。
        Self& operator++()
        {
            _pNode = _pNode->_pNext;
            return *this;
        }

        // 后置++ 
        // 拷贝当前迭代器,返回tmp ,相当于是旧的*this,仍然 *this后是个节点,可以改对象
        // 上面把迭代器类型起别名为Self,self本身是个迭代器类型,self&接收效率高
        Self operator++(int)
        {
            Self tmp(*this);    // 这里的前置++,返回了一个*this解引用,是个迭代器,
            _pNode = _pNode->_pNext;
            return tmp;
        }

        // 前置-- :要迭代器中的本身,返回this即可
        Self& operator--()
        {
            _pNode = _pNode->_pNext;
            return *this;
        }

        // 后置--:需要返回迭代器,拷贝一个tmp,而迭代器的值可以改变,用引用接收。内部值也需要改变,所以用引用很合适。
        // 不用引用,也可以,只是拷贝了迭代器中的节点,也没啥。
        Self operator--(int)
        {
            Self tmp(*this);
            _pNode = _pNode->_pPre;
            return tmp;
        }

        // != 迭代器不等于,想比的是节点等不到,而看节点等不等,比看看是不是指向同一个节点,
        // 看看成员变量存的地址相等不。
        bool operator!=(const Self& l)
        {
            return _pNode != l._pNode;
        }

        // 比存着的地址即可。 
        bool operator==(const Self& l)
        {
            return _pNode == l._pNode;
        }

        PNode _pNode;

    };

    //list类

    template<class T>

    class list

    {

        typedef ListNode<T> Node;

        typedef Node* PNode;

    public:
        // 两个的作用
        typedef ListIterator<T, T&, T*> iterator;

        typedef ListIterator<T, const T&, const T&> const_iterator;

    public:

        ///

        // List的构造

        // 无参普通构造
        list()
        {
            _pHead = new Node;
            _pHead->_pPre = _pHead;
            _pHead->_pNext = _pHead;
        }

        // 带参普通构造: list存n个同样的值  参数带const为了保护 引用为了效率高
        list(int n, const T& value = T())
        {
            _pHead = new Node;
            _pHead->_pPre = _pHead;
            _pHead->_pNext = _pHead;
            for (int i = 0; i < n; ++i)
                push_back(value);
        }

        // 迭代器构造函数:外部传值用 begin()、end()即可
        template <class Iterator>
        list(Iterator first, Iterator last)
        {
            _pHead = new Node;
            _pHead->_pPre = _pHead;
            _pHead->_pNext = _pHead;
            while (first != last)
            {
                push_back(*first);
                ++first;
            }
        }

        // 拷贝构造:现代写法,通过迭代器拷贝函数构造tmp 再做交换
        list(const list<T>& l)
        {
            _pHead = new Node;
            _pHead->_pPre = _pHead;
            _pHead->_pNext = _pHead;
            list<T> tmp(l.begin(), l.end());
            swap(tmp);
        }

        // 赋值需要让内部值相等。所以先清空。也可以用现代写法。直接拷贝构造,再交换。
        /*list<T>& operator=(const list<T> l)
        {
            if (this != l)
            {
                clear();
                for (const auto& e : l)
                {
                    push_back(e);
                }
            }
            return *this;
        }*/

        // 重载= : 现代写法 :利用参数做交换,必然不能用cosnt
        list<T>& operator=(list<T> l)
        {
            swap(l);
            return *this;
        }

        // 先清空再删头的空间,置不置null其实都能
        ~list()
        {
            clear();
            delete _pHead;
            _pHead = nullptr;
        }

        ///

        // List Iterator

        iterator begin()
        {
            return iterator(_pHead->_pNext);
        }

        // list的end()其实是头节点,这里不存值。
        // 不用引用,因为返回的是临时变量。不是类成员变量,只有类成员变量,才有资格。如:*this等
        iterator end()
        {
            return iterator(_pHead);
        }

        const_iterator begin()const
        {
            return const_iterator(_pHead->_pNext);
        }

        const_iterator end()const
        {
            return const_iterator(_pHead->_pNext);
        }
        ///

        // List Capacity:挨个统计吧
        size_t size()const
        {
            size_t sz = 0;
            const_iterator it = begin();    // 安全
            while (it != end())
            {
                sz++;
                it++;
            }
            return sz;
        }
        
        // empty():
        bool empty()const
        {
            return begin()==end();
        }

        

        // List Access
        // front是要第一个节点中存的值
        // front 返回了对begin()的解引用,迭代器解引用返回节点所存内容,且迭代器类中重载*返回类型是引用,所以这里也可以用引用类型。
        T& front()
        {
            return *begin();
        }

        const T& front()const   // 返回类型上,自己会限定的
        {
            return *begin();
        }

        T& back()
        {
            return *(--end());
        }

        const T& back()const
        {
            return *(--end());
        }

        

        // List Modify

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

        void pop_back() { erase(--end()); }

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

        void pop_front() { erase(begin()); }

        // insert,只能用迭代器插,vector和list都一样   
        // node的指针别名:PNode  有了迭代器的好处是,插入方便,直接能拿到这个点的前后。
        // 返回的是临时变量 不能用引用 如果是该类的成员变量,用&很好。  
        // list插入不必考虑扩容
        iterator insert(iterator pos, const T& val)
        {
            //assert(pos._pNode); //检测pos的合法性 不能在头位置之前插入
            PNode pNewNode = new Node(val);
            
            PNode pCur = pos._pNode;
            
            // 先将新节点插入

            pNewNode->_pPre = pCur->_pPre;

            pNewNode->_pNext = pCur;

            pNewNode->_pPre->_pNext = pNewNode;

            pCur->_pPre = pNewNode;

            return iterator(pNewNode);

        }


        // 删除pos位置的节点,返回该节点的下一个位置
        // 返回下一个迭代器较好
        iterator erase(iterator pos)
        {
            assert(pos._pNode);     // 头结点不能删,且本来就没值
            assert(pos != end());   // end位置在尾节点的下一个
            // 找到待删除的节点
            PNode pDel = pos._pNode;    // 通过迭代器定位这个节点地址
            PNode pRet = pDel->_pNext;  // 待删的下一个

            // 将该节点从链表中拆下来并删除   
            pDel->_pPre->_pNext = pDel->_pNext;
            pDel->_pNext->_pPre = pDel->_pPre;
            delete pDel;
            return iterator(pRet);
        }

        // 删除:
        void clear()
        {
            iterator it = begin();
            while (it != end())
            {
                it = erase(it);
            }
        }

        // swap要交换两个链表的头,
        // 节点指针,备份当前头节点。
        // l出去会调用析构函数,所以l的部分删了,而p的部分没有删除
        void swap(list<T>& l)
        {
            PNode tmp = _pHead;
            _pHead = l._pHead;
            l._pHead = tmp;
        }

    private:
        PNode _pHead;
    };
};

测试文件
list.cpp

#include"l2list.h"

void testl1()
{
    lz::list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    lz::list<int>::iterator it = lt.begin();
    cout << "构造函数测试、迭代器测试、解引用修改值、push测试" << endl;
    while (it != lt.end())
    {
        cout << *it << "改之后  : ";
        *it *= 2;
        cout << *it << endl;
        ++it;
    }

}

int main()
{
    testl1();
    return 0;
}

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

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

相关文章

vue3+ts项目 笔记总结

一、首先得先建好vue3ts的项目&#xff1a; 在终端新建项目输入&#xff1a;vue create vue3-ts接下来的每一步如图所示&#xff1a;注意要通过空格选中TypeScript二、项目建好后&#xff0c;开始正常的开发&#xff0c;注意区分vue2与vue3的区别&#xff1a; 在vue3项目中不…

移动设备软件开发-Spape详解

Spape详解 1.自定义背景shape 1.1gradient 1.简介 定义渐变色&#xff0c;可以定义两色渐变和三色渐变&#xff0c;及渐变样式&#xff0c;它的属性有下面几个2.属性 angle&#xff0c;只对线性渐变是有效的放射性渐变必须指定放射性的半径&#xff0c;gradientRadiouscentetX和…

(一)汇编语言——基础知识

目录 基础知识 总线 地址总线 数据总线 控制总线 内存地址空间 总结 今天我们就开始学习有关汇编的相关知识了&#xff0c;感觉和之前学的STM32相类似&#xff0c;所以学习起来并没有感觉很困难&#xff0c;相反&#xff0c;感觉有点好玩&#xff0c;并且理解了底层原理…

网络技术期末复习~重点考题

解题思路&#xff1a; 标准答案&#xff1a; 11. 如图所示&#xff0c;网络145.13.0.0/16划分为四个子网N1,N2,N3,N4。它们与路由器R相连的接口分别是m0,m1,m2,m3,R的第五个接口m4连接到互联网路由器&#xff08;接口地址为1.1.1.1&#xff09;。 (1)请给出路由器R的路由表。 …

Anaconda+VSCode+QT Designer配置PyQt5环境

AnacondaVSCodeQT Designer配置PyQt5环境 本文使用AnacondaVSCode配置PyQt5环境&#xff0c;在开始之前新建Anaconda的虚拟环境&#xff0c;如果不需要虚拟环境可以直接使用默认的Base环境。另外针对ui文件转py文件报错ImportError: DLL load failed: 找不到指定的模块给出了解…

D3D11和Vulkan共享资源 (二) - 和Intel MediaSDK sample_decode 集成

转过头再找个复杂的播放程序验证一下&#xff0c;还是用我比较熟悉的MediaSDK的播放程序。基本思路就是 在初始化解码输出显示的窗口的时候同时也初始化一个vulkan显示的窗口初始化d3d11设备的时候初始化vulkan, 同时多创建一个D3D11Texture2D的共享纹理最后在MSDK每个frame在…

MySQL——幻读是什么,有什么问题,怎么解决。

数据库有以下的实现&#xff1a; CREATE TABLE t (id int(11) NOT NULL,c int(11) DEFAULT NULL,d int(11) DEFAULT NULL,PRIMARY KEY (id),KEY c (c) ) ENGINEInnoDB;insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25); 对于下面的语句&am…

Zabbix监控部署

目录 编译安装nginx 编译安装PHP 编译安装mysql 安装zabbix 编译安装nginx 参考文章 源码下载 [root8a-1 opt]# uname -a Linux 8a-1 3.10.0-1160.71.1.el7.x86_64 #1 SMP Tue Jun 28 15:37:28 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux [root8a-1 opt]# cat /etc/redh…

OpManager 网络图工具

由于企业网络规模的扩大&#xff0c;网络管理正变得越来越复杂。巨大的规模和动态特性使得扩展网络以跟上其复杂性的上升变得困难。如果没有适当的可视化&#xff0c;网络管理员可能会做出不明智的决策&#xff0c;从而导致意外的网络中断。这可能会对企业造成严重打击&#xf…

计算机网络期末每章计算题总复习

第三章数据链路层 CRC检验问题 知识点 例题 要发送的数据为1101011011。采用CRC的生成多项式是 P(X)X^4 X 1试求应添加在数据后面的余数。数据在传输过程中最后一个1变成了0&#xff0c;问接收端能否发现&#xff1f; 若数据在传输过程中最后两个1都变成了0&#xff0c;问接…

客户关系管理对企业起到至关重要的作用

客户关系管理对企业的发展至关重要。客户会密切关注你为他们提供的服务质量&#xff0c;因此必须有效地管理客户关系。即使是延迟回复这样的小事也可能会对企业的发展产生重大影响。 管理客户关系的好处远远超出了经营你的业务&#xff1b;它们有助于改善你与现有客户的关系&…

后AlphaFold时代的蛋白质结构预测

最新一届的蛋白质结构预测奥林匹克大赛&#xff0c;即15届CASP比赛(CASP15)&#xff0c;在日前拉下了帷幕。这正值谷歌团队AlphaFold2在上一届CASP大赛给该领域带来革命性冲击后两周年。两年后&#xff0c;该领域的状况如何&#xff0c;蛋白质结构预测该何去何从&#xff1f;为…

数字化办公,就选流畅、清晰的华为云桌面

人工智能、大数据算法蓬勃发展的时代&#xff0c;企业的数字化发展与之关联密切&#xff0c;企业纷纷追求业务上云。 云上办公模式相较于线下办公模式而言&#xff0c;不再受到场地的限制、业务流程也加快很多&#xff0c;企业的成本得到一定的节省。在技术和成本的影响下&…

电视动画片的制作与发行

电视动画片的制作与发行 一、电视动画片的备案和公示 &#xff08;一&#xff09;电视动画片的备案和公示基本要求 电视动画片的拍摄制作实行备案公示制度。 国家广播电视总局负责全国拍摄制作电视动画片的公示。北京市广播电视局负责受理本行政区域内制作机构拍摄制作电视…

【LeetCode每日一题】——面试题 08.01.三步问题

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 动态规划 二【题目难度】 简单 三【题目编号】 面试题 08.01.三步问题 四【题目描述】 三步问…

Nacos2.2使用PostgreSQL数据源插件存储数据手把手教程

一、背景 Nacos2.2在2022年的12月份正式发布了&#xff0c;该版本可以让开发者开发支持PostgreSQL数据库的插件&#xff0c;从而实现将配置信息存储到PostgreSQL中。 本文基于自己开发的PostgreSQL数据源插件进行说明&#xff0c;希望可以帮助到大家。 数据源插件开源仓库地…

Go语言设计与实现 -- 接口

接口实际上是一个中间层&#xff0c;用于上下游的解耦&#xff0c;在框架和操作系统中&#xff0c;接口都随处可见&#xff0c;而Go语言将接口作为了内置类型&#xff0c;接下来&#xff0c;我们就来重点学习一下&#xff0c;Go语言的接口。 将实现接口的结构体实例赋值给接口结…

(三)汇编语言——DOSBox

本篇主要用来介绍我们的实验平台——DOSBox的使用与调试&#xff0c;主要就是改一下窗口大小以及挂载&#xff0c;并且作为学习汇编实验的汇总&#xff0c;不定期更新。 下载与安装 这个可以到官网去下载&#xff0c;然后安装也很简单&#xff0c;就不介绍了&#xff0c;而且一…

力扣(LeetCode)1753. 移除石子的最大得分(C++\C)

贪心模拟 贪心思路 : 循环从石子数量最多的两堆取石子&#xff0c;直到有两堆以上(含两堆)空石子&#xff0c;维护取子次数&#xff0c;即是答案。贪心的正确性&#xff0c;暂无数学证明。直觉来看&#xff0c;这么做是对的。 CPP class Solution { public:int maximumScore…

设计模式之观察者模式

Observer design pattern 观察者模式的概念、观察者模式的结构、观察者模式的优缺点、观察者模式的使用场景、观察者模式的实现示例、观察者模式的源码分析 1、观察者模式的概念 观察者模式&#xff0c;又称为发布-订阅模式&#xff0c;即它定义了一种对象间一对多的依赖关系&…