详解 Leetcode 344.反转字符串 | 代码随想录 day8

一.反转字符串

1.问题描述

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

示例 1:
输入:["h","e","l","l","o"]
输出:["o","l","l","e","h"]

示例 2:
输入:["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]

力扣题目链接

2.基本思路

c++中有库函数 reverse 可以直接实现本题要求,但太简单了,一定不是题目所要考察的。

本题考查的点就是如何利用双指针进行数据的翻转。大致思路和曾经学过的206.反转链表 (opens new window)相似。偷懒就不再写了,直接附代码:

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

    }
};

二.反转字符串II

1.问题描述

给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。

如果剩余字符少于 k 个,则将剩余字符全部反转。

如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

示例:

输入: s = "abcdefg", k = 2
输出: "bacdfeg"

力扣题目链接

2.问题分析

本题只要用一个指针找到数组字符串中的所有以 2k 为单位的小组合即可,之后利用 reverse 函数直接对 2k 中的前 k 个字符进行反转。

需要注意的是:

(1)为了简化代码,直接让指针每次加 2k ,不用每次 +1了,如果每次 +1,还要设置一个计数器找到每次字符总量到达 2k 时的位置,利用  i += 2k 直接就能找到该位置。

(2)reverse 函数的用法

reverse 函数 是将 区间 [ first, second ) 内的数据进行翻转,其中,reverse函数有两个参数,分别指向 first 数据的位置和 second 数据的位置的迭代器。

(3)i + k <= s.size()

这是因为,一般情况下,编译器所提供的库函数都是左闭右开区间,举个例子,假如只有 3 个数据,k 也正好 等于 3,按理来说 这三个数据是要进行反转的,所以 i+k 可以 = size。

这里要求 i + k <= s.size() 是因为要确保当剩余数据不足 k 个时 ,要对剩余数据进反转,此时如果不写此条件,i + k > size,就会访问到数组之外的空间,属于非法访问了。

附代码:

class Solution {
public:
    string reverseStr(string s, int k) {
        for(int i =0; i < s.size(); i += 2*k) {
            if(i + k <= s.size()) {
                reverse(s.begin()+i, s.begin()+i+k);
                continue;  //这是对一般情况下 2k 中的前 k 个数据 以及 剩余数据不足 2k 但足 k 个时进行的翻转,若剩余数据不足 k 个,则应该对剩余所有数据进行反转,这里就要使用continue 而不是 break,因为使用 break 就直接跳出循环了。
}
            reverse(s.begin()+i, s.end());
}
        return s;

    }
};

三.替换空格 

1.问题描述

力扣题目链接(opens new window)

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1: 输入:s = "We are happy."
输出:"We%20are%20happy."

2.思路分析

首先找出字符串s 中的所有空格,计算出新字符串应有的长度:原字符串长度 + 空格 *2,然后利用双指针,一个指针指向旧数组的 最后一个数据的位置,另一个指针指向新数组最后一个位置。

当旧数组中,指针指向的值不为 “  ” 时,将该值赋给新数组,当旧数组中该值为空时,新数组指针指向的位置相应的依次替换为 “%20”.

附代码:

class Solution {
public:
	void replaceSpace(char *str,int length) {

		//先求空格数量
		int i =0;
		int oldNum = 0;  //这里给出的是数组的指针 str,不是数组,所以无法直接获取数组的长度,只能一个一个求
		int space = 0;   //统计空格数量
		int newNum = 0;   //新数组的长度

		if(str == NULL || length < 0) {
			return;
		}

		else {
			while(str[i] != '\0') {  //字符型数据存放时,到最后一个数据时,后面还多了个 /0,所以,/0 就是数组结束与否的标志

				oldNum ++;

				if(str[i] == ' ') {
					space++;
				}

				i++;
			}
		}

		newNum = oldNum + space*2;  //新数组的长度

		if(newNum > length) {
			return;
		}

		for(int i = newNum , j = oldNum ; j < i; j--, i--) {  //新旧指针的初始值,在原题中给出的是数组s ,所以最后一个数据的位置是 s.size()-1,但现在,oldNum 与 newNum 就是数组的长度,其最后一个值的位置就是 oldNUm,所以不用再减去一了。

			if(str[j] != ' ') {
				str[i] = str[j];
			}
			else {
				str[i] = '0';
				str[i-1] ='2';
				str[i-2] = '%';
				i -= 2;
			}
		}


	}
};

四.反转字符串中的单词

1.问题描述

(这道题真的折磨我好久.....所以写的非常非常非常认真,几乎涵盖了所有可能错的地方(我认为)

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

示例 1:

输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

2.基本思路

大致思路就是,先移除字符串中所有多余的空格,然后将整个字符串进行翻转,最后找出每个单词之间的分隔,分别翻转每个单词。

1)移除多余空格

这一步的思路和27.移除元素差不多,27 题是找出字符串中所有与 value 值相同的元素并移除,这道题的 value 就是空格。

但,有所不同的是:

字符串中的第一个单词前面不能有空格,其余单词之间都要有空格隔开。所以就涉及一个问题:如何找到两个单词并设置一个空格呢?

在 27 题中所使用的方法是双指针法,即,快指针遍历字符串,找到不是 value 值的数据,然后将快指针所指位置的数据赋给慢指针。本题与他相似。

void deleteSpace() {
    int fast = 0, slow = 0;
    for( ; fast < s.size(); fast++) {
        if(s[fasst] != ' ') {

            if(slow != 0) 
                s[slow++] = ' ';    //这里是简写,也可以写成: s[slow] = ' '; slow ++; 

            while(s[fast] != ' ' && fast < s.size()) {
                s[slow++] = s[falst]++ ;  //同样简写,也可以写成:s[slow] = s[fast]; slow++; fast++;
                
            }
 
        }

    }
s.resize(slow);
    
}

一样定义快慢指针,fast 用来遍历字符串,slow 用来确定数据应该放的位置。

首先进入循环中:

第一个 if 判断条件:s[fast] != '   ',这是来移除第一个单词前面的空格以及其余单词前面除了预留的一个空格外多余的空格。

例如,s = “_ _ he _ _ _ ww”  ( _ 代表空格)

slow = 0,fast = 0;

for () {

        if( ) 大条件不满足,直接退出此次循环,fast++,直到 fast 所指位置不为空 才会进入 if 语句中,所以很容易理解这行代码用来去除多余空格的原理。

}

第二步,满足 if 大条件后,即 fast 所指值不为空了,进入 if 语句里,首先,又碰到一个 if             “  if ( slow != 0)  ” ,这句话的作用就是在给每个单词间添加一个空格,和下面的 while 语句结合更好理解:while 语句说白了就是 在 把 fast 的值赋给 slow,这里要注意一点:

一定要是  while  而不是  if !!!

还拿上述例子来说,当 fast = 2 时,即他所指的数值是 h,单词是 he,那么他是要把整个单词赋给slow 的如果写的是 if,那么他就只会把 h 赋给 slow,从而退出这次循环,进入下一次,那么 fast 值将被 +1,变成 3,他所指位置 将会是 e,但是此时,由于 slow 也被赋了值(就在上一步,fast 把 h 赋给了 slow),那么 slow 是要进行 ++ 操作的,此时 slow 就由 0 变成了 1,那么进入新循环后,碰到的第一个 if ,由于 fast 所指不为空,所以可以进入,在进入后 ,碰到第二个循环,slow 现在的值 已经不为空了,所以他就会给 1 这个位置赋空,slow +1,之后在进入 while 循环,此时就是把 slow = 2 的位置 赋成了 e,这样,单词就被拆解开了。最终会出现如下结果:

明白了为什么要写成 while 之后,我们再回过头说说 第二个 if 的条件及其作用。

上面也说过了,这个 if 的作用就是给单词之间赋空格,具体实现过程就是,进入这个大循环后,首先判断 slow 是否为 0,也就是判断 slow 所指位置是不是首位置,如果是,即 slow == 0,不符合条件,直接跳过这个 if,执行下面的循环,假设下面的 while 循环成立,即 fast 已经指向 h 的位置了,那么他就会一直在 while 循环里,一直给 slow 赋不为 空 的值,直到 fast 所指值为空

为空之后,退出此次循环,fast ++,此时,fast 就指向了 e 之后的第一个空格,就说明上一个单词已经结束了,因为他指向空,所以他就进不了第一个 if 里,直到他指向了下一个单词的首字母 w,此时进入 大 if 了,遇到 小if,判断,此时 slow = 2 != 0,说明这里该加空格以隔开两个单词了,加完之后,再到下一条语句,将之后的单词字母再一一赋给slow。

还有一步非常重要!!!就是最后的 s.resize(slow);

一定要写这一步!因为 如果不写,那么字符串还是原来的长度,该删的空格已经删完了,该赋的值也付了,就留下剩余的没赋值的数了,如果不重新定义字符串长度,就会让剩余的多余的字符还在字符串中,导致结果出错。如下图所示:

最后的 ld 就是原来字符串中剩余的 ld。 

2)将整个字符串翻转

这里是使用自定义函数 myReverse 进行翻转,也方便后续的单词翻转工作。

void myReverse(string &s, int start, int end) {
    for(int i = start, j = end; i < j; i++,j--) {
        swap(s[i],s[j]);
    }
}

这里注意:一定要使用 & 引用!!!

因为我们知道,使用引用就是在同一个值上进行修改,而不使用引用,就是创建了一个新的字符串,他与原来的字符串完全相等,但所有的翻转操作都是在它上面进行的,而当这几行代码执行结束后,这个字符串就会被释放,而我们想要的 翻转后的字符串根本得不到!!!

为什么一定要自定义翻转函数?直接用库函数reverse 不行吗?

答案是这样的。我们都知道,编译器所提供的所有库函数,其区间范围都是  左闭右开  的,那么对于这道题来说,例如对单词 skyyy 进行翻转的话,s.begin() 指向的是第一个 “ s ”,s.end() 指向的是最后一个 “ y ”,如果使用的是 reverse 函数的话,他的翻转区间就是从 s —— y,但是由于他是左闭右开区间,所以 翻转时,包含了 第一个 s ,但并不包含最后一个 y,所以每次对单词进行翻转都要对其边界进行判断,看他是否到了单词的最后一个位置,下一次翻转是从哪里开始,非常麻烦。所以这里直接自定义了翻转函数解决这个问题。

3)将每个单词进行翻转

这一步最主要的就是找到每个单词的结束标志,仍然是用双指针,fast 来遍历字符串,什么时候就标志着该反转了呢?答案就是:当 fast 遇到空格 或者 fast 到字符串尾部时。

for(int slow = 0, fast = 0; fast <= s.size(); fast++) {
    if(s[fast] == '' && fast == s.size()) {
        myReverse(s,slow,fast);
        slow = fast + 1;
    }

}

这里注意: 此时的 fast 是可以取到 s.size() 的,当他取到后,也就是说此时 fast 已经指向了 字符串最后一个字符的下一个位置,就已经标志着字符串的结束了,如果写成 fast = s.size()-1,那么在下一步的单词反转时,就会出现反转错误:从单词的 第1个位置到 单词的 倒数第二个位置 被翻转了。如图:

 最后,总代码:

class Solution {
public:

    void myReverse(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.移除多余空格
        int slow = 0;
        for(int fast = 0; fast < s.size(); fast++) {
            if(s[fast] != ' ') {
                if(slow != 0) {
                    s[slow++] = ' ';
                }
                while(s[fast] != ' ' && fast < s.size()) {
                    s[slow++] = s[fast++];
                }
            }
        }
        s.resize(slow);

        //2.翻转字符串
        myReverse(s,0,s.size()-1);

        //3.翻转每个单词
        for(int i = 0, j = 0; j <= s.size(); j++) {
            if(s[j] == ' ' || j == s.size()) {
                myReverse(s,i,j-1);
                i = j + 1;
            }
        }
        return s;
    }
};

五.左旋字符串

1.题目描述

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:
输入: s = "abcdefg", k = 2
输出: "cdefgab"

示例 2:
输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"

限制:
1 <= k < s.length <= 10000

2.问题分析

由上一题得来的经验,可以这样做:

1)翻转 前 target 个字符

2)翻转剩余字符

3)翻转整个字符

代码如下:

class Solution {
public:
    string dynamicPassword(string password, int target) {
        reverse(password.begin(),password.begin()+target);
        reverse(password.begin()+target,password.end());
        reverse(password.begin(),password.end());
        return password;

    }
};

六.字符串翻转总结

在这篇文章344.反转字符串 (opens new window),第一次讲到反转一个字符串应该怎么做,使用了双指针法。

然后发现541. 反转字符串II (opens new window),这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在for循环的表达式上做做文章。

后来在151.翻转字符串里的单词 (opens new window)中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。

最后再讲到本题,本题则是先局部反转再 整体反转,与151.翻转字符串里的单词 (opens new window)类似,但是也是一种新的思路。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值