趣谈Leecode——仅含 1 的子串数(思路交流)

文章讲述了如何解决一个关于二进制字符串的问题,即计算所有只包含1的子串数量。通过分析字符串中的连续1段,利用等差数列求和公式优化了计算过程,减少了时间和空间复杂度。原始解法使用了字符串分割和递归计算,而官方优化则采用动态计算和一次性求和,降低了内存开销。
摘要由CSDN通过智能技术生成

仅含 1 的子串数

给你一个二进制字符串 s(仅由 ‘0’ 和 ‘1’ 组成的字符串)。

返回所有字符都为 1 的子字符串的数目。

由于答案可能很大,请你将它对 10^9 + 7 取模后返回。

提示:

  • s[i] == ‘0’ 或 s[i] == ‘1’
  • 1 <= s.length <= 10^5

题解分析

看到题目求子串,脑海里最先出来的是不是要用到KMP算法去求解。阅读题干后能够理解题目是想让我们去完成求所有的子串数量,这样的子串有一定要求:

  1. 首先子串只能包含“1”
  2. 子串是可以重复的

我们根据这两个条件进行深入分析,由于子串只能包含“1”,所以当子串中出现0时,我们就不再考虑这种情况。也就是说所有的子串一定是出现在某一段连续的“1”之中,基于这些连续的子串,我们再去求他们的子串。总结来说题目是想让我们求所有规定子串的子串数之和(我们将其称之为“孙串”),并且允许有重复。
在这里插入图片描述

编码思路

首先我们基于过程式的编程来思路来看待这道题,我们要完成解决问题的话需要进行三步操作。

  1. 先将字符串进行分割得到连续的“1”子串
  2. 对于每一个连续的子串再求其孙串的数量
  3. 将每一组子串下合法的孙串累加起来再求模就得到了正确结果

我们根据这三个步骤依次来设计对应的函数实现

1.先将字符串进行分割得到连续的“1”子串

这里我考虑到的是使用go语言中内置的字符串分割函数,以“0”为分割就能得到一段段合格的子串,但是实践发现以strs := strings.Split(str, "0")分割后会产生空串,它不能很好的解决多个连续“0”出现的情况。所以使用如下分割函数进行实现:

func splitSrc(s string) []string {
	str := strings.FieldsFunc(s, func(c rune) bool {
		return c == '0'
	})
	return str
}

2.对于每一个连续的子串再求其孙串的数量

这里我先是对每一个长度的子串都进行了观察,子串的长度和孙串的数量之间有什么对应关系,我们可以容易的发现如下规律:

  • “1” length:1 - > sum:“1x1”
  • “11” length:2 - > sum:“2x1+1x1”
  • “111” length:3 - > sum:“3x1+2x1+1x1”
  • “1111” length:4 - > sum:“4x1+3x1+2x1+1x1”
  • “11111” length:5 - > sum:“5x1+4x1+3x1+2x1+1x1”

基于上述观察容易写出函数:

func complexNum(num int) int {
	sum := 0
	for i := 1; i <= num; i++ {
		sum += i
	}
	return sum
}

3.对于每一个连续的子串再求其孙串的数量

在这里我考虑到另一个问题:分割完成之后的子串可能存在长度相等的情况,因为孙串可以重复,那我们就不用再去执行一遍冗余的操作来求孙串的数量。所以对于新出现的子串长度我们求孙串数量,并将此结果存放至map的键值对当中。当出现重复的子串长度时我们直接去map中读取孙串的数量。将所有求完后的数累积便能得到结果。

const mod = 1000000007

func numSub(s string) int {
	//添加计数器
	sum := 0
	//添加辅助map
	tempMap := make(map[int]int)
	//对字符串进行拆分
	var str = splitSrc(s)
	//遍历每一个连续含1的字符串
	for _, i := range str {
		length := len(i)
		v, ok := tempMap[length]
		if ok {
			sum = (sum + v) % mod
		} else {
			var tempNum = complexNum(length)
			tempMap[length] = tempNum
			sum = (sum + tempNum) % mod
		}
	}
	return sum
}

提交结果

请添加图片描述

官方优化

func numSub(s string) int {
    cnt := 0
    ans := 0
    for _, d := range s {
        if d == '0' {
            ans += (1 + cnt) * cnt / 2
            cnt = 0
        } else {
            cnt++
        }
    }
    ans += (1 + cnt) * cnt / 2
    return ans % (1e9+7)
}

在阅读了官方的代码实例后,发现自己找对了该题解的思路,但是代码的质量真的是很low很low。接下来我将分别讲讲从时间和空间上的优化。

  • 时间上的优化
  1. 明明自己已经发现了不同长度字符串的子串数量规律,但我还是用了最笨的累积计算,这不就是高中便学过的等差数列求和,利用公式便可一步获得结果,经过改动尝试。时间消耗直接减少了4ms。
  • 空间上的优化

可以看到官方代码的执行空间仅仅用到了3.7MB,而我的却达到了6.2MB。我认为是因为我的切片数组和map增大了代码的开销,将分割完后的子串存入了一个切片,当子串非常多时,这样的确会浪费一定的空间,官方示例中直接以O(n)的复杂度动态的切割完了字符串并动态的去求解孙串数量。此外由于等差数列的公式存在,知道子串长度求孙串数量相当便捷,时间复杂度仅为O(1),没有必要额外开辟空间记录。当s.length < 10^5 的情况下,开辟map最大可能会有10^5条记录,内存浪费肯定会巨大。

心得总结

虽然我的字符串切割算法在本体当中没有很好的时间复杂度和空间复杂度的展现,但在某些情况下还是有必要去使用的。

	str := strings.FieldsFunc(s, func(c rune) bool {
		return c == '0'
	})

其次,在题目解决期间一定要考虑不必要的空间浪费问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值