【C++】string类模拟实现

news2024/12/23 18:10:29

🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🚁 个人主页:不 良
🔥 系列专栏:🛸C++  🛹Linux
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹


文章目录

  • 默认成员函数
    • 构造函数
    • 析构函数
    • 拷贝构造函数
    • 赋值运算符重载
  • 访问及遍历函数
    • 重载运算符[]
    • 迭代器函数begin和end
  • 容量相关函数
    • size函数
    • capacity函数
    • empty函数
    • clear函数
    • reserve函数
    • resize函数
  • 修改操作相关函数
    • push_back函数
    • append函数
    • +=运算符重载
    • c_str函数
    • find函数
    • insert函数
    • swap函数
    • erase函数
  • string类的非成员函数
    • 重载流插入<<和流提取>>
    • getline函数
  • 关系运算符重载

模拟实现的string类在库里面已经存在,为了避免命名冲突我们可以使用命名空间的方式来定义string类以示区分。

string类底层是字符数组,所以我们可以根据库中的string类来定义模拟实现的string类中的成员变量。

#pragma once
namespace Niu {
	class string {
	private:
        char* _str;
        size_t _size;
        size_t _capacity;
	};
}

默认成员函数

构造函数

string库中的构造函数:

string()//构造空的string类对象,即空字符串
string(const char* s)//用 C-string 来构造 string 类对象

模拟实现一个无参的构造函数和一个有参的构造函数,并且在初始化列表中显示定义:

//无参构造函数
string()
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
{}
//有参构造函数
string(const char* str)
    :_str(str)//error,属于权限的放大问题
    ,_size(strlen(str))
    //VS下_capacity不包含\0的大小,所以用strlen,在cstring头文件中
    ,_capacity(strlen(str))
{}

但是_str变量为char*类型,而形参strconst char*类型,所以上述有参构造函数初始化列表中_str(str)存在权限的放大问题:

image-20230626171424712

此时可以将类成员变量改成const char*类型,但是虽然对于构造函数可以通过编译了,但是_str指向的内容位于常量区,常量区的内容无法再修改。而且如果_str为空指针,当我们想实现c_str函数时会出现错误,因为c_str函数是通过\0来打印字符串的,_str为空,解引用空指针就会报错,而string类是通过size大小打印字符串的。

那我们应该怎么修改呢?

**对于无参构造函数,初始化列表中初始化_str的时候不要给空指针,使用new char[1]开辟一个空间。**new的时候也可以不加[],但是为了保证析构时方便,最好加上[]。

//无参构造函数
string()
    :_str(new char[1])
     ,_size(0)
     ,_capacity(1)
{	
     //保证c_str返回值解引用时不为空指针
     _str[0] = '\0';//这里使用了重载运算符[]
}

对于有参数的构造函数,我们可以通过在函数体内开空间的方式+拷贝的方式来定义构造函数,注意初始化列表顺序和声明顺序:

我们可以先将str指向内容的长度用strlen函数记录在_size中,然后让_capacity等于_size,再开辟空间并且将str指向的内容拷贝到_str中。

string(const char* str)
	:_size(strlen(str))
{
    _capacity = _size;
    //_capacity 是存储的有效数据个数,所以要多开一个空间用来存储\0
    //这里多开的空间是_str指向的空间,和size和capacity没有关系
    _str = new char[_capacity + 1];
    //将str中的内容拷贝到_str
    strcpy(_str, str);//按字节将str指向的内容拷贝到_str中
}

上面两个构造函数我们可以使用一个全缺省的构造函数替代:

//全缺省构造函数,可以使用一个空的字符串来代替
string(const char* str = "")
	:_size(strlen(str))
{
    _capacity = _size;
    //_capacity 是存储的有效数据个数,所以要多开一个空间用来存储\0
    //这里多开的空间是_str指向的空间,和size和capacity没有关系
    _str = new char[_capacity + 1];
    //将str中的内容拷贝到_str
    strcpy(_str, str);//按字节将str指向的内容拷贝到_str中
}

注意:默认构造函数在类中只能存在一个,所以无参的构造函数和全缺省的构造函数不能同时存在,最好使用全缺省的构造函数。

析构函数

string类的析构函数需要我们自己实现,因为申请的空间是在堆区,当对象销毁时堆区空间,不会自动销毁,为了避免内存泄漏,需要使用delete[]手动释放堆区的空间。

//析构函数
~string()
{
    delete[] _str;//使用delete[]进行析构
    _str = nullptr;//置空
    _capacity = _size = 0;
}

拷贝构造函数

在实现拷贝构造函数之前,我们先了解一下深浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

深拷贝:给每个对象独立分配资源,保证多个对象之间不会因共享资源而造成多次释放造成程序崩溃问题。

当我们使用编译器自动生成的拷贝构造函数运行如下代码时会出现报错,原因就是因为浅拷贝问题。

void test_string1()
{
    string s1;
    string s2("hello world");
    string s3(s2); //拷贝构造s3

    cout << s1.c_str() << endl;
    cout << s2.c_str() << endl;
    cout << s3.c_str() << endl;
}

拷贝构造时,浅拷贝只是按照字节将s2中的指针_str_size_capacity拷贝到s3中,并没有在堆上开辟新的空间,也就是说s3中的指针_str指针和s2中的_str指针相同,指向的是同一块堆空间。

image-20230626230555981

示意图:

image-20230626230755230

两个指针指向同一块空间会有两个大问题:

1.一个修改会影响另一个

2.要析构两次

所以,为了避免出现上述问题,我们在实现拷贝构造的时候必须在堆上开空间,让新的对象有自己的空间:

image-20230626231131536

此时要在拷贝构造中开空间从而使新对象有自己独立空间,称为深拷贝。

这里提供两种深拷贝的方法:

传统写法:先在堆上申请一份新空间,让新对象的_str指针指向这份空间,将原有对象_str指向的内容拷贝到新空间之后再给新对象的_size_capacity赋值使其与原有对象相等。

string(const string& s)
//多开一个空间用来存放\0
	:_str(new char[strlen(s._str) + 1])
{
    //strcpy(_str, s._str);
    //也可以使用memcpy函数,+1将原字符串中的\0也拷贝过来
    memcpy(_str, s._str, strlen(s._str) + 1);
    _size = s._size;
    _capacity = s._capacity;
}

现代写法:利用构造函数先将s对象_str指针指向的内容生成一个新的对象,然后利用swap函数将两个对象进行数据交换,交换之后新对象的_str指针指向的内容就是原有对象指针指向的内容,完成之后函数中创建的对象调用析构函数完成对象的销毁。

string(const string& s)
    :_size(s._size)
    ,_capacity(s._capacity)
{
    string s1(s._str);
    //使用尚未实现的swap函数交换两个指针
    //swap(s1);
    //也可以使用算法库中的swap函数
    std::swap(s1._str,_str);
}

image-20230627085413849

赋值运算符重载

赋值运算符重载也存在深浅拷贝问题,所以实现赋值运算符重载时也要进行深拷贝。

传统写法:先将目标对象的原有空间释放,避免过大造成空间的浪费,然后再开空间,使用strcpy函数或者memcpy函数将_str指针指向的内容拷贝过去并将_size_capacity赋值。

//传统写法
string& operator=(const string& s)
{
	//if避免自己给自己赋值
    if (this != &s)
    {
        delete[] _str;//先将原来的空间释放,避免原有空间过大造成空间的浪费
        _str = new char[s._capacity + 1];//申请新的空间,+1是为了\0
        //strcpy(_str, s._str);//拷贝
        memcpy(_str, s._str, strlen(s._str) + 1);
        _size = s._size;
        _capacity = s._capacity;
    }
}

现代写法:通过采用传值传参的方法接收右值,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换。

string& operator=(string s)
{
    //尚未实现的swap函数
    //swap(s);
    //也可以通过交换str指针的方式
    std::swap(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
    return *this;
}

但是这种写法无法避免自己给自己赋值这种情况。

访问及遍历函数

重载运算符[]

重载运算符[]使其可以通过访问下标的方式来访问和修改字符。

实现[] 运算符重载时先判断下标是否合法,然后返回对象C字符串对应位置字符的引用,便能实现对该位置的字符进行读取或者修改操作。

//返回引用可以修改字符串内容,可以读取和修改字符串内容
char& operator[](size_t pos)
{
    assert(pos < _size);//要判断下标是否合法
    return _str[pos];
}

//此函数只能读取字符串内容不可修改
const char& operator[] (size_t pos) const
{
    assert(pos < _size);//要判断下标是否合法
    return _str[pos];
}

迭代器函数begin和end

在string和vector中数据是连续的,所以我们可以将迭代器认为是指针。

string类中的迭代器可以认为是字符指针,只是给字符指针起了一个别名叫iterator。

typedef char* iterator;
typedef const char* const_iterator;

begin函数的作用就是返回字符串的第一个字符的地址。

//两个重载函数
iterator begin() //返回之后指向的内容可以修改
{
    return _str;
}
const_iterator begin() const //返回之后指向的内容不可以修改
{
    return _str;
}

end函数的作用就是返回字符串最后一个字符的下一个字符,即\0

//end函数
iterator end()
{
    return _str + _size;
}
const_iterator end() const
{
    return _str + _size;
}

范围for是由迭代器支持的,如果迭代器begin函数名改为Begin函数名,则范围for就不能支持了。

实际运用:

string s2("hello world");
const string s3 = s2;

string::iterator it = s2.begin();
while (it != s2.end())
{
    (*it)++;//修改内容
    cout << *it;
    it++;
}
cout << endl;
//范围for
for (auto& e : s2)
{
    e++;//修改字符串内容
}
cout << s2.c_str() << endl;

//const对象,不支持范围for修改字符串内容
string::const_iterator cit = s3.begin();
while (cit != s3.end())
{
    //(*cit)++;//error,不能修改内容
    cout << *cit ;
    cit++;
}

容量相关函数

size函数

返回字符串中已经存在的字符串个数,不包含\0

size_t size() const
{
    return _size;
}

capacity函数

返回字符串中当前容量。

size_t capacity() const
{
    return _capacity;
}

empty函数

string类可以通过_size是否为0来判断字符串是否为空。

//判空
bool empty() const
{
    return _size == 0;
}

clear函数

clear函数可以将已有字符串内容清空,string类打印时是根据size大小打印的,我们可以将size更改为0,然后将字符串首个字符改为\0即可。

//将字符串清空
void clear()
{
    //只用将首字母更改为\0,然后将_size改为0即可
    _str[0] = '\0';
    _size = 0;
}

reserve函数

reserve是扩容函数。

只有当n大于_capacity时才取扩容,当n小于等于_capacity时不需要扩容。

//扩容函数
void reserve(size_t n)
{
    //当n大于capacity时再扩容,否则不用扩容
    if (n > _capacity)
    {
        char* tmp = new char[n + 1];//开一个空间用来放\0
        strcpy(tmp, _str);//将_str指向的内容拷贝到tmp指向的空间中
        delete[] _str;//释放_str指向的空间
        _str = tmp;//让_str也指向tmp指向的空间
        _capacity = n;//改变_capacity大小
    }
}

resize函数

resize函数可以改变当前对象的有效字符的个数。

image-20230627162237691

但是当n<=_size的时候只会改变_size的大小,不会改变_capacity的大小,因为缩容的代价很大,要开空间还要挪动数据。

void resize(size_t n ,char ch = '\0')
{
    if (n <= _size)
    {
        _size = n;
        _str[_size] = '\0';
    }
    else
    {
        if (n > _capacity)
        {
            reserve(n);//扩容
        }
        size_t i = _size;
        while (i < n)
        {
            _str[i] = ch;
            i++;
        }
        _size = n;
        _str[_size] = '\0';

    }
}

修改操作相关函数

push_back函数

在字符串后尾插字符c

_size == _capacity时,说明空间已经满了,此时需要扩容,可以使用reserve函数进行扩容,然后将要插入的字符插入到字符串结尾部分,并++_sizepush_back的时候还要注意当_capacity等于0的情况,此时可以修改构造函数或者添加判断条件。

void push_back(char c)
{
    //如果空间已经满了就要扩容,2倍扩容
    if (_size == _capacity)
    {
        //判断_capacity是否为0
		_capacity == 0 ? 4 : _capacity;
        reserve(_capacity * 2);
    }
    _str[_size] = c;
    ++_size;
    //还要在后面加一个\0
    _str[_size] = '\0';
}

append函数

在字符串后追加一个字符串

void append(const char* str)
{
    size_t len = strlen(str); //str字符串的大小,不包含\0
    if (_size + len > _capacity)
    {
        reserve(_size + len);//扩容
    }
    //strcat追加函数
    //strcat(_str, str);
    strcpy(_str + _size, str);//插入到_str字符串的后面

    //也可以使用循环插入
    /*size_t i = 0;
			while (i <= len) //=len是将\0也插入
			{
				_str[_size] = str[i];
				_size++;
				i++;
			}*/
    _size += len;
}

推荐使用strcpy函数,因为strcat是自己去找\0,找到了才在后面追加;strcpy拷贝字符串的时候会把\0拷贝到后面,不需要单独处理。

+=运算符重载

对字符串进行插入操作的时候更喜欢使用+=,实现可以复用push_back和append函数。

//重载+=
//返回自定义类型尽量用引用,减少拷贝
string& operator+=(char ch)//实现连接字符
{
    push_back(ch);//复用push_back
    return *this;
}
string& operator+=(const char* str)//实现连接字符串
{
    append(str);//复用append
    return *this;
}

c_str函数

函数作用就是返回C格式字符串,C格式字符串打印时候是遇到\0就停止打印,string类字符串打印的时候是按照size大小去打印的。

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

find函数

从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置,如果找到了返回第一个字符匹配出现的位置,如果没有找到返回npos(因为字符串不会有npos那么长,所以不用担心比字符串npos大)。

npos是string类的一个静态成员变量,其值为整型最大值。此时在模拟实现类中的成员变量中加一个静态成员变量npos,并在类外定义。

#pragma once
namespace Niu {
	class string {
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
        static size_t npos;
    };
	size_t string::npos = -1;
}

库中函数定义:

//从pos位置向后找与str匹配的第一个位置
size_t find (const string& str, size_t pos = 0) const;
//从pos位置向后找与s匹配的第一个位置
size_t find (const char* s, size_t pos = 0) const;
//从pos位置向后找与字符c匹配的第一个位置
size_t find (char c, size_t pos = 0) const;

查找字符串中指定字符的位置

先判断pos位置是否合法,然后通过遍历字符串找到等于ch的字符位置,返回字符所在位置的下标;如果没有找到就返回npos。

//find函数查找字符
size_t find(char ch,size_t pos = 0) const
{
    assert(pos < _size);
    size_t i = pos;
    while (i < _size)
    {
        if (_str[i] == ch)
        {
            return i;
        }
    }
    return npos;
}

查找字符串中指定子串的位置

先判断pos位置是否合法,然后通过strstr函数找到子串的首个字符的下标位置,如果没有找到返回npos,找到了返回首个字符的下标位置。

size_t find(const char* str, size_t pos = 0) const
{
    char* p = strstr(_str + pos, str);//strstr是暴力匹配
    if (p == nullptr)
    {
        return npos;
    }
    else
    {
        return p - _str;//指针-指针得到的是两者之间的数据个数
    }
}

insert函数

在字符串中插入字符或者字符串。

1.在字符串的下标为pos位置插入字符

我们先观察下面的程序中是否存在问题?

//在下标为pos的位置插入ch
string& insert(size_t pos , char ch)
{
    assert(pos <= _size);//检测下标合法性
    if (_size  == _capacity)
    {
        _capacity = 0 ? 4 : _capacity;//避免_capacity等于0
        reserve(_capacity * 2);//使用reserver函数扩容
    }
    //但是这种写法当pos为0时会出现错误
    size_t end = _size; //下标为_size位置是\0
    while (end >= pos)
    {
        _str[end + 1] = _str[end];
        --end;
    }
    _str[pos] = ch;
    ++_size;
    return *this;
}

代码中当pos = 0 时,循环条件end = pos时仍然能够进入循环,然而因为endsize_t(无符号整数)类型,end不可能会出现小于0的情况,while循环条件将永远成立,所以我们可以将end和pos都改为int类型或者使用下面的代码:

string& insert(size_t pos, char ch)
{
    assert(pos <= _size);//检测下标合法性
    if (_size == _capacity)
    {
        _capacity = 0 ? 4 : _capacity;//避免_capacity等于0
        reserve(_capacity * 2);//使用reserver函数扩容
    }
    size_t end = _size + 1; //下标为_size位置是\0
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        --end;
    }
    _str[pos] = ch;
    ++_size;
    return *this;
}

2.在字符串的下标为pos位置之前插入字符串

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--;
    }
    还可以这样进行操作
    //size_t end = _size;	
    //for (size_t i = 0; i < _size + 1; i++)
    //{
    //	_str[end + len] = _str[end];
    //	--end;
    //}
    //strncpy(_str + pos, str, len);//在pos位置后连接上字符串str,包含pos位置
    //_size += len;
    //return *this;

    strncpy(_str + pos, str, len);//在pos位置后连接上字符串str,包含pos位置
    _size += len;
    return *this;
}

插入字符串和插入字符有相同之处,我们可以将插入字符看作是len = 1,那么插入字符串的代码同样使用于插入字符。

插入字符串的时候使用strncpy,不能使用strcpy,否则会将待插入的字符串后面的\0也插入到字符串中。

实现insert之后,push_back函数和append函数都可以复用insert:

void push_back(char ch)
{
    insert(_size, ch);
}
void append(const char* str)
{
    insert(_size, str);
}

swap函数

swap就是将对象中的指针,_size_capacity进行交换。

void swap(string& s)
{
    std::swap(_str, s._str);//交换指针
    std::swap(_size, s._size);//交换_size
    std::swap(_capacity, s._capacity);//交换_capacity
}

erase函数

erase函数的作用是删除字符串任意位置开始的长度为len的字符。删除前也需要判断pos的合法性,进行删除操作的时候分两种情况:

1、pos位置和之后的有效字符都需要被删除,这时只需要在pos位置放上\0,然后将对象的_size更新即可。

2、pos位置开始需要删除的字符只是字符串中的一部分,此时我们可以用strcpy函数将后方需要保留的有效字符覆盖前面需要删除的有效字符即可。strcpy会将字符串结尾处的\0也覆盖过去。

void erase(size_t pos = 0, size_t len = npos)
{
    assert(pos < _size);
    assert(pos >= 0);
    //当len大于字符串长度时直接在pos位置加上\0
    if (len == npos || pos + len > _size) //pos位置及其后面的字符都被删除
    {
        _size = pos;//_size更新
        _str[_size] = '\0';//在pos处加上\0
    }
    else
    {
        strcpy(_str + pos, _str + pos + len);//将后面的内容覆盖过去
        _size -= len;
    }
}

string类的非成员函数

重载流插入<<和流提取>>

重载流插入<<

重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印,可以使用范围for进行遍历。

ostream& operator<<(ostream& out, const string& s)
{
    for (size_t i = 0; i < s._size; i++)
    {
        out << s._str[i];
    }
    //使用范围for进行遍历
    //for (auto e : s)
    //{
    //	out << e;
    //}
    return out;
}

重载流提取>>

使用>>时字符和字符之间的分隔符就是空格或者换行符,所以我们可以设置直到读取到空格或是换行符时便停止读取。输入的时候是输入到缓冲区里面,我们可以使用in.get()读取字符。

istream& operator>>(istream& in, string& s)
{
    //先清空字符串
    s.clear();
    char ch = in.get();
    while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或\n的时候继续读取
    {
        s += ch;//将读取到的字符尾插到字符串后面
        ch = in.get(); //继续读取字符
    }
    return in;
}

+=要不断的扩容,我们可以开一个字符数组用来存储字符,等达到一定数量的字符后,再放到对象中,可以一定程度增加效率。

istream& operator>>(istream& in, string& s)
{
    //先清空字符串
    s.clear();
    char ch = in.get();
    char buff[128];
    size_t i = 0;
    while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或\n的时候继续读取
    {
        buff[i++] = ch;
        if (i == 127)
        {
            buff[128] = '\0';
            s += buff;//将读取到的字符尾插到字符串后面
            i = 0;
        }
        ch = in.get();
    }
    if (i != 0)
    {
        buff[i] = '\0';
        s += buff;
    }
    return in;
}

扩展:

对象中指针指向的字符串内容是存在堆上的,不占用对象的空间,所以计算对象大小时只当作一个指针的大小。

在VS下string库中的对象类的大小为28,因为还有一个buff数组,当指针指向的内容小时就在自己的buff数组中,大的话存到堆里面。

image-20230628114328704

s2对象中还有一个char [16]的buff数组:

image-20230628114450318

在 Linux 的 g++ 编译器中32位下是4个字节,64位下是8个字节,只有一个指针,指针指向的空间存有_size_capacity_refcount等,这与他们的底层实现有关,g++编译器下的string类的结构如下图所示:

image-20230628114954461

string类中只有一个指针 _ptr ,其他属性以及数据都存放在堆区开辟的空间中,如果后面进行了拷贝构造,则直接实例化出一个新的对象,并把指针指向同一个空间,同时将 _refcount +1:

image-20230628115629996

如果想要修改 s1 或 s2 的内容,修改哪个都会触发写时拷贝,再在堆区上另外开辟出一块空间,进行写操作:

image-20230628120133524

在析构时,如果_refcount的值大于 1 ,则进行--操作;如果等于 1 ,就把该空间释放掉。

getline函数

getline函数的实现我们只要将流提取的while循环条件改为ch != ' '就实现getline函数。

istream& getline(istream& in, string& s)
{
    s.clear(); //清空字符串
    char ch = in.get();
    while (ch != '\n') //只要不等于\n就继续
    {
        s += ch;
        ch = in.get();
    }
    return in;
}

关系运算符重载

string类比较大小时,比较的是ASCII码的大小。当我们实现了 > 和 == 之后可以通过复用来实现其他运算符重载。

不修改成员变量数据的情况下,最好都加上const。

bool operator>(const string& s) const
{
    return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s) const
{
    return strcmp(_str, s._str) == 0;
}
bool operator>=(const string& s) const
{
    return s == *this || *this > s;
}
bool operator<(const string& s) const
{
    return !(*this >= s);
}
bool operator<=(const string& s) const
{
    return !(*this > s);
}

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

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

相关文章

基于 Docker + Nginx + Gitlab-runner 实现前端自动化部署流程

本篇会用到Docker&#xff0c;Gitlab-runner等相关工具&#xff0c;如果对其不是特别了解&#xff0c;可以参考下相关文档&#xff1a; GitLab RunnerDocker 快速入门CI/CD&#xff1a;持续集成/持续部署 在早期部署前端项目时&#xff0c;我们通常会通过ftp把前端代码直接传…

AcWing 239. 奇偶游戏—带边权并查集、带扩展域并查集解法

AcWing 239. 奇偶游戏—带边权并查集、扩展域 问题带边权并查集解法扩展域解法并查集所要掌握的知识技能如下图所示 问题 题目链接: AcWing 239. 奇偶游戏 问题描述 分析 这道题比较有意思&#xff0c;可以由前缀和的思想来解决&#xff0c;[l,r]为偶数&#xff0c;说明[0,l…

CentOS8.5 安装时配置镜像源

CentOS8.5 安装时配置镜像源 阿里云镜像地址 http://mirrors.aliyun.com/centos/8-stream/BaseOS/x86_64/os/ 安装时录入镜像源 点击Done等待… 搞定

day30-Auto Text Effect(自动文本吐字效果)

50 天学习 50 个项目 - HTMLCSS and JavaScript day30-Auto Text Effect&#xff08;自动文本吐字效果&#xff09; 效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewpo…

玩转 PI 系列-如何在 Rockchip Arm 开发板上安装 Docker Tailscale K3s Cilium?

概述 618 买了几个便宜的 Purple PI OH 开发板 (500 块多一点买了 3 个&#x1f911;), 这个开发板类似树莓派&#xff0c;是基于 Rockchip&#xff08;瑞芯微&#xff09; 的 rx3566 arm64 芯片。如下&#xff1a; 买来是用作家庭服务器或家庭实验室的。主要考虑就是&#xf…

CTFHub | 过滤目录分隔符

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习&#xff0c;实训平台。提供优质的赛事及学习服务&#xff0c;拥有完善的题目环境及配套 writeup &#xff0c;降低 CTF 学习入门门槛&#xff0c;快速帮助选手成长&#xff0c;跟随主流比赛潮流。 0x01 题目描述…

订单逆向履约系统的建模与PaaS化落地实践 | 京东云技术团队

导读 本文重点介绍了京东零售电商业务在订单逆向履约上面的最佳技术实践&#xff0c;京东零售快退平台承接了零售几乎所有售前逆向拦截和退款业务&#xff0c;并在长期的业务和技术探索中沉淀了丰富的业务场景设计方案、架构设计经验&#xff0c;既能承接面向消费者C端用户的高…

【计算机视觉 | 目标检测 | 图像分割】arxiv 计算机视觉关于目标检测和图像分割的学术速递(7 月 17 日论文合集)

文章目录 一、检测相关(5篇)1.1 TALL: Thumbnail Layout for Deepfake Video Detection1.2 Cloud Detection in Multispectral Satellite Images Using Support Vector Machines With Quantum Kernels1.3 Multimodal Motion Conditioned Diffusion Model for Skeleton-based Vi…

记录--卸下if-else 侠的皮衣!- 策略模式

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 &#x1f92d;当我是if-else侠的时候 &#x1f636;怕出错 给我一个功能&#xff0c;我总是要写很多if-else,虽然能跑&#xff0c;但是维护起来确实很难受&#xff0c;每次都要在一个方法里面增加逻辑…

索引使用——单列索引、联合索引、索引设计原则

1.单例索引和联合索引 尽量使用联合索引&#xff0c;避免使用单列索引&#xff0c;因为使用联合索引性能相对而言会比较高&#xff0c;如果联合索引使用得当&#xff0c;可以避免回表查询&#xff0c;使用单列索引很容易造成回表查询的情况&#xff0c;造成性能降低。 create…

双Bank OTA升级:N32L40X BootLoader (一)

什么是双Bank升级&#xff1a;将Flash划分为以下4个区域。 BootLoader区&#xff1a;程序进行升级的引导程序&#xff0c;根据Upade_Flag来判断程序是跳转Bank1区运行程序或是将Bank2区的程序搬运到Bank1区&#xff0c;然后在运行程序。 Upade_Flag区&#xff1a;判断BootLoa…

OpenCV自带的HAAR级联分类器对脸部(人脸、猫脸等)的检测识别

在计算机视觉领域&#xff0c;检测人脸等是一种很常见且非常重要的应用&#xff0c;我们可以先通过开放计算机视觉库OpenCV来熟悉这个人脸识别领域。另外OpenCV关于颜色的识别&#xff0c;可以查阅&#xff1a;OpenCV的HSV颜色空间在无人车中颜色识别的应用HSV颜色识别的跟踪实…

WAIC2023:图像内容安全黑科技助力可信AI发展

目录 0 写在前面1 AI图像篡改检测2 生成式图像鉴别2.1 主干特征提取通道2.2 注意力模块2.3 纹理增强模块 3 OCR对抗攻击4 助力可信AI向善发展总结 0 写在前面 2023世界人工智能大会(WAIC)已圆满结束&#xff0c;恰逢全球大模型和生成式人工智能蓬勃兴起之时&#xff0c;今年参…

MQTT 与 Kafka|物联网消息与流数据集成实践

MQTT 如何与 Kafka 一起使用&#xff1f; MQTT (Message Queuing Telemetry Transport) 是一种轻量级的消息传输协议&#xff0c;专为受限网络环境下的设备通信而设计。Apache Kafka 是一个分布式流处理平台&#xff0c;旨在处理大规模的实时数据流。 Kafka 和 MQTT 是实现物…

模拟实现atoi函数

请记住那些对你好的人&#xff0c;因为他们本可以不这么做 文章目录 atoi函数介绍 模拟实现 大家好&#xff0c;我是纪宁。 atoi函数&#xff0c;它的功能是将数字字符转化为数字。我第一次见这个函数还是在大一上在刷蓝桥杯的时候&#xff0c;有一个关于回文数字的题&#x…

08.计算机网络——其他重要协议和技术

文章目录 DNSICMPNAT代理服务器 DNS DNS是一整套从域名映射到IP的系统 ​ TCP/IP中使用IP地址和端口号来确定网络上的一台主机的一个程序&#xff0c;但是IP地址不方便记忆&#xff0c;于是人们发明域名&#xff0c;其本质是一个字符串&#xff0c;映射了它和IP地址的关系。 …

融合黄金正弦算法和纵横交叉策略的秃鹰搜索算法(GSCBES)-附代码

融合黄金正弦算法和纵横交叉策略的秃鹰搜索算法(GSCBES) 文章目录 融合黄金正弦算法和纵横交叉策略的秃鹰搜索算法(GSCBES)1.秃鹰优化算法2.改进秃鹰优化算法2.1 基于纵横交叉策略2.2 基于惯性权重的位置更新2.3 黄金正弦捕食机制 3.实验结果4.参考文献5.Matlab代码6.python代码…

FreeRTOS-列表和列表项

列表和列表项&#xff1a; 列表是FreeRTOS中的一个数据结构&#xff0c;用来跟踪FreeRTOS中的任务。 列表项就是存放在列表中的项目&#xff0c;属于列表的子集。 列表就相当于一个链表&#xff0c;列表项就相当于节点&#xff0c;在FreeRTOS中的列表是一个双向的环形链表。 …

基于FPGA的视频接口之PAL(NTSC)编码

简介 PAL又称帕尔制&#xff0c;是咱们中国早期视频所是使用的视频广播模式&#xff0c;基本上现在的电视都兼容这种视频模式&#xff0c;使用的接口也是传统的BNC插头&#xff0c;有兴趣的伙伴可以看看电视屁股后面是不是有一个单独的BNC接口&#xff0c;百分之98就是支持PAL格…

FastReport.Net FastReport.Core 2023.2.23 Crack

FastReport.Net & FastReport.Core 2023.2.23适用于 .NET 7、.NET Core、Blazor、ASP.NET、MVC 和 Windows 窗体的全功能报告库。它可用于 Microsoft Visual Studio 2022 和 JetBrains Rider。 利用数据呈现领域专家针对 .NET 7、.NET Core、Blazor、ASP.NET、MVC、Windo…