代码随想录C++ Day8 | 151.翻转字符串里的单词 55.右旋字符串 28. 找出字符串中第一个匹配项的下标

151.翻转字符串里的单词

最简单的是使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加。进阶做法是分为三步:

  • 移除多余空格
  • 将整个字符串反转
  • 将每个单词反转

举个例子,源字符串为:"the sky is blue "

  • 移除多余空格 : "the sky is blue"
  • 字符串反转:"eulb si yks eht"
  • 单词反转:"blue is sky the"

 在移除多余空格时,如果采用erase方法,由于erase方法本身的复杂度是O(n),考虑到外层的for循环,只是移除多余空格,时间复杂度就是O(n^2)

void removeExtraSpaces(string& s) {
    for (int i = s.size() - 1; i > 0; i--) {
        if (s[i] == s[i - 1] && s[i] == ' ') {
            s.erase(s.begin() + i);
        }
    }
    // 删除字符串最后面的空格
    if (s.size() > 0 && s[s.size() - 1] == ' ') {
        s.erase(s.begin() + s.size() - 1);
    }
    // 删除字符串最前面的空格
    if (s.size() > 0 && s[0] == ' ') {
        s.erase(s.begin());
    }
}

注意到移除空格和题27.移除元素很像,只是这里移除的元素是空格。但又有所不同,因为首尾的空格要全部删除,而单词间的空格要保留一个,所以要分开处理。思路一就是按照上述思路修改题27的算法,思路二是先去除所有空格,但循环中在非第一个单词的其它单词前手动添加空格,思路二写出来的代码更简洁

// 思路一
void removeExtraSpace(string& s) {
	int slow_index = 0, fast_index = 0;
	// 去除字符串前边所有空格,也就是将fast_index移动到第一个非空格字符
	while(s.size() > 0 && fast_index < s.size() && s[fast_index] == ' ') fast_index++;
	// 去除单词之间多余的空格
	for (;fast_index < s.size(); fast_index++) {
		// 跳过多余的空格
		if (fast_index > 0 // 确保fast_index-1 >= 0
			&& s[fast_index] == s[fast_index - 1]
			&& s[fast_index] == ' ') {
			continue;
		}
		s[slow_index++] = s[fast_index]; // 复制其它字符
	}
	// 第二步结束后,slow_index-1的位置可能存在一个空格
	if(slow_index > 0 && s[slow_index - 1] == ' ') {
		s.resize(slow_index - 1);
	} else {
		s.resize(slow_index);
	}
}
// 思路二
void removeExtraSpace(string& s) {
	int slow_index = 0;
	for (int fast_index = 0; fast_index < s.size(); fast_index++) {
		// 只处理不等于空格的字符
		if (s[fast_index] != ' ') {
			if (slow_index != 0) s[slow_index++]= ' '; // 除第一个单词,其余单词前手动加一个空格
			while(fast_index < s.size() && s[fast_index] != ' ') {
				s[slow_index++] = s[fast_index++]; // 循环复制字符,直到遇到下一个空格
			}
		}
	}
	s.resize(slow_index);
}

翻转整个字符串和翻转单个单词的函数在题344.翻转字符串

// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    void removeExtraSpace(string& s) {
        int slow_index = 0;
        for (int fast_index = 0; fast_index < s.size(); fast_index++) {
            // 只关注不为空格的字符
            if (s[fast_index] != ' ') {
                if (slow_index != 0) s[slow_index++] = ' '; // 除第一个单词,其余单词前手动加空格
                while (fast_index < s.size() && s[fast_index] != ' ') {
                    s[slow_index++] = s[fast_index++]; // 循环复制字符,直到遇到下一个空格
                }
            }
        }
        s.resize(slow_index);
    }

    void reverse(string&s, int start, int end) { //翻转,区间写法:左闭右闭 []
        for (int i = start, j = end; i < j; i++, j--) {
            swap(s[i], s[j]);
        }
    }

    string reverseWords(string s) {
        // 1. 去除多余空格
        removeExtraSpace(s);
        // 2. 翻转整个字符串
        reverse(s, 0, s.size() - 1);
        // 3. 翻转单个单词
        int start = 0; // 单词第一个字母索引
        for (int i = 0; i <= s.size(); i++) { // <=是因此要循环到串尾后一个位置,才知道最后一个单词结束
            if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
                reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转
                start = i + 1; //更新下一个单词的开始下标start
            }
        }
        return s;
    }
};

 55.右旋字符串

可以在本串上直接操作,具体来说,就是先翻转整串,再将两个字串翻转,负负得正,就得到题目要求的结果了

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

int main() {
    int n;
    string s;
    cin >> n;
    cin >> s;
    
    reverse(s.begin(), s.end()); // 翻转整个字符串
    reverse(s.begin(), s.begin() + n); // 翻转前一段,长度n
    reverse(s.begin() + n, s.end()); // 翻转后一段,长度为s.size() - n
    
    cout << s << endl;
}

 也可以先局部翻转,再整体翻转,只是注意第一段的长度就变成了s.size() - n

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

int main() {
    int n;
    string s;
    cin >> n;
    cin >> s;
    
    
    reverse(s.begin(), s.begin() + s.size() - n); // 翻转前一段,长度length - n;
    reverse(s.begin() + s.size() - n, s.end()); // 翻转后一段,长度为n
    reverse(s.begin(), s.end()); // 翻转整个字符串
    
    cout << s << endl;
}

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

 本题其实就是KMP算法所要解决的主要问题,即字符串匹配问题

最直接的办法就是两层for循环,外层遍历主字符串,用于确定匹配开始的位置。内层从此位置比较主字符串与模式字符串每个字符是否相同,完全相同就停止匹配,返回外层循环变量,否则继续外层循环。

KMP算法的精髓:回想上述暴力算法,内层循环时,当遇到有字符不相同就break,然后外层循环将开始匹配的位置后移一位,这是必须的么?假设模式字符串里,在冲突发生位置之前的部分是abcde,这部分确认是可以和主字符串相应位置匹配的,于是可以确定主字符串这部分内容也是abcde,那么将匹配开始位置后移一位,前边的部分就是要确认主字符串中的bcde和模式字符串abcd是否相同,也就是要确认模式字符串里的abcd和bcde是否相同。如果不同,那自然又要将匹配开始位置后移一位,前边的部分要确认主字符串里的cde和模式字符串里的abc是否相同。每次要确认的部分就是KMP算法所谓的前缀和后缀,以上过程在每次冲突发生时,都会发生

  • 前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
  • 后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。

那么,KMP算法的思想就是,将这个重复的过程利用起来,提前计算出来若当前字符冲突了,将开始匹配位置右移,也就是模式字符串右移几位,才可以确保移动之后,在冲突位置之前的部分是可以匹配的上。这样不用一次次右移,就加快了匹配的速度。

这里的右移几次,又叫做最大相等前后缀长度,

将每个模式字符串的位置冲突时的,最大相等前后缀长度计算出来,就得到了前缀表。

因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。

所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

时间复杂度分析:n为文本串长度,m为模式串长度,从上图可以看出匹配的过程是O(n),由于要额外生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。

那么,前缀表怎么计算呢,这其实还挺复杂的,具体查看代码随想录相应内容代码随想录 (programmercarl.com)

前缀表与next数组:很多KMP算法的实现都是使用next数组来做回退操作,next数组可以是前缀表,但是很多实现都是把前缀表统一减一(或右移一位,初始位置为-1)之后作为next数组。其实这并不涉及到KMP的原理,只是具体实现。以下我们以前缀表统一减一之后的next数组来做演示

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

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况 

// 前缀表所有值减一,对应的next表求法
void getNext(int* next, const string& s){
    int j = -1;  // j初始化为-1
    next[0] = j; // 第一个位置值恒为-1
    // i指向后缀末尾,j+1指向前缀末尾
    // 比如aabaaf, 一种情况是,前缀为aab, j+1指向b, 后缀为aaf, i指向f
    // 前缀后缀长度相同
    // i从1开始,因为第一个有效后缀索引就是1
    for(int i = 1; i < s.size(); i++) {
        // 新的前后缀末尾不同
        while (j >= 0 && s[i] != s[j + 1]) { // j=-1时就不需要回退了,跳出循环
            // 比如aabaaf, 前缀为aab, 后缀为aaf, 末尾一个为b, 一个为f,不相同
            // 于是寻找比较aa与af, 再比较a与f
            // 可以看到上述过程类似于以后缀aaf为主串,以前缀aab为模式串进行的匹配
            // 这里用KMP的思想,前缀向右移动几位,才能保证移动后后缀f之前的字符能匹配上
            // 那不就是f前一位的最大相等前后缀么,也就是next[j]的值
            // 这在之前已经求出来了
            j = next[j]; // 向前回退
        }
        // 新的前后缀末尾相同,最大相等前后缀长度自然加1
        if (s[i] == s[j + 1]) {
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

寻找模式串第一个匹配位置的整体代码如下

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = -1;
        next[0] = j;
        for (int i = 1; i < s.size(); i++) {
            // 新的前后缀末尾不同了
            while (j >= 0 && s[i] != s[j + 1]) {
                j = next[j]; // 回退
            }
            // 新的前后缀末尾相同
            if (s[i] == s[j + 1]) {
                j++;
            }
            // 将j(前缀的长度)赋给next[i]
            next[i] = j;
        }
    }

    int strStr(string haystack, string needle) {
        // 如果needle为空返回0
        if (needle.size() == 0) {
            return 0;
        }

        vector<int> next(needle.size());
        getNext(&next[0], needle);

        // 将主字符串和模式字符串,看作next数组构造时的后缀和前缀
        int j = -1;
        for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始,因为这里毕竟不是后缀,后缀在needdle的第一个位置是1,而主字符串第一个位置是0
            // 不匹配
            while (j >= 0 && haystack[i] != needle[j + 1]) {
                j = next[j]; // j寻找之前匹配的位置
            }
            // 匹配,j和i同时向后移动
            if (haystack[i] == needle[j + 1]) {
                j++; // i的增加放在for循环里
            }

            // j指针遍历到了模式串末尾,说明匹配成功,注意j起始值是-1不是0
            if (j == needle.size() - 1) {
                return (i - needle.size() + 1); // +1的原因是这里i的增加在for循环里,此时i还没有加1,要手动修改
            }
        }
        return -1; // 没找到,返回-1
    }
};

前缀表不减一的写法也是类似的

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) {
            return 0;
        }
        vector<int> next(needle.size());
        getNext(&next[0], needle);
        int j = 0;
        for (int i = 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;
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值