代码随想录算法训练营day8day9字符串344反转字符串541反转字符串II卡码54替换数字151翻转字符串里的单词卡码55.右旋转字符串28实现 strStr()459.重复的子字符串

day8

344.反转字符串

题目
思路
双指针法,两倍往中间移动的同时交换数组就可以达到不新增数组也能反转的效果;
边界问题可以用实际例子思考;
i<length/2即可;j不用单独定义,因为是跟随i一起移动的。
(注意本题不能直接用库函数reverse,虽然不知道js是哪个函数)
如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。
定义指针–>指针理解为索引下标即可
代码

/**
 * @param {character[]} s
 * @return {void} Do not return anything, modify s in-place instead.
 */
var reverseString = function(s) {
    //Do not return anything, modify s in-place instead.
    reverse(s)
};

var reverse = function(s) {
    let l = -1, r = s.length;
    while(++l < --r) [s[l], s[r]] = [s[r], s[l]];
};

关于注释:

  • @param {character[]} s:这表示函数有一个参数’s’,它是一个字符数组。在这个上下文中,character[]意味着’s’是一个由字符组成的数组,每个元素都是一个字符。
  • @return {void}:这说明这个函数没有返回值。void是一个特殊的类型,表示没有任何返回值。
  • Do not return anything, modify s in-place instead.:这句说明不应该从函数中返回任何东西,而是应该直接在传入的参数’s’上进行修改。即,这个函数的目的是对输入的字符数组进行某种形式的修改,而不是返回一个新的值或数组。

综上所述,这段注释是在说明一个函数的接口,这个函数接受一个字符数组作为输入,然后在原地(不创建新的数组)对这个数组进行某些操作或修改,而不返回任何值。这种类型的函数通常用于数组处理任务,如反转数组、排序等,其中直接修改输入数组比返回一个新数组更高效或更符合题目要求。

关于代码两个函数分开:

  • 代码重用性:通过将反转逻辑封装在reverse函数中,可以在不同的地方重用这段代码,而不仅仅是在reverseString函数内。这种做法遵循了DRY(Don’t Repeat Yourself)原则,即避免重复代码的实践。如果在其他地方也需要进行类似的反转操作,可以直接调用reverse函数。

  • 清晰的职责分离:reverseString函数的职责是提供一个对外的接口,专门用于反转字符串,而reverse函数则专注于实现反转的具体逻辑。这种分离使得代码更加模块化,每个模块或函数都有明确的责任,从而使得代码更易于理解和维护。

  • 易于测试:将反转逻辑单独放在reverse函数中,使得对这一逻辑的测试更加方便。可以单独对reverse函数进行单元测试,确保其反转逻辑正确无误,而不需要在每次测试时都通过reverseString接口。

  • 灵活性和扩展性:如果将来需要对反转逻辑进行修改或扩展(比如添加特定条件下的特殊处理),只需要修改reverse函数即可,而不需要重新设计外部接口reverseString。这样就可以保持接口的稳定性,同时提供内部实现的灵活性。

总之,虽然在这个特定的示例中,使用两个函数可能看起来有些过度设计,但这种做法在更复杂的项目中是一种常见且有益的设计模式,它有助于代码的组织、重用、测试和维护。

541. 反转字符串II

题目链接
思路:
步进距离可以直接写i+=2*k,然后对i到i+k之间的数据进行操作,这样比较简洁,而不是i++,然后用count计数,计到2k再返回去操作。
一般的库函数,这里指reverse都是左闭右开(i+k就是不包含i+k);自己写可以自定义
又因为reverse都是左闭右开,reverse(s,i,i+k)反转的时候不包含i+k,所以判断条件中i+k<s.length是可以等于边界的
区间的定义也很重要
带着例子试一下。
代码:

/**
 * @param {string} s
 * @param {number} k
 * @return {string}
 */
var reverseStr = function(s, k) {
    const len = s.length;
    let resArr = s.split(""); 
    for(let i = 0; i < len; i += 2 * k) {  // 每隔 2k 个字符的前 k 个字符进行反转
        let l = i - 1, r = i + k > len ? len : i + k;
        while(++l < --r) [resArr[l], resArr[r]] = [resArr[r], resArr[l]];
    }
    return resArr.join("");
};

代码中变量l:
在这段JavaScript代码中,l 是一个变量,用于表示当前子字符串(要进行反转的部分)的左边界索引。这个代码段的目的是按照指定的规则反转字符串 s 中的部分字符。具体来说,每隔 2k 个字符的前 k 个字符会被反转。

让我们分解这个循环中 l 的作用和它如何工作的:

  • ①let l = i - 1: 这行代码初始化了 l,设置为当前子段起始索引 i 减去 1。这样做的原因是,在循环体中,会先执行 ++l(l 自增),这样在第一次循环迭代中,l 实际上指向的是从 i 开始的位置。这是一个技巧,用于确保 l 从当前子段的第一个字符位置开始。
    这个代码片段中的目的是在字符串 s 的指定部分执行反转操作,每次处理的段落长度为 2k,其中前 k 个字符需要被反转。为了实现这个反转,代码使用了两个指针(索引变量)l 和 r 来指向当前需要反转部分的开始和结束位置。

  • 初始化 l 为 i - 1 这一步可能初看起来有些令人困惑,但这实际上是为了配合后续的 while 循环中的 ++l 操作设计的。这里的 i 代表当前处理段落的起始索引。

  • 在 while 循环的条件判断中,使用了 ++l(即在使用 l 之前先进行自增)。这意味着在循环的每一次迭代开始之前,l 都会增加 1。所以,如果你希望 l 在循环的第一次迭代中指向 i(即段落的真正起始位置),你需要在进入循环之前将其设置为 i - 1。这样,当第一次迭代中 ++l 执行时,l 实际上就变成了 i。

  • 这样做的目的是什么?

  • 这个技巧确保了 l 可以正确地从每个段落的第一个字符开始工作,同时也允许代码以一种非常紧凑和优雅的方式来表达。通过先自减后自增,这个方法避免了需要在循环内部进行更复杂的判断或者调整 l 的值,使得代码更加清晰易读。

  • 如何理解这个过程?

  • 假设 i = 0 并且你想从字符串的第一个字符开始反转。如果你直接设置 l = i,那么在循环的第一次迭代中,执行 ++l 会导致 l 指向索引 1(字符串的第二个字符)。这显然不是我们想要的,因为我们想从第一个字符开始反转。因此,通过设置 l = i - 1,我们确保了在循环的第一次迭代中,l 通过 ++l 自增后,指向的是索引 0,即字符串的第一个字符。

  • 这种方式是一种编程技巧,用于在遍历或操作序列时精确控制索引位置,特别是在需要前置或后置操作(如自增或自减)的场景中。

  • 这种模式确保了操作的范围正好从 i 开始,直到满足特定的条件(比如在字符串反转的场景中,直到达到 k 的长度或字符串的末尾)。通过这种方式,可以避免在循环的开始就错过了起始索引 i 的处理

  • 优点:
    Ⅰ.避免冗余代码: 这种模式避免了在循环体的开始就需要写一个单独的行来增加 l,使得代码更加紧凑。
    Ⅱ.提高代码可读性: 对于熟悉这种模式的开发者来说,这种方式提高了代码的可读性,因为它遵循了一种常见的模式,即在循环的每一步之前准备好下一步的条件。
    Ⅲ.优化性能: 虽然性能提升可能微不足道,但减少循环内部的操作可以轻微地提高代码的执行效率。

  • l=i-1 与循环中的 ++l 结合使用是一种编程技巧,它可以使代码更加简洁和高效。这种模式特别适用于需要精确控制循环迭代起始点的场景,如字符串处理、数组遍历等。掌握这种技巧可以帮助你更好地理解和编写循环控制逻辑。


  • 如果直接设置 l = i 而不在循环开始之前进行 ++l 操作,代码的结构需要相应地调整来保证相同的逻辑结果。原代码利用 ++l 和 --r 在 while 循环的条件判断中来实现反转的边界控制,这样做的好处是代码简洁且易于理解,尤其是在处理边界逻辑时。
  • 如果改为直接让 l = i,那么我们就不能在循环条件中使用 ++l(因为这会跳过第一个字符),而需要调整循环内部的逻辑来确保 l 和 r 指向正确的字符进行交换。这种情况下,一个可能的修改方案是直接在 while 循环中对 l 和 r 进行操作,但不在条件判断中改变它们的值。代码示例如下:
let l = i, r = i + k > len ? len : i + k; // 直接设置 l = i,并调整 r 的初始化以适应边界条件
while(l < r) {
    [resArr[l], resArr[r-1]] = [resArr[r-1], resArr[l]]; // 注意这里的 r-1,因为 r 初始化时已经指向了正确的边界
    l++;
    r--;
}

在这个修改后的版本中,l 和 r 分别在循环体的开始和结束位置直接设置,而不是通过自增或自减操作。这样做确实可以达到同样的反转效果,但会使代码略显复杂,因为需要手动控制两个索引的增减,而原始方法通过在条件判断中直接使用 ++l 和 --r,以一种更简洁的方式实现了相同的逻辑。

选择哪种实现方式取决于代码的可读性、易理解性以及个人偏好。原始方法通过减少变量的直接操作并利用循环条件简化了逻辑,而修改后的方法虽然直观但增加了循环体的复杂度。

  • ②在循环 while(++l < --r) 中:

++l:在比较之前将 l 自增,这意味着 l 会从当前子段的起始位置向右移动。
–r:在比较之前将 r 自减,这意味着 r 从当前子段的结束位置(或 k 个字符后的位置,取决于哪个更小)向左移动。

  • ③循环的目的是将索引 l 与索引 r 之间的字符进行交换,直到 l 和 r 相遇或者 l 大于 r。这样,每次循环都会交换两个位置上的字符,从而实现字符串的部分反转。

因此,l 在这里起到的是遍历字符串子段(即每次循环需要反转的部分)的左边界索引的角色,配合 r(右边界索引),用于实现字符串的局部反转。

代码中的三元运算:
这行代码中的 r = i + k + > len ? len : i + k 是一个使用三元运算符的表达式,用于确定反转操作中右边界 r 的位置。这个表达式可以分解为以下几个部分:

  • i + k: 这部分计算当前子段起始索引 i 加上 k 的值,即尝试确定当前需要反转的子字符串的结束位置。理想情况下,如果 s 的长度足够,这个值将是子字符串反转部分的实际结束索引。

  • i + k > len ? len : i + k: 这是一个三元运算符表达式,用于处理当 i + k 的结果超出字符串 s 的长度时的情况。这个表达式的含义如下:

  • 条件: i + k > len 检查通过添加 k 到当前索引 i 上之后的值是否超过了字符串 s 的总长度 len。

  • 第一个结果 (len): 如果条件为真(即 i + k 的结果确实超出了字符串的总长度),则表达式的结果为 len。这意味着 r 将被设置为字符串的实际长度,防止索引越界。

  • 第二个结果 (i + k): 如果条件为假(即 i + k 的结果没有超过字符串的总长度),则表达式的结果就是 i + k 本身。这意味着 r 将被设置为子字符串反转部分的理想结束索引。

这个表达式确保了无论 s 的实际长度如何,r 都不会超过字符串的边界,从而在进行子字符串反转操作时避免了索引越界的错误。简而言之,这个表达式的目的是安全地确定每个需要反转的子段的右边界。

关于JavaScript中设置左右边界进行字符串或数组的部分处理
是一种常见的操作模式,特别是在需要执行子段反转、搜索或其他局部操作时。确定索引起始和索引结束即边界是核心;以及边界检查(可以防止诸如尝试访问不存在的索引之类的错误。)和动态边界( 在某些情况下,左右边界可能会根据算法的进展动态变化,特别是在迭代过程中。)

卡码网:54.替换数字

题目链接
思路:
双指针法;先将数组扩充到替换数字为单词后的长度;i指针指向旧数组末尾,j指向新数组末尾;s[i]赋值给s[j];j开始从后往前一个个扩充,把单词补充完整;最后s[i]赋值给s[j],j与i指向同一个元素。
很多数组填充类的问题,其做法都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

这么做有两个好处:

  • 不用申请新数组。
  • 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。

但是js不能直接对字符串操作,需要转换为数组,最后再转换回字符串((gpt代码解释))
代码:

function replaceDigitsWithWord(s) {
    // 计算替换后的总长度
    let countNumbers = 0;
    for (let i = 0; i < s.length; i++) {
        if (!isNaN(parseInt(s[i], 10))) {
            countNumbers++;
        }
    }
    const totalLength = s.length + countNumbers * 5; // 每个数字替换为'number'增加5个字符长度

    // 创建一个新的数组来存储结果
    let result = new Array(totalLength);
    let i = s.length - 1, j = totalLength - 1;

    while (i >= 0) {
        if (isNaN(parseInt(s[i], 10))) {
            // 如果是字母,直接复制
            result[j] = s[i];
            i--;
            j--;
        } else {
            // 如果是数字,替换为'number'
            j -= 6; // 跳过'number'的长度
            result.fill('number', j + 1, j + 7); // 使用fill填充'number'
            i--; // 移动原字符串的指针
        }
    }

    return result.join(''); // 将数组转换回字符串
}

// 测试示例
const inputString = "a1b2c3";
const outputString = replaceDigitsWithWord(inputString);
console.log(outputString); // 应输出: "anumberbnumbercnumber"

151.翻转字符串里的单词

题目链接
不要使用辅助空间,空间复杂度要求为O(1)。

  • 移除多余空格(也就是之前数组中移除元素的思想)
  • 将整个字符串反转
  • 将每个单词反转

使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。


在JavaScript中,没有一个标准函数的名称直接对应C++中的erase,但是有几种方法可以用来删除数组或对象中的元素,这些操作可以看作是类似的。让我们看看这些操作和它们的时间复杂度以及空间复杂度:

在这里插入图片描述

代码:

/**
 * @param {string} s
 * @return {string}
 */
 var reverseWords = function(s) {
   // 字符串转数组
   const strArr = Array.from(s);
   // 移除多余空格
   removeExtraSpaces(strArr);
   // 翻转
   reverse(strArr, 0, strArr.length - 1);

   let start = 0;

   for(let i = 0; i <= strArr.length; i++) {
     if (strArr[i] === ' ' || i === strArr.length) {
       // 翻转单词
       reverse(strArr, start, i - 1);
       start = i + 1;
     }
   }

   return strArr.join('');
};

// 删除多余空格
function removeExtraSpaces(strArr) {
  let slowIndex = 0;
  let fastIndex = 0;

  while(fastIndex < strArr.length) {
    // 移除开始位置和重复的空格
    if (strArr[fastIndex] === ' ' && (fastIndex === 0 || strArr[fastIndex - 1] === ' ')) {
      fastIndex++;
    } else {
      strArr[slowIndex++] = strArr[fastIndex++];
    }
  }

  // 移除末尾空格
  strArr.length = strArr[slowIndex - 1] === ' ' ? slowIndex - 1 : slowIndex;
}

// 翻转从 start 到 end 的字符
function reverse(strArr, start, end) {
  let left = start;
  let right = end;

  while(left < right) {
    // 交换
    [strArr[left], strArr[right]] = [strArr[right], strArr[left]];
    left++;
    right--;
  }
}

卡码网:55.右旋转字符串

题目链接
不能申请额外空间,只能在本串上操作。 (Java不能在字符串上修改,所以使用java一定要开辟新空间)
思路:
字符串相当于分成了两个部分,如果n为2,符串相当于分成了两个部分;整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,负负得正,这样就不影响子串里面字符的顺序了。
左反转和右反转 有什么区别呢?其实就是反转的区间不同
代码:

//gpt思路
function rotateStringRight(k, s) {
    // 获取字符串长度
    let len = s.length;
    // 确保k在字符串长度范围内
    k = k % len;
    // 分割字符串并重新组合
    // 先获取尾部k个字符,然后获取剩余的字符串,最后将它们拼接起来
    return s.substring(len - k) + s.substring(0, len - k);
}

// 示例
let k = 2;
let s = "abcdefg";
console.log(rotateStringRight(k, s));
//作者思路
function rotateStringRight(s, k) {
    // 计算需要旋转的位数,确保k在字符串长度范围内
    k = k % s.length;

    // 倒序整个字符串
    let reversed = reverseString(s);
    // 分别倒序两个子串:前k个字符和剩余的字符
    let part1 = reverseString(reversed.substring(0, k));
    let part2 = reverseString(reversed.substring(k));

    // 拼接两个倒序后的子串
    return part1 + part2;
}

// 辅助函数:倒序字符串
function reverseString(str) {
    return str.split('').reverse().join('');
}

// 示例
let k = 2;
let s = "abcdefg";
console.log(rotateStringRight(s, k));


day9

28. 实现 strStr()

KMP算法第一遍会有难度, 先跳过
全称Knuth-Morris-Pratt字符串搜索算法,是一种用于快速字符串搜索的算法
KMP算法的核心思想是,当在文本字符串中进行模式匹配时,如果发现不匹配的字符,算法会利用已经匹配成功的部分信息,避免从头开始匹配,从而跳过一些不必要的比较。这是通过预处理模式字符串来实现的,预处理过程生成一个部分匹配表(也称为"失配表"或"next数组"),该表包含了模式字符串中每个位置之前的子字符串的最长相同前缀和后缀的长度。
KMP算法的步骤大致如下:

  1. 预处理模式字符串:计算部分匹配表,该表指示每个位置处如果发生失配,模式字符串应该回溯到哪个位置继续匹配,而不是从头开始。

  2. 搜索匹配:使用部分匹配表来指导在文本字符串中的搜索过程。当遇到不匹配时,根据部分匹配表决定模式字符串的滑动距离,而不是每次只滑动一个位置。

KMP算法的优点:

  1. 效率高:避免了无用的比较,最坏情况下的时间复杂度为O(n),其中n是文本字符串的长度。
  2. 无需回溯:文本字符串指针无需回溯,只需向前移动,大大提高了匹配效率。
    应用场景:
    KMP算法广泛应用于计算机科学中的各种场景,包括文本编辑器的查找功能、数据压缩、生物信息学中的序列匹配等领域。由于其高效性,KMP算法是解决字符串搜索问题的重要工具之一。

题目链接

讲解
觉得KMP那个匹配跟生信课上讲的动态规划有点像。(前缀表数组整体右移即减一,作为next数组,这样遇见不匹配的元素时的next数组的值是它应该跳转去的位置;如果直接前缀表数组,应该跳到值前一位下标对应的元素)

459.重复的子字符串

也可以跳过
题目链接
讲解
看视频再领会领会硬啃太难

字符串总结

主要就是双指针 和KMP

双指针回顾

笔记

  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值