1. 题目来源
链接:压缩字符串
来源:LeetCode
2. 题目说明
给定一组字符,使用原地算法将其压缩。
压缩后的长度必须始终小于或等于原数组长度。
数组的每个元素应该是长度为1 的字符(不是 int 整数类型)。
在完成原地修改输入数组后,返回数组的新长度。
进阶:
你能否仅使用O(1) 空间解决问题?
示例1:
输入:
[“a”,“a”,“b”,“b”,“c”,“c”,“c”]
输出:
返回6,输入数组的前6个字符应该是:[“a”,“2”,“b”,“2”,“c”,“3”]
说明:
"aa"被"a2"替代。"bb"被"b2"替代。"ccc"被"c3"替代。
示例2:
输入:
[“a”]
输出:
返回1,输入数组的前1个字符应该是:[“a”]
说明:
没有任何字符串被替代。
示例2:
输出:
返回4,输入数组的前4个字符应该是:[“a”,“b”,“1”,“2”]。
说明:
由于字符"a"不重复,所以不会被压缩。"bbbbbbbbbbbb"被“b12”替代。
注意每个数字在数组中都有它自己的位置。
注意:
- 所有字符都有一个ASCII值在[35, 126]区间内。
- 1 <= len(chars) <= 1000。
3. 题目解析
方法一:简洁代码、双指针版本
这道题给了一个字符串,让我们进行压缩,即相同的字符统计出个数,显示在该字符之后,根据例子分析不难理解题意。本题要求不使用额外空间,最后让返回修改后的新数组的长度。
首先想,数组的字符不一定是有序的,如果用Map来建立字符和出现次数之间的映射,不管是用HashMap还是TreeMap,一定无法保证原有的顺序。所以不能用Map,而又需要统计个数,那么双指针就是不二之选。
既然双指针,其中一个指针指向重复字符串的第一个,然后另一个指针向后遍历并计数,就能得到重复的个数。仔细研究例子3,可以发现,当个数是两位数的时候,比如12,这里是将12拆分成1和2,然后存入数组的。那么比较简便的提取出各个位上的数字的办法就是转为字符串进行遍历。另外,由于需要对原数组进行修改,则需要一个指针cur来标记下一个可以修改的位置,那么最终cur的值就是新数组的长度,直接返回即可。
具体来看代码,用i和j表示双指针,开始循环后,我们用j来找重复的字符串的个数,用一个while循环,最终j指向的是第一个和i指向字符不同的地方,此时需要先将i位置的字符写进chars中,然后判断j是否比i正好大一个,因为只有一个字符的话,后面是不用加个数的,所以直接跳过。 否则将重复个数转为字符串,然后提取出来修改chars数组即可,注意每次需要将i赋值为j,从而开始下一个字符的统计,参见代码如下:
class Solution {
public:
int compress(vector<char>& chars) {
int n = chars.size(), cur = 0;
for (int i = 0, j = 0; i < n; i = j) {
while (j < n && chars[j] == chars[i]) ++j;
chars[cur++] = chars[i];
if (j - i == 1) continue;
for (char c : to_string(j - i)) chars[cur++] = c;
}
return cur;
}
};
简述一下:
本题的题意不难理解,思路也很简单,但是如果没有很清晰的逻辑就开始写代码的话容易出错,而且审题很重要,题目中虽然没给用例,但已经提示每个元素都是一个字符,所以当数字超过两位后,需要进行拆分。步骤如下:
- 大循环:采用双指针迭代的方法,由一个指针带着另一个走(j = i),由i跳过重复的元素
- 小循环1:对i进行自增,找到j的下一个位置
- 结果变量所指位置被赋为上一个处理完的元素
- 小循环2:将重复次数(即i和j的间隔)转为字符串,注意要用sprintf(itoa不是标准库函数),对结果变量所指位置赋值
- 返回结果变量
class Solution {
public:
int compress(vector<char>& chars) {
int n = chars.size();
int cur = 0;
for(int i = 0, j = 0; i < n; j = i) {
while(i < n && chars[i] == chars[j]) {
i++;
}
chars[cur++] = chars[j];
if(i - j == 1) {
continue;
}
string s = to_string(i - j);
for(int t = 0; t < s.size(); t++) {
chars[cur++] = s[t];
}
}
return cur;
}
};
方法二:朴素写法详解版
此题直观理解只需要一个字符一个字符遍历即可
首先定义char target和 int count用于存贮当前应该存入的字符和字符个数。依次遍历字符,当遍历到一样的就进行count加一操作,如果遍历到不为一样的,那么这个时候就应该把当前target和count放入原来的字符串中即可。
完全不用担心会覆盖后面没有遍历的字符的问题,因为此题已经简化了,只有count>1才会放入,count=1是不会放入count的,这就限制了target至少出现了两次我们才会放入两个字符,这个刚刚好,完全没有覆盖后面的字符,而对于count=1我们就只放入了target这样取出一位,又放入一位这样也没有覆盖后面的字符,而对于count>2而言,那取出的位数一定大于我们重新放进去的位数,所以此题不必担心字符覆盖的问题。
此题还需要注意就是count>9的时候我们需要把数字转化为字符再进行存入字符。采用除10取余的方法转化数字与字符会导致放入的数字是逆序的,所以还需要一个小的翻转。
class Solution {
public:
void insetNum(vector<char>& chars, int num, int& index) {
int begin = index;
while (num != 0) {
chars[index++] = num % 10 +' 0';
num /= 10;
}
int end = index - 1;
while (begin < end) {
int temp = chars[begin];
chars[begin] = chars[end];
chars[end] = temp;
begin++;
end --;
}
}
int compress(vector<char>& chars) {
char target = chars[0];
int count = 1;
int index = 0;
for (int i = 1; i < chars.size(); ++i) {
if (chars[i] != target) {
chars[index++] = target;
target = chars[i];
if (count != 1) insetNum(chars, count, index);
count = 1;
}
else count++;
}
chars[index++] = target;
if (count != 1) insetNum(chars,count,index);
return index;
}
};
胡乱申请空间的写法…
class Solution {
public:
int compress(vector<char>& chars) {
vector<char> vt;
vector<char> vt1;
int cnt = 0;
chars.push_back(' ');
for (int i = 0; i < chars.size() - 1; ) {
char ch = ' ';
int num = 0;
ch = chars[i];
vt.push_back(ch);
while (ch == chars[i]) {
++num;
++i;
}
if (num == 1) continue;
if (num > 1 && num < 10) {
vt.push_back('0' + num);
continue;
}
while (num > 9) {
vt1.push_back('0' + (num % 10));
num /= 10;
}
vt1.push_back('0' + num);
for (int i = vt1.size() - 1; i >= cnt; --i) {
vt.push_back(vt1[i]);
}
cnt = vt1.size();
}
for (int i = 0; i < vt.size(); ++i) {
chars[i] = vt[i];
}
return vt.size();
}
};