python 将字符串中两个相邻的重复字符替换为字母表中的下一个字符

2wnc66cl  于 2023-03-11  发布在  Python
关注(0)|答案(5)|浏览(259)

我想写一个函数,它会找到第一次出现的两个相同的相邻字符,用字母表中下一个字符替换它们,并遍历字符串,直到没有重复的字符。在“zz”的情况下,它应该以循环的方式回到“a”。字符串只能包括字符a-z,也就是说,没有大写字母或非字母字符。我已经写了一个函数来做这件事,但它是不够有效的一个非常长的字符串。

def solve(s):
    i = 1
    while i < len(s):
        if s[i] == s[i-1]:
            r = s[i+1:]
            l = s[:i-1]
            if s[i] == "z":
                x = "a"
            else:
                x = chr(ord(s[i])+1)
            i = 1
            s = l+x+r
        else:
            i += 1
    return s

例如,如果s = 'aabbbc',函数应该像aabbbc --〉bbbc--〉cbbc--〉ccc一样工作,最后返回dc,我怎样才能使它更有效呢?
编辑:例如,如果s = 'ab'*10**4 + 'cc'*10**4 + 'dd'*10**4,则此函数将花费大量时间。

umuewwlo

umuewwlo1#

作为一个简单的优化:可以使用软复位i = i-1来代替硬复位i = 1。实际上,如果在转换之前1和i之间没有重复,那么在转换之后1和i-1之间仍然不会有任何重复。

def solve(s):
    i = 1
    while i < len(s):
        if s[i] == s[i-1]:
            l = s[:i-1]
            r = s[i+1:]  # I swapped these two lines because I read left-to-right
            if s[i] == "z":
                x = "a"
            else:
                x = chr(ord(s[i])+1)
            s = l+x+r
            i = max(1, i-1)  # important change is here
        else:
            i += 1
    return s

s = 'ab'*10**4 + 'cc'*10**4 + 'dd'*10**4
t = solve(s)

print(t[2*10**4:])
# rqpnlih

print(t == 'ab'*10**4 + 'rqpnlih')
# True
7dl7o3gd

7dl7o3gd2#

你可以把结果构建成一个列表,像堆栈一样使用,添加与上一个不同的字母,并在它们匹配时弹出下一个字母:

def compact(s):
    result = []
    nextChar = str.maketrans({i+96:i%26+97 for i in range(1,27)})
    for c in s:
        while result and result[-1] == c:
            c = result.pop(-1).translate(nextChar)
        result.append(c)
    return "".join(result)

输出:

print(compact("aabbbc"))
# dc

s = 'ab'*10**4 + 'cc'*10**4 + 'dd'*10**4
print(compact(s)[-11:])
# ababrqpnlih
u3r8eeie

u3r8eeie3#

我认为,在这种情况下,更容易想到的是在现有字符串的右边逐个添加新的字母,如果在任何时候我们在末尾遇到两个相同的字符,那么我们将它们都删除,并按字母顺序添加下一个字符。
这样,您将避免索引错误和复制整个字符串后,每次修改.
另外,注意由于每个操作都会减少结果的长度,所以复杂度将是O(len(s))。

def solve(s):
    def next_char(char):
        if char == "z":
            return "a"
        else:
            return chr(ord(char)+1)
        
    stack = []
    for char in s:
        while len(stack) > 0 and stack[-1] == char:
            stack.pop()
            char = next_char(char)
        stack.append(char)        

    return ''.join(stack)
olqngx59

olqngx594#

这里减速的主要原因是O(N^2)行为,其原因是:

  • s的重复切片和重新创建(每次都必须分配和复制一个新的字符串,因为Python的字符串是不可变的)。
  • 每当发现重复的字母时,将迭代重置到新创建的s的开始,这样它必须再次读取所有非重复的字母。例如,当减少'abcdeffhh'时,在将'ff'组合成'g'之后,下一次迭代将在考虑'hh'之前扫描所有非重复的字母。(这在Stef的回答中已经指出)

我想到了与Alain T.相同的基本方法,尽管我可以提供一些微观优化。首先,我想解释一下为什么这种基于堆栈的方法要快得多:

  • 从列表的末尾开始的.pop是O(1);该列表是可变的,因此它只需要清除对最后一个对象的引用并更新其元素计数。然而,从任何其他位置删除元素都涉及将所有后续元素下移以填充差距。)类似地,由于新元素将进入单独的数据结构,因此永远不需要重新创建s字符串。
  • 这种迭代方式允许我们使用自然的Python迭代,而不必通过辅助的range对象进行索引。(它将与栈顶比较)。(通常,遍历列表的重叠对可以also be done much more elegantly,但是标准方法假设输入不会被修改。当在输入序列上迭代are a bad ideahard to get right时试图修改输入序列的算法)。
  • 最后,实际替换为“the next letter,looping around at z”可以通过准备一个查找表来简化,string类型提供了一个静态方法maketrans,可以很容易地创建这样的查找表;它适用于字符串的translate方法,该方法将查找应用于字符串的每个字符,然而,由于我们在这部分只处理单字符字符串,因此构建一个直接将字母Map到字母的字典并正常使用它也同样有效。(str.maketrans生成一个字典,但它将整数(基本上是ord的结果)Map到其他整数或None。)

当然,“实用性胜过纯粹性”,所以没有测试这些都不重要,我制作了一个文件q75690334.py来测试所有这些方法的性能:

def solve_user932895(s):
    i = 1
    while i < len(s):
        if s[i] == s[i-1]:
            r = s[i+1:]
            l = s[:i-1]
            if s[i] == "z":
                x = "a"
            else:
                x = chr(ord(s[i])+1)
            i = 1
            s = l+x+r
        else:
            i += 1
    return s

def solve_stef(s):
    i = 1
    while i < len(s):
        if s[i] == s[i-1]:
            l = s[:i-1]
            r = s[i+1:]
            if s[i] == "z":
                x = "a"
            else:
                x = chr(ord(s[i])+1)
            s = l+x+r
            i = max(1, i-1)
        else:
            i += 1
    return s

def solve_alain(s):
    result = []
    nextChar = str.maketrans({i+96:i%26+97 for i in range(1,27)})
    for c in s:
        while result and result[-1] == c:
            c = result.pop(-1).translate(nextChar)
        result.append(c)
    return "".join(result)

# with my suggestion about the lookup, more readable initialization,
# and some other micro-optimizations
abc = 'abcdefghijklmnopqrstuvwxyz'
g_lookup = dict(zip(abc, abc[1:] + abc[:1]))
def solve_karl(s):
    # put a dummy element in the list to simplify the while loop logic
    result = [None]
    # faster attribute access with a local
    lookup = g_lookup 
    for c in s:
        while result[-1] == c:
            c = lookup[result.pop(-1)]
        result.append(c)
    return "".join(result[1:])

def make_test_string(n):
    return 'ab'*n + 'cc'*n + 'dd'*n

if __name__ == '__main__':
    s = make_test_string(10**3)
    assert solve_user932895(s) == solve_alain(s) == solve_karl(s) == solve_stef(s)

验证正确性:

$ python q75690334.py

(noAssert被提出)
在命令行使用n == 10**3计时:

$ python -m timeit -s 'import q75690334' -s 's = q75690334.make_test_string(10**3)' -- 'q75690334.solve_user932895(s)'
1 loop, best of 5: 1.34 sec per loop
$ python -m timeit -s 'import q75690334' -s 's = q75690334.make_test_string(10**3)' -- 'q75690334.solve_stef(s)'
50 loops, best of 5: 5.56 msec per loop
$ python -m timeit -s 'import q75690334' -s 's = q75690334.make_test_string(10**3)' -- 'q75690334.solve_alain(s)'
200 loops, best of 5: 1.42 msec per loop
$ python -m timeit -s 'import q75690334' -s 's = q75690334.make_test_string(10**3)' -- 'q75690334.solve_karl(s)'
500 loops, best of 5: 901 usec per loop

然而,我想在这里强调的一点是,Stef的方法仍然涉及O(N^2)行为(因为切片的缘故)--只是需要更长的时间才能显现出来:

$ python -m timeit -s 'import q75690334' -s 's = q75690334.make_test_string(10**4)' -- 'q75690334.solve_stef(s)'
1 loop, best of 5: 215 msec per loop
$ python -m timeit -s 'import q75690334' -s 's = q75690334.make_test_string(10**4)' -- 'q75690334.solve_alain(s)'
20 loops, best of 5: 14.3 msec per loop
$ python -m timeit -s 'import q75690334' -s 's = q75690334.make_test_string(10**4)' -- 'q75690334.solve_karl(s)'
50 loops, best of 5: 9.14 msec per loop

在这里,我们看到基于堆栈的方法对于10倍的输入花费了大约10倍的时间,但是改进的切片方法花费了将近40倍的时间。(渐进地,它最终应该花费100倍的时间;但足够长的输入来使其清楚,将花费太长的时间来测试。)

crcmnpdw

crcmnpdw5#

我希望提供一个Ruby的解决方案。考虑到Ruby和Python有许多相似之处,大多数读者至少应该能够了解我正在做的事情的要点。我发布这个答案是希望读者可能会喜欢它,并发布一个等效的(如果不是改进的话)Python解决方案。
首先创建一个保存正则表达式的常量。

RGX = /(.)\1/

此表达式匹配一对相同的字符(例如"aa")。.匹配保存到捕获组1的任何字符(行终止符除外)。\1匹配捕获组1的内容。
接下来,我将定义一个常量H,该常量保存一个哈希值,该哈希值将字母对Map到它们的“后继者”,例如"aa"->"b""zz"->"a"
x一个一个一个一个x一个一个二个x
现在我们可以定义一个方法,它将字符串作为参数,并返回所需的字符串。

def doit(str)
  loop do
    break str if str.sub!(RGX, H).nil?
  end
end

我们试试看。

doit("barrsxffzzdggh")
  #=> "batxgadi"

可以看出,每次迭代时str的值如下。

bassxffzzdggh
batxffzzdggh
batxgzzdggh
batxgadggh
batxgadhh
batxgadi

现在我将分解该方法的每一步。
首先创建一个包含字母表中字母的数组。

a = ('a'..'z').to_a
   #=> ["a", "b",..., "y", "z"]

'a'..'z'表示一个范围,Range#to_a方法将其Map到一个数组中。
散列H的构造分为四个步骤。

x = a.map { |c| c*2 }
  #=> ["aa", "bb", "cc",..., "yy", "zz"]
b = a.rotate
  #=> ["b", "c",..., "z", "a"]
c = x.zip(b)
  #=> [["aa", "b"], ["bb", "c"],..., ["yy", "z"], ["zz", "a"]]
H = c.to_h
  #=> {"aa"=>"b", "bb"=>"c",..., "yy"=>"z", "zz"=>"a"}

这些步骤使用的方法有Array#map、String#*、Array#rotate、Array#zip和Array#to_h。
现在将给定的字符串赋给变量str

str = "barrsxffzzdggh"

Ruby的方法Kernel#循环或多或少地循环到end语句,直到遇到break关键字。

str.sub!(RGX, H)
      #=> "bassxffzzdggh"

它使用Ruby的String#sub!方法的形式,用它的第一个参数(字符串或正则表达式)匹配子字符串,并用它的第二个参数(散列)进行替换。这个方法修改字符串的位置。如果进行了替换,则返回字符串;else返回nil(Ruby有一个非破坏性的对应方法sub,以及用于进行多次替换的方法gsub!gsub)。
最初尝试将str与正则表达式RGX匹配。结果与"rr"匹配。由于H["rr"] #=> "s""s"将替换"rr"并返回修改后的字符串(以及在适当的地方改变),所以我们重复这个循环。这个循环一直持续到sub!返回nil,这时我们完成了,所以我们跳出循环并返回str的当前内容。

相关问题