[Python] 算法心得—字符串篇

六个算法问题,使用python学习算法。

1.1 旋转字符串

给定字符串,要求把字符串前面若干个字符移动到字符串尾部。要求时间复杂度O(n),空间复杂度O(1)。
如:'abcdefg'前面的2个字符'a'和'b'移到字符串尾部,就是'cdefgab'。

解法一:暴力移位法
def left_shift_one(s):
    slist = list(s)
    temp = slist[0]
    for i in range(1, len(s)):
        slist[i - 1] = slist[i]
    slist[len(s) - 1] = temp
    s = ''.join(slist)
    return s


def left_rotate_str(s, n):
    while n > 0:
        s = left_shift_one(s)
        n -= 1
    return s

时间复杂度O(n*len(s)), 空间复杂度O(1)。

解法二:三步反转法

例如给定字符串'abcdefg',若要让ab翻转到最后,只需三步操作:
step 1. 字符串分为两部分,即:X=ab, Y=cdefg;
step 2. 将X、Y均反转,得到:X=ba, Y=gfedc;
step 3. 将step2得到的结果反转,即将'bagfedc'反转,得到:cdefgab,即为想要的结果。

def reserve_str(s):
    s_list = list(s)
    start, end = 0, len(s)-1
    while start < end:
        s_list[start], s_list[end] = s_list[end], s_list[start]
        start += 1
        end -= 1
    return ''.join(s_list)


def left_rotate_str(s, n):
    x, y = list(s[:n]), list(s[n:])
    x = reserve_str(x)
    y = reserve_str(y)
    res = reserve_str(x+y)
    return ''.join(res)

时间复杂度O(n),空间复杂度O(1)。

1.2 链表反转

给出一个链表和一个数k,比如:链表为1->2->3->4->5->6, k=2,翻转后为2->1->6->5->4->3;若k=3,翻转后3->2->1->6->5->4。

链表反转的方式
解法一: 循环迭代

基本思想就是将每个节点的向前、向后指向变换。

def reverse_loop(head):
    if not head or not head.next:
        return head
    pre = None
    while head:
        next = head.next # 缓存当前节点的向后指针,下次迭代用
        head.next = pre # 把当前节点的向前指针作为当前节点的向后指针
        pre = head # 作为下次迭代(当前节点)的向前指针
        head = next # 作为下次迭代的(当前)节点
    return pre
解法二:递归

递归的思想就是:

head.next = None
head.next.next = head.next
head.next.next.next = head.next.next
...

代码实现:

def reverse_recursion(head):
    if not head or not head.next:
       return head
    new_head = reverse_recursion(head.next)
    
    head.next.next = head
    head.next = None
    
    return new_head
解法
def divide_linked_list(head, k):
    if head is None or head.next is None:
        return head
    p1 = head
    p2 = head.next
    while k >0:
        tmp = p2.next
        p2.next = p1
        p1 = p2
        p2 = tmp
        k -= 1
    p3 = reverse_recursion(p2)   # 后部分翻转
    head.data = p3.data
    head.next = p3.next
    return p1

2.1 字符串包含

给定一长一短的两个字符串A,B,假设A长B短,要求判断B是否包含在字符串A中。

解法一:暴力法
def string_contain(string_a, string_b):
    for b in string_b:
        if b not in string_a:
            return False
    return True

假设A字符串长n,B字符串长m,那么这种暴力解需要O(n*m)次操作。

解法二:线性扫描

这种方法首先需要将A, B字符串排序,随后同时对这两个字符串依次轮询。

def string_contain(string_a, string_b):
    list_a = sorted(string_a)
    list_b = sorted(string_b)
    pa, pb = 0, 0
    while pb < len(list_b):
        while pa < len(list_a) and (list_a[pa] < list_b[pb]):
            pa += 1
        if (pa >= len(list_a)) or (list_a[pa] > list_b[pb]):
            return False
        pb += 1
    return True

排序需要O(mlogm) + O(nlogn)次操作,之后的线性扫描则只需要O(m+n)次操作。

解法三:素数对应

将每个字母与一个素数相对应。遍历长字符串A,将每个字母对应的素数相乘得到一个乘积。再遍历短字符串B,将每个字符对应的素数除A的乘积,若不能整除,说明B中对应的字符不在A字符串中。

from functools import reduce

def string_contain(string_a, string_b):
    char_map = {'a': 2, 'b': 3, 'c': 5, 'd': 7, 'e': 11, 'f': 13, 'g': 17, 'h': 19, 'i': 23, 'j': 29, 'k': 31, 'l': 37,
                'm': 41, 'n': 43, 'o': 47, 'p': 53, 'q': 59, 'r': 61, 's': 67, 't': 71, 'u': 73, 'v': 79, 'w': 83,
                'x': 89, 'y': 97, 'z': 101}
    pd_a = reduce(lambda x, y: x * y, [char_map[x] for x in string_a])
    for b in list(string_b):
        if pd_a % char_map[b] != 0:
            return False
    return True

需要O(m+n)次操作,但要考虑素数相乘的结果有可能导致正数溢出。

3.1 回文字符串

判断一个字符串正着读和倒着读是否一样,比如:'abcba'即为回文字符串。

解法一:头尾指针向中间扫描

同时从字符串的头尾向中间扫描,知道头尾相遇。如果所有字符都一样,那么这就是一个回文字符串。

def is_palindrome(s):
    s = list(s)
    if len(s)>0:
        start, end = 0, len(s)-1
        while start
解法二:中间向头尾扫描

也可以从字符串的中间向头尾扫描,如果所有字符都一样,就认为这是个回文字符串。

def is_palindrome(s):
    s = list(s)
    if len(s) > 0:
        front = len(s) // 2 - 1 if len(s) % 2 == 0 else len(s) // 2
        back = len(s) // 2
        while front >= 0 and back <= len(s)-1:
            if s[front] != s[back]:
                return False
            front -= 1
            back += 1
        return True
    return True

3.2 回文链表

如何判断一条单向链表是不是'回文'呢?
对于单项链表也可以像字符串那样,找到其中点,然后将后半翻转(见 1.1.2 链表反转),再同时从链表头部和中间开始遍历比较。

def is_palindrome(self, head):
    fast = slow = head
    while fast and fast.next:
        fast = fast.next.next
        slow = slow.next
    node = None
    while slow:
        current = slow
        slow = slow.next
        current.next = node
        node = current
    while node:
        if node.data != head.data:
            return False
        node = node.next
        head = head.next
    return True

4.1 最长回文子串

给定一个字符串,求它的最长回文子串的长度。

解法一:遍历所有元素

首先想到的是遍历字符串中的所有元素,用两个指针以某个元素为中心向左右扩展,记录并更新得到的最长的回文长度。在这里需要注意,奇数长度与偶数长度的回文字符串的判断稍有区别,需要分开处理。

def _odd(i, max, s, id):
    j, temp = 0, 0
    while j <= i and i + j < len(s):
        if s[i - j] != s[i + j]:
            break
        temp = j * 2 + 1
        j += 1
    if temp > max:
        max = temp
        id = i
    return max, id

def _even(i, max, s, id):
    j, temp = 0, 0
    while j <= i and i + j + 1 < len(s):
        if s[i - j] != s[i + j + 1]:
            break
        temp = j * 2 + 2
        j += 1
    if temp > max:
        max = temp
        id = i
    return max, id

def longest_palindrome(s):
    if len(s) == 0:
        return 0
    max, id = -1, 0
    for i in range(len(s)):
        odd_res = _odd(i, max, s, id)
        max, id = odd_res[0], odd_res[1]
        even_res = _even(i, max, s, id)
        max, id = even_res[0], even_res[1]
    return s[id - max // 2:id + (max + 1) // 2] if max % 2 != 0 else s[id - max // 2+1:id + max // 2 + 1]
解法二: 统一处理奇偶

上述解法中,我们需要特别区分奇偶回文字符串的情况。那么有没有方法可以将所有情况都转换为奇数处理呢?

可以通过在每个字符的两边都插入一个特殊的符号,将所有可能的奇数或偶数长度的回文子串都转换成了奇数长度。比如 abba 变成 #a#b#b#a#,aba变成 #a#b#a#。

此外,还可以在首位各加上特殊符号,这样就不用特殊处理越界问题,比如^#a#b#a#$。

def longest_palindrome(s):
    s_j = '#'.join('^{}$'.format(s))
    if len(s_j) == 3:
        return 0
    max, id = -1, 0
    for i in range(len(s_j)):
        j = 0
        temp = 0
        while j <= i and i + j < len(s_j):
            if s_j[i - j] != s_j[i + j]:
                break
            temp = j * 2
            j += 1
        if temp > max:
            max = temp
            id = i // 2 - 1
    return s[id - max // 4:id + (max + 1) // 4 + 1] if max % 4 != 0 else s[id - max // 4 + 1:id + max // 4 + 1]
解法三: Manacher算法

下面将用到神奇的Manacher算法,使求最长回文子串的时间复杂度为线性O(N)。

Manacher算法解析

def longest_palindrome(s):
    # Transform S into T.
    # For example, S = "abba", T = "^#a#b#b#a#$".
    # ^ and $ signs are sentinels appended to each end to avoid bounds checking
    s_j = '#'.join('^{}$'.format(s))
    n = len(s_j)
    P = [0] * n
    id = mx = 0
    for i in range(1, n - 1):
        P[i] = (mx > i) and min(mx - i, P[2 * id - i])  # equals to i' = id - (i-id)
        # Attempt to expand palindrome centered at i
        while s_j[i + 1 + P[i]] == s_j[i - 1 - P[i]]:
            P[i] += 1

        # If palindrome centered at i expand past mx,
        # adjust center based on expanded palindrome.
        if i + P[i] > mx:
            id, mx = i, i + P[i]

    # Find the maximum element in P.
    maxLen, centerIndex = max((n, i) for i, n in enumerate(P))
    return s[(centerIndex - maxLen) // 2: (centerIndex + maxLen) // 2]

5 交替字符串

输入三个字符串s1、s2和s3,判断第三个字符串s3是否由前两个字符串s1和s2交错而成,即不改变s1和s2中各个字符原有的相对顺序,例如当s1 = “aabcc”,s2 = “dbbca”,s3 = “aadbbcbcac”时,则输出True,但如果s3=“accabdbbca”,则输出False。

这道题我们考虑用动态规划的方法,定义dp[i][j]为s1的前i和字串和s2和前j个字串是否构成交替字符串。

可以分两种情况讨论:

    1. 如果s1当前字符(即s1[i-1])等于s3当前字符(即s3[i+j-1]),且dp[i-1][j]为真,那么可以取s1当前字符而忽略s2的情况,dp[i][j]返回真;
    1. 如果s2当前字符等于s3当前字符,并且dp[i][j-1]为真,那么可以取s2而忽略s1的情况,dp[i][j]返回真,其它情况,dp[i][j]返回假

状态转移为:
dp[i][j] = (dp[i-1][j]&& s1[i-1]==s3[i+j-1]) || dp[i][j-1] && s2[j-1]==s3[i+j-1]

代码实现如下:

def is_inter_leave(s1, s2, s3):
    l_1, l_2, l_3 = len(s1), len(s2), len(s3)
    if l_1 + l_2 != l_3:
        return False
    dp = [[False for _ in range(l_1 + 1)] for _ in range(l_2 + 1)]
    dp[0][0] = True
    for i in range(1, l_1 + 1):
        dp[0][i] = dp[0][i - 1] and (s3[i - 1] == s1[i - 1])
    for i in range(1, l_2 + 1):
        dp[i][0] = dp[i - 1][0] and (s3[i - 1] == s2[i - 1])
    for i in range(1, l_2 + 1):
        for j in range(1, l_1 + 1):
            if (dp[i][j - 1] and s1[j - 1] == s3[i + j - 1]) or (dp[i - 1][j] and s2[i - 1] == s3[i + j - 1]):
                dp[i][j] = True
    return dp[l_2][l_1]

6 字符串编辑距离

给定一个源串和目标串,能够对源串进行如下操作:

  • 在给定位置上插入一个字符
  • 替换任意字符
  • 删除任意字符
    写一个程序,返回最小操作数,使得对源串进行这些操作后等于目标串,源串和目标串的长度都小于2000。

这题我们依旧可以采用动态规划的方法。例如字符串'ALGORITHM'变成'ALTRUISTIC',那么把相关字符各自对齐后,如下图:

[Python] 算法心得—字符串篇_第1张图片

把源串S[0...i]='ALGORITHM'编辑成下面的目标串T[0...j]='ALTRUISTIC',对于字符s[i]和t[j]的对应,有以下四种情况:

  • 目标串空白,即S + 字符X,T + 空白,意味着源串要删字符,则操作数为dp[i-1, j] + 1
  • 源串空白,即S + 空白,T + 字符,意味着源串需要插入字符,则操作数为dp[i, j-1] + 1
  • 源串的字符和目标串的字符不一样,即S + 字符X,T + 字符Y,S变成T,意味着源串要修改字符,操作数为dp[i - 1, j - 1]
  • 源串的字符和目标串的字符一样,不用修改,操作数为dp[i - 1, j - 1] + 1

代码如下:

def edit_distance(source, target):
    len_s = len(source)
    len_t = len(target)
    dp = [[0 for _ in range(len_t+1)] for _ in range(len_s+1)]
    for i in range(len_s+1):
        dp[i][0] = i
    for j in range(len_t+1):
        dp[0][j] = j
    for i in range(1, len_s+1):
        for j in range(1, len_t+1):
            if source[i-1] == target[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = 1+ min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
    return dp[len_s][len_t]

你可能感兴趣的:([Python] 算法心得—字符串篇)