刷题笔记:力扣第6题-Z字形变换

1.拿到题目后想到该题目的字符串有周期性,可以以Z的左上角字符开始,到下一个左上角字符结束(不包含该字符),具体如下图所示:

黄色标注的部分即为一个周期单元mem,根据规律可得出mem的值为行数 * 2 - 2。

2.每一行的字符下标也有规律,可以发现每一行都一定包含该行的第一个字符,如果不是第一行或者最后一行,还会包含s[cur + mem - 2 * row],cur为当前指针位置,row为正被扫描的该行号。根据以上思想,可以初步写出如下代码:

cpp 复制代码
 1. char* convert(char* s, int numRows) {
 2.     int len = strlen(s);
 3.     char res[len];
 4.     int mem = numRows * 2 - 2;
 5.     int cnt = 0;
 6.     
 7.     for (int i = 0; i < numRows; i++){
 8.         int cur = i;
 9.         while (cur < len){
10.             res[cnt++] = s[cur];
11.             if (mem == 0){
12.                 break;
13.             }
14.             if (i > 0 && i < numRows - 1 && cur + mem - 2 * i < len){
15.                 res[cnt++] = s[cur + mem - 2 * i];
16.             }
17.             cur += mem;
18.         }
19.     }
20.  
21.     return res;
22. }

3.但是写的代码报错了,检查后发现有如下两点原因:

①我将结果数组定义为局部变量了,储存在栈上的局部变量会在函数结束后被回收并销毁,这样会导致每次传出去的指针指向的内存全部为NULL。正确做法应该是使用malloc,将结果数组储存在堆上。

②没有正确处理行数为1的情况。我现在的代码虽然加了if (mem == 0)的判断,会跳出while循环,但那样会导致处于同一行的剩余字符全都被跳过了。正确做法是在运行for循环之前就进行行数判断,如果行数为1或者行数大于等于字符串长度(字符全都在第一列上)时直接返回s。

4.修正了以上为题后,写出的完整代码如下:

cpp 复制代码
 1. char* convert(char* s, int numRows) {
 2.     int len = strlen(s);           // 字符串长度
 3.     char* res = (char*)malloc(sizeof(char) * (len + 1)); // 结果字符串(堆内存)
 4.     int mem = numRows * 2 - 2;     // 一个周期的长度(Z字形一个来回)
 5.     int cnt = 0;                   // 结果数组下标
 6.  
 7.     // 边界条件:1行 或 字符串很短,直接返回原串
 8.     if (numRows == 1 || len <= numRows){
 9.         return s;
10.     }
11.     
12.     // 按行遍历:第 0 行 → 第 1 行 → ... → 第 numRows-1 行
13.     for (int i = 0; i < numRows; i++){
14.         int cur = i; // 第 i 行的起始位置就是 i
15.         
16.         // 沿着周期往下走
17.         while (cur < len){
18.             res[cnt++] = s[cur]; // 把当前字符放入结果
19.             
20.             // 【核心】中间行需要多取一个斜着的字符
21.             if (i > 0 && i < numRows - 1 && cur + mem - 2 * i < len){
22.                 res[cnt++] = s[cur + mem - 2 * i];
23.             }
24.             
25.             cur += mem; // 跳到下一个周期
26.         }
27.     }
28.  
29.     res[cnt] = '\0'; // 字符串必须结尾加结束符
30.     return res;
31. }

需要注意的是,给字符串进行malloc时需要给末尾的'\0'也留出空间,所以需要(char*)malloc(sizeof(char) * (len + 1))。该算法的时间复杂度为O(n),空间复杂度也为O(n)。

5.但目前的代码不是最快的,以下是力扣最快的完整代码:

cpp 复制代码
 1. char* convert(char* s, int numRows) {
 2.     // ===================== 边界条件 =====================
 3.     // 如果只有1行 或 行数 >= 字符串长度,不需要变换,直接返回原串
 4.     if (numRows == 1 || numRows >= strlen(s)) {
 5.         return s;
 6.     }
 7.  
 8.     int len = strlen(s);               // 字符串长度
 9.     char* result = (char*)malloc((len + 1) * sizeof(char)); // 堆内存分配(必须!)
10.     if (result == NULL) {              // 安全判断:内存分配失败
11.         return NULL;
12.     }
13.  
14.     int index = 0;                     // 结果数组的写入下标
15.     int cycleLen = 2 * numRows - 2;    // 【核心】一个 Z 字形周期的长度
16.  
17.     // ===================== 按行遍历:逐行取出字符 =====================
18.     for (int i = 0; i < numRows; i++) {
19.         // j 是周期起点,每次跳一个周期
20.         for (int j = 0; j + i < len; j += cycleLen) {
21.             
22.             // 1. 先取「竖列」上的字符
23.             result[index++] = s[j + i];
24.  
25.             // 2. 中间行需要额外取「斜向上」的字符
26.             // 第一行 & 最后一行 没有斜向字符
27.             if (i != 0 && i != numRows - 1) {
28.                 int midIndex = j + cycleLen - i; // 斜向字符的下标
29.                 if (midIndex < len) {            // 不越界才取
30.                     result[index++] = s[midIndex];
31.                 }
32.             }
33.         }
34.     }
35.  
36.     result[index] = '\0'; // C 字符串必须加结束符
37.     return result;
38. }

咨询了ai后得知,虽然算法思路一样,但该代码在以下几个方面比我做得好:

①提前了行数和边界判断的代码,放到了malloc前面。malloc是一个相对耗时的操作系统底层调用。而该代码把判断放在最前面,遇到不需要变换的情况,直接O(1)极速返回,且不消耗任何额外内存。

②对于cur的推导公式,我的为:cur + mem - 2 * i,而该代码为:j + cycleLen -- i,其中j是周期起点,相当于cur = j + i。在经过代数替换后会发现我的公式和该代码的一样,但是我的代码比它多算了一次乘法,导致慢了一些。

③该代码将循环中的if判断条件拆成了如下:

cpp 复制代码
1. if (i != 0 && i != numRows - 1) {
2.     int midIndex = j + cycleLen - i;
3.     if (midIndex < len) {
4.         result[index++] = s[midIndex];
5.     }
6. }

这样做可以把不依赖内层循环的 if 条件剥离出来,方便编译器做分支预测,节省时间。

相关推荐
bIo7lyA8v8 小时前
算法稳定性分析中的输入扰动建模的技术9
算法
CoderCodingNo8 小时前
【GESP】C++三级真题 luogu-B4499, [GESP202603 三级] 二进制回文串
数据结构·c++·算法
sinat_286945198 小时前
AI Coding 时代的 TDD:从理念到工程落地
人工智能·深度学习·算法·tdd
炽烈小老头8 小时前
【 每天学习一点算法 2026/04/12】x 的平方根
学习·算法
ASKED_20198 小时前
从排序到生成:腾讯广告算法大赛 2025 baseline解读
人工智能·算法
田梓燊8 小时前
leetcode 160
算法·leetcode·职场和发展
_深海凉_9 小时前
LeetCode热题100-颜色分类
python·算法·leetcode
hetao17338379 小时前
2026-04-09~12 hetao1733837 的刷题记录
c++·算法
6Hzlia9 小时前
【Hot 100 刷题计划】 LeetCode 136. 只出现一次的数字 | C++ 哈希表&异或基础解法
c++·算法·leetcode
MWWZ10 小时前
最近的一些软件更新
opencv·算法·计算机视觉