五、字符串类的实现及子串查找算法

一、字符串类的创建

问题提出:C语言不支持真正意义上的字符串

C语言使用字符数组(“\0”结束)和一组函数实现字符串操作

C语言不支持自定义类型,因此无法获得字符串类型。

C++中的原生类型系统中是否包含字符串类型?——No!

C++通过库来支持字符串类型——标准库。

STL和Qt都有自己实现的字符串类型,会出现不兼容的情况。

所以我们可以自己实现字符串类来作为一个通用的库来使用。

通过面向对象的技术来对C语言中字符串相关函数进行封装,实现代码复用。

class String : public Object
{
protected:
    char* m_str;
    int m_length;

    void init(const char* s);
public:
    String();
    String(const char* s); //重载构造函数
    String(const String& s);  //拷贝构造函数
    int length() const;   //字符串长度
    const char* str() const;  //自定义字符串类与C语言字符串进行转换
    String(char c);   //重载构造函数

    bool operator ==(const String& s) const;
    bool operator ==(const char* s) const;
    bool operator !=(const String& s) const;
    bool operator !=(const char* s) const;
    bool operator >(const String& s) const;
    bool operator >(const char* s) const;
    bool operator <(const String& s) const;
    bool operator <(const char* s) const;
    bool operator >=(const String& s) const;
    bool operator >=(const char* s) const;
    bool operator <=(const String& s) const;
    bool operator <=(const char* s) const;

    String operator +(const String& s) const;
    String operator +(const char* s) const;

    String& operator +=(const String& s) ;
    String& operator +=(const char* s) ;

    String& operator =(const String& s); 
    String& operator =(const char* s); //赋值操作符的重载
    String& operator =(const char c);  //赋值操作符的重载

    ~String();   //析构函数
};
在具体的实现前,要注意放在同一个名称空间中,同时注意对C库的包含。
#include <cstring>
#include <cstdlib>

构造函数的实现:

    void String::init(const char *s)
    {
        m_str = strdup(s);

        if( m_str)
        {
            m_length = strlen(m_str) ;
        }
        else
        {
            THROW_EXCEPTION(NoEnoughMemoryException,"No memory to create Sting object...");
        }
    }


    String::String()
    {
       init("");
    }

    String::String(const char* s)
    {
       init(s ? s : ""); //如果s为空指针,则将空指针转换为空字符串
    }

    String::String(const String& s)
    {
        init(s.m_str);
    }

    String::String(char c)
    {
        char s[] = {c,'\0'};

        init(s);
    }

    int  String::length() const
    {
        return m_length;
    }

    const char* String::str() const
    {
        return m_str;
    }

操作符重载是自定义字符串类类型最重要的部分,并且需要进行重载。(返回值为STRING类型的引用,目的是为了实现链式操作)

第一次针对自定义字符串类类型,

第二种针对C中的字符串。

操作符重载只列出下面几个函数的实现,其余请读者自行实现:

    String String::operator +(const char* s) const
    {
        String ret;
        int len = m_length + strlen(s ? s : "");
        char* str = reinterpret_cast<char*>(malloc(len + 1));

        if( str )
        {
            strcpy(str, m_str);
            strcat(str,s ? s : "");

            free(ret.m_str);

            ret.m_str = str;
            ret.m_length =len;
        }
        else
        {
            THROW_EXCEPTION(NoEnoughMemoryException,"No memory to add String values...");
        }

        return ret;
    }
    String& String::operator =(const char* s)
    {
        if( m_str != s )
        {
            char* str = strdup(s ? s : "");

            if(str)
            {
                free(m_str);

                m_str = str;
                m_length = strlen(m_str);
            }
            else {
                THROW_EXCEPTION(NoEnoughMemoryException,"No memory to assign new String...");
            }
        }
    }

实现自定义字符串类类型中的常用成员函数:

    char& operator [] (int i);  //操作符重载函数,访问指定下标的字符
    char operator [](int i)const;

    bool startWith(const char* s)const;  //判断字符串是否以s开头
    bool startWith(const String& s)const;
    bool endOf(const char* s)const;  //判断字符串是否以s结束
    bool endOf(const String& s)const;

    String& insert(int i, const char* s);  //在字符串的位置i处插入s
    String& insert(int i, const String& s);

    String& trim();   //去掉字符串两端的空白

分别对上述成员函数进行实现:

    char& String::operator [] (int i)  //非const对象,返回值为引用,可以出现在赋值操作符的左边
    {
       if( (0 <= i) && (i < m_length))
       {
            return m_str[i];
       }
       else
       {
           THROW_EXCEPTION(InvalidOperationException,"Parameter i is invalid...");
       }
    }
    char String::operator [](int i)const
    {
        return (const_cast<String&>(*this))[i];
    }

在实现字符串开头、结尾比较的函数前,可以实现一个字符串相等与否的函数:

    bool String::equal(const char* l, const char* r, int len)const
    {
        bool ret = true;

        for(int i=0; i<len && ret; i++)
        {
            ret = ret && (l[i] == r[i]);
        }
        return ret;
    }

这样就可以通过两个字符串以及需要比较的长度信息进行下面的函数实现:

    bool String::startWith(const char* s)const
    {
        bool ret = ( s != NULL);

        if(ret)
        {
            int len = strlen(s);

            ret = (len < m_length) && equal(m_str, s, len);
        }

        return ret;
    }

    bool String::startWith(const String& s)const
    {
        return startWith(s.m_str);
    }

    bool String::endOf(const char* s)const
    {
        bool ret = ( s != NULL);

        if(ret)
        {
            int len = strlen(s);

            char* str = m_str + (m_length - len) ;  //指针运算,最后n个字符起始的下标

            ret = (len < m_length) && equal(str, s, len);
        }

        return ret;
    }

    bool String::endOf(const String& s)const
    {
        return endOf(s.m_str);
    }
插入函数也有两个版本,一个是字符串对象,一个是字符指针。
    String& String::insert(int i, const char* s)//返回值为STRING类型的引用,目的是为了实现链式操作
    {
        if( ( 0 <= i) && ( i <= m_length))
        {
            if( (s != NULL) && ( s[0] != '\0') )
            {
                int len =strlen(s);

                char* str = reinterpret_cast<char*>(malloc(m_length + len + 1));//强制类型转换;为什么加1?: \0

                if( str != NULL)
                {
                    strncpy(str, m_str, i);
                    strncpy(str+i, s , len);
                    strncpy(str+i+len, m_str+i, m_length - i);

                    str[m_length  +len] = '\0';
                    //**更新字符串数据**//
                    free(m_str);

                    m_str = str;
                    m_length = m_length + len;
                }
                else
                {
                   THROW_EXCEPTION(InvalidOperationException,"Parameter i is invalid...");
                }
            }
        }
        else
        {
           THROW_EXCEPTION(InvalidOperationException,"Parameter i is invalid...");
        }

        return *this;
    }

    String& String::insert(int i, const String& s)
    {
        return insert(i,s.m_str);
    }
去掉字符串两端的空白字符:
    String& String::trim()
    {
        int b = 0;
        int e = m_length - 1;

        while (m_str[b] == ' ')  b++;
        while (m_str[e] == ' ')  e--;

        cout<< b <<endl;
        cout<< e <<endl;

        if( b == 0)  //前端无空格
        {
            m_str[e + 1] = '\0';

            m_length = e + 1;
        }
        else
        {
            for(int i=0,j=b; j<=e;i++,j++)
            {
                m_str[i] = m_str[j];
            }

            m_str[e-b+1] = '\0';  //添加结束符

            m_length = e-b+1;
        }

        return *this;
    }

测试代码:

int main()
{
    String s = "   willing   ";

    if( s.trim().insert(0, "good").endOf("willing") && s.startWith("good"))
    {
        cout<< s.str() <<endl;
    }

    return 0;
}

思考:如何在目标字符串中查找是否存在指定的子串?


二、KMP子串查找算法

如何在目标字符串S中,查找是否存在子串P?

朴素算法:


朴素算法是可以想到的最简单的算法,通过不断的进行每个字符的匹配来对整个字符串进行检查,但是时间复杂度高,效率比较低。

朴素算法中存在的一个优化线索:



伟大的发现:

匹配失败时的右移位数与子串本身相关,而与目标串是无关的;

移动位数 = 已匹配的字符数 - 对应的部分匹配值;

任意子串都存在一个唯一的部分匹配表。


所以如何获取部分匹配表呢?

 先看部分匹配值是怎么求得的。

前缀:除了最后一个字符外,一个字符串的全部头部组合;

后缀:除了第一个字符以外,一个字符串的全部尾部组合;

部分匹配值:前缀和后缀最长共有元素的长度。


怎样通过编程产生部分匹配表呢?

实现关键:(贪心算法)

注:贪心算法:将优化问题转换成这样的一个问题:即先做出选择,再解决剩下的一个子问题;

即在贪心算法中,先做出总是当前看似最佳的选择,然后再解决选择之后所出现的子问题。


先通过编程实现部分匹配表的实现:

注意下面一张图的逻辑,第五步,当进行匹配的时候,ll=3,前缀后缀都右移一位,发现匹配不上。这个时候!注意!注意方框中重叠的部分,根据上一级的ll值,得知能够匹配上的最大长度,然后根据最大长度,对右移的元素再进行匹配,PMT(3)= ll` = 1,然后再进行匹配,发现仍然匹配不上,然后ll`` = 0。

此处的3,来源于已经匹配的最大长度。

关键在于,匹配失败之后进行的操作,即将已经匹配的元素作为“种子”再进行扩展。


int* make_pmt(const char* p) //建立指定字符串的部分匹配表      O(n)
{
    int len = strlen(p);

    int* ret = static_cast<int*>(malloc(sizeof(int)* len)); //指向部分匹配表的指针

    if( ret != NULL)
    {
        int ll = 0;  //前缀后缀交集的最大长度

        ret[0] = 0;   //长度为1的字符串,ll值为0

        for(int i=1; i<len; i++)
        {
            while( ( ll > 0) && (p[ll] != p[i]) )//不成立的时候
            {
                ll = ret[ll-1];  //正确理解这行代码:已经匹配好的子串中重新进行匹配
            }

            if( p[ll] == p[i])  //意味着扩展  //成立的时候
            {
                ll++;
            }

            ret[i] = ll;
        }
    }

    return ret;
}

通过上述代码,可以得出目标字符串的部分匹配值。

知道了部分匹配值后,就可以进行匹配中的右移操作:


编程实现:

int kmp(const char* s, const char* p) //  O(m) + O(n)  ==> O(m+n)
{
    int ret = -1;
    int sl = strlen(s);
    int pl = strlen(p);
    int* pmt = make_pmt(p);

    if( (pmt != NULL) && (0 < pl) && (pl < sl))  //判断是否可以进行查找
    {
        for(int i=0, j=0; i<sl; i++)
        {
            while ( (j > 0) && (s[i] != p[j]))  //不相等的话,改变j(子串)的值;结束条件(j=0)
            {
                j= pmt[j-1];  //得出右移的位数
            }

            if( s[i] == p[j])  //理想的情况   (贪心算法的体现)
            {
                j++;
            }

            if( j == pl ) //结束条件
            {
                ret = i+1-pl;  //匹配成功后,i的值在最后一个匹配成功的字符上(下标),用这个下标减去pl+1,得到开始匹配成功时的下标。
                break;
            }
        }
    }

    free(pmt);  //释放内存

    return ret;
}

函数返回匹配成功后字符串的下标。

测试代码:

int main()
{
    cout<< kmp("ABCDE","CD") <<endl;

    return 0;
}

运行结果:


部分匹配表是提高子串查找效率的关键;

部分匹配值定义为前缀和后缀共有元素的长度;

可以用递推的方法产生部分匹配表;

KMP利用部分匹配表和子串移动位数的关系来提高查找效率。

三、KMP算法的应用

如何在目标字符串中查找是否存在指定的子串?——增加新的成员函数进行封装。

    static int* make_pmt(const char* p);
    static int kmp(const char* s, const char* p);

增加以下成员函数的声明:

    int indexOf(const char* s) const;  //查找子串s在字符串中的位置
    int indexOf(const String& s) const;

    String& remove(const char* s);  //将字符串中的子串s删除
    String& remove(const String& s);

    String& remove(int i, int len);  //自定义删除操作

    String sub(int i, int len)const;  //从字符串中创建子串

    String operator -(const String& s) const;  //定义字符串减法
    String operator -(const char* s) const;
    String& operator -=(const String& s) ;
    String& operator -=(const char* s) ;

    String& replace(const char* t, const char* s);  //将字符串中的子串s替换为t
    String& replace(const String& t, const char* s);
    String& replace(const char* t, String& s);
    String& replace(String&, String& s);

具体实现(KMP算法的直接运用):

    //查找子串s在字符串中的位置
    int String::indexOf(const char* s) const
    {
       return kmp(m_str, s ? s : "");
    }

    int String::indexOf(const String& s) const
    {
       return kmp(m_str, s.m_str);
    }

删除操作:

根据KMP在目标字符串中查找子串的位置;

通过子串位置和子串长度进行删除。

String& String::remove(int i, int len)  //删除第i个元素开始的len个元素
    {
        if( (0 <= i) && ( i < m_length))
        {
            int n = i;
            int m = i+ len;

            //cout<< n <<endl;

            while ( (n<m) && (m<m_length))
            {
                m_str[n++] = m_str[m++];
            }

            m_str[n] = '\0';
            m_length = n;

            //cout<< n <<endl;
        }
        else
        {
            cout<< "No such String in current String"<<endl;
        }

        return *this;
    }

    String& String::remove(const char* s)
    {
        return remove(indexOf(s), s ? strlen(s):0);
    }

    String& String::remove(const String& s)
    {
        return remove(indexOf(s), s.length());
    }

字符串的减法操作operator-:

使用remove()实现字符串间的减法操作:

字符串本身不被修改;

返回新的子串。

    String  String::operator -(const String& s) const
    {
        return String(*this).remove(s);  //直接调用构造函数产生新的临时字符串对象,就可以不改变原先的字符串
    }
    String  String::operator -(const char* s) const
    {
        return String(*this).remove(s);
    }

    String&  String::operator -=(const String& s)  //原字符串被修改
    {
        return remove(s);
    }

    String&  String::operator -=(const char* s)
    {
        return remove(s);
    }

字符串的子串替换:

    String& String::replace(const char* t, const char* s)
    {
        int index = indexOf(t);   //Step 1

        if( index >= 0)
        {
            remove(t);            //Step 2
            insert(index, s);    //Step 3
        }
        else
        {
            cout<< "No such String in current String"<<endl;
            //THROW_EXCEPTION(InvalidOperationException,"No such String in current String");
        }

        return *this;
    }

    String& String::replace(const String& t, const char* s)
    {
        return replace(t.m_str,s);
    }

    String& String::replace(const char* t, String& s)
    {
        return replace(t,s.m_str);
    }

    String& String::replace(String& t, String& s)
    {
        return replace(t.m_str,s.m_str);
    }

从字符串创建子串:

以i为起点提取长度为len的子串;

子串提取不会改变字符串本身的状态。

    String String::sub(int i, int len)const
    {
        String ret;  //构造新的字符串

        if( (0 <= i) && (i < m_length))
        {
            if( len < 0)  len = 0;
            if(len+i > m_length)
                len = m_length - i;   //归一化 防止越界
            char* str = reinterpret_cast<char*>(malloc(len + 1));

            strncpy(str, m_str + i, len);

            str[len] = '\0';

            ret = str;

            free(str);
        }
        else
        {
            THROW_EXCEPTION(IndexOutOfBoundsException,"Parameter i is invalid ...");
        }

        return ret;
    }

字符串类的实现,一定要包含常见字符串的操作:增删查改。

代码的复用很重要!

代码的复用很重要!

代码的复用很重要!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值