python_16_Manacher算法和Morris遍历

Manacher算法解决在一个字符串中最长回文子串问题

1 回文问题

1.1 暴力解

没有实轴碰虚轴的情况
加#号辅助找回文
在这里插入图片描述
但时间复杂度为O(N方)

1.2 Manacher算法

1.2.1 理论

  1. 回文半径、直径、区域
  2. 回文半径数组pArr[ ]
  3. 回文最右边界(int R)
  4. 取得R更新的回文中心点(int C)

比如
#1#2#2#1#…

  • R = -1 C = -1 ,初始在-1位置,开始以0为中心向两边扩,比-1大,所以R=0,C是当R更新时,是哪个点让R更新的,此时是以0为中心更新的R,所以C=0. R = 0 C = 0
    以1为中心向两边扩,1左边位置0为#,右边位置2为#,R取到了更右边的边界,R = 2,谁让R更新的,是1位置,所以C=1
    只要变得更右了,R就更新,C就更新

大前提:i在R内,i撇是对称位置
有三种情况
第一大情况:
i位置在R外,可以用暴力做


第二大情况:i位置在R内

  1. 第一种情况,i的对称点(i撇)回文区域彻底在L…R内部

在这里插入图片描述

  1. 第二种情况,i的对称点 i撇回文区域有一部分超过LR了

在这里插入图片描述

  1. 第三种情况:i的对称点i撇的回文区域左边界压线
    i到R距离和i撇的回文半径一样
    在这里插入图片描述

https://www.bilibili.com/video/BV1FR4y117UQ/?spm_id_from=333.337.search-card.all.click&vd_source=8094924cd2ccfafb6ee1359567f94439

1.2.2 伪代码

# manacher算法,返回回文串中最长的长度
def maxPLen(s):
    # "12321" -> "#1#2#3#2#1#"
    s -> s2
    pArr = []  长度为s2长度
    R = -1
    C = -1
    for i in range(len(s2)):
        if i在R外:
            暴力做    成功了,R会变大
        else i在R内:
            if i的对称点回文区域彻底在L...R内部:
                pArr[i] = pArr[i的对称点]   这已经是确认答案了
            elif i的对称点回文区域跑到L...R外部:
                pArr[i] = i...R的距离       也已经确定了距离
            else i的对称点回文区域左边界和L压线:
                从R外开始扩   R内不扩,i一定关于i自己是回文
                外扩成功了,向右扩,R会变大

        pArr[i] = ?
    return  pArr最大值/2   ->   就是原始字符串回文长度,返回

1.2.3 代码

def manacher(s):
    if s is None or len(s) == 0:
        return 0
    str = manacherString(s)   # 中间加 # 字符 12321 -> #1#2#3#2#1#
    # pArr数组中记录回文半径大小
    pArr = [0] * len(str)    # 长度为sx的长度
    C = -1
    R = -1   # R代表最右的扩成功的位置,这里小修改成了,是失败的第一个位置,而之前都是成功的。中:最右的扩成功位置的,再下一个位置
    Max = float("-inf")   # 记录所有回文半径数组的最大值
    # 每个i位置从左往右依次扩,每个位置求答案
    for i in range(len(str)):         # 每个位置求一次最大半径
        # 13-26行很顺利的把pArr求完了
        # 这就是所有情况
        # R是第一个违规的位置,刚才的笔记是最后一个成功的位置,稍改一下,i >= R 表示i在R外
        # i位置扩出来的答案,i位置扩的区域,至少是多大
        # i不用验的区域是1,R>i 表示i在R内, 2 * C - i 是对称点
        # min(pArr[2 * C - i],R - i)   i撇区域大小和R...L的距离谁小,谁是不用验证的区域
        pArr[i] = min(pArr[2 * C - i],R - i) if R > i else 1   # 4种情况都在这句了,不在盒内直接赋值1
        while i + pArr[i] < len(str) and i - pArr[i] > -1:    # 能否继续再扩
            # 情况一跑这个没问题的
            if str[i + pArr[i]] == str[i - pArr[i]]:
                pArr[i] += 1                    # 这个位置的半径扩1
            else:
                break
            # 有没有刷新更右得边界
            if i + pArr[i] > R:
                R = i + pArr[i]   # R更新右边界
                C = i             # C是使R更新的中心

            Max = max(Max,pArr[i])
    return Max - 1


def manacherString(s):
    charArr = list(s)
    res = []
    index = 0
    for i in range(len(charArr)*2+1):
        if i % 2 == 0:
            res.append('#')
        else:
            res.append(charArr[index])
            index += 1
    return res

a = "btasa"
print(manacher(a))

1.2.3 相关题目

一个字符串,将字符串变成整体都是回文串,只能在字符串后边添加字符,问在最后添加字符串最短是多少才能变成回文串?
比如:abc12321->abc12321cba 把abc逆序添加到1后面,而且这个1是回文半径的最后一个数,把最左的中心求出来,保证回文串最长。
比如:ab121312141213121
在这里插入图片描述
一定要求最左的中心

如何改写Manacher:
1.manacher算法求完,哪一个i位置把最后一个位置包住了,就停
2.不用求完manacher算法,某一个位置扩到最后一个位置就停

修改一些代码,第38行,数值关系如下
在这里插入图片描述

def manacher(s):
    if s is None or len(s) == 0:
        return 0
    str = manacherString(s)  # 中间加 # 字符 12321 -> #1#2#3#2#1#
    # pArr数组中记录回文半径大小
    pArr = [0] * len(str)    # 长度为sx的长度
    C = -1
    R = -1   # R代表最右的扩成功的位置,这里小修改成了,是失败的第一个位置,而之前都是成功的。中:最右的扩成功位置的,再下一个位置
    maxContainsEnd = -1      # 记录所有回文半径数组的最大值
    # 每个i位置从左往右依次扩,每个位置求答案
    for i in range(len(str)):

        # 13-26行很顺利的把pArr求完了
        # 这就是所有情况
        # R是第一个违规的位置,刚才的笔记是最后一个成功的位置,稍改一下,i >= R 表示i在R外
        # i位置扩出来的答案,i位置扩的区域,至少是多大
        # i不用验的区域是1,R>i 表示i在R内, 2 * C - i 是对称点
        # min(pArr[2 * C - i],R - i)   i撇区域大小和R...L的距离谁小,谁是不用验证的区域
        pArr[i] = min(pArr[2 * C - i],R - i) if R > i else 1   # 4种情况都在这句了

        while i + pArr[i] < len(str) and i - pArr[i] > -1:
            # 情况一跑这个没问题的
            if str[i + pArr[i]] == str[i - pArr[i]]:
                pArr[i] += 1
            else:
                break

            # 有没有刷新更右得边界
            if i + pArr[i] > R:
                R = i + pArr[i]   # R更新右边界
                C = i             # C是使R更新的中心

            if R == len(str):     # 若R到了最右边界,就停
                maxContainsEnd = pArr[i]   # 提取回文半径,pArr没有求完,跳出来
                break

    res = [0] * (len(s) - maxContainsEnd + 1)
    for i in range(len(res)):
        res[len(res) - 1 - i] = s[i]
    return ''.join(res)

def manacherString(s):
    charArr = list(s)
    res = [0] * (len(s) * 2 + 1)
    index = 0
    for i in range(len(res)):
        res.append('#' if (i & 1) == 0 else charArr[index])
        index += 1 if (i & 1) == 1 else 0
    return res

a = "abb12321"
print(manacher(a))

2 Morris遍历

关于树遍历,时间复杂度O(N),空间复杂度O(1)
流程:
当前节点cur,一开始cur来到整棵树头部

  • cur无左树,cur = cur.right
  • cur有左树,找到左树最右节点,记为mostright
    (1)如果mostright的右指针是指向None的,mostright.right指向当前节点,cur = cur.left
    (2)mostright的右指针指向自己cur(因为第一步会指向自己),mostright.right = None,cur = cur.right向右走
    (3)cur来到空的时候整个流程停
    在这里插入图片描述

2.1 结构关系

  • 经过例子得出:Morris序列是当树有左子树的会来到节点两次,只能两次递归自己,不能进行三次。来到节点一次,有左树回来一次,没有左树只来一次。
  • morris序加工先、中、后序遍历时,没有左树,不用区分输出几次了,直接打印

2.2 代码

2.2.1 Morris序列(类似先后中序名称)(无打印)

# Morris序列
def morris(head):
    if head is None:
        return
    cur = head          # 最初指向头节点
    mostRight = None    # 人为操作修改的指针
    while cur:  # cur指向空就停
        # 下面cur要么左动,要么右动
        # cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
        mostRight = cur.left   # 指向左子树的头节点 或 指向左孩子
        if mostRight is not None:    # cur.left 有左树
            # 找到cur左树上,真实的最右节点
            # 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
            while mostRight.right is not None and mostRight.right != cur:
                mostRight = mostRight.right    # 往右走
            # 从while中出来,mostright一定是cur左树上的最右节点
            #
            if mostRight.right is None:  # 第一次到来
                mostRight.right = cur  # 人为改动,如果右孩子空,就指向cur指向的节点
                cur = cur.left
                continue
            else:   # 一定等于cur,第二次来到cur
                # mostRight.right != None -> mostRight.right == cur
                mostRight.right = None   # 让它重新指向空

        cur = cur.right

2.2.2 Morris序列加工成先序序列

一个节点没有左树说明只会来到一次,直接打印
打印第一次遇到的点,就是先序遍历
在这里插入图片描述


# Morris序列 加工 先序遍历
def morrispre(head):
    if head is None:
        return
    cur = head          # 最初指向头节点
    mostRight = None    # 人为操作修改的指针
    while cur:  # cur指向空就停
        # 下面cur要么左动,要么右动
        # cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
        mostRight = cur.left   # 指向左子树的头节点 或 指向左孩子
        if mostRight is not None:    # cur.left 有左树
            # 找到cur左树上,真实的最右节点
            # 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
            while mostRight.right is not None and mostRight.right != cur:
                mostRight = mostRight.right    # 往右走
            # 从while中出来,mostright一定是cur左树上的最右节点
            #
            if mostRight.right is None:  # 第一次到来
                mostRight.right = cur
                print(cur.value)   # 第一次打印
                cur = cur.left
                continue
            else:   # 一定等于cur,第二次来到cur
                # mostRight.right != None -> mostRight.right == cur
                mostRight.right = None   # 让它重新指向空
        else:  # 直接打印一次的节点
            print(cur.value)

        cur = cur.right
    print()

2.2.3 Morris序列加工成中序序列

只遇到一次的节点打印,而遇到两次的节点只打印第二次出现的节点。
在这里插入图片描述

# Morris序列 加工 中序遍历
def morrisIn(head):       
    if head is None:
        return
    cur = head          # 最初指向头节点
    mostRight = None    # 人为操作修改的指针
    while cur:  # cur指向空就停
        # 下面cur要么左动,要么右动
        # cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
        mostRight = cur.left   # 指向左子树的头节点 或 指向左孩子
        if mostRight is not None:    # cur.left 有左树
            # 找到cur左树上,真实的最右节点
            # 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
            while mostRight.right is not None and mostRight.right != cur:
                mostRight = mostRight.right    # 往右走
            # 从while中出来,mostright一定是cur左树上的最右节点
            #
            if mostRight.right is None:  # 第一次到来
                mostRight.right = cur  # 人为改动,如果右孩子空,就指向cur指向的节点
                cur = cur.left
                continue
            else:   # 一定等于cur,第二次来到cur
                # mostRight.right != None -> mostRight.right == cur
                mostRight.right = None   # 让它重新指向空
        print(cur.value)   # 往右移动就打印,回到两次会continue直接跳过        多加了打印
        cur = cur.right
    print()

2.2.4 Morris序列加工成后序序列

加工成后序遍历是有一些区别的。
第一次来到节点,逆序打印左树右边界,再打印整棵树的右边界

# Morris序列 加工 后序遍历
def morrisPos(head):
    if head is None:
        return
    cur = head          # 最初指向头节点
    mostRight = None    # 人为操作修改的指针
    while cur:  # cur指向空就停
        # 下面cur要么左动,要么右动
        # cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
        mostRight = cur.left   # 指向左子树的头节点 或 指向左孩子
        if mostRight is not None:    # cur.left 有左树
            # 找到cur左树上,真实的最右节点
            # 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
            while mostRight.right is not None and mostRight.right != cur:
                mostRight = mostRight.right    # 往右走
            # 从while中出来,mostright一定是cur左树上的最右节点
            #
            if mostRight.right is None:  # 第一次到来
                mostRight.right = cur
                cur = cur.left
                continue
            else:   # 能回到自己两次,且是第二次回到自己,逆序打印左树右边界
                # mostRight.right != None -> mostRight.right == cur
                mostRight.right = None   # 让它重新指向空
                printEdge(cur.left)   # 打印子树的左边界

        cur = cur.right
    printEdge(head)    # 整棵树的右边界也得逆序打印一下

    print()

def printEdge(head):
    tail = reverseEdge(head)
    cur = tail
    while cur:
        print(cur.value)
        cur = cur.right
    reverseEdge(tail)         # 再逆序回来,恢复原形

def reverseEdge(head):    # 单链表反转再掉回来
    pre = None
    next = None
    while head:
        next = head.right
        head.right = pre
        pre = head
        head = next

一棵树的分支,看作单链表,然后逆序
在这里插入图片描述

2.3 应用

2.3.1 判断二叉树是否为搜索二叉树

用Morris改动

# Morris序列 加工 中序遍历
def isBST(head):       
    if head is None:
        return
    cur = head          # 最初指向头节点
    mostRight = None    # 人为操作修改的指针
    pre = None
    while cur:  # cur指向空就停
        # 下面cur要么左动,要么右动
        # cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
        mostRight = cur.left   # 指向左子树的头节点 或 指向左孩子
        if mostRight is not None:    # cur.left 有左树
            # 找到cur左树上,真实的最右节点
            # 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
            while mostRight.right is not None and mostRight.right != cur:
                mostRight = mostRight.right    # 往右走
            # 从while中出来,mostright一定是cur左树上的最右节点
            #
            if mostRight.right is None:  # 第一次到来
                mostRight.right = cur  # 人为改动,如果右孩子空,就指向cur指向的节点
                cur = cur.left
                continue
            else:   # 一定等于cur,第二次来到cur
                # mostRight.right != None -> mostRight.right == cur
                mostRight.right = None   # 让它重新指向空
        # print(cur.value)    把中序打印时机改成比对时机
        if pre != None and pre >= cur.value:   # 中序遍历必须是递增的
            return False
        pre = cur.value   # 值为cur指向的值
        cur = cur.right   # cur移动
    print()

2.3.2 返回二叉树最小高度

叶子节点距离头结点高度,才是树的高度
二叉树递归套路:

# 递归套路解二叉树高度
def minHeight1(head):
    if head == None:
        return 0
    return p(head)

def p(x):
    if x.left is None and x.right is None:  # 左右树都为空+1
        return 1 
    # 左右子树起码有一个不为空
    leftH = float("inf")
    if x.left:
        leftH = p(x.left)
    rightH = float("inf")
    if x.right:
        rightH = p(x.right)
    return 1 + min(leftH,rightH)

Morris遍历套路:
问题:cur是否为叶节点,cur节点高度是多少?
不是叶节点不参与更新高度,最后看看整棵树的右边界即可。
在这里插入图片描述

# Morris序列 加工 二叉树高度
def minHeight2(head):
    if head is None:
        return
    cur = head          # 最初指向头节点
    mostRight = None    # 人为操作修改的指针
    curLevel = 0
    minHeight = float("inf")

    while cur:  # cur指向空就停
        # 下面cur要么左动,要么右动
        # cur如果没有左树,那么给mostright就是None,直接跳到最后一行代码了
        mostRight = cur.left   # 指向左子树的头节点 或 指向左孩子
        if mostRight:    # cur.left 有左树
            # 找到cur左树上,真实的最右节点
            # 找最右节点,右指针为空(第一次来cur) and 右指针指向cur指向的节点(第二次来到cur)
            rightBoardSzie = 1   # 右边界
            while mostRight.right and mostRight.right != cur:
                rightBoardSzie += 1
                mostRight = mostRight.right    # 往右走
            # 从while中出来,mostright一定是cur左树上的最右节点
            if mostRight.right is None:  # 第一次到来
                curLevel += 1
                mostRight.right = cur  # 人为改动,如果右孩子空,就指向cur指向的节点
                cur = cur.left
                continue
            else:   # 一定等于cur,第二次来到cur
                if mostRight.left is None:
                    minHeight = min(minHeight,curLevel)    # 先把最小值高度抓出来

                curLevel -= rightBoardSzie   # 当前的值减去左树右边界节点个数就能更新我的层数
                # mostRight.right != None -> mostRight.right == cur
                mostRight.right = None   # 让它重新指向空

        else:
            curLevel += 1   # 只能达到一次的节点 level++
    finalRight = 1
    cur = head
    while cur.right:
        finalRight += 1
        cur = cur.right   # cur移动
    if cur.left and cur.right:
        minHeight = min(minHeight,finalRight)
    return minHeight

在这里插入图片描述

2.4 总结

如果需要左右树信息的话,就用二叉树递归套路
如果左右树的信息可以被代表,并不需要继续留着左右信息了,或用单独变量继承下去,用Morris遍历

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值