LeetCode 第6题:Z字形变换(Python3解法)

1:问题描述

来源:LeetCode

难度:中等


问题详情:
将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:

P   A   H   N
A P L S I I G
Y   I   R

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"


2:问题分析

2.1 时间复杂度和空间复杂度

在真正开始介绍各种算法前,先以表格形式展示各自的时间复杂度和空间复杂度, n n n 表示字符串 s s s 的长度。

算法 时间复杂度 空间复杂度
二维矩阵 O ( n u m R o w s × n ) O(numRows×n) O(numRows×n) O ( n u m R o w s × n ) O(numRows×n) O(numRows×n)
二维矩阵改进 O( n n n) O ( n ) O(n) O(n)
构造法 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)

2.2 二维矩阵

使用二维矩阵依次按照 第1节 中那样的顺序存进二维数组,没有字符的用空字符串代替。在竖线中的元素是行不断增加,列保持不变;在斜线中的元素依次是行不断减少,列不断增大;

2.2.1 构建矩阵

首先分析该字符串写成Z字形后,一共有几列。
在这里插入图片描述

我们以这个倒N字形的竖线和斜线上的元素为一个单元。 竖线上的元素个数= n u m R o w s numRows numRows;斜线上的元素个数为 n u m R o w s − 2 numRows-2 numRows2;因此一个单元中的元素个数总共 r = n u m R o w s × 2 − 2 r=numRows×2-2 r=numRows×22;每一个单元的列数: 1 + n u m R o w s − 2 = n u m R o w s − 1 1+numRows-2=numRows-1 1+numRows2=numRows1

则共有 ⌊ n r ⌋ \lfloor \dfrac{n}{r}\rfloor rn个单元,而那些组不成一个单元的元素个数,也要看是否 > n u m R o w s >numRows numRows。如果小于,则余下的元素构成一列(因为一个竖线有 n u m R o w s numRows numRows个元素);如果大于,超过numRows的部分(即斜线部分)每一个元素独占一列。

则求列数的公式如下:
c o l = ⌊ n r ⌋ × ( n u m R o w s − 1 ) r e = n % r c o l 1 = ⌊ r e n u m R o w s ⌋ + r e % n u m R o w s c o l = c o l + c o l 1 \begin{aligned} &col=\lfloor \dfrac{n}{r}\rfloor ×(numRows-1) \\ &re=n\% r\\ &col1=\lfloor \dfrac{re}{numRows}\rfloor +re \% numRows \\ &col = col + col1 \end{aligned} col=rn×(numRows1)re=n%rcol1=numRowsre+re%numRowscol=col+col1

而这个矩阵的行数为 n u m R o w s numRows numRows
因此该矩阵的尺寸 ( n u m R o w s , c o l ) (numRows,col) (numRows,col)。‘

而官方解法则是选择了一个偷懒的方法,让那些构不成一个单元的,通过填补空白使其构成一个单元。这样的解法需要 ⌈ n r ⌉ \lceil \dfrac{n}{r}\rceil rn 列,是向上取整的。虽然这样做代码比较简洁,但是会比我的解法占用更多的空间。

2.2.2 判断位置

为了判断当前元素是在竖线还是在斜线上,我们需要构建一个判断标准。

官方的解法在前面已经略有提及,通过当前字符在字符串中的索引 i n d e x index index 对一个单元的元素个数 r r r 求余,其余数小于 n u m R o w s numRows numRows,则表明在竖线上;大于等于numRows则表明在斜线上;

我的解法则是:

  • 设置一个 f l a g flag flag ,默认为 F a l s e False False
  • 如果行数 i = 0 i=0 i=0时, f l a g = F a l s e flag=False flag=False,表明将进入竖线
  • i = n u m R o w s − 1 i=numRows-1 i=numRows1时, f l a g = T r u e flag=True flag=True,表明下一个元素将进入斜线
  • 如果 f l a g = F a l s e flag=False flag=False,行数 i + = 1 i+=1 i+=1,列数 j j j保持不变
  • 如果 f l a g = T r u e flag=True flag=True,行数 i − = 1 i-=1 i=1,列数 j + = 1 j+=1 j+=1

2.2.3 边界

该问题的边界问题就是,转换后只有一行或者一列的状态。当只有一行时,那就是 n u m R o w s = 1 numRows=1 numRows=1;如果只有一列,那就是 n u m R o w s > = n numRows >= n numRows>=n;

2.2.4 代码

def convert(s: str, numRows: int) -> str:
    length = len(s)
    # 计算一共占了多少列
    # 当排列后为一行或者一列的情况,则直接返回原字符串
    if length <= numRows or numRows == 1:
        return s
    else:
        # 以Z字形中的一个横线和对角线为一个单元,计算s中能有多少个这样的单元
        t = length // (numRows + numRows - 2)
        temp = length % (numRows + numRows - 2)
        col1 = temp // numRows
        col1 += temp % numRows
        # 一个单元占了 (numRows-2 + 1)列,而剩下的不成单元的占col1列
        col = (numRows - 2 + 1) * t + col1
    # 先创建一个空字符列表,一共numRows行col列
    result = [[""] * col for i in range(numRows)]
    index = 0
    i = 0  # 第几行
    j = 0  # 第几列
    diag = False  # 表示是否在对角线上
    # 通过分析,可以得知当行标走到numRows-1时,下一步就要走到对角线处了,此时行不断往上,列不断往右
    # 当行标再次回到0的时候,就是走出了对角线,此时行不断往下,列保持不变
    while index <= length - 1:
        if i == 0:
            diag = False
        if i == numRows - 1:
            diag = True
        result[i][j] = s[index]
        index += 1
        if not diag:
            i += 1
        else:
            i -= 1
            j += 1

    result_2 = ''
    for row_item in result:
        for col_item in row_item:
            if col_item != '':
                result_2 += col_item

    return result_2

对于时间复杂度,创建和遍历 r e s u l t result result的消耗较高,为 O ( 行数 × 列数 ) O(行数×列数) O(行数×列数),行数 = n u m R o w s =numRows =numRows,列数可以粗略地看作 O ( n ) O(n) O(n),时间复杂度为 O ( n u m R o w s × n ) O(numRows×n) O(numRows×n) w h i l e while while内部时间复杂度为 O ( n ) O(n) O(n),因此总的时间复杂度为 O ( n u m R o w s × n ) O(numRows×n) O(numRows×n)

空间复杂度,则同样是 O ( n u m R o w s × n ) O(numRows×n) O(numRows×n)

2.3 改进的二维矩阵

上述的二维矩阵存在大量空白,浪费了许多空间。

因此我们可以将每一行作为一个列表,每次移动到该行时,就将该元素添加至这一行对应的列表中。其余则与第一种解法相同。

2.3.1 代码

def convert2(s: str, numRows: int) -> str:
    """使用多个空白列表存储每一行的字符"""
    length = len(s)
    if length <= numRows or numRows == 1:
        return s
    else:
        # 生成一个numRows行的列表存储每一行的字符
        result = [[] for i in range(numRows)]
        i = 0  # 表示行
        diag = False
        for item in s:
            result[i].append(item)
            if i == 0:
                diag = False
            elif i == numRows - 1:
                diag = True
            if diag:
                i -= 1
            else:
                i += 1

    from itertools import chain
    return ''.join(chain(*result))

对于时间复杂度, w h i l e while while和列表的创建和遍历时间复杂度都是 O ( n ) O(n) O(n),因此算法的时间复杂度为 O ( n ) O(n) O(n)

对于空间复杂度,列表中存储了所有的字符串,空间复杂度 O ( n ) O(n) O(n)

2.4 构造法

我们可以直接去探究某一下标的字符应该放在第几行。
| |0列 | 1列| ...| t-1列|t列|
--|--|---|---|---|--|
**0行**| |  |  || 
**1行**| |  | | | 
**2行**| |  | | |
如上图所示,假设 n u m R o w s = 4 numRows=4 numRows=4.

这里的 t = r t=r t=r(2.2.1节提到,每个单元的元素个数)。

  1. 我们可以发现在每个单元注意,这里说的是每个单元,而不是这个表格中的一行)中第0行的元素下标都是 t t t的整数倍,也就是 x t xt xtd的形式
  2. 每个单元中在最后一行的元素的下标应该是这样的形式 x t + n u m R o w s − 1 xt+numRows-1 xt+numRows1
  3. 中间这些行每个单元有两个元素,第一个元素与最后一行的元素规律相同的,只需要满足下标符合这样的形式 x t + i xt+i xt+i,就是在第 i i i行;而第二个元素则满足 ( x t + t − i ) (xt + t - i) (xt+ti)的形式;
  4. 我们统一一下上述的情形,所有行的第一个元素下标都是符合 x t + i xt+i xt+i的形式;
    0 < i < n u m R o w s − 1 0<i<numRows-1 0<i<numRows1, 其第二个元素的下标满足 ( x t + t − i ) (xt + t - i) (xt+ti)的形式

2.4.1 代码

其中的 j j j是值为 0 t , 1 t , 2 t , 3 t . . . 0t, 1t,2t,3t... 0t,1t,2t,3t...的序列;而 i i i表示第 i i i行, 再次提示这里的 r = t r=t r=t.

def convert4(s: str, numRows: int) -> str:
    """在方法1的基础上,分析字符串下标对应二维数组索引"""
    length = len(s)
    if length <= numRows or numRows == 1:
        return s
    else:

        ans = []
        r = 2 * numRows - 2  # 一个单元拥有的字符数
        for i in range(numRows):
            # 因为每一行最少有一个元素,因此-i,可以避免i与j相加后下标越界
            # j = 0r, 1r, 2r, 3r,  s.t. j < length-i
            for j in range(0, length - i, r):
                # 添加每个单元中竖线中的元素
                # j + i:
                # 第0行元素下标,0r+0, 1r+0,2r+0
                # 第1行元素下标,0r+1, 1r+1, 2r+1
                # 第i行元素下标,j+i
                ans.append(s[j + i])  # 第一个元素
                # 因为斜线中的元素下标为
                # 第0行没有斜线
                # 第1行元素下标,1r-1, 2r-1
                # 第i行元素下标,j+r-i
                if 0 < i < numRows - 1 and j + r - i < length:
                    ans.append(s[j + r - i])  # 第二个元素

    return ''.join(ans)

对于时间复杂度,是将所有字符串中的元素都添加到列表中,所以时间复杂度为 O ( n ) O(n) O(n);

对于空间复杂度,ans是返回值,不计入在空间复杂度的统计当中,因此空间复杂度为O(1)。

猜你喜欢

转载自blog.csdn.net/weixin_43490422/article/details/126513310