【算法】求字符串子串的高效实现

1 篇文章 0 订阅
1 篇文章 0 订阅

出处

有一道秋招笔试题是这样:

输入两个正整数n、d(1<=d<=1000),把n看成字符串,求n的所有子串中能被d整除的子串的个数。

示例1:

输入:1234 4,输出:4,说明:12、124、24、4

示例2:

输入:616 3,输出:3,说明:6、66、6

分析

主要难点是求看成字符串的n的所有子串,必须要快速高效地求出,除了回溯法,还有一种二进制移位操作算法如下:

组合

首先看一下组合的含义:

从m个不同元素中,任取出n个成一组,称为一个组合。

注意:得是按照原顺序取,例如在124中,24是组合,但42不是(是排列)。

组合总数:

从n个元素中,取0个元素的组合数+取1个元素的组合数+……+取n个元素的组合数=2^n

移位

移位操作如下:

假如要求124的组合数,那么有2^3=8种组合,用二进制位表示如下:

000、001、010、011、100、101、110、111,

就是将0-7用二进制位表示而已,注意这里二进制位有0有1,我们可以规定:0表示不取,1表示取。

如何判断某一位上是0还是1?

重点来了,通过移位操作,这里用Java实现,int类型的长度为32,现举例如下:

(1)011,判断第0个位是1。

这里的第0位意思是2的0次方。

首先将011左移位31位,这步的作用是将第0位的1移动到int的首位31位,即:

10000000 00000000 00000000 00000000,

然后再右移位31位,就将首位移动到了末位,即:

11111111 11111111 11111111 11111111,

注意这里是有符号右移>>,不是无符号>>>,所以如果首位是0,那么右移位之后是0,如果首位是1,那么右移位后全是1。那么这个二进制数是十进制的几呢?是-1,这里用到了补码的知识。

 

补码

对于负数的补码,求十进制的补码是:取绝对值的二进制表示,取反,加一,然后首位为1即可。举例如下:

(1)求-1的补码。

-1的绝对值是1,即0000000 00000000 00000000 00000001,注意这里只有31位,暂不考虑首位。

取反:1111111 11111111 11111111 11111110,

加一:1111111 11111111 11111111 11111111,

首位为1:11111111 11111111 11111111 11111111。

所以32个1的二进制位表示十进制的-1。再看一个示例加强记忆。

(2)求-128的补码,这里为了方便,假设数位长度为8,即Java中的byte类型。

-128的绝对值为128,即1000 0000,不考虑首位,那就是000 0000。

取反:111 1111,

加一:000 0000,这里溢出了,不管。

首位为1:1000 0000。

所以-128的补码为1000 0000,这按原码来算,正是128,所以有点巧妙。

 

言归正传,下面看第2个例子。

(2)011,判断第1个位是1。

因为是第1位,所以要左移位的位数就比末位少了1位,即左移31-1=30位,左移位后为:

11000000 00000000 00000000 00000000,

可以看到,末位的1也保留了,但不要紧,在右移位时会消除掉它。

右移位31位,这个位数是固定的,目的是将首位移至末位。右移位后为:

11111111 11111111 11111111 11111111,

不要忘记这里是有符号右移位,所以还是全1,那么是十进制的几呢?是-1。

第3个例子应该就简单了,如下:

(3)011,判断第2个位是0。

因为是第2位,所以要左移位的位数就比末位少了2位,即31-2=29位,左移位后为:

01100000 00000000 00000000 00000000,

可以看到,后面的位都保留了,但也不要紧,在右移位时会消除掉它们。

右移位31位,这个位数还是固定的,目的是将首位移至末位。右移位后为:

00000000 00000000 00000000 00000000,

这个毫无疑问是0。

再回到要解决的问题上来,目标是求1234的全组合,那么它的组合总数是多少呢?是2^4=16,可以通过将1左移位来求得,即1<<4,4是1234的长度。

然后这些组合分别是什么呢?是0-15的二进制位,即0000、0001、……、1111,那么如何得到每个位上是0还是1呢?当然是左移位和右移位啦。计算机运算的原理是二进制的位操作,所以移位的效率还是蛮高的。

解决

那么问题解决的算法就出来了:

(1)判断字符串的长度n,得到组合总数1<<n,此为外循环。

(2)每个组合数的二进制长度n,此为内循环,对每一位都左移右移判断是0还是1。

(3)若是1,则取之,若是0,则舍之。这里可以用StringBuilder来暂存字符。

(4)具体问题,判断是否整除,那么取余操作%就好。

代码如下:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(new Main().getClass().getResourceAsStream("Main"));
        while (in.hasNext()) {
            long n = in.nextLong();
            int d = in.nextInt();
            char str[] = String.valueOf(n).toCharArray();
            int dd = String.valueOf(d).length();
            int nCnt = str.length;
            int nBit = 1 << nCnt;
            int count = 0;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < nBit; i++) {
                // 每个组合都要先清空StringBuilder
                sb.delete(0, sb.length());
                for (int j = 0; j < nCnt; j++) {
                    // -1意为第j位是1,否则是0
                    if ((i << (31 - j)) >> 31 == -1) {
                        sb.append(str[j]);
                    }
                }
                String s = sb.toString();
                if (s.length() >= dd && Integer.valueOf(s) % d == 0) {
                    count++;
                    System.out.print(s + " ");
                }
            }
            System.out.println(count);
        }
    }
}

代码说明:

Scanner中用到了类所在目录中文件Main中的测试数据:

1234 4
616 3
1234567890 23

运行结果如下:

12 4 24 124 4
6 6 66 3
23 345 46 2346 1357 138 368 124568 13478 45678 2345678 69 2369 34569 123579 1679 24679 12489 14789 1256789 230 3450 460 23460 13570 1380 3680 1245680 134780 456780 23456780 690 23690 345690 1235790 16790 246790 124890 147890 12567890 40

这个算法实测是AC的。

以上就是我做这道题的感悟,如有疏忽,请大家不吝赐教。

参考:

高效全组合算法实现

Java中StringBuilder的清空方法比较

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值