【C++STL详解(二)】——string类模拟实现

news2024/11/29 2:36:35

目录

前言

一、接口总览

二、默认成员函数

1.构造函数

2.拷贝构造

写法一:传统写法

写法二:现代写法(复用构造函数)

3.赋值构造

写法一:传统写法

写法二:现代写法(复用拷贝构造)

4.析构函数

三、迭代器

begin和end函数

四、遍历与访问

operator[]函数

五、容量和大小

size

capacity

empty

resever(容量)

resize(大小)

六、修改

insert

erase

push_back

append

operator+=

clear(清理字符串内容)

c_str

swap

七、查找

find

substr(左闭右开区间)

八、非成员函数重载

swap(全局,string)

关系运算符重载

"<<"重载

">>"重载

getline函数


前言

在前面我们已经了解了string中常见的接口,我们也说过,对于STL库的学习,不仅仅只是熟用,还要明理!但是要注意一个道理:模拟实现不是为了比库里面更好,而是去学习它的一些底层,能够让自己有更深的了解,就比如我们并不需要去造车,这不是我们干的,但我们需要了解它为什么能这样做,它的底层是什么在驱动,这样才能更好的去发挥车的性能!string类的底层就是个字符顺序表!

一、接口总览

#include<assert.h>
#include<iostream>

namespace STR
{
	class string
	{
   public:
      typedef char* iterator;
      typedef const char* const_iterator;
       static const size_t npos;

   public:
      string(const char* str = "");//构造函数,提供全缺省可以是空参数调用,也可以有参调

      //s2(s1)
       string(const string& s);//拷贝构造,传统写法
       string(const string& s);//拷贝构造,现代写法

     //s1=s2
     string& operator=(const string& s);//赋值重载,传统写法
     string& operator=(string tmp);//赋值重载,现代写法!
      ~string();//析构函数


      //迭代器
      iterator begin();
      iterator end();
      const iterator begin()const;
      const iterator end()const;

     // 遍历
      char& operator[](size_t index);
      const char& operator[](size_t index)const;

       // 容量
      size_t size()const;
      size_t capacity()const;
      bool empty()const;
      void reserve(size_t n);
      void resize(size_t n, char c = '\0');

      // 修改
      void push_back(char c);
      string& operator+=(char c);
      void append(const char* str);
      string& operator+=(const char* str);
      void clear();
      void swap(string& s);
      const char* c_str()const;

       // 返回c在string中第一次出现的位置
       size_t find(char c, size_t pos = 0) const;
       // 返回子串s在string中第一次出现的位置
       size_t find(const char* s, size_t pos = 0) const;

      // 在pos位置上插入字符c/字符串str,并返回该字符的位置
       string& insert(size_t pos, char c);
       string& insert(size_t pos, const char* str);
        // 删除pos位置开始向后len个元素,并返回该元素的下一个位置
       string& erase(size_t pos, size_t len=npos);
    
       string sub(size_t pos = 0, size_t len = npos); 
    private:
       char* _str;
       size_t _capacity;
       size_t _size;
	};

   const size_t string::npos = -1;
   //relational operators
    //s1<s2
   bool operator==(const string& s1, const string& s2);
   bool operator<(const string&s1,const string& s2);
   bool operator<=(const string& s1, const string& s2);
   bool operator>(const string& s1, const string& s2);
   bool operator>=(const string& s1, const string& s2);
   bool operator!=(const string& s1, const string& s2);

   ostream &operator<<(ostream& _out, const string& s);
   istream &operator>>(istream& _cin, string& s);
   istream& getline(istream& in, string& s);


}

注意:这里模拟实现,需要我们使用我们自己的命名空间,防止与库里面的发生冲突!

二、默认成员函数

1.构造函数

构造函数一般提供缺省参数,这样使得无参有参也可以一起调用!

   string(const char* str = "")//构造函数,提供全缺省可以是空参数调用,也可以有参调用,但是单参数构造会有隐式类型转化
        :_size(strlen(str))//初始时,大小设置为字符串长度
   {
          _capacity = _size;//容量设置为字符串长度
          _str = new char[_capacity + 1];//多开一个是为存放'\0'
          strcpy(_str, str);//按字节拷贝
   }

2.拷贝构造

对于拷贝构造,前面我们也说过一定要注意深拷贝和浅拷贝的区别,以及影响,这里再回顾一下!

浅拷贝:不写编译器默认生成的函数,多个对象共同指向同一块资源,那么释放时会造成同一块空间多次释放的问题!

深拷贝:我们自己写的,给每个对象分配独立的资源,保证多个对象之间不会因为共享资源而造成多次释放问题!

写法一:传统写法

思路简单:先开一个与原对象大小相同的空间,这样两个对象指向的空间就不一样了,相互独立!然后把字符串按字节拷贝过去,再把相应的值传给拷贝对象!

string(const string& s)//拷贝构造
{
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);

    _size = s._size;
   _capacity = s._capacity;
 }

写法二:现代写法(复用构造函数)

思路与传统的不同:它需要借助一个tmp对象,先将原对象的字符串通过调用构造函数创建出tmp对象,最后在将tmp和拷贝对象交换,这样两个对象指向的空间也是不一样的!

  string(const string& s)
        :_str(nullptr)//拷贝对象,一定要先置空
        ,_size(0)
        ,_capacity(0)
  {
        string tmp(s._str);//调用构造函数,不是拷贝构造,因为参数不是对象,是个字符串
        swap(tmp);//交换拷贝对象和tmp

   //tmp出了作用域会自动销毁,此时它指向的空间是个空,析构时不会出问题,如果是随机值就会出现问题
    //这就为什么_str先前要置空的原因
  }

需要注意上面的细节问题!!!!!!!

两种写法效率上都是一样,因为都要开空间,只不过传统写法是要自己写,而现代写法直接复用构造函数,喜欢写哪个就写哪个,没啥区别!!!

3.赋值构造

这里也是有深浅拷贝的问题,但是我们时刻都不能让多个对象指向同一块空间,所以还是要自己写深拷贝!!!

写法一:传统写法

注意:我们不能直接将原对象拷贝过去,因为两者空间容量大小可能不一样,所以要新开一个空间,在让赋值对象指向这块空间即可!

string& operator=(const string& s)//赋值重载
{
    if (this != &s)
    {
        char* tmp = new char[s._capacity + 1];//开个空间
        strcpy(tmp, s._str);
        delete[] _str;
         _str = tmp;
        _size = s._size;
        _capacity = s._capacity;
    }
  
}

写法二:现代写法(复用拷贝构造)

string& operator=(string tmp)//赋值重载
{
   swap(tmp);//交换两个对象
   return *this;//返回左值,支持连续赋值
}

可以看到,这里的参数我们利用值传递,接受右值的方法,编译器会自动调用拷贝构造函数去生成一个tmp对象出来,最后在与左值进行交换即可,这样两者指向的空间就是不一样的!那么,这里或许会有疑问,传值不会死循环吗?这里当然不会,因为赋值它只会调用一次拷贝构造!但是拷贝构造是一定不能传值的,因为传值会再一次调用拷贝构造,一次又一次,最终形成死循环!

4.析构函数

对于string类,我们需要自己手动写析构,因为每个对象中的_str指向的是一块堆空间,不写编译器自动生成的析构,他只是会对内容进行清理,而不会自己释放空间,所以为了避免内存泄漏,我们需要手动delete!

 ~string()//析构函数
{
    delete[] _str;
    _str = nullptr;
    _capacity = _size=0;
}

三、迭代器

string类的迭代器底层就是个字符指针,只不过是把它重命名罢了!

  typedef char* iterator;
  typedef const char* const_iterator;

但是需要注意的是并不是所有的迭代器都是指针!!只能说像,不一定是!上面我说的就是简单版本的

begin和end函数

这两个函数简单得可怕,begin实际上是返回第一个字符的地址,end就是返回最后一个字符的下一个位置的地址!

//begin
iterator begin()
{
     return _str;
}


iterator begin()const  //让const对象也能用
{
    return _str;
}
//end
iterator end()
{
   return _str + _size;
}


iterator end()const
{
   return _str + _size;
}

这里提一嘴,在上一篇文章string类中提到过范围for,它并神奇,它的底层就是迭代器!实际上这个东西在底层是写死的,虽然是迭代器,但是如果我们在模拟实现时,把begin函数改成Begin函数,那么范围for会立马失效!!!也就是说它只认识begin和end,而不认识Begin和End,死的!大家可以尝试尝试!

四、遍历与访问

operator[]函数

我们之前能够通过【】+下标进行访问字符串,其实底层就是这个函数!这个函数就是参数就是一个位置,直接返回对应字符的引用即可!

//这个是可读可写的
char& operator[](size_t index)
{
   assert(index < _size);//对下标是否越界的判断
   return _str[index];//返回对应位置即可
}


//这个是只读的,访问const对象用的
char& operator[](size_t index)const
{
   assert(index < _size);
   return _str[index];
}

五、容量和大小

size

//直接返回对象的变量_size就好了
//对于一些只需要进行读操作的,我们一般习惯加上const修饰
size_t size()const
{
   return _size;
}

capacity

size_t capacity()const
{
   return _capacity;
}

empty

bool empty()const
{
   if (_size == 0)
   {
       return true;
   }
       return false;
}

resever(容量)

规则:如果n大于当前的capacity,那么就扩容到n甚至更大;如果小于,那就什么都不做

//只是改变容量,大小不变
void reserve(size_t n)
{
    if (n > _capacity)
    {
       char* tmp = new char[n + 1];//多开一个存放‘\0’
       strcpy(tmp, _str,_size+1);//连着‘\0’也一起拷贝
       delete[] _str;
       _str = tmp;
       _capacity = n;
    }
}

resize(大小)

规则:如果n小于当前字符串长度,那就缩短至n个字符的长度,其他部分全部删除;如果n大于当前长度,那就扩大到n个字符的长度,扩大的部分用特定的字符表示,若未给出,则默认给'\0'!所以要提供个缺省值!

void resize(size_t n, char c = '\0')//半缺省
{
    if (n <=_size)//小于就缩到n个字符的长度
   {
        _str[n] = '\0';//字符串结尾都是以'\0'结尾
        _size = n;
    }
    else
    {
        reserve(n);//扩容
        for (size_t i = _size; i < n; i++)
        {
            _str[i] = c;//多出来的部分用特定字符补充
        }
       _str[n] = '\0';
       _size = n;
    }
}

六、修改

insert

 我们熟知,这个函数接口,它的功能:在pos位置前插入一个字符或者字符串,并返回字符该字符位置!既然是插入操作,那必然会引起扩容问题,这时我们又可以复用reserve!

// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c)
{
     assert(pos <= _size);//判断pos位置是否合法

     if (_size == _capacity) //容量问题
     {
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
     }

     //这里有个细节,那就是隐式类型提升
     //如果end的类型是size_t,那么就会一直循环
     //如果是int的话,因为pos是size_t,end还是会隐式类型转化成size_t
     //所以对于这样的情况,只能是将pos强转成int,才能对pos=0这样的情况处理
      /* int end = _size;
       while (end >= (int)pos)
       {
          _str[end + 1] = _str[end];
          --end;
       }*/

     //写法二就不会出现上面的情况
      size_t end = _size + 1;
      while (end > pos)
      {
           _str[end] = _str[end-1];//依次向后挪动数据
           --end;
      }
         _str[pos] = c;
         ++_size;
         return *this;
}

除了插入单个字符以外,我们还可以插入字符串,思路也是大差不差,我们首先都是对pos位置的合法性进行判断!然后再在要插入的位置后面的字符串整体向后挪动len个长度(这里的len长度实际上就是你要插入的字符串的长度),然后插入即可

 string& insert(size_t pos, const char* str)
{
   assert(pos <= _size);//位置合法性
   size_t len = strlen(str);//待插入的字符串长度
  //扩容问题
   if (_size + len > _capacity)
   {
     reserve(_size+len);
   }
   size_t end = _size +len;
   while (end > pos+len-1)
   {
       _str[end] = _str[end - len];
       --end;
    }
   strncpy(_str + pos, str,len);
   _size += len;
   return *this;
}

erase

规则:擦除字符串中从字符位置pos开始并跨越len字符的部分(如果内容太短或len为string::npos,则擦除直到字符串末尾)

从规则就可以看出它的实现首先就是需要判断位置是否合法,如果合法那么长度是否达到要求。所要删除的字符串过长或者过短索处理的情况都是一样的!!!!

// 删除pos位置开始向后len个元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len=npos)
{
   assert(pos < _size);//位置的合法性判断
   if (len == npos || len >= _size - pos)//长度的合法性判断
   {
       _str[pos] = '\0';//直接在pos位置加上‘\0’即可将后面的字符全部清除
       _size = pos;
   }

    //长度和位置都合法,直接用pos位置后面len个长度的字符覆盖前面即可
  else
  {
     strcpy(_str + pos, _str + pos + len);//
    _size -= len;
  }
   return *this;
}

值得注意的是:字符串是以"\0"结尾的,所以要清楚后面的字符,直接在特定的位置上加上"\0"即可!!

push_back

这个函数的作用就是在尾部插入一个字符,实现起来十分的容易,一个简单的尾插操作!!

void push_back(char c)
{
   //写法一:常规写法
    if (_size == _capacity)//扩容问题
    {
       reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }
   _str[_size] = c;
   ++_size;
   _str[_size] = '\0';


    //写法二:复用inser函数
   insert(_size, c);
}

append

函数作用:在尾部追加字符串 !!

void append(const char* str)
{
    //写法一:常规写法
  size_t len = strlen(str);
    if (_size + len > _capacity)
   {
       reserve(_size + len);
   }

   strcpy(_str + _size, str);//将字符串拷贝到_size所指向的位置
   _size += len;


 //写法二:复用insert
   insert(_size, str);
}

operator+=

这个函数的功能也是尾部追加,不过它既可以追加字符,也可以追加字符串!!!实现也是十分的简单!

//追加字符,复用push_back
string& operator+=(char c)
{
   push_back(c);
   return *this;
}


//追加字符串,复用append
string& operator+=(const char* str)
{
   append(str);
   return *this;
}

clear(清理字符串内容)


//不改变空间大小,也就是_capacity不变
void clear()
{
   _size = 0;
   _str[_size] = '\0';
}

c_str

这个函数实际上将string对象转化为C类型的字符串,底层十分简单,就是返回_str即可

const char* c_str()const
{
   return _str;
}

swap

这里的交换实际上是调用库里面的全局的swap进行交换,但是要注意加上作用域限定符哦!!

void swap(string& s)
{
   //调用的是库里的全局交换函数
   std::swap(_str, s._str);
   std::swap(_size,s._size);
   std::swap(_capacity, s._capacity);
}

七、查找

find

这个函数的功能:从字符串pos位置(不写默认是0)开始往后找字符c/或者字符串,返回的是第一个匹配的第一个字符的位置。如果没有找到匹配项,函数返回string::npos(-1)。

// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const//仅仅只是查找
{
   assert(pos < _size);//位置的合法性判断

  for (size_t i = pos; i < _size; i++)//遍历查找
   {
       if (c == _str[i])
        {
            return i;
        }
   }

   return npos;
}

当然了,除了可以查找单个字符,也可以查找一个字符串,查找字符串底层我们使用的是strstr函数,这个函数如果找到匹配的字符串,会返回目标字符串起始的位置,找不到就会返回空!若找到,那就用当前位置减去起始位置(指针-指针)就可以得到起始字符的下标!

// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
   assert(pos < _size);
   const char* p=strstr(_str+pos, s);

   if (p)//不为空,找到
    {
      return p - _str;//指针-指针
    }

    else {
        return npos;
    }
}

substr(左闭右开区间)

功能:从pos位置开始,截取n个字符,返回的结果是一个新的字符串对象,这个新对象的内容就是使用截取到子字符串去初始化!

string sub(size_t pos = 0, size_t len = npos) 
{
          
   string sub;//定义一个接受子串的对象

   //所截长度大于当前字符串的长度,就截取到末尾
   if (len >= _size - pos)
    {
      for (size_t i = pos; i < _size; i++)
        {
           sub += _str[i];//将字符尾插到新对象即可
        }
    }

    //长度小于,直接从pos位置开始截len个字符,在尾插入新对象
    else
    {
       for (size_t i = pos; i < pos+len; i++)
        {
           sub += _str[i];
        }
    }
    return sub;//返回结果
}

八、非成员函数重载

swap(全局,string)

在介绍string类时,我们说过,这个函数在库里面有两个,一个是作为string类的成员函数(上面提到的),一个就是这个全局的swap,但是要注意的是,这个全局的swap不是在算法库里的!算法库里的是一个模板!

调用算法库里的模板:

string s1("skkald");
string s2("vckjn");

swap(s1,s2);//调用算法库里的swap模板!

但是需要注意的是,算法库的swap,会拷贝构造一个临时对象C,而后又进行两次赋值构造,最后在析构临时对象C,代价有点大,而且在底层调试时,实际走的是深拷贝,也就是他会重新开出两块空间,在交换,一定程度上也有开销!这也是为啥要在全局专门搞了一个string的swap的原因!为的就是避免上述情况的发生!!!那就相当于有三个swap(成员函数,string全局非成员函数,算法库的)!

//string的非成员函数
void swap(string& x, string& y)
{
	x.swap(y);//这里调用成员函数swap
}


string s1("skkald");
string s2("vckjn");

swap(s1,s2);//调用全局的string中swap,也就是上面的代码

同时需要注意,这里全局的swap和算法库里的模板swap并不会冲突,因为模板中我们提到,如果有现成的,那么编译器会优先去调用现成函数,没有的情况下才去考虑使用算法库的模板!!!

关系运算符重载

这包含了==,<,<=,>,>=,!=,其实他们的底层实现需要用到就是C语言中的strcmp函数,然后在进行相互之间的复用!

 bool operator==(const string& s1, const string& s2)
{
    int ret = strcmp(s1.c_str(), s2.c_str());
    return ret == 0;//两字符串相等就返回0
}

bool operator<(const string&s1,const string& s2)
{
   int ret = strcmp(s1.c_str(), s2.c_str());
   return ret < 0;//第一个小于第二个就返回<
}

bool operator<=(const string& s1, const string& s2)
{
   return s1< s2 || s1 == s2;//复用上面的两个
}

bool operator>(const string& s1, const string& s2)
{
   return (!(s1 <= s2));
}
bool operator>=(const string& s1, const string& s2)
{
    return (s1 > s2) || (s1 == s2);
}
bool operator!=(const string& s1, const string& s2)
{
    return !(s1 == s2);
}

"<<"重载

这个重载就使得我们能使用cout像内置类型那样输出,底层实现十分的简单,遍历就完事!

ostream &operator<<(ostream& _out, const string& s)
{
   for (auto ch : s)
   {
       _out << ch;
  }
  return _out;
}

">>"重载

重载后,便可以像内置类型那样去输入内容,但是要先将对象字符串内容清空,然后再依次读入,直到遇到空格或者"\n"为止!

写法一:

istream &operator>>(istream& _cin, string& s)
{

    s.clear();//防止里面有脏数据!
    char ch;
    ch = _cin.get();//C++中可以使用get获取一个字符
    while (ch != ' ' || ch != '\n')
    {
         s += ch;//尾插
         ch = _cin.get();
     }
    return _cin;//支持连续提取
}

其实上面的写法是有点缺陷的,因为它涉及到空间消耗问题,尾插必定会引起扩容,所以上面的写法会频繁的去扩容,但是如果先reserve,那么要开多大的空间又是个问题,太大,万一字符串长度过小,又浪费空间,太小又可能会频繁扩容!所以这时就诞生了第二种写法,第二种写法的思路就是开个128大小的字符数组,当字符个数到128时,数组满,直接尾插到字符串,不足128时,也不会消耗太大空间!

写法二:

istream &operator>>(istream& _cin, string& s)
{

 //消耗小的写法,需要多大就开多大,这就不会频繁的去扩容,空间也不会浪费
    s.clear();
    char buff[128];
    size_t i = 0;
    char ch;
    ch = _cin.get();
    while (ch != ' ' || ch != '\n')
    {
        buff[i++] = ch;
     
        if (i == 127)//满128
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
       ch = _cin.get();
     }
  //小于128
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }
      return _cin;
}

getline函数

这个函数的存在就是弥补cind的缺陷,cin遇到空格就会停下,无法读取完整的一行,而这个函数就是读完一行为止!遇到空格不会停止

写法一:

istream& getline(istream& in, string& s)
{

    s.clear();//防止里面有脏数据!
    char ch;
    ch = in.get();//C++中可以使用get获取一个字符
    while (ch != '\n')
    {
         s += ch;//尾插
         ch = in.get();
     }
    return in;//支持连续提取
}

写法二:

istream& getline(istream& in, string& s)
{

 //消耗小的写法,需要多大就开多大,这就不会频繁的去扩容,空间也不会浪费
    s.clear();
    char buff[128];
    size_t i = 0;
    char ch;
    ch = in.get();
    while (ch != '\n')
    {
        buff[i++] = ch;
     
        if (i == 127)//满128
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
       ch =in.get();
     }
  //小于128
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }
      return in;
}

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

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

相关文章

武汉星起航:引领跨境电商新纪元,助力卖家实现全球业务飞跃

在全球化的时代背景下&#xff0c;越来越多的中国卖家正选择跨境电商作为他们拓展国际市场的重要途径。然而&#xff0c;对于许多新手卖家而言&#xff0c;如何进入海外市场、如何运营店铺、如何推广产品&#xff0c;都是一道道难以逾越的难题。在这个关键时刻&#xff0c;武汉…

智慧公厕:改变城市生活的革命性基础设施

在现代城市的高品质生活要求背景下&#xff0c;公共厕所成为了不可或缺的基础设施。然而&#xff0c;传统的公厕在服务质量、管理效率方面存在一定的问题。为了提供更好的公厕服务&#xff0c;智慧公厕应运而生。通过智能化的管理模式&#xff0c;智慧公厕实现了公厕使用与管理…

【并发编程】线程的基础概念

一、基础概念 1.1 进程与线程A 什么是进程&#xff1f; 进程是指运行中的程序。 比如我们使用钉钉&#xff0c;浏览器&#xff0c;需要启动这个程序&#xff0c;操作系统会给这个程序分配一定的资源&#xff08;占用内存资源&#xff09;。 什么线程&#xff1f; 线程是CP…

【Java八股面试系列】数据库(总结市面所有数据库知识点)

文章目录 索引索引是什么聚簇&非聚簇索引索引的实现方式HashBTree&#xff08;多路平衡二叉树&#xff09;两种实现方式区别 引申红黑树红黑树和AVL树的区别最大堆和最小堆 索引的优劣优点劣势 索引的使用索引失效的时候 事务特性事务并发问题隔离级别 锁锁的分类行锁表锁意…

udpflood是一种什么攻击,如何防御?

一、定义 UDPFlood攻击是一种利用UDP协议进行的拒绝服务&#xff08;DoS&#xff09;攻击&#xff0c;攻击者通过发送大量的UDP数据包来占用目标系统的网络带宽和资源&#xff0c;从而使系统无法正常响应合法用户的请求。 二、攻击方式 1. UDP协议特点&#xff1a; UDP协议是一…

【详解】运算放大器工作原理及其在信号处理中的核心作用

什么是运算放大器 运算放大器&#xff08;简称“运放”&#xff09;是一种放大倍数非常高的电路单元。在实际电路中&#xff0c;它常常与反馈网络一起组成一定的功能模块。它是一种带有特殊耦合电路和反馈的放大器。输出信号可以是输入信号的加法、减法、微分和积分等数学运算…

最大连续子序列和求值

文章预览&#xff1a; 题目算法遍历枚举代码如下 优化版本代码如下 陈越姥姥题目代码 题目 最大连续子序列求和&#xff0c;什么是连续最大子序列&#xff0c;先简单来说一个数组int a[]{-9,1,-10,2,3,4,-2,5,6,-2,-3}; 那它的连续最大子序列是2,3,4,-2,5,6 是18&#xff0c;就…

首页HF粗排模型优化

[work rus_env]$ pwd /home/work/xx/du-rus/offline-tools/du_rus/rus_env [work rus_env]$ python buildenv_rus.py 5a0e771e938a486df3b8b3e1cde1a39c2006882d 5f3241963a3e39a8e1eae05d7075fc5b9278a7c7 打开日志级别 [workxx conf]$ vim /home/work/xx/du-rus/du_rus_…

线程中的核心操作

线程中的核心操作 1:start()2:中断(终止)一个线程2.1:自己定义线程结束的代码2.1.1 存在的问题 2.2:使用Thread提供的interrupt()方法和isInterrupted()2.2.1 继续执行2.2.2 立即结束2.2.3 打印异常信息,再立即结束2.2.1 继续执行 22三级目录 1:start() start() 真正的创建线程…

LeetCode Python - 80. 删除有序数组中的重复项 II

目录 题目描述解法运行结果 题目描述 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须在 原地 修改输入数组 并在使用 O…

奔赴智慧医院建设浪潮,迈瑞创新产品亮相中国医学装备大会

3月28日&#xff0c;第32届中国医学装备大会暨2024医学装备展览会在重庆顺利开展。迈瑞医疗以“助力医院高质量发展&#xff0c;共建智慧医院生态”为主题参展&#xff0c;多款核心零部件、创新产品及智慧医疗生态集体亮相。 满足需求&#xff0c;推动智慧医院建设提速 迈瑞医疗…

天下三分明月夜,独有快慢指针法(链表面试题篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

什么是CVE? CVE漏洞应该如何防护?

CVE&#xff08;Common Vulnerabilities and Exposures&#xff09;的全称是公共漏洞和暴露&#xff0c;是公开披露的网络安全漏洞列表。IT人员、安全研究人员查阅CVE获取漏洞的详细信息&#xff0c;进而根据漏洞评分确定漏洞解决的优先级。 在CVE中&#xff0c;每个漏洞按CVE-…

Type-C一分二快充线智能分配方案

随着移动设备的普及和快充技术的迅猛发展&#xff0c;Type-C接口已成为众多手机、平板和笔记本电脑的标配。然而&#xff0c;在日常使用中&#xff0c;我们经常会遇到需要同时为多个设备充电的情况。这时&#xff0c;Type-C一分二快充线就显得尤为重要。为了更好地满足用户的充…

CSS之动画

一&#xff0c;动画的制作 实现盒子绕圈走 二&#xff0c; 动画的常用属性 三&#xff0c;动画简写属性 前面两个属性一定要写&#xff0c;第三个linear是指匀速的意思&#xff08;默认是ease&#xff09;

Day26 手撕各种集合底层源码(一)

Day26 手撕各种集合底层源码&#xff08;一&#xff09; 一、手撕ArrayList底层源码 1、概念&#xff1a; ArrayList的底层实现是基于数组的动态扩容结构。 2、思路&#xff1a; 1.研究继承关系 2.研究属性 3.理解创建集合的过程 – 构造方法的底层原理 4.研究添加元素的过程…

【Linux】图文详解Xshell远程连接服务器:以Amazon EC2 VPS为例

文章目录 问题描述解决方案Q&A 问题描述 本地cmd或powershell使用ssh -i “your.pem” user_nameip_address是可以登录Amazon EC2云服务器的。 然而&#xff0c;当使用XShell以SSH加载PEM文件方式登录亚马逊EC2云服务器&#xff0c;一直出现输入密码的问题&#xff0c;如…

FPGA亚稳态学习总结

首先是组合逻辑电路考虑的是竞争冒险&#xff0c;冒险会产生毛刺。重点研究如何去毛刺 时序逻辑电路考虑的是时序不满足会产生的亚稳态问题&#xff1a;如何考量时序满不满足呢&#xff1f;根据不同的场景又有不同的说法。 时序分析的两组基本概念 建立时间与保持时间 1.在…

使用Node.js常用命令提高开发效率

Node.js是一个基于Chrome V8引擎的JavaScript运行时环境&#xff0c;广泛用于构建服务器端应用程序和命令行工具。Node.js提供了丰富的命令和工具&#xff0c;可以帮助开发者更高效地开发应用程序。在日常开发中&#xff0c;除了Node.js本身的核心功能外&#xff0c;npm&#x…

加密/ 解密 PDF:使用Python为PDF文档设置、移除密码

在数字化时代&#xff0c;文档的安全性变得越来越重要。特别是对于包含敏感信息的PDF文件&#xff0c;确保其不被未经授权的人员访问或修改是至关重要的。本文将介绍如何使用Python在PDF文档中设置密码&#xff0c;以及如何移除已经设置的密码。 目录 PDF加密基础知识 Pytho…