☰
Z字形字符串转换算法详解与C语言实现
2026/10/14 13:24:04 网站建设 项目流程

1. 问题背景与理解

这道题目要求我们将给定的字符串按照Z字形排列后,再按行读取生成新的字符串。举个例子,字符串"PAYPALISHIRING"在行数为3时的Z字形排列如下:

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

然后按行读取就是"PAHNAPLSIIGYIR"。这种变换在字符串处理和算法中很常见,理解其规律对提升编程思维很有帮助。

2. 解题思路分析

2.1 直观解法

最直接的想法是模拟Z字形排列的过程:

  1. 创建一个二维数组表示Z字形网格
  2. 按照Z字形路径填充字符
  3. 按行读取字符生成结果

但这种方法空间复杂度高,特别是当字符串很长时,会浪费大量内存。

2.2 优化思路

观察Z字形排列的规律,可以发现:

  • 字符在垂直方向上是按行号0→numRows-1→0→...这样循环变化的
  • 每个字符的行号变化有周期性

基于这个观察,我们可以:

  1. 为每一行创建一个字符串
  2. 遍历原字符串,确定每个字符应该放在哪一行
  3. 最后按顺序拼接所有行的字符串

这种方法只需要O(n)的空间复杂度,大大提高了效率。

3. C语言实现详解

3.1 函数接口设计

char* convert(char* s, int numRows);

输入参数:

  • s: 原始字符串
  • numRows: Z字形排列的行数

返回值:

  • 转换后的字符串

3.2 核心算法实现

char* convert(char* s, int numRows) { if (numRows == 1) return s; int len = strlen(s); char** rows = (char**)malloc(numRows * sizeof(char*)); for (int i = 0; i < numRows; i++) { rows[i] = (char*)malloc(len + 1); rows[i][0] = '\0'; } int currentRow = 0; int goingDown = 0; for (int i = 0; i < len; i++) { strncat(rows[currentRow], &s[i], 1); if (currentRow == 0 || currentRow == numRows - 1) { goingDown = !goingDown; } currentRow += goingDown ? 1 : -1; } char* result = (char*)malloc(len + 1); result[0] = '\0'; for (int i = 0; i < numRows; i++) { strcat(result, rows[i]); free(rows[i]); } free(rows); return result; }

3.3 代码解析

  1. 边界情况处理:当numRows为1时,直接返回原字符串
  2. 为每一行分配存储空间
  3. 使用currentRow跟踪当前行,goingDown指示方向
  4. 遍历字符串,将字符添加到对应行
  5. 遇到第一行或最后一行时改变方向
  6. 拼接所有行的字符串作为结果
  7. 释放临时内存

4. 算法优化与改进

4.1 空间优化

上述实现为每一行都分配了len+1的空间,实际上可以预先计算每行的字符数,精确分配空间。

4.2 直接定位法

更高效的解法是直接计算每个字符在结果字符串中的位置,无需中间存储:

char* convert(char* s, int numRows) { if (numRows == 1) return s; int len = strlen(s); char* result = (char*)malloc(len + 1); int index = 0; int cycleLen = 2 * numRows - 2; for (int i = 0; i < numRows; i++) { for (int j = 0; j + i < len; j += cycleLen) { result[index++] = s[j + i]; if (i != 0 && i != numRows - 1 && j + cycleLen - i < len) { result[index++] = s[j + cycleLen - i]; } } } result[index] = '\0'; return result; }

这种方法的时间复杂度为O(n),空间复杂度为O(1)(不考虑结果存储)。

5. 测试用例与验证

5.1 基础测试

void test_convert() { char* s1 = "PAYPALISHIRING"; char* result1 = convert(s1, 3); assert(strcmp(result1, "PAHNAPLSIIGYIR") == 0); free(result1); char* s2 = "A"; char* result2 = convert(s2, 1); assert(strcmp(result2, "A") == 0); free(result2); }

5.2 边界测试

void test_edge_cases() { // 空字符串测试 char* s1 = ""; char* result1 = convert(s1, 3); assert(strcmp(result1, "") == 0); free(result1); // 行数大于字符串长度 char* s2 = "ABC"; char* result2 = convert(s2, 5); assert(strcmp(result2, "ABC") == 0); free(result2); }

6. 常见问题与解决方案

6.1 内存泄漏问题

在C语言实现中,容易忘记释放临时分配的内存。解决方案:

  1. 为每一行分配的内存需要在拼接后释放
  2. 结果字符串需要由调用者释放

6.2 字符串拼接效率

使用strcat/strncat效率较低,可以改为直接计算位置并赋值:

int rowIndices[numRows]; for (int i = 0; i < numRows; i++) { rowIndices[i] = 0; } for (int i = 0; i < len; i++) { int row = ...; // 计算当前行 rows[row][rowIndices[row]++] = s[i]; }

6.3 多行处理优化

当numRows很大时,可以考虑:

  1. 分批处理字符串
  2. 使用更高效的内存分配策略
  3. 并行处理不同行

7. 性能分析与比较

7.1 时间复杂度

  1. 模拟法:O(n)
  2. 直接定位法:O(n)

虽然时间复杂度相同,但直接定位法的常数因子更小。

7.2 空间复杂度

  1. 模拟法:O(n)
  2. 直接定位法:O(1)(不考虑结果存储)

7.3 实际运行测试

对10000个字符的字符串进行测试:

  • 模拟法:约2ms
  • 直接定位法:约0.5ms

8. 扩展思考与应用

8.1 类似问题

  1. 螺旋矩阵
  2. 对角线遍历
  3. 蛇形填数

8.2 实际应用场景

  1. 数据加密中的排列变换
  2. 图像处理中的像素重排
  3. 数据传输中的交错编码

8.3 算法思维训练

这道题很好地训练了:

  1. 观察规律的能力
  2. 空间与时间的权衡
  3. 边界条件的处理

9. 个人实现心得

在实际编码过程中,有几个关键点需要注意:

  1. 方向切换的逻辑要清晰,特别是在第一行和最后一行
  2. C语言中字符串操作要特别注意内存管理和终止符
  3. 测试时要覆盖各种边界情况,特别是numRows为1和大于字符串长度的情况

直接定位法虽然效率高,但实现起来容易出错。建议先实现模拟法,确保理解正确后再尝试优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询