关键字标识:
问题描述
你有一个关键词列表和一个字符串。你需要在字符串中用 HTML 标签 <b></b>
标记出所有出现的关键词,并且将所有相邻或交叠的标签合并成一个标签。例如:
- 如果字符串中有
"opqr"
和"cd"
这两个关键词,并且它们在字符串中相邻或重叠,那么最终输出的字符串应该是<b>opqr</b>a<b>cdf</b>g
。
解决步骤
-
找到关键词在字符串中的所有位置:
- 对于每个关键词,我们在字符串中找到它的所有出现位置,并记录这些位置。
-
合并相邻或交叠的标签:
- 如果两个标签在字符串中相邻或交叠,我们需要将它们合并成一个标签。比如,
<b>op</b><b>qr</b>
应该合并成<b>opqr</b>
。
- 如果两个标签在字符串中相邻或交叠,我们需要将它们合并成一个标签。比如,
-
生成最终的字符串:
- 使用合并后的标签创建最终的字符串,确保每个标签都正确地标记了关键词。
代码解析
以下是详细的代码解析:
function mergeIntervals(intervals) {if (intervals.length === 0) return [];// 按照开始位置排序intervals.sort((a, b) => a[0] - b[0]);const merged = [];let [start, end, label] = intervals[0]; // 初始化第一个标签区间for (let i = 1; i < intervals.length; i++) {const [currentStart, currentEnd, currentLabel] = intervals[i];if (currentStart <= end + 1) { // 如果标签区间重叠或相邻end = Math.max(end, currentEnd); // 更新结束位置label = `<b>${inputStr.slice(start, end + 1)}</b>`; // 更新合并后的标签} else {merged.push([start, end, label]); // 将当前标签区间加入结果start = currentStart;end = currentEnd;label = currentLabel;}}merged.push([start, end, label]); // 推入最后一个标签区间return merged;
}function labelKeywords(words, inputStr) {const n = inputStr.length;const labels = [];// 遍历每个关键词,查找其在字符串中的所有位置words.forEach(word => {let start = 0;while (start < n) {start = inputStr.indexOf(word, start);if (start === -1) break;const end = start + word.length - 1;labels.push([start, end, `<b>${word}</b>`]);start += word.length;}});// 合并所有重叠或相邻的标签区间const mergedIntervals = mergeIntervals(labels);// 构建最终输出的字符串let result = '';let lastIndex = 0;mergedIntervals.forEach(([start, end, label]) => {if (lastIndex < start) {result += inputStr.slice(lastIndex, start); // 添加非标签部分}result += label; // 添加标签lastIndex = end + 1;});if (lastIndex < n) {result += inputStr.slice(lastIndex); // 添加字符串剩余部分}return result;
}// 示例输入
const count = 4;
const words = ["cd", "df", "op", "qr"];
const inputStr = "opqracdfg";// 处理并输出结果
console.log(labelKeywords(words, inputStr));
详细解释
-
找到关键词的位置:
labelKeywords
函数遍历所有关键词,找到它们在字符串中的位置,并记录开始和结束位置以及标签内容。
-
合并标签:
mergeIntervals
函数接收这些标签区间,按照开始位置排序,然后合并相邻或交叠的标签。合并时,我们更新结束位置并重新构建标签。
-
生成最终字符串:
- 最后,我们在
labelKeywords
函数中,利用合并后的标签生成最终的字符串。在这个过程中,我们将标签区间和非标签部分组合起来,生成最终的结果。
- 最后,我们在
这个问题涉及几个重要的算法和技术概念,主要包括:
1. 字符串查找:
2. 区间合并:
- 算法:这个问题的核心算法是区间合并(Interval Merging)。在解决合并标签的问题时,我们实际上是在处理区间合并问题:
- 排序:首先,将所有的区间按起始位置排序。
- 合并:然后,遍历这些区间,合并所有相邻或重叠的区间。合并时,需要更新区间的结束位置,并构建合并后的标签。
区间合并问题通常出现在处理时间段、区间覆盖等问题中。常见的步骤包括:
- 排序:按开始位置对区间进行排序。
- 合并:通过遍历排序后的区间,合并相邻或重叠的区间。
3. 标签生成:
- 技术:在合并区间时,我们需要重新生成标签。这个操作涉及到将合并后的区间从原始字符串中提取出来,并生成适当的 HTML 标签。在此步骤中,我们利用了字符串切割操作(
slice
方法)来构建标签。
示例解析
-
查找关键词:
- 通过遍历每个关键词在字符串中查找其所有位置,生成标签区间。每个标签区间记录了关键词的位置和标签。
-
合并区间:
- 通过
mergeIntervals
函数对这些区间进行排序和合并。合并过程涉及到判断区间是否相邻或重叠,并更新合并后的标签。
- 通过
-
构建最终字符串:
- 结合合并后的标签区间和原始字符串,生成最终的带有标签的字符串。这需要在合并后的区间之间插入原始字符串的非标签部分。
总结
这个问题结合了字符串处理和区间合并的算法。主要的算法步骤包括字符串查找、区间排序和合并。这些步骤在处理类似的字符串标记、时间段合并和区间覆盖等问题时都非常有用。
最长内存空间
问题
我们有一个数组 arr
,里面的元素是 0
和 1
。我们允许将最多 n
个 1
转换成 0
,我们的目标是找出最长的连续 0
子数组的长度。
思路
我们使用滑动窗口的技术来解决这个问题。滑动窗口是一种高效的数组遍历方法,可以帮助我们在 O(n) 时间复杂度内解决问题。
代码解释
function longestZeroSegment(arr, n) {let left = 0; // 窗口的左边界let maxLength = 0; // 记录最长的连续0的长度let countOnes = 0; // 记录窗口内1的数量for (let right = 0; right < arr.length; right++) {// 如果当前元素是1,增加countOnesif (arr[right] === 1) {countOnes++;}// 当窗口内1的数量超过允许的n时,移动左边界while (countOnes > n) {if (arr[left] === 1) {countOnes--;}left++;}// 更新最大长度maxLength = Math.max(maxLength, right - left + 1);}return maxLength;
}// 示例
const arr = [0,0,1,0,0,1,0,0,1,0,0];
const n = 2;
console.log(longestZeroSegment(arr, n)); // 输出 8
详细步骤
-
初始化:
left
:窗口的左边界,初始值为0
。maxLength
:记录最长的连续0
的长度,初始值为0
。countOnes
:记录窗口内1
的数量,初始值为0
。
-
遍历数组:
- 使用
for
循环遍历数组,每次移动right
指针(窗口的右边界)。
- 使用
-
处理窗口内的
1
:- 如果当前元素是
1
,增加countOnes
。
- 如果当前元素是
-
调整窗口:
- 当窗口内的
1
的数量超过n
时,通过移动left
指针来缩小窗口,直到countOnes
不超过n
。
- 当窗口内的
-
更新最长长度:
- 计算当前窗口的长度
right - left + 1
,并更新maxLength
为更大的值。
- 计算当前窗口的长度
-
返回结果:
- 返回
maxLength
,即最长的连续0
的长度。
- 返回
例子解析
对于数组 [0,0,1,0,0,1,0,0,1,0,0]
和 n = 2
:
- 将最多 2 个
1
转换为0
后,最长的连续0
的长度是8
。
这个代码的关键在于滑动窗口的维护,确保在任何时刻窗口内的 1
的数量不超过 n
,同时计算出最长的连续 0
的长度。
当然可以!下面我将用图示的方式来帮助你理解滑动窗口的过程。
图示说明
考虑数组 [0,0,1,0,0,1,0,0,1,0,0]
和 n = 2
。我们需要找到最长的连续 0
,允许最多将 2
个 1
转换成 0
。下面是滑动窗口的演示:
初始状态
窗口: [0,0,1,0,0,1,0,0,1,0,0]^leftright
过程演示
-
扩展右边界:
- 初始时,
left = 0
,right = 0
。 countOnes
为0
(窗口内的1
的数量)。
- 初始时,
-
移动右边界:
- 右边界扩展到
right = 1
(arr[right] = 0
,不增加countOnes
)。 countOnes
仍然是0
。- 当前窗口内最长连续
0
的长度是2
(right - left + 1 = 2
),更新maxLength
为2
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 2
(arr[right] = 1
,增加countOnes
为1
)。 countOnes
现在是1
。- 当前窗口内最长连续
0
的长度是3
(right - left + 1 = 3
),更新maxLength
为3
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 3
(arr[right] = 0
,countOnes
不变)。 countOnes
仍然是1
。- 当前窗口内最长连续
0
的长度是4
(right - left + 1 = 4
),更新maxLength
为4
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 4
(arr[right] = 0
,countOnes
不变)。 countOnes
仍然是1
。- 当前窗口内最长连续
0
的长度是5
(right - left + 1 = 5
),更新maxLength
为5
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 5
(arr[right] = 1
,增加countOnes
为2
)。 countOnes
现在是2
。- 当前窗口内最长连续
0
的长度是6
(right - left + 1 = 6
),更新maxLength
为6
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 6
(arr[right] = 0
,countOnes
不变)。 countOnes
仍然是2
。- 当前窗口内最长连续
0
的长度是7
(right - left + 1 = 7
),更新maxLength
为7
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 7
(arr[right] = 0
,countOnes
不变)。 countOnes
仍然是2
。- 当前窗口内最长连续
0
的长度是8
(right - left + 1 = 8
),更新maxLength
为8
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 8
(arr[right] = 1
,增加countOnes
为3
,超过n
)。 - 移动左边界
left
直到countOnes
不超过n
。 - 移动
left
到left = 1
,countOnes
变为2
,窗口内长度是7
。 - 继续调整窗口,直到
countOnes
符合要求。 - 最大长度保持
8
。
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 右边界扩展到
-
继续扩展右边界:
- 右边界扩展到
right = 9
(arr[right] = 0
,countOnes
不变)。 countOnes
仍然是2
。- 当前窗口内最长连续
0
的长度是8
(right - left + 1 = 8
),maxLength
仍然是8
。
- 右边界扩展到
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
- 继续扩展右边界:
- 右边界扩展到
right = 10
(arr[right] = 0
,countOnes
不变)。 countOnes
仍然是2
。- 当前窗口内最长连续
0
的长度是9
(right - left + 1 = 9
),更新maxLength
为9
。
- 右边界扩展到
窗口: [0,0,1,0,0,1,0,0,1,0,0]^left^right
结果
最终,maxLength
是 8
,即最长的连续 0
的长度为 8
。
最大负载:
要解决这个问题,我们可以按照以下步骤来实现:
步骤解析
- 循环分配服务器:每次消息到达时,我们按照顺序循环分配服务器。
- 检查服务器的繁忙状态:如果当前要分配的服务器繁忙,则跳过它,分配给下一个空闲的服务器。
- 计算每个服务器的总负载:记录每个服务器处理的负载总和。
- 找出处理负载最高的服务器:最后,找出那些处理负载最多的服务器,并按编号升序返回。
JavaScript 实现
function findBusiestServers(k, requests) {const servers = new Array(k).fill(0); // 记录每个服务器的总负载const busyUntil = new Array(k).fill(0); // 记录每个服务器的繁忙截止时间let maxLoad = 0; // 记录最高负载值const result = []; // 记录处理最高负载的服务器编号for (let i = 0; i < requests.length; i++) {const [time, load] = requests[i];let assigned = false;for (let j = 0; j < k; j++) {const server = (i + j) % k; // 计算当前应该检查的服务器if (busyUntil[server] <= time) { // 检查服务器是否空闲servers[server] += load; // 记录服务器处理的负载busyUntil[server] = time + load; // 更新服务器繁忙截止时间maxLoad = Math.max(maxLoad, servers[server]); // 更新最高负载值assigned = true;break;}}// 如果所有服务器都繁忙,这条消息就被丢弃,不处理}// 找出处理负载最多的服务器for (let i = 0; i < k; i++) {if (servers[i] === maxLoad) {result.push(i + 1); // 服务器编号从1开始}}return result;
}// 示例输入
const k = 3;
const requests = [[1, 15],[2, 10],[12, 10],[5, 10],[6, 10],[30, 15],[32, 10]
];// 计算并输出结果
console.log(findBusiestServers(k, requests)); // 输出 [1, 3]
代码解释
-
初始化:
servers
数组用来记录每个服务器处理的总负载。busyUntil
数组用来记录每个服务器处理完当前任务的时间,即何时变得空闲。maxLoad
记录当前服务器中处理的最高负载值。
-
循环处理每条消息:
- 对于每条消息,按照顺序尝试将其分配给一个空闲的服务器。
- 如果找到空闲的服务器,就将消息分配给该服务器,并更新该服务器的负载和繁忙截止时间。
-
查找最高负载的服务器:
- 最后,遍历
servers
数组,找出负载等于maxLoad
的服务器,将其编号加入结果中。
- 最后,遍历
-
返回结果:
- 返回处理负载最高的服务器编号,按照升序排序。
这个实现逻辑清晰且简洁,能够有效地解决问题。在时间和空间限制内,性能也是可以接受的。
优化版:
没问题,让我用更简单的方式解释一下,并且简化实现。
问题理解
假设我们有 k
台服务器,每台服务器按顺序循环接收请求消息。如果一台服务器忙碌,就跳过它分配给下一台空闲的服务器。如果所有服务器都忙碌,那么这个请求就会被丢弃。我们要找出处理负载最多的服务器,并返回它们的编号。
步骤分解
-
初始化状态:
- 创建一个数组来记录每台服务器的负载总和。
- 创建一个数组记录每台服务器何时会空闲。
-
循环处理每个请求:
- 根据当前请求的时间,找出可以处理该请求的服务器(即空闲的服务器)。
- 如果找到空闲的服务器,更新它的负载和下次空闲时间。
- 如果没有找到空闲服务器,这个请求就被丢弃。
-
找出处理负载最多的服务器:
- 遍历所有服务器,找出负载最多的服务器。
简化版 JavaScript 实现
function findBusiestServers(k, requests) {const loads = new Array(k).fill(0); // 记录每个服务器的总负载const busyUntil = new Array(k).fill(0); // 记录每个服务器的忙碌截止时间requests.forEach(([time, load], i) => {let assigned = false;// 尝试分配给服务器for (let j = 0; j < k; j++) {const server = (i + j) % k; // 循环找到服务器if (busyUntil[server] <= time) { // 检查是否空闲loads[server] += load; // 更新服务器负载busyUntil[server] = time + load; // 更新服务器忙碌到的时间assigned = true;break;}}// 如果没有空闲服务器,消息被丢弃});const maxLoad = Math.max(...loads); // 找到最大负载值const result = [];for (let i = 0; i < k; i++) {if (loads[i] === maxLoad) {result.push(i + 1); // 服务器编号从1开始}}return result;
}// 示例输入
const k = 3;
const requests = [[1, 15],[2, 10],[12, 10],[5, 10],[6, 10],[30, 15],[32, 10]
];// 计算并输出结果
console.log(findBusiestServers(k, requests)); // 输出 [1, 3]
代码详细解释
-
初始化:
loads
数组用于记录每台服务器处理的总负载。busyUntil
数组用于记录每台服务器处理完当前任务后什么时候会空闲。
-
处理请求:
- 对于每个请求,按顺序检查服务器是否空闲,如果空闲则分配给它。
- 使用
(i + j) % k
来找到合适的服务器,这样可以实现循环分配。
-
找到最大负载的服务器:
- 计算每台服务器的负载,找到负载最大的服务器并返回它们的编号。
总结
这个简化版代码通过简单的循环和数组操作,轻松实现了服务器的负载均衡分配。每个请求只需要遍历最多 k
个服务器,因此在时间和空间上都非常高效。