LeeCode 006-Z 字形变换

LeeCode 006-Z 字形变换

  • 题目地址:6. Z 字形变换(中等)
  • 标签:字符串
  • 题目描述:
    将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。
    比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:
    在这里插入图片描述

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

string convert(string s, int numRows);

理解题意

  • 根据官方的例子,我们首先要明白题中的z字形是怎么来的。
    在这里插入图片描述
  • 上方是博主绘制的第一个z字形循环,符合题意描述的从上至下,从左到右。(其实从视觉上看是顺时针旋转90度的z)
  • 进一步理解:z的边长其实就是题中所给的numRows,且根据题中所绘制的图案不难发现,z字的三个边长是等长的。

解题思路

构建矩阵

  1. z字绘制流程清晰之后,便是将流程数字化找到其循环规律:
    首先找到循环的一个周期,虽然题目引导我们绘制z,但是我们发现再绘制第一个z(PAYPALI)后,第二个z的起点又要回到A,是有字母重叠的,因此我们在一个周期里可以将z的最后一笔去掉,那么一个周期循环就变成了一个类似斜v,见下图。
    在这里插入图片描述
  2. 绘制规律找到后,思路蓝图基本出来:最直接想到的便是创建一个矩阵,根据绘制流程,向矩阵中填入字符串,最后遍历矩阵,过滤掉无用的占位数字。
  3. 构建矩阵的第一步便是找到矩阵的长宽,宽就是题目所给参数numRows,长度不难发现就是循环周期的个数*一个周期的长度周期个数=字符串长度/一个周期的字母数。
  4. 理清思路开始赋值即可
    // 字符串长度
    const len = s.length
    
    // 一个周期的字符串个数(一共2笔边长,减去底部交点,减去右上角-因为是下一个周期的起点)
    const cycle = 2 * numRows - 2
    
     // 一共有n个周期(这里加上cycle-1的含义是避免有剩下的字母不够一个周期)见下图
    let n = Math.floor((len + cycle -1)/ cycle)
    

在这里插入图片描述
可以看到最后一个黄色的周期,已经只剩下两个字母(N、G)不够一个周期,如果直接Math.floor((len/ cycle)则会漏掉这个周期的字母,因此为了避免剩余字母,加上一个周期的字母数-1,譬如上述例子:2+cycle-1 = cycle +1 这样在运算时,就会增加一个周期。同时如果本来不多字母的话,加上(cycle-1)/cycle 之后小于1,Math.floor也无法进位。

// 矩阵的列数(周期数*一个周期的列数)
const columns = n * (numRows - 1)
  1. 至此矩阵的行数、列数便已清晰,之后便需要构造初始全是0占位的矩阵。
 // 根据矩阵的列数与行数生成矩阵,并初始化填充0
 const initial = new Array(numRows).fill(0).map(() => new Array(columns).fill(0))
  1. 初始矩阵构造之后,便要根据绘制流程,将指定位置的0替换成字符串中的字母。(先向下x++,再右上x–、y++)根据i%cycle与numRows的大小判断下一步移动。
// 根据z字形将字符串填充至矩阵
   let x = 0, y = 0
   for (let i = 0; i < len; i++) {
       initial[x][y] = s[i]
       // i模cycle小于行数时,x++,即向下移动
       if (i%cycle < numRows - 1) {
           x++
       } else {
           // 反之,y++,即向右上移动
           x--
           y++
       }
   }
  1. 至此得到了结果矩阵,只需将其遍历过滤占位0即可。
   let result = ''
   for (let i = 0; i < numRows; i++) {
       for (let y = 0; y < columns; y++) {
           if (initial[i][y] !== 0) {
               result += initial[i][y]
           }
       }
   }
   return result
  1. 完整代码
/**
 * @param {string} s
 * @param {number} numRows
 * @return {string}
 */
var convert = function (s, numRows) {
    // 字符串长度
    const len = s.length
    // 注意特殊情况,只有一行或者一列时直接返回字符串
    if (numRows === 1 || numRows >= len) {
        return s;
    }
    // 一个周期的字符串个数
    const cycle = 2 * numRows - 2
    // 一共有n个周期
    let n = Math.floor((len)/ cycle)
    // 矩阵的列数(周期数*一个周期的列数)
    const columns = n * (numRows - 1) + cycle -1
    console.log(cycle,n, columns)

    // 根据矩阵的列数与行数生成矩阵,并初始化填充0
    const initial = new Array(numRows).fill(0).map(() => new Array(columns).fill(0))
    // 根据z字形将字符串填充至矩阵
    let x = 0, y = 0
    for (let i = 0; i < len; i++) {
        initial[x][y] = s[i]
        // x小于行数时,x++,即向下移动
        if (i%cycle < numRows - 1) {
            x++
        } else {
            // 反之,y++,即向右上移动
            x--
            y++
        }
    }
    // 遍历矩阵,过滤占位数字0,得到结果
    let result = ''
    for (let i = 0; i < numRows; i++) {
        for (let y = 0; y < columns; y++) {
            if (initial[i][y] !== 0) {
                result += initial[i][y]
            }
        }
    }
    return result

};

在这里插入图片描述
复杂度分析

  • 时间复杂度:O(numRows⋅n),n 为字符串 s 的长度。时间主要消耗在矩阵的创建和遍历上,矩阵的行数为 numRows,列数可以视为 O(n)。
  • 空间复杂度:O(numRows⋅n)。矩阵需要O(numRows⋅n) 的空间。

优化-压缩矩阵

  • 上述构造的矩阵不难发现,使用了大量的无用空间0,那么优化的第一个思路便是能否将占位0全部优化?
  • 观察最后矩阵填入字符的过程:每次是都向该行的最后push新字母。(可以观察博主上方绘制的周期示意图,每行的最新字母都在本行末尾)
  • 所以思路很清晰了,只需要构造给定行数的空列表,根据构造位移规律,依次向目标行push字母即可。
// 根据矩阵的行数生成压缩矩阵,并初始化构造空列表
    const initial = new Array(numRows)
     for (let i = 0; i < numRows; ++i) {
        initial[i] = [];
    }
// 根据z字形将字符串填充至矩阵,此时无需y,只需找到x位置即可(上下移动)
    let x = 0
    for (let i = 0; i < len; i++) {
        initial[x].push(s[i])
        // 模小于行数时,x++,即向下移动
        if (i%cycle < numRows - 1) {
            x++
        } else {
            // 反之,x++,即向上移动
            x--
        }
    }
    // 遍历矩阵,过滤占位数字0,得到结果
    const res = [];
    for (const row of initial) {
        res.push(row.join(''));
    }
    return res.join('')

(为了让大家看懂没有优化代码,只提供思路,其实可以将构造矩阵代码优化,譬如:不用循环初始数组,直接fill字符串,每次遍历追加。)
在这里插入图片描述

复杂度分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(n)。压缩后的矩阵需要 O(n) 的空间。

优化-直接构造法

  • 纯找规律的数学公式法:直接找到矩阵的每个非空字符会对应到 s 的哪个下标(记作 idx),从而直接构造出答案。

  • 由于 Z 字形变换的周期为 t=2r−2,因此对于矩阵第一行的非空字符,其对应的 idx 均为 t 的倍数,即 idx≡0(modt);同理,对于矩阵最后一行的非空字符,应满足 idx≡r−1(modt)。

  • 对于矩阵的其余行(行号设为 i),每个周期内有两个字符,第一个字符满足 idx≡i(modt),第二个字符满足 idx≡t−i(modt)。

  • 上面是官方的原话,博主感觉很明了就直接cv过来了,有的小伙伴可能不太理解,其实一张图就可以解释。
    在这里插入图片描述其实官方说的两个字符的含义是两种(个)字符,第一种是在z竖直边的字符,第二种是在z的斜边。(每行的每个周期)

  • 第一种(个)字符所在行数就是字符下标 i 模行数的结果(本质就是当前的行数+n个周期) idx≡i(modt)

  • 第二种(个)用逆向思维,一个周期的最后一个字符是下一个周期的第一个字母下标i-1的结果,也就是nt-1,斜边的倒数第二个呢?没错,就是nt-2 =》 归纳:在斜边的第i行 字母下标(本质就是n个周期-当前行数): idx≡t−i(modt)

  • 通过上述归纳,其实在结果中每个周期内的每行中只有两种,也分别找到了数学表达式,那么开始编程。

/**
 * @param {string} s
 * @param {number} numRows
 * @return {string}
 */
var convert = function(s, numRows) {
    let len = s.length
     if (numRows === 1 || numRows >= len) {
        return s;
    }
    // 一个周期所含字符个数
    let cycle = 2*numRows -2
    let res = []
    // 遍历行数
    for(let i =0; i <numRows;i++){
        // 遍历不同周期起点
        for(let y = 0;y<len;y+=cycle){
            // 第一个字符是直边上的
            res.push(s[y+i])
             if (0 < i && i < numRows - 1 && y + cycle - i < len) {
                //第二个字符在斜边(本周期可能在当前行没有第二个字符)
                res.push(s[y + cycle - i]); 
                // 当前周期的第二个字符(为什么+t,已经解释了:逆向思维,下一个周期-行数)
             }
        }
    }
    return res.join('');
};

在这里插入图片描述
复杂度分析

  • 时间复杂度:O(n),其中 n 为字符串 s 的长度。s 中的每个字符仅会被访问一次,因此时间复杂度为 O(n)。
  • 空间复杂度:O(1)。返回值不计入空间复杂度。
  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值