题目描述:
给你一个字符数组 chars
,请使用下述算法压缩:
从一个空字符串 s
开始。对于 chars
中的每组 连续重复字符 :
- 如果这一组长度为
1
,则将字符追加到s
中。 - 否则,需要向
s
追加字符,后跟这一组的长度。
压缩后得到的字符串 s
不应该直接返回 ,需要转储到字符数组 chars
中。需要注意的是,如果组长度为 10
或 10
以上,则在 chars
数组中会被拆分为多个字符。
请在 修改完输入数组后 ,返回该数组的新长度。
你必须设计并实现一个只使用常量额外空间的算法来解决此问题。
示例 1:
输入:chars = ["a","a","b","b","c","c","c"] 输出:返回 6 ,输入数组的前 6 个字符应该是:["a","2","b","2","c","3"] 解释:"aa" 被 "a2" 替代。"bb" 被 "b2" 替代。"ccc" 被 "c3" 替代。
示例 2:
输入:chars = ["a"] 输出:返回 1 ,输入数组的前 1 个字符应该是:["a"] 解释:唯一的组是“a”,它保持未压缩,因为它是一个字符。
示例 3:
输入:chars = ["a","b","b","b","b","b","b","b","b","b","b","b","b"] 输出:返回 4 ,输入数组的前 4 个字符应该是:["a","b","1","2"]。 解释:由于字符 "a" 不重复,所以不会被压缩。"bbbbbbbbbbbb" 被 “b12” 替代。
提示:
1 <= chars.length <= 2000
chars[i]
可以是小写英文字母、大写英文字母、数字或符号
思路:
-
初始化索引:定义两个索引,
readIndex
用于遍历原始字符数组chars
,writeIndex
用于记录压缩后数据在数组中的位置。 -
遍历数组:使用
while
循环,通过readIndex
从数组的开始位置遍历到结束。 -
记录当前字符和计数:在循环内部,使用
currentChar
记录当前遍历到的字符,count
记录该字符连续出现的次数。 -
计算连续字符数:使用内部的
while
循环来确定currentChar
连续出现的次数,同时更新readIndex
以跳过这些连续的字符。 -
压缩逻辑:
- 如果
count
大于1,表示有多个连续的相同字符,需要压缩。此时,将currentChar
写入chars
数组的writeIndex
位置,然后递增writeIndex
。 - 将
count
转换为字符串countStr
,然后遍历countStr
的每个字符,将它们依次写入chars
数组,每次写入后递增writeIndex
。 - 如果
count
等于1,表示没有压缩的必要,直接将currentChar
写入chars
数组的writeIndex
位置,然后递增writeIndex
。
- 如果
-
移动读取指针:在每次内部循环结束后,将
readIndex
向前移动一位,以便在下一次迭代中读取下一个字符。 -
返回新长度:当
readIndex
遍历完整个数组后,循环结束。此时,writeIndex
表示压缩后的数据在数组中的长度。返回writeIndex
作为函数的结果。
代码实现:
/**
* @param {character[]} chars
* @return {number}
*/
var compress = function (chars) {
let readIndex = 0; // 用于遍历原始数组
let writeIndex = 0; // 用于记录压缩后字符串在数组中的位置
while (readIndex < chars.length) {
let count = 1; // 记录当前字符的连续出现次数
let currentChar = chars[readIndex];
// 计算当前字符连续出现的次数
//当前指针+1小于数组长即没有越界时&&当前元素和下一个元素相同
//也可以用当读指针 read 位于字符串的末尾,或读指针 read 指向的字符不同于下一个字符时来结束循环
//readIndex === length - 1 || chars[readIndex] !== chars[readIndex + 1]
while (readIndex + 1 < chars.length && chars[readIndex + 1] === currentChar) {
readIndex++;
count++;
}
// 如果当前字符连续出现次数大于1,则需要记录字符和次数
if (count > 1) {
// 将字符写入数组
chars[writeIndex] = currentChar;
writeIndex++;
// 将次数转换为字符串,并根据需要拆分写入数组
let countStr = count.toString();
for (let i = 0; i < countStr.length; i++) {
chars[writeIndex] = countStr[i];
writeIndex++;
}
} else {
// 如果连续出现次数为1,直接将字符写入数组
chars[writeIndex] = currentChar;
writeIndex++;
}
// 移动读取指针
readIndex++;
}
// 返回压缩后数组的新长度
return writeIndex;
};
运行结果分析
运行后发现内存的消耗比较高
上述代码的效率主要取决于字符数组的遍历次数和写入次数。由于题目要求使用常量额外空间,我们不能使用额外的数据结构来存储中间结果。因此优化主要集中在减少不必要的操作和优化写入逻辑上。
性能优化:
在查询资料后,发现要想优化性能可以从以下几个方面进行
-
减少字符到字符串的转换次数:在处理重复字符组的计数时,如果计数大于9,则需要将其转换为字符串并逐个字符写入数组。我们可以预先计算出需要写入的字符数,然后一次性写入,而不是在循环中重复转换和写入。
-
优化写入逻辑:在写入字符和计数时,我们可以预先分配好空间,然后一次性写入,减少数组的多次修改。
-
减少条件判断:尽量减少在循环中的条件判断,尤其是在内层循环中。
在查询资料后,发现要想优化性能可以从以下几个方面进行
优化实现思路:
-
初始化指针:
writeIndex
初始化为0,用于记录压缩后字符串在数组中的位置。 -
遍历字符数组:使用一个for循环遍历
chars
数组,其中i
作为当前遍历到的位置的索引。 -
记录当前字符:在循环内部,
currentChar
变量用来存储当前遍历到的字符。 -
计算连续字符数:使用一个while循环来计算当前字符连续出现的次数。如果下一个字符与当前字符相同,
count
加1,并且i
向前移动一位。 -
写入当前字符:无论连续字符数是1还是更多,当前字符都会被写入到
chars
数组的writeIndex
位置,然后writeIndex
递增。 -
写入连续字符数:如果
count
大于1,表示有连续的字符需要压缩。此时,将count
转换为字符串countStr
,然后使用另一个for循环将countStr
中的每个字符写入到chars
数组中,每次写入后writeIndex
递增。 -
更新索引:在while循环中,由于已经确定了连续字符的数量,所以
i
索引会跳过这些连续的字符,直接移动到下一个不同的字符处。 -
返回新长度:遍历完成后,
writeIndex
表示压缩后的字符串在数组中所占的最终位置,即新的长度。函数返回这个值。
优化代码实现:
/**
* @param {character[]} chars
* @return {number}
*/
var compress = function (chars) {
let writeIndex = 0; // 用于记录压缩后字符串在数组中的位置
for (let i = 0; i < chars.length; i++) {
let currentChar = chars[i];
let count = 1; // 记录当前字符的连续出现次数
// 计算当前字符连续出现的次数
//当前指针+1小于数组长即没有越界时&&当前元素和下一个元素相同
//也可以用当读指针 read 位于字符串的末尾,或读指针 read 指向的字符不同于下一个字符时来结束循环
//readIndex === length - 1 || chars[readIndex] !== chars[readIndex + 1]
while (i + 1 < chars.length && chars[i + 1] === currentChar) {
count++;
//当前已遍历元素的下标
i++;
}
// 写入字符
chars[writeIndex++] = currentChar;
// console.log(chars[writeIndex++])
// 如果连续出现次数大于1,需要写入次数
if (count > 1) {
// 将次数转换为字符串并逐个字符写入数组
let countStr = count.toString();
for (let j = 0; j < countStr.length; j++) {
chars[writeIndex++] = countStr[j];
}
}
}
// 返回压缩后数组的新长度
return writeIndex;
};