无重复字符的最长子串-Scheme实现

实现

下面将使用Scheme语言来实现“无重复字符的最长子串”,即,对于字符串"abbabcx",其最长无重复字符的子串为"abcx",长度4,详细描述可访问https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

(let*
    (
        [s "abbabcx"]
        [arr-index 0]
        [ans 0]
        [d -1]
        [vec (make-vector 128 -1)]
        [keep-last-position (lambda (c pos-in-s) (vector-set! vec c pos-in-s))]
        [scan (lambda (c pos-in-s)
            (let*
                ([previous-pos (vector-ref vec c)] [len 0])
                (begin
                    (when
                        (and (>= previous-pos 0) (> previous-pos d))
                        (set! d previous-pos)
                    )
                    (set! len (- pos-in-s d))
                    (keep-last-position c pos-in-s)
                    (when (> len ans) (set! ans len))
                )
            )
        )]
    )

    (
        string-for-each
            (lambda (e) (scan (char->integer e) arr-index) (set! arr-index (+ arr-index 1)))
            s
    )
    ans
)

实现的思路为:

由于字符可以有ASCII码表示,故可以用一个长度为128的向量vec来保存每个字符在给定的字符串中出现的最后一次的位置。维护一个变量d来记录最近一次出现重复的字符的起始位置。 如果:

  1. 判断某字符是否重复: 1.1 某字符第一次出现,执行第2步 1.2 某字符非第一次出现,如果该字符上次出现的位置,在d之后,则将d设置为该字符上次出现的位置。由于之前的最大长度已经由ans保存,所以只需要计算剩下的子串的长度能不能大于ans,这就是为什么d记录最近一次出现重复的字符串的起始位置就好了
  2. 计算当前位置和d的长度len
  3. 保存其位置到vec
  4. 如果len大于ans,改变anslen

这种实现只需遍历一遍就可以计算出结果,性能非常棒。当然暴力计算或者滑动窗口也可以实现该功能,只是需要多次遍历,性能比不上。

概要分析

上文说到性能问题,拿点数据出来证明一下吧。

将上面的代码稍微改造一下,作为一个definition,并保存在longest_substr.ss文件中,

(define longest-substr
    (lambda (str)
        (let*
            (
                [s str]
                [arr-index 0]
                [ans 0]
                [d -1]
                [vec (make-vector 128 -1)]
                [keep-last-position (lambda (c pos-in-s) (vector-set! vec c pos-in-s))]
                [scan (lambda (c pos-in-s)
                    (let*
                        ([previous-pos (vector-ref vec c)] [len 0])
                        (begin
                            (when
                                (and (>= previous-pos 0) (> previous-pos d))
                                (set! d previous-pos)
                            )
                            (set! len (- pos-in-s d))
                            (keep-last-position c pos-in-s)
                            (when (> len ans) (set! ans len))
                        )
                    )
                )]
            )

            (
                string-for-each
                    (lambda (e) (scan (char->integer e) arr-index) (set! arr-index (+ arr-index 1)))
                    s
            )
            ans
        )
    )
)

Chez Scheme提供了一个概要分析工具,非常好用,来试用一下:

生成的html可以非常直观的看到该函数的执行情况

字符串越长,执行次数越多,执行时间是多少呢?选一个上面例子中最长的字符串来看看:

> (time (longest-substr "abcabcxsfsdfw232sdfssdfsdfsdfsf1234567890"))
(time (longest-substr "abcabcxsfsdfw232sdfssdfsdfsdfsf1234567890"))
    no collections
    0.000003672s elapsed cpu time
    0.000002000s elapsed real time
    1792 bytes allocated
12
>

消耗的CPU时间0.000003672s,可以说非常短了,且分配的内存仅仅1792个字节。超乎寻常的性能,我想应该有两个方面原因:

  1. 算法
  2. Scheme 和 Chez Scheme