刷题笔记:力扣第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 条件剥离出来,方便编译器做分支预测,节省时间。

相关推荐
努力学习的小廉2 小时前
我爱学算法之——记忆化搜索
算法
m0_730115112 小时前
C++与Python混合编程实战
开发语言·c++·算法
一个人旅程~3 小时前
虚数与量子迷踪
经验分享·笔记·微信·电脑·量子计算
郝学胜-神的一滴4 小时前
Leetcode 969 煎饼排序✨:翻转间的数组排序艺术
数据结构·c++·算法·leetcode·面试
智者知已应修善业11 小时前
【proteus中lm339电压滞回比较器达到三角波转换成方波】2023-4-13
驱动开发·经验分享·笔记·硬件架构·proteus·硬件工程
I_LPL11 小时前
hot100贪心专题
数据结构·算法·leetcode·贪心
551只玄猫11 小时前
新编大学德语1第三版笔记 第5课Essen und Trinken
笔记·学习笔记·德语·外语·德语a1·自学德语·新编大学德语
颜酱12 小时前
DFS 岛屿系列题全解析
javascript·后端·算法
WolfGang00732112 小时前
代码随想录算法训练营 Day16 | 二叉树 part06
算法