跳到主要内容

97 篇博文 含有标签「字符串」

查看所有标签

· 阅读需 5 分钟

1、题干

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

 

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "1111"
输出:["1.1.1.1"]

示例 4:

输入:s = "010010"
输出:["0.10.0.10","0.100.1.0"]

示例 5:

输入:s = "10203040"
输出:["10.20.30.40","102.0.30.40","10.203.0.40"]

 

提示:

  • 0 <= s.length <= 3000
  • s 仅由数字组成

 

注意:本题与主站 93 题相同:https://leetcode-cn.com/problems/restore-ip-addresses/ 

2、解法1-嵌套循环

三层嵌套循环确定 IP 地址的4个子串,并检验其合法性


3、代码

var restoreIpAddresses = function (s) {
const n = s.length;
const valid = (str) => !((str.length > 1 && str[0] === '0') || +str > 255);

const res = [];
for (let i = 1; i < 4; i++) {
if (n - i < 3 || n - i > 9) continue;
for (let j = i + 1; j < i + 4; j++) {
if (n - j < 2 || n - j > 6) continue;
for (let k = j + 1; k < j + 4; k++) {
if (n - k < 1 || n - k > 3) continue;
const s1 = s.slice(0, i), s2 = s.slice(i, j);
const s3 = s.slice(j, k), s4 = s.slice(k);
if (valid(s1) && valid(s2) && valid(s3) && valid(s4)) res.push(`${s1}.${s2}.${s3}.${s4}`);
}
}
}

return res;
};

代码中的校验可以简化,但遍历次数会增加

var restoreIpAddresses = function (s) {
const valid = (str) => !(str.length < 1 || str.length > 3 || (str.length > 1 && str[0] === '0') || +str > 255);

const res = [];
for (let i = 1; i < 4; i++) {
for (let j = i + 1; j < i + 4; j++) {
for (let k = j + 1; k < j + 4; k++) {
const s1 = s.slice(0, i), s2 = s.slice(i, j);
const s3 = s.slice(j, k), s4 = s.slice(k);
if (valid(s1) && valid(s2) && valid(s3) && valid(s4)) res.push(`${s1}.${s2}.${s3}.${s4}`);
}
}
}

return res;
};

4、执行结果

image.png


5、解法2-回溯

使用回溯算法,在字符串 s 中选取3个索引作为选择路径(idxArr),其中索引值用于将字符串 s 分割成4个 IP 子串,以子串长度作为选择列表。

注意

  • 选择列表(子串长度)只有 123 三种选择
  • 为了代码简洁性,每次递归都传入不同引用的选择路径(idxArr),这样可以省略撤销操作,但是消耗的内存会偏多

6、代码

var restoreIpAddresses = function (s) {
const n = s.length;
const valid = (str) => !((str.length > 1 && str[0] === '0') || +str > 255);

const res = [];
function backtrace(idxArr) {
const ln = idxArr.length, li = idxArr[ln - 1] || 0;

if (n - li < 4 - ln || n - li > 3 * (4 - ln)) return;
if (ln === 3) {
const s1 = s.slice(0, idxArr[0]), s2 = s.slice(idxArr[0], idxArr[1]);
const s3 = s.slice(idxArr[1], idxArr[2]), s4 = s.slice(idxArr[2]);
if (valid(s1) && valid(s2) && valid(s3) && valid(s4)) res.push(`${s1}.${s2}.${s3}.${s4}`);
return;
}

for (let i = 1; i <= 3; i++) backtrace([...idxArr, li + i]);
}

return backtrace([]), res;
};

7、执行结果

  • 执行用时: 68 ms
  • 内存消耗: 43.4 MB

· 阅读需 8 分钟

1、题干

给定三个字符串 s1s2s3,请判断 s3 能不能由 s1 和 s2 交织(交错) 组成。

两个字符串 st 交织 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

  • s = s1 + s2 + ... + sn
  • t = t1 + t2 + ... + tm
  • |n - m| <= 1
  • 交织s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...

提示:a + b 意味着字符串 ab 连接。

 

示例 1:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

示例 2:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

示例 3:

输入:s1 = "", s2 = "", s3 = ""
输出:true

 

提示:

  • 0 <= s1.length, s2.length <= 100
  • 0 <= s3.length <= 200
  • s1s2、和 s3 都由小写英文字母组成

 

注意:本题与主站 97 题相同: https://leetcode-cn.com/problems/interleaving-string/

遍历解题需要用到3个指针,分别对应字符串 s1s2s3。官解说双指针不可行应该只考虑了 s1s2 的指针且没有考虑指针重置的情况,实际可以借助指针重置进行解题,详情见解法2。


2、解法1-回溯+记忆化

回溯过程可以理解为同时按字符串 s1s2s3 步进,若 s1 当前字符与 s3 当前字符相等则 s1s3 的下标同时加1,若 s2 当前字符与 s3 当前字符相等则 s2s3 的下标同时加1,若最终3个字符串能走到字符串末尾说明结果为 true


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径,则直接退出当前递归。


3、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const visited = new Set();
let res = false;
function backtrace(i1, i2, i3) {
if (i1 === s1.length && i2 === s2.length && i3 === s3.length) return res = true;
if (visited.has(`${i1}-${i2}-${i3}`)) return false;
visited.add(`${i1}-${i2}-${i3}`);

if (s1[i1] === s3[i3]) backtrace(i1 + 1, i2, i3 + 1);
if (s2[i2] === s3[i3]) backtrace(i1, i2 + 1, i3 + 1);
}

backtrace(0, 0, 0);

return res;
};

4、执行结果

image.png


5、解法2-三指针+模拟回溯+记忆化

本质上也是回溯+记忆化,只是使用三指针遍历模拟回溯过程,这是个非常规解法随便看看就好。


三指针遍历模拟回溯的大体方案是:遍历 s3,若遇到 s1s2 都可选的情况则选择 s1,并使用栈存储当前3个指针作为快照;若后续遍历无法继续下去且栈不为空则弹出栈顶快照,重置3个指针并选择 s2 继续遍历;若后续遍历无法继续且栈为空,则结果为 false


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径则看栈是否为空,栈为空则直接退出当前递归,栈不为空则弹出栈顶快照并重置指针。


6、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let i1 = -1, i2 = -1, useStack = false;
const stack = [], visited = new Set();

for (let i = 0; i < s3.length; i++) {
if (s1[i1 + 1] === s3[i] && s2[i2 + 1] === s3[i]) {
if (!useStack) stack.push([i, i1, i2]), i1 += 1;
else i2 += 1, useStack = false;
}
else if (s1[i1 + 1] === s3[i]) i1 += 1;
else if (s2[i2 + 1] === s3[i]) i2 += 1;
else {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
continue;
}

if (visited.has(`${i}-${i1}-${i2}`)) {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
}
else visited.add(`${i}-${i1}-${i2}`);
}

return true;
};

7、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 45.4 MB

8、解法3-BFS+去重

这里使用非递归BFS解题,大体思路是:外层遍历字符串 s3,内层遍历当前可选择的所有 s1s2 的下标组合,并且把下一层可选择的所有 s1s2 的下标组合添加到下一层的队列中,如此遍历所有可行组合直至结束,若最终队列不为空则结果为 true


其中添加下标组合到队列中时一定要去重,否则会超时或内存溢出,去重可以使用双层 map 或者字符串哈希集合。


9、代码

// BFS+双层map去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], map = {};
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && (!map[i1 + 1] || !map[i1 + 1][i2])) {
nextChoices.push([i1 + 1, i2]);
map[i1 + 1] ? (map[i1 + 1][i2] = 1) : (map[i1 + 1] = { [i2]: 1 });
}
if (s3[i] === s2[i2 + 1] && (!map[i1] || !map[i1][i2 + 1])) {
nextChoices.push([i1, i2 + 1]);
map[i1] ? (map[i1][i2 + 1] = 1) : (map[i1] = { [i2 + 1]: 1 });
}
}
choices = nextChoices;
}

return choices.length > 0;
};

// BFS+字符串哈希集合去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], set = new Set();
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && !set.has(`${i1 + 1},${i2}`)) {
nextChoices.push([i1 + 1, i2]);
set.add(`${i1 + 1},${i2}`);
}
if (s3[i] === s2[i2 + 1] && !set.has(`${i1},${i2 + 1}`)) {
nextChoices.push([i1, i2 + 1]);
set.add(`${i1},${i2 + 1}`);
}
}
choices = nextChoices;
}

return choices.length > 0;
};

10、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 48.3 MB

11、解法4-动态规划

官解和其他优秀题解对于动态规划解法已经说得很清楚了,这里就贴下代码和执行结果


12、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const dp = new Array(s1.length + 1).fill(0).map(() => new Array(s2.length + 1).fill(true));
for (let i = 1; i < dp.length; i++) dp[i][0] = dp[i - 1][0] && s3[i - 1] === s1[i - 1];
for (let j = 1; j < dp[0].length; j++) dp[0][j] = dp[0][j - 1] && s3[j - 1] === s2[j - 1];

for (let i = 1; i < dp.length; i++) {
for (let j = 1; j < dp[i].length; j++) {
dp[i][j] = (dp[i - 1][j] && s3[i + j - 1] === s1[i - 1]) || (dp[i][j - 1] && s3[i + j - 1] === s2[j - 1]);
}
}

return dp[s1.length][s2.length];
};

13、执行结果

  • 执行用时: 92 ms
  • 内存消耗: 47.7 MB

· 阅读需 2 分钟

1、题干

如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)的形式组成的,那么该字符串是 单调递增 的。

给你一个二进制字符串 s,你可以将任何 0 翻转为 1 或者将 1 翻转为 0

返回使 s 单调递增的最小翻转次数。

 

示例 1:

输入:s = "00110"
输出:1
解释:翻转最后一位得到 00111.

示例 2:

输入:s = "010110"
输出:2
解释:翻转得到 011111,或者是 000111。

示例 3:

输入:s = "00011000"
输出:2
解释:翻转得到 00000000。

 

提示:

  • 1 <= s.length <= 105
  • s[i]'0''1'

2、解题思路

字符串中的 10 打乱了递增顺序,要保证递增性只需要将其替换成 000111 中任意一种,至于替换成哪一种不重要,因为总有一个合适的,重要的是每出现1个 10 翻转次数需要加 1

因此要求最小翻转次数,只需要循环地把所有 10 全部剔除,并累加翻转次数即可


3、代码

var minFlipsMonoIncr = function (s) {
let c = 0;
while (s.indexOf('10') > -1) s = s.replace(/10/g, () => (c++, ''));
return c;
};

4、执行结果

image.png

· 阅读需 3 分钟

1、题干

如果一个由 '0''1' 组成的字符串,是以一些 '0'(可能没有 '0')后面跟着一些 '1'(也可能没有 '1')的形式组成的,那么该字符串是 单调递增 的。

我们给出一个由字符 '0''1' 组成的字符串s,我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'

返回使s 单调递增 的最小翻转次数。

 

示例 1:

输入:s = "00110"
输出:1
解释:我们翻转最后一位得到 00111.

示例 2:

输入:s = "010110"
输出:2
解释:我们翻转得到 011111,或者是 000111。

示例 3:

输入:s = "00011000"
输出:2
解释:我们翻转得到 00000000。

 

提示:

  • 1 <= s.length <= 20000
  • s 中只包含字符 '0' 和 '1'

 

注意:本题与主站 926 题相同: https://leetcode-cn.com/problems/flip-string-to-monotone-increasing/

2、解题思路

字符串中的 10 打乱了递增顺序,要保证递增性只需要将其替换成 000111 中任意一种,至于替换成哪一种不重要,因为总有一个合适的,重要的是每出现1个 10 翻转次数需要加 1

因此要求最小翻转次数,只需要循环地把所有 10 全部剔除,并累加翻转次数即可


3、代码

var minFlipsMonoIncr = function (s) {
let c = 0;
while (s.indexOf('10') > -1) s = s.replace(/10/g, () => (c++, ''));
return c;
};

4、执行结果

image.png

· 阅读需 3 分钟

1、题干

如果一个由 '0''1' 组成的字符串,是以一些 '0'(可能没有 '0')后面跟着一些 '1'(也可能没有 '1')的形式组成的,那么该字符串是 单调递增 的。

我们给出一个由字符 '0''1' 组成的字符串s,我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'

返回使s 单调递增 的最小翻转次数。

 

示例 1:

输入:s = "00110"
输出:1
解释:我们翻转最后一位得到 00111.

示例 2:

输入:s = "010110"
输出:2
解释:我们翻转得到 011111,或者是 000111。

示例 3:

输入:s = "00011000"
输出:2
解释:我们翻转得到 00000000。

 

提示:

  • 1 <= s.length <= 20000
  • s 中只包含字符 '0' 和 '1'

 

注意:本题与主站 926 题相同: https://leetcode-cn.com/problems/flip-string-to-monotone-increasing/

2、解题思路

字符串中的 10 打乱了递增顺序,要保证递增性只需要将其替换成 000111 中任意一种,至于替换成哪一种不重要,因为总有一个合适的,重要的是每出现1个 10 翻转次数需要加 1

因此要求最小翻转次数,只需要循环地把所有 10 全部剔除,并累加翻转次数即可


3、代码

var minFlipsMonoIncr = function (s) {
let c = 0;
while (s.indexOf('10') > -1) s = s.replace(/10/g, () => (c++, ''));
return c;
};

4、执行结果

image.png

· 阅读需 3 分钟

1、题干

给你一个字符串 s 和一个字符 c ,且 cs 中出现过的字符。

返回一个整数数组 answer ,其中 answer.length == s.lengthanswer[i]s 中从下标 i 到离它 最近 的字符 c距离

两个下标 ij 之间的 距离abs(i - j) ,其中 abs 是绝对值函数。

 

示例 1:

输入:s = "loveleetcode", c = "e"
输出:[3,2,1,0,1,0,0,1,2,2,1,0]
解释:字符 'e' 出现在下标 3、5、6 和 11 处(下标从 0 开始计数)。
距下标 0 最近的 'e' 出现在下标 3 ,所以距离为 abs(0 - 3) = 3 。
距下标 1 最近的 'e' 出现在下标 3 ,所以距离为 abs(1 - 3) = 2 。
对于下标 4 ,出现在下标 3 和下标 5 处的 'e' 都离它最近,但距离是一样的 abs(4 - 3) == abs(4 - 5) = 1 。
距下标 8 最近的 'e' 出现在下标 6 ,所以距离为 abs(8 - 6) = 2 。

示例 2:

输入:s = "aaab", c = "b"
输出:[3,2,1,0]

 

提示:
  • 1 <= s.length <= 104
  • s[i]c 均为小写英文字母
  • 题目数据保证 cs 中至少出现一次

题目简单,但是处理各种边界情况还是有点繁琐,代码量很难压缩,

71d74efddaafe529dc032181988da153.gif


2、解题思路

lr 分别表示左边和右边最近出现字符 c 的下标,遍历区间范围 [l,r)[l,r) 内的字符,记录字符离边界最近的距离即可


注意处理好边界情况

  • 左边没有字符 c 时,l 取值为 -Infinity
  • 右边没有字符 c 时,r 取值为 Infinity
  • 内层循环要控制好 i 的范围,不能越出 [0,s.length)[0,s.length) 这个范围

3、代码

var shortestToChar = function (s, c) {
const res = [];
for (let l = -Infinity, r = s.indexOf(c); l < s.length;) {
for (let i = Math.max(0, l); i < Math.min(s.length, r); i++) {
res.push(Math.min(r - i, i - l));
}
l = r, r = s.indexOf(c, r + 1), r = r < 0 ? Infinity : r;
}
return res;
};

4、复杂度

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

5、执行结果

image.png

· 阅读需 4 分钟

1、题干

给你一个整数 n ,请你返回所有 0 到 1 之间(不包括 0 和 1)满足分母小于等于  n 的 最简 分数 。分数可以以 任意 顺序返回。

 

示例 1:

输入:n = 2
输出:["1/2"]
解释:"1/2" 是唯一一个分母小于等于 2 的最简分数。

示例 2:

输入:n = 3
输出:["1/2","1/3","2/3"]

示例 3:

输入:n = 4
输出:["1/2","1/3","1/4","2/3","3/4"]
解释:"2/4" 不是最简分数,因为它可以化简为 "1/2" 。

示例 4:

输入:n = 1
输出:[]

 

提示:

  • 1 <= n <= 100

2、解法1-哈希表

用哈希映射存储小数和分数字符串,其中小数作为键分数字符串作为值,最后返回哈希映射中的所有值


3、代码

var simplifiedFractions = function (n) {
const map = new Map();
for (let i = 1; i < n; i++) {
for (let j = i + 1; j <= n; j++) {
if (!map.has(i / j)) map.set(i / j, `${i}/${j}`);
}
}

return [...map.values()];
};

4、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(n)O(n)

5、执行结果

  • 执行用时: 108 ms
  • 内存消耗: 46.3 MB

6、解法2-求公约数

参考求素数的方法,从 22 开始遍历到 n\sqrt{n},看分子分母是否存在公约数


7、代码

var simplifiedFractions = function (n) {
function skip(min, max) {
for (let i = 2; i * i <= max; i++) {
if (max % i === 0 && (min % i === 0 || min % (max / i) === 0)) return true;
}
return false;
}

const res = [];
for (let i = 1; i < n; i++) {
for (let j = i + 1; j <= n; j++) {
if (!skip(i, j)) res.push(i + '/' + j);
}
}

return res;
};

8、复杂度

  • 时间复杂度:O(n2n)O(n^2*\sqrt{n})
  • 空间复杂度:O(n)O(n)

9、执行结果

  • 执行用时: 88 ms
  • 内存消耗: 47.2 MB

image.png


10、解法3-分母因数倍乘+哈希集合

参考求素数的方法,从 22 开始遍历到 n\sqrt{n},求出所有小于分母的因数及其倍乘结果并存储到哈希集合 cdSet 中,若分子存在于 cdSet 中则说明分子分母存在公约数


11、代码

var simplifiedFractions = function (n) {
const res = [];
for (let i = 2; i <= n; i++) {
const cdSet = new Set();
for (let c = 2; c * c <= i; c++) {
if (i % c) continue;
for (let m = 1; m * c < i; m++) {
cdSet.add(m * c);
if (m * i / c < i) cdSet.add(m * i / c);
}
}

for (let j = 1; j < i; j++) {
if (!cdSet.has(j)) res.push(j + '/' + i);
}
}

return res;
};

12、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(n)O(n)

13、执行结果

  • 执行用时: 84 ms
  • 内存消耗: 46.9 MB

image.png

· 阅读需 3 分钟

1、题干

给定一个非空的正整数数组 nums ,请判断能否将这些数字分成元素和相等的两部分。

 

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:nums 可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:nums 不可以分为和相等的两部分

 

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

 

注意:本题与主站 416 题相同: https://leetcode-cn.com/problems/partition-equal-subset-sum/

2、解题思路

根据题意需要找到数组 nums 所有元素和的一半 target,可以这么做:遍历数组 nums,用哈希集合 set 记录所有元素和,若 numsset 中存在 target,则返回 true

注意 :这个解法的关键在于必须限定 set 只能存储比 target 更小的元素和,否则大概率会超时。由于数组 nums 长度已经固定,而 set 被限制后其长度必然小于 target,因此总体时间复杂度为:O(ntarget)O(n*target),计算量级最大约为 10610^6,超时的可能性不大。


3、代码

var canPartition = function (nums) {
const sum = nums.reduce((a, c) => a + c, 0);
if (sum % 2) return false;

const target = sum / 2, set = new Set();
for (const n of nums) {
if (n > target) continue;
if (target === n || set.has(target - n)) return true;
for (const s of [...set]) if (n + s < target) set.add(n + s);
set.add(n);
}

return false;
};

4、复杂度

  • 时间复杂度:O(ntarget)O(n*target)
  • 空间复杂度:O(target)O(target)

5、执行结果

  • 执行用时: 112 ms
  • 内存消耗: 47.1 MB

· 阅读需 3 分钟

1、题干

给定一个非空的正整数数组 nums ,请判断能否将这些数字分成元素和相等的两部分。

 

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:nums 可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:nums 不可以分为和相等的两部分

 

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

 

注意:本题与主站 416 题相同: https://leetcode-cn.com/problems/partition-equal-subset-sum/

2、解题思路

根据题意需要找到数组 nums 所有元素和的一半 target,可以这么做:遍历数组 nums,用哈希集合 set 记录所有元素和,若 numsset 中存在 target,则返回 true

注意 :这个解法的关键在于必须限定 set 只能存储比 target 更小的元素和,否则大概率会超时。由于数组 nums 长度已经固定,而 set 被限制后其长度必然小于 target,因此总体时间复杂度为:O(ntarget)O(n*target),计算量级最大约为 10610^6,超时的可能性不大。


3、代码

var canPartition = function (nums) {
const sum = nums.reduce((a, c) => a + c, 0);
if (sum % 2) return false;

const target = sum / 2, set = new Set();
for (const n of nums) {
if (n > target) continue;
if (target === n || set.has(target - n)) return true;
for (const s of [...set]) if (n + s < target) set.add(n + s);
set.add(n);
}

return false;
};

4、复杂度

  • 时间复杂度:O(ntarget)O(n*target)
  • 空间复杂度:O(target)O(target)

5、执行结果

  • 执行用时: 112 ms
  • 内存消耗: 47.1 MB

· 阅读需 5 分钟

1、题干

如果字符串中不含有任何 'aaa''bbb''ccc' 这样的字符串作为子串,那么该字符串就是一个「快乐字符串」。

给你三个整数 abc,请你返回 任意一个 满足下列全部条件的字符串 s

  • s 是一个尽可能长的快乐字符串。
  • s最多a 个字母 'a'b 个字母 'b'c 个字母 'c'
  • s 中只含有 'a''b''c' 三种字母。

如果不存在这样的字符串 s ,请返回一个空字符串 ""

 

示例 1:

输入:a = 1, b = 1, c = 7
输出:"ccaccbcc"
解释:"ccbccacc" 也是一种正确答案。

示例 2:

输入:a = 2, b = 2, c = 1
输出:"aabbc"

示例 3:

输入:a = 7, b = 1, c = 0
输出:"aabaa"
解释:这是该测试用例的唯一正确答案。

 

提示:

  • 0 <= a, b, c <= 100
  • a + b + c > 0

题目对我来说挺有难度,想了蛮久,用了两种思路,都是基于3种字母的数量关系解题,这里记录其中一种

e9de6ffd302988140f868f8f54c96d26.gif


2、解题思路

总体思路

  • 按数量对 'a''b''c' 三种字母进行降序排序
  • 由于快乐字符串中不能出现3个连续相同字符,可以先取数量最多的字母,每两个组成一个字符串并存入数组中,记为 arr
  • 遍历 arr 中的所有字符串,把剩余字母逐个拼接到每个字符串末尾,由于剩余任意一种字母数量都不会超过 2arr.length2*arr.length,因此任意一种字母必定可以在两轮循环内消耗完,且 arr 中任意一个字符串都不会出现3个连续相同字符
  • 最后拼接所有字符串,只可能在末尾出现3个以上连续相同字符,把末尾多余字符处理掉即可

举个例子a = 1, b = 1, c = 7

  • 1、取数量最多的字母 c,每两个组成一个字符串,得到:['cc','cc','cc','c']
  • 2、把剩余字母 a 逐个拼接到每个字符串末尾,得到:['cca','cc','cc','c'],此时一轮循环尚未结束
  • 3、一轮循环没结束,字母 a 已耗尽,继续该轮循环,把剩余字母 b 逐个拼接到每个字符串末尾,得到:['cca','ccb','cc','c']
  • 4、拼接所有字符串再处理末尾多余字符,得到:'ccaccbcc'

再举个例子a = 6, b = 6, c = 6

  • 1、取数量最多的字母 a,每两个组成一个字符串,得到:['aa','aa','aa']
  • 2、把剩余字母 b 逐个拼接到每个字符串末尾,得到:['aab','aab','aab'],此时已结束一轮循环
  • 3、一轮循环没有消耗所有字母 b,再来一轮,得到:['aabb','aabb','aabb'],此时已结束两轮循环
  • 4、同理把剩余字母 c 逐个拼接到每个字符串末尾,得到:['aabbcc','aabbcc','aabbcc']
  • 5、拼接所有字符串再处理末尾多余字符,得到:'aabbccaabbccaabbcc

3、代码

var longestDiverseString = function (a, b, c) {
const m = [['a', a], ['b', b], ['c', c]].sort((a, b) => b[1] - a[1]);

const len = Math.ceil(m[0][1] / 2);
const arr = new Array(len).fill(m[0][0] + m[0][0]);
if (m[0][1] % 2) arr[len - 1] = m[0][0];

for (let i = 0; m[1][1] || m[2][1]; i++) {
if (m[1][1]) arr[i % len] += m[1][0], m[1][1]--;
else arr[i % len] += m[2][0], m[2][1]--;
}

return arr.join('').replace(new RegExp(m[0][0] + '{3,}$'), m[0][0] + m[0][0]);
};

4、执行结果

image.png