[Leetcode 6] ZigZag问题的一种新思路

来源:互联网 发布:德州扑克入门知乎 编辑:程序博客网 时间:2024/05/16 05:15

原始问题(leetcode ZigZag Conversion)

如果固定一个行数(这里是3),字符串 "PAYPALISHIRING" 用“之”字形的方式可以写成:

P   A   H   NA P L S I I GY   I   R
 这个结果按照一行一行访问的话是: "PAHNAPLSIIGYIR"。给定一个字符串以及一个表示行数的整数,实现一个函数,返回按行序拼接而成的新串。
string convert(string text, int nRows);

convert("PAYPALISHIRING", 3) 返回 "PAHNAPLSIIGYIR".

思路

如果把字母的排列看成一个二维的表格,那么最左边的那些字母无疑落在了第0列上。题目要求按照行号从小到大、相同行号从左到右的方式组成新字符串。典型方法有两个:第一,用一个二维数组来存放,按照ZigZag的方式一列一列填充数组。然后按行访问就是新数组;第二,不使用额外空间,直接找出每一行不同字母在原字符串中的序号。leetcode上大多数方法应该都是这两种之一或其变体。

这里给出一个新思路,从另一个角度来看问题。如果把每个字母看成一个结点,每对相邻字母之间有条边,那么字符串就映射成了一张图。反过来看,对该图,原始序列其实就是对该图进行深度优先遍历(Depth First Search)的结果。

比如,对于字符串"ABCDE",ZigZag的排列方式如下,期望得到的新字符串为"AEBDC"。

A   EB DC

按照上述思路建立出下图。唯一不同的是多了个根结点R。R的出现是为了后续的广度优先遍历(Breadth First Search)。第0行的每个结点(如'A', 'E')都必须与R相连

对这张图进行BFS,遍历序列为"RAEBDC",与期望结果相比只在最左端多了一个R,可以为R赋一个空值""来解决。下面说到的虚拟结点也可采用相同技术。

特殊情况

在大功告成前还需要考虑一个情况。通过BFS得到目标字符串序列的前提,是每个字母与源结点R有正确的距离。如果R的行号是-1,那么每个字母所在的行号(假设从0开始)与R的距离就是距离源点R的深度。因为深度优先遍历是按照深度从小到大的顺序访问结点。问题是E不一定存在。如原始串是"ABCD",对应的期望字符串为"ABDC"。但如果简单地把D与R相连的话就会产生"ADBC"的错误结果,这是因为D并不在第0行。一个方法是在最后一个处于非0行的字母与R之间建立一个或多个虚拟结点,形成一条虚拟路径,如下图所示。


还有一点需要注意的是,为了保证BFS的时候按照从左到右的顺序访问邻居(如对于R来说,先"A"后"E"),在建图的时候也要按此顺序先后加入邻居(把"A"、"E"分别加入R中)。幸运的是,按照DFS顺序(即原始顺序)访问结点可以满足该条件。

Python代码

from collections import deque class Node:    def __init__(self, value):        self.visited = False        self.value = value        self.neighbors = [] class Solution:    # @param {string} s    # @param {integer} numRows    # @return {string}    def convert(self, s, numRows):        self.__s = s        self.__numRows = numRows        return self.__BFS(self.__buildGraph())     def __connect(self, prev, this):        prev.neighbors.append(this)        this.neighbors.append(prev)     def __buildGraph(self):        '''Build the graph from DFS traversal of the string'''        root = Node('')        prev = None        row = 0        up = True        for i in range(len(self.__s)):            this = Node(self.__s[i])            if prev is not None:                self.__connect(prev, this)            prev = this            # Connect nearest nodes(those on row #0) to the root            if row == 0:                self.__connect(root, this)             if up:                if row < self.__numRows - 1:                    row += 1                elif row > 0:                    row -= 1                    up = False            else:                if row > 0:                    row -= 1                elif row < self.__numRows - 1:                    row += 1                    up = True         # The triky part, for BFS later        # Build a virtual path to connect to the root for the last branch, if not already        if not up and row < self.__numRows - 2:            for i in range(row, -1, -1):                this = Node('')                self.__connect(prev, this)                prev = this            self.__connect(prev, root)         return root  def __BFS(self, root):    '''Breadth First Search gives the desired string'''    work_queue = deque()    root.visited = True    work_queue.append(root)     s = ''    while work_queue:        node = work_queue.popleft()        # No special action for the root as it's an empty string;)        s += node.value        for i in node.neighbors:            if not i.visited:            i.visited = True            work_queue.append(i)    return s

这个基于图的深度优先遍历的方法可能没有其他常规方法快(时间复杂度都是O(n),n为字符串长度),但无疑是一种值得尝试的方法;) 下面给出两种容易想到的解法。

常规方法之——“打表”法

创建一个二维数组,以原字符串中的顺序从左到右一列一列填充,第一列从上往下填、第二列从下往上填,以此类推……填充完毕后按照从上到下的顺序一行一行访问即可。

class Solution:    # @param {string} s    # @param {integer} numRows    # @return {string}    def convert(self, s, numRows):        res = ''        # If numRows is 0, the step should be 1        step = max((numRows - 1) * 2, 1)        # Exit if numRows is larger        for line in range(min(numRows, len(s))):            if line == 0 or line == numRows - 1:                step1 = step2 = step            else:                step1 = step - line * 2                step2 = step - step1             j = line            while j < len(s):                res += s[j]                j += step1                step1, step2 = step2, step1        return res

常规方法之——找规律法

要是知道某一行某一列的字母在原串中的序号就好了。在例子"ABCDE"中,第二行第一列的元素在原字符串中的下标一定是1(从0开始),因此是"B"。这里就不给出分析过程了,具体参考下面代码。在纸上画10个结点,行数为3,从0开始标记。不难找出同一行上相邻结点间隔的规律(及下面的step)。

class Solution:    # @param {string} s    # @param {integer} numRows    # @return {string}    def convert(self, s, numRows):        table = [[] for i in range(numRows)]        line_in_table = 0        up = True        for i in s:            table[line_in_table].append(i)            if up:                if line_in_table < numRows - 1:                    line_in_table += 1                else:                    line_in_table -= 1                    up = False            else:                if line_in_table > 0:                    line_in_table -= 1                else:                    line_in_table += 1                    up = True         res = ''        for row in table:            for col in row:                res += col        return res

小结

他山之石,可以攻玉。多想想问题,会有不同的启发!

2 0