算法题详解

关键字标识:

问题描述

你有一个关键词列表和一个字符串。你需要在字符串中用 HTML 标签 <b></b> 标记出所有出现的关键词,并且将所有相邻或交叠的标签合并成一个标签。例如:

  • 如果字符串中有 "opqr""cd" 这两个关键词,并且它们在字符串中相邻或重叠,那么最终输出的字符串应该是 <b>opqr</b>a<b>cdf</b>g

解决步骤

  1. 找到关键词在字符串中的所有位置

    • 对于每个关键词,我们在字符串中找到它的所有出现位置,并记录这些位置。
  2. 合并相邻或交叠的标签

    • 如果两个标签在字符串中相邻或交叠,我们需要将它们合并成一个标签。比如,<b>op</b><b>qr</b> 应该合并成 <b>opqr</b>
  3. 生成最终的字符串

    • 使用合并后的标签创建最终的字符串,确保每个标签都正确地标记了关键词。

代码解析

以下是详细的代码解析:

javascript 复制代码
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));

详细解释

  1. 找到关键词的位置

    • labelKeywords 函数遍历所有关键词,找到它们在字符串中的位置,并记录开始和结束位置以及标签内容。
  2. 合并标签

    • mergeIntervals 函数接收这些标签区间,按照开始位置排序,然后合并相邻或交叠的标签。合并时,我们更新结束位置并重新构建标签。
  3. 生成最终字符串

    • 最后,我们在 labelKeywords 函数中,利用合并后的标签生成最终的字符串。在这个过程中,我们将标签区间和非标签部分组合起来,生成最终的结果。

这个问题涉及几个重要的算法和技术概念,主要包括:

1. 字符串查找

  • 算法indexOf 方法用于查找关键词在字符串中的位置。这是一个基础的字符串操作算法,用于确定子字符串的起始位置。

2. 区间合并

  • 算法 :这个问题的核心算法是区间合并(Interval Merging)。在解决合并标签的问题时,我们实际上是在处理区间合并问题:
    • 排序:首先,将所有的区间按起始位置排序。
    • 合并:然后,遍历这些区间,合并所有相邻或重叠的区间。合并时,需要更新区间的结束位置,并构建合并后的标签。

区间合并问题通常出现在处理时间段、区间覆盖等问题中。常见的步骤包括:

  • 排序 :按开始位置对区间进行排序。

  • 合并:通过遍历排序后的区间,合并相邻或重叠的区间。

3. 标签生成

  • 技术 :在合并区间时,我们需要重新生成标签。这个操作涉及到将合并后的区间从原始字符串中提取出来,并生成适当的 HTML 标签。在此步骤中,我们利用了字符串切割操作(slice 方法)来构建标签。

示例解析

  1. 查找关键词

    • 通过遍历每个关键词在字符串中查找其所有位置,生成标签区间。每个标签区间记录了关键词的位置和标签。
  2. 合并区间

    • 通过 mergeIntervals 函数对这些区间进行排序和合并。合并过程涉及到判断区间是否相邻或重叠,并更新合并后的标签。
  3. 构建最终字符串

    • 结合合并后的标签区间和原始字符串,生成最终的带有标签的字符串。这需要在合并后的区间之间插入原始字符串的非标签部分。

总结

这个问题结合了字符串处理和区间合并的算法。主要的算法步骤包括字符串查找、区间排序和合并。这些步骤在处理类似的字符串标记、时间段合并和区间覆盖等问题时都非常有用。

最长内存空间

问题

我们有一个数组 arr,里面的元素是 01。我们允许将最多 n1 转换成 0,我们的目标是找出最长的连续 0 子数组的长度。

思路

我们使用滑动窗口的技术来解决这个问题。滑动窗口是一种高效的数组遍历方法,可以帮助我们在 O(n) 时间复杂度内解决问题。

代码解释

javascript 复制代码
function longestZeroSegment(arr, n) {
    let left = 0;          // 窗口的左边界
    let maxLength = 0;     // 记录最长的连续0的长度
    let countOnes = 0;     // 记录窗口内1的数量

    for (let right = 0; right < arr.length; right++) {
        // 如果当前元素是1,增加countOnes
        if (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

详细步骤

  1. 初始化:

    • left:窗口的左边界,初始值为 0
    • maxLength:记录最长的连续 0 的长度,初始值为 0
    • countOnes:记录窗口内 1 的数量,初始值为 0
  2. 遍历数组:

    • 使用 for 循环遍历数组,每次移动 right 指针(窗口的右边界)。
  3. 处理窗口内的 1:

    • 如果当前元素是 1,增加 countOnes
  4. 调整窗口:

    • 当窗口内的 1 的数量超过 n 时,通过移动 left 指针来缩小窗口,直到 countOnes 不超过 n
  5. 更新最长长度:

    • 计算当前窗口的长度 right - left + 1,并更新 maxLength 为更大的值。
  6. 返回结果:

    • 返回 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,允许最多将 21 转换成 0。下面是滑动窗口的演示:

初始状态
复制代码
窗口: [0,0,1,0,0,1,0,0,1,0,0]
        ^
        left
        right
过程演示
  1. 扩展右边界

    • 初始时,left = 0right = 0
    • countOnes0(窗口内的 1 的数量)。
  2. 移动右边界

    • 右边界扩展到 right = 1arr[right] = 0,不增加 countOnes)。

    • countOnes 仍然是 0

    • 当前窗口内最长连续 0 的长度是 2 (right - left + 1 = 2),更新 maxLength2

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  3. 继续扩展右边界

    • 右边界扩展到 right = 2arr[right] = 1,增加 countOnes1)。

    • countOnes 现在是 1

    • 当前窗口内最长连续 0 的长度是 3 (right - left + 1 = 3),更新 maxLength3

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  4. 继续扩展右边界

    • 右边界扩展到 right = 3arr[right] = 0countOnes 不变)。

    • countOnes 仍然是 1

    • 当前窗口内最长连续 0 的长度是 4 (right - left + 1 = 4),更新 maxLength4

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  5. 继续扩展右边界

    • 右边界扩展到 right = 4arr[right] = 0countOnes 不变)。

    • countOnes 仍然是 1

    • 当前窗口内最长连续 0 的长度是 5 (right - left + 1 = 5),更新 maxLength5

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  6. 继续扩展右边界

    • 右边界扩展到 right = 5arr[right] = 1,增加 countOnes2)。

    • countOnes 现在是 2

    • 当前窗口内最长连续 0 的长度是 6 (right - left + 1 = 6),更新 maxLength6

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  7. 继续扩展右边界

    • 右边界扩展到 right = 6arr[right] = 0countOnes 不变)。

    • countOnes 仍然是 2

    • 当前窗口内最长连续 0 的长度是 7 (right - left + 1 = 7),更新 maxLength7

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  8. 继续扩展右边界

    • 右边界扩展到 right = 7arr[right] = 0countOnes 不变)。

    • countOnes 仍然是 2

    • 当前窗口内最长连续 0 的长度是 8 (right - left + 1 = 8),更新 maxLength8

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  9. 继续扩展右边界

    • 右边界扩展到 right = 8arr[right] = 1,增加 countOnes3,超过 n)。

    • 移动左边界 left 直到 countOnes 不超过 n

    • 移动 leftleft = 1countOnes 变为 2,窗口内长度是 7

    • 继续调整窗口,直到 countOnes 符合要求。

    • 最大长度保持 8

      窗口: [0,0,1,0,0,1,0,0,1,0,0]
      ^
      left
      ^
      right

  10. 继续扩展右边界

    • 右边界扩展到 right = 9arr[right] = 0countOnes 不变)。
    • countOnes 仍然是 2
    • 当前窗口内最长连续 0 的长度是 8 (right - left + 1 = 8),maxLength 仍然是 8

    窗口: [0,0,1,0,0,1,0,0,1,0,0]
    ^
    left
    ^
    right

  11. 继续扩展右边界

    • 右边界扩展到 right = 10arr[right] = 0countOnes 不变)。
    • countOnes 仍然是 2
    • 当前窗口内最长连续 0 的长度是 9 (right - left + 1 = 9),更新 maxLength9

    窗口: [0,0,1,0,0,1,0,0,1,0,0]
    ^
    left
    ^
    right

结果

最终,maxLength8,即最长的连续 0 的长度为 8

最大负载:

要解决这个问题,我们可以按照以下步骤来实现:

步骤解析

  1. 循环分配服务器:每次消息到达时,我们按照顺序循环分配服务器。
  2. 检查服务器的繁忙状态:如果当前要分配的服务器繁忙,则跳过它,分配给下一个空闲的服务器。
  3. 计算每个服务器的总负载:记录每个服务器处理的负载总和。
  4. 找出处理负载最高的服务器:最后,找出那些处理负载最多的服务器,并按编号升序返回。

JavaScript 实现

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]

代码解释

  1. 初始化

    • servers 数组用来记录每个服务器处理的总负载。
    • busyUntil 数组用来记录每个服务器处理完当前任务的时间,即何时变得空闲。
    • maxLoad 记录当前服务器中处理的最高负载值。
  2. 循环处理每条消息

    • 对于每条消息,按照顺序尝试将其分配给一个空闲的服务器。
    • 如果找到空闲的服务器,就将消息分配给该服务器,并更新该服务器的负载和繁忙截止时间。
  3. 查找最高负载的服务器

    • 最后,遍历 servers 数组,找出负载等于 maxLoad 的服务器,将其编号加入结果中。
  4. 返回结果

    • 返回处理负载最高的服务器编号,按照升序排序。

这个实现逻辑清晰且简洁,能够有效地解决问题。在时间和空间限制内,性能也是可以接受的。

优化版:

没问题,让我用更简单的方式解释一下,并且简化实现。

问题理解

假设我们有 k 台服务器,每台服务器按顺序循环接收请求消息。如果一台服务器忙碌,就跳过它分配给下一台空闲的服务器。如果所有服务器都忙碌,那么这个请求就会被丢弃。我们要找出处理负载最多的服务器,并返回它们的编号。

步骤分解

  1. 初始化状态

    • 创建一个数组来记录每台服务器的负载总和。
    • 创建一个数组记录每台服务器何时会空闲。
  2. 循环处理每个请求

    • 根据当前请求的时间,找出可以处理该请求的服务器(即空闲的服务器)。
    • 如果找到空闲的服务器,更新它的负载和下次空闲时间。
    • 如果没有找到空闲服务器,这个请求就被丢弃。
  3. 找出处理负载最多的服务器

    • 遍历所有服务器,找出负载最多的服务器。

简化版 JavaScript 实现

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]

代码详细解释

  1. 初始化

    • loads 数组用于记录每台服务器处理的总负载。
    • busyUntil 数组用于记录每台服务器处理完当前任务后什么时候会空闲。
  2. 处理请求

    • 对于每个请求,按顺序检查服务器是否空闲,如果空闲则分配给它。
    • 使用 (i + j) % k 来找到合适的服务器,这样可以实现循环分配。
  3. 找到最大负载的服务器

    • 计算每台服务器的负载,找到负载最大的服务器并返回它们的编号。

总结

这个简化版代码通过简单的循环和数组操作,轻松实现了服务器的负载均衡分配。每个请求只需要遍历最多 k 个服务器,因此在时间和空间上都非常高效。

相关推荐
YJlio几秒前
PsPing 学习笔记(14.8):常见错误与排障实战(超时、拒绝连接、权限问题)
开发语言·笔记·python·学习·django·pdf·pygame
LegendNoTitle6 分钟前
Windows和Linux下Rust-init、Cargo下载慢的解决
开发语言·windows·rust
亓才孓11 分钟前
深浅拷贝--Java
java·开发语言·windows
alphaTao14 分钟前
LeetCode 每日一题 2025/12/29-2026/1/4
算法·leetcode
ShaderJoy14 分钟前
ShaderJoy —— 《对称镜面下的绞肉机》【算法悬疑短文】【Python】
算法·leetcode·面试
潲爺15 分钟前
Java笔记总结
java·开发语言·笔记·学习
菜的不敢吱声17 分钟前
swift学习第一天
开发语言·学习·swift
智驱力人工智能17 分钟前
在安全与尊严之间 特殊人员离岗检测系统的技术实现与伦理实践 高风险人员脱岗预警 人员离岗实时合规检测 监狱囚犯脱岗行为AI分析方案
人工智能·深度学习·opencv·算法·目标检测·cnn·边缘计算
培林将军20 分钟前
C语言指针
c语言·开发语言·算法
云栖梦泽22 分钟前
鸿蒙分布式应用全链路性能调优实战
开发语言·鸿蒙系统