字符串->算法实现

本文概述了几种编程挑战,涉及LeetCode中的字符串反转(344、151)、字符串反转中的单词、替换数字(非原题)、右旋字符串、找出字符串中第一个匹配项的下标(28)以及重复的子字符串(459),介绍了相应的题解和算法,包括时间复杂度和空间复杂度分析。
摘要由CSDN通过智能技术生成

综述

反转字符串:leetcode344
反转字符串 II:leetcode541
替换数字:非leetcode原题
反转字符串中的单词:leetcode151
右旋字符串:非leetcode原题
找出字符串中第一个匹配项的下标:leetcode28
重复的子字符串:leetcode459

引言

刷题总结

反转字符串

题目

leetcode344
在这里插入图片描述

题解

简单题不多说

class Solution {
public:
    void reverseString(vector<char>& s) {
        int i = 0;
        int j = s.size() - 1;
        while (i < j) {
            std::swap(s[i++], s[j--]);
        }
    }
};

反转字符串 II

题目

leetcode541
在这里插入图片描述

题解

每 2k 个字符串就反转前 k 个字符串
循环中记得处理最后的边界,即“最后剩余的字符串的处理”
这里面有个小技巧,如果自己实现 reverse 函数的话,尽量保持和库函数一样的“左闭右开”,这样方便,而且像使用库函数一样,不容易出错

class Solution {
public:
    string reverseStr(string s, int k) {
        int index = 0;

        //每2k个反转前k个字符串
        while (index < s.size()) {
            //处理最后剩余的字符串
            if (s.size() - index < k) {
                reverse(s, index, s.size());
                break;
            }
            if (s.size() - index >= k && s.size() - index < 2 * k) {
                reverse(s, index, index + k);
                break;
            }

            reverse(s, index, index + k);
            index += 2 * k;
        }

        return s;
    }

private:
    void reverse(string& s, int begin, int end) { //左闭右开反转
        for (int i = begin, j = end - 1; i < j; i++, j--) {
            std::swap(s[i], s[j]);
        }
    }
};

时间复杂度: O(n)
空间复杂度: O(1)

替换数字

题目

非leetcode原题
在这里插入图片描述

题解

很多数组填充类的问题,其做法都是先预先给数组扩容到填充后的大小,然后在从后向前进行操作。
这么做有两个好处:

  1. 不用申请新数组。
  2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。

ACM 代码:

#include<iostream>
using namespace std;

class Solution {
public:
    void ti_huan_number(string& s) {
        //统计数字的个数
        int count = 0;
        for (char ele : s) {
            if (ele >= '0' && ele <= '9') count++;
        }
        
        //扩充字符串
        int beforeSize = s.size();
        s.resize(beforeSize + count * 5);
        
        //从后往前赋值
        for (int i = beforeSize - 1, j = s.size() - 1; i >= 0; i--) {
            if (s[i] >= '0' && s[i] <= '9') {
                s[j--] = 'r';
                s[j--] = 'e';
                s[j--] = 'b';
                s[j--] = 'm';
                s[j--] = 'u';
                s[j--] = 'n';
            } else {
                s[j--] = s[i];
            }
        }
    }
};


int main() {
    string s;
    Solution solution;
    while (cin >> s) {
        solution.ti_huan_number(s);
        cout << s <<endl;
    }
    return 0;
}

反转字符串中的单词

题目

leetcode151
在这里插入图片描述

题解

要求:不要使用辅助空间,空间复杂度要求为O(1)
如果使用辅助空间,那这道题就是简单题了,没意义

这道题得多练,里面坑很多,不容易一下子写出来,得debug较长时间
思路就是:先总体反转,再挨个反转单词,反转单词的过程中需要处理多个空格的情况

class Solution {
public:
    string reverseWords(string s) {
        //先删除尾随空格
        int index = s.size() - 1;
        while (s[index] == ' ') index--;
        s.resize(index + 1);

        //字符串整体颠倒顺序
        reverse(s.begin(), s.end());

        //按照空格为间隔,给每个单词再次颠倒顺序,需要注意多个空格删成一个空格
        for (int i = 0, j = 0; j < s.size(); j++) {
            if (s[j] == ' ' && s[j - 1] == ' ') continue; //处理多余空格
            if (s[j] == ' ') {
                reverse(s.begin() + i, s.begin() + j);
                while (s[i] != ' ') i++;
                i++;
            }
            if (j == s.size() - 1) reverse(s.begin() + i, s.end());
        }

        //删除尾随空格
        index = s.size() - 1;
        while (s[index] == ' ') index--;
        s.resize(index + 1);

        return s;
    }
};

右旋字符串

题目

非leetcode原题
在这里插入图片描述

题解

如果允许额外开辟空间,即再定义一个字符串,那这道题太简单了
因此作出限制:不能申请额外空间,只能在本串上操作
那么这道题就类似于上道题:整体反转 + 局部反转
也就是说:

  1. 整体反转
  2. 局部反转,前k个字符反转
  3. 局部反转,后面的字符反转

ACM 代码:

#include<iostream>
#include<algorithm>
using namespace std;

class Solution {
public:
    void rightRerverse(string& s, int k) {
        //整体反转
        reverse(s.begin(), s.end());
        
        //局部反转,前k个字符反转
        reverse(s.begin(), s.begin() + k);
        
        //局部反转,后面的字符反转
        reverse(s.begin() + k, s.end());
    }
};

int main() {
    int k;
    string s;
    Solution sol;
    
    cin >> k >> s;
    sol.rightRerverse(s, k);
    cout << s << endl;
    
    return 0;
}

剑指 offer 中这个题是 左反转,其实思路是一样的,只不过局部反转时的区间不一样

找出字符串中第一个匹配项的下标

题目

leetcode28
在这里插入图片描述

题解

这道题是 KMP 算法的经典题目
一般面试中可能很难直接写出 KMP,可以先试试暴力,暴力也可以过 leetcode 的这道题

暴力:
思路就是一个一个的匹配,如果 haystack[i] == needle[j],就 j++,然后继续匹配后面的字符串
如果全部匹配,直接返回
如果中间有的不匹配,终止内循环,将 j 置为0,继续从 i + 1 的位置开始匹配

class Solution {
public:
    int strStr(string haystack, string needle) {
        for (int i = 0, j = 0; i < haystack.size(); i++) {
            if (haystack[i] == needle[j]) { //两个字符串的第一个匹配,继续匹配后面的
                for (int k = i; j < needle.size(); k++) {
                    if (haystack[k] == needle[j]) j++; //每匹配一次 j 就 ++
                    else break; //有一次不匹配就 break
                    if (j == needle.size()) return i; //全部匹配就直接返回
                }
                j = 0; //中间有一次不匹配,需要将 j 置为0,后序继续从 第一位开始匹配
            }
            
        }
        return -1;
    }
};

KMP 算法:

KMP 算法介绍

KMP 主要应用在字符串匹配上。
KMP 的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容(通过前缀表也就是 next 数组),可以利用这些信息避免从头再去做匹配了。
我们需要注意的是:next 数组里的数字表示的是什么,为什么这么表示?
面试有可能会问

前缀表是什么

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
举个例子:
在这里插入图片描述
文本串中第六个字符 b 和 模式串的第六个字符 f 不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。但是通过前缀表可以知道直接退回到第三个字符 b 的位置即可

首先明确概念:
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
对于 aabaaf 来说,f 的前缀是 aabaa,f 的后缀是 abaaf
前缀表(这里直接用 next 数组表示前缀表)记录的就是最长相等前后缀的长度
比如 aabaaf 的前缀表:
在这里插入图片描述
举个例子:对于下标为 3 的 a 来说,前缀是 aab,后缀是 aba
从后往前匹配 aab 和 aba,发现第三位的 b 和 第三位的 a 不匹配,第二位的 a 和 第二位的 b 不匹配,第一位的 a 和第一位的 a 匹配,所以最长相等前后缀的长度是 1,next[3] = 1;
再举个例子:对于下标为 4 的 a 来说,前缀是 aaba,后缀是 abaa,第四位和第一位都相等,所以 next[4] = 2;

next 数组(前缀表)为什么这样表示

那 next 数组为什么这样表示呢?
因为如果找到不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少,从而进行回退,比如动图中的 b 和 f 不匹配,那么就从 f 回退到下标为 2 的 b,因为 next[4] = 2; 也就是说 f 前面的一位的 next 数值是 2,所以回退到下标为 2 的位置。
但是为什么 next 这样表示就可以正确回退呢?
因为 f 不匹配,但是前面两位的 aa 和 后面两位的 aa 相等,所以回退到 前面两位的 aa 后面那个下标位置最合适。我们想直接跳到 前缀aa 的后面,而 前缀aa 的后面那个下标 就是前缀的长度,f 位置不匹配,那么找前缀表中前一位的数值,也就是 f 前面的 a 对应的数值 2,而前缀表的数值代表的是
最长相等前后缀的长度,所以通过 next 可以直接找到 下标为 2 的位置。

构造 next 数组

构造 next 数组其实就是计算模式串 needle 前缀表的过程。 主要有如下 4 步:

  • 初始化
  • 处理前后缀不相同的情况
  • 处理前后缀相同的情况
  • 更新 next 数组
  1. 需要明确的一点
    定义两个指针 i 和 j,j 指向前缀末尾位置(同时也代表前缀的长度),i 指向后缀末尾位置。
    而 next[i] 表示 i(包括 i)之前的最长相等前后缀的长度,其实就是 j,所以更新 next 数组时肯定是这样的:next[i] = j;
  2. 初始化
    对于模式串 aabaaf,第一位 a 只有后缀没有前缀,所以初始化为 0,即 next[0] = 0;
  3. 处理前后缀不相同的情况
    j 是从 0 开始的,i 是从 1 开始的(因为第一位已经初始化了,肯定是从第二位进行对比的,类似于动态规划)
    所以: for (int i = 1; i < needle.size(); i++)
    如果前后缀不相等即 needle[i] != needle[j],需要向前回退,也就是找到 j 前面的一个元素的 next 的数值 即 next[j - 1],进行回退(和上面讲的模式串和文本串不匹配时的回退操作是一样的)
    但是此时需要注意的是,只要不相等,就需要回退,所以使用的不是 if,而是 while。
    并且回退时需要用到 next[j - 1] 所以 j 需要 > 0,一来是防止数组角标越界,二来是意味着回退到 j = 0 就不再回退
    所以代码是:
while (j > 0 && needle[i] != needle[j]) {
	j = next[j - 1];
}
  1. 处理前后缀相同的情况
    找到相等的前后缀即 needle[i] == needle[j],则 j++,意味着 前后缀的长度加1
    所以代码是:
if (needle[i] == needle[j]) j++;
  1. 更新 next 数组
next[i] = j;

所以整个获取 next 数组的代码:

void getNext(int* next, string& needle) {
	//初始化
    next[0] = 0;
    
    //i表示后缀最后字符的索引,j表示前缀最后字符的索引
    for (int i = 1, j = 0; i < needle.size(); i++) {
    
        //前后缀不相等
        while (j > 0 && needle[i] != needle[j]) {
            j = next[j - 1];
        }

        //前后缀相等
        if (needle[i] == needle[j]) j++;

        //更新
        next[i] = j;
    }
}

使用 next 数组进行模式串和文本串的匹配

主要有如下 4 步:

  • 获取 next 数组
  • 处理模式串和文本串不匹配的情况
  • 处理模式串和文本串匹配的情况
  • 判断是否匹配成功并返回
  1. 定义 i 和 j,i 指向文本串的下标,j 指向模式串的下标,二者都是从 0 开始
  2. 获取 next 数组
    直接调用 刚才的函数 即可:
int next[needle.size()];
getNext(next, needle);
  1. 处理模式串和文本串不匹配的情况
    只要不匹配,就回退,和 getNext 函数一样的
while (j > 0 && haystack[i] != needle[j]) {
	j = next[j - 1];
}
  1. 处理模式串和文本串匹配的情况
    匹配 j 就++,继续匹配后面的字符。代码 和 getNext 函数一样
if (haystack[i] == needle[j]) j++;
  1. 判断是否匹配成功并返回
if (j == needle.size()) {
	return i - needle.size() + 1;
}

所以根据 next 数组进行字符串匹配的代码是:

int strStr(string haystack, string needle) {
	//获取 next 数组
    int next[needle.size()];
    getNext(next, needle);
    
    for (int i = 0, j = 0; i < haystack.size(); i++) {
    	//处理模式串和文本串不匹配的情况
        while(j > 0 && haystack[i] != needle[j]) {
            j = next[j - 1];
        }
		
		//处理模式串和文本串匹配的情况
        if (haystack[i] == needle[j]) {
            j++;
        }

		//判断是否匹配成功并返回
        if (j == needle.size() ) {
            return (i - needle.size() + 1);
        }
    }
    return -1;
}

整道题的代码:

class Solution {
public:
    int strStr(string haystack, string needle) {
        //获取next数组
        int next[needle.size()];
        getNext(next, needle);

        for (int i = 0, j = 0; i < haystack.size(); i++) {
            //模式串和文本串的字符不匹配
            while (j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }

            //模式串和文本串的字符匹配
            if (haystack[i] == needle[j]) j++;

            //判断并返回
            if (j == needle.size()) return i - needle.size() + 1;
        }

        return -1;
    }

private:
    void getNext(int* next, string& needle) {
        //初始化
        next[0] = 0;

        for (int i = 1, j = 0; i < needle.size(); i++) {
            //前后缀不匹配
            while (j > 0 && needle[i] != needle[j]) {
                j = next[j - 1];
            }

            //前后缀匹配
            if (needle[i] == needle[j]) j++;

            //更新 next
            next[i] = j;
        }
    }
};

暴力算法的时间复杂度是:O(n * m)
KMP 算法的时间复杂度是:O(n + m)

重复的子字符串

题目

leetcode459

题解

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值