具体题目就不详细说了,大致意思如下:
为了说明这个题目我们用STR表示这个给定的字符串,用NUM表示这个传入的数字,同时就用原题的测试例子来表示
STR = “PAYPALISHIRING”
NUM = 3
首先这个题目并不是很难,一般而言我们可以通过模拟这个例子中的过程,来实现我们的算法,大致就两步
1. 按照顺序画出这个Z字形
2. 按照从左到右的顺序从Z字形中读出我们想要的字符串
那么怎么画出这个Z字形呢?一种比较简单的做法是初始化一个二维数组,然后用某个字符来填充这个二维数组,表示空位,接下来只要按照某个规律把STR填进去就好了,这时候不难发现这个填充方式是由周期性的,先是向下移动,再是向右上方向移动,而且这个移动的距离(字符个数)也是固定的,这样一来我们就可以很轻松的完成这个二维数组了。
下面详细说说这个过程
首先我们先确定一下这个周期cycle = 2 * NUM - 2,这个应该很容易发现
接下来对于STR中的每个字符假设处在第len个长度,我们可以计算这个字符在周期中所处的位置:
pos = len % cycle
我们假设上一个字符存放的位置为(row, col),row表示行,col表示列
那么可以知道当前的字符应该存放在哪个位置
当pos < row && pos != 0, 则这个字符应该存放在(row + 1, col)的位置
当pos >=row && pos == 0,则这个字符应该存放在(row - 1, col)的位置
如此一来就能画出这个Z字形了。
下面是主要的代码:
char **
mat_init(int row, int col) {
char **mat = (char **)malloc(row * sizeof(char *));
int i, j;
for (i = 0; i < row; ++i) {
*(mat + i) = (char *)malloc(col * sizeof(char));
for (j = 0; j < col; ++j) {
*(*(mat + i) + j) = '*';
}
}
return mat;
}
void
mat_free(char **mat, int row) {
int i;
for (i = 0; i < row; ++i) {
free(*(mat + i));
}
free(mat);
}
void
show_mat(char **mat, int row, int col) {
int i, j;
for (i = 0; i < row; ++i) {
for (j = 0; j < col; ++j) {
printf("%c ", mat[i][j]);
}
printf("\n");
}
}
char*
convert_way1(char* s, int numRows) {
if (NULL == s || numRows < 1) {
return "";
}
if (1 == numRows) {
return s;
}
int s_len = strlen(s);
char *result = (char *)malloc((s_len + 1) * sizeof(char));
int cycle = 2 * numRows - 2;
int num_col = ((s_len / cycle ) + 1) * (numRows - 1);
//初始化二维数组,'*'表示空白位置
char **mat = mat_init(numRows, num_col);
show_mat(mat, numRows, num_col);
int i, pos, j;
//初始化标记的起点
int cur_row = 1;
int cur_col = -1;
for (i = 0; i < s_len; ++i) {
pos = i % cycle;
if (pos != 0 && pos < numRows) {
cur_row++;
} else {
cur_col++;
cur_row--;
}
mat[cur_row][cur_col] = s[i];
}
show_mat(mat, numRows, num_col);
pos = 0;
//根据已经画好的Z字形,读出我们需要的字符串
for (i = 0; i < numRows; ++i) {
for (j = 0; j < num_col; ++j) {
if (mat[i][j] == '*') {
continue;
}
*(result + pos) = mat[i][j];
pos++;
if (pos == s_len) {
break;
}
}
}
result[s_len] = '\0';
mat_free(mat, numRows);
return result;
}
简单的结果展示:
这个算法其实并不复杂,相反从实现逻辑上来看,它很简单,但问题是不管是内存还是执行时间 都让人不是很满意
那么有没有更快速的算法呢?实际也是有的,我们真的需要老老实实的通过画出的Z字形才能确定字符串的顺序吗?答案是否定的,实际上当我们确定了NUM,那么STR中的第x个字符所在的位置y(新字符串的第y个位置)也是确定的,所以我们仅仅是需要需找一个映射,把映射到y,那么就可以完成这道题目了,那么如何确定这个映射关系呢?当然了我们要寻找的映射实际是函数
y = f(x)的反函数,我们希望能通过y来确定x的位置。
还记得我们上面提到的这个STR其实满足一定的周期性,那么我先一个满周期的STR为例子经行说明
这个例子中的STR刚刚好为两个周期
cycle = 2 * NUM - 2 = 2 * 5 - 2 = 8
我们先来看看第一行,第一行有两个字符在STR中的位置分别为0 以及 (0 + 1 * cycle)
再来看看最后一行,同样是两个字符,分别位于4 以及 (4 + 1 * cycle)
那么中间3行呢?他们有4个字符,怎么处理呢?这里要注意的是,虽然有4个字符,但是它们也只是2个周期。比如这本例中,我们先看看第三行,Y与R分别位于第2以及(2 + 1 * cycle),I的位置可以通过Y来确定,我们假设他们之间的间隔为dis,实际上A与R的间隔同样也是dis,同理其他行数也可以这么处理
这里总结一下,对首行和尾行的,我们只需确定一个基础值base,然后往上加n个周期就可以,这里只需要确定周期数即可
而对于中间的几行而言,仅仅是一个基础值和周期还是不够,我们需要确定2n以及2n + 1的字符之间的间隔space,
现在我们假设cur_line表示我们要计算的行数,它以0开始(要注意起始值)
那么我们可以得到(可以拿真实数据自己算算哦)
base = cur_line
space = cycle - 2 * cur_line
到了这一步,我们实际上已经基本掌握了这个所谓的映射,下一步我们要确定周期,每一行的周期
之前我们假设了STR是完整的周期长度,如果是这样显然不论是那一行周期数=cycle就可以了,但是真实情况是STR不一定是完整的周期,甚至对于中间的那几行甚至可能存在半周期。所以我们需要用一个标记位来表示,中间的 那几行是否是半周期,如果是的话,我们还需要单独的打出某个字符(正常周期的,需要打印两个字符)。
考虑这种情况
实际上除第一行外,第二,三行都是3个周期,但是第二行却输出了7个字符,这和我们前面讨论的有些不同?这里需要用到我们的标记位了,当标记位记为1的时候,我们就要在多打一个字符
需要考虑几种情况:
最后一个字符所在的位置pos>=NUM 以及pos<NUM的情况,通过考虑这两种情况我们生成两个数组times_array(记录每一行的周期数)以及flag_array(记录每一行的标记位)
这样子这道题基本的解题思路已经给出,下面贴一下代码
void set_array(int *array, int start, int end, int value) {
int i;
for (i = start; i < end; ++i) {
array[i] = value;
}
}
//初始化每一行对应的周期以及标记数
void get_array_message(int *flag_array, int *times_array, int str_len, int pos, int cycle, int row) {
set_array(flag_array, 0, row, 0);
set_array(times_array, 0, row, cycle);
printf("row %d pos %d", row, pos);
if (pos != 0) {
set_array(times_array, 0, 1, cycle + 1);
if (pos >= row - 1) {
set_array(times_array, 2 * (row - 1) - (pos - 1), row, cycle + 1);
set_array(flag_array, 0, 2 * (row - 1) - (pos - 1), 1);
} else {
set_array(times_array, 1, row, cycle);
set_array(flag_array, 1, pos, 1);
}
}
}
//首行以及尾行的调用方法
void single_set(char *result, char *s, int *pos, int base, int cycle, int cycle_num) {
int i;
for (i = 0; i < cycle_num; ++i) {
result[*pos] = s[base + cycle * i];
(*pos) ++;
}
}
//中间行的调用方法
void double_set(char *result, char *s, int *pos, int base, int cycle, int space, int flag, int cycle_num) {
int i;
for (i = 0; i < cycle_num; ++i) {
result[*pos] = s[base + cycle * i];
(*pos) ++;
result[*pos] = s[base + cycle * i + space];
(*pos) ++;
}
if (flag == 1) {
result[*pos] = s[base + cycle * i];
(*pos) ++;
}
}
char*
convert_way2(char* s, int numRows) {
int s_len = strlen(s);
int cycle = 2 * numRows - 2;
char *result = (char *)malloc((s_len + 1) * sizeof(char));
if (NULL == result) {
printf("malloc fail\n");
return NULL;
}
int pos, cur_line, i, *times_array, *flag_array;
int cycle_num = s_len / cycle;
pos = cur_line = 0;
times_array = (int *)malloc(numRows * sizeof(int));
flag_array = (int *)malloc(numRows * sizeof(int));
get_array_message(flag_array, times_array, s_len, s_len % cycle, cycle_num, numRows);
while (1) {
if (cur_line == 0 || cur_line == numRows - 1) {
single_set(result, s, &pos, cur_line, cycle, times_array[cur_line]);
} else {
int space = cycle - cur_line * 2 ;
double_set(result, s, &pos, cur_line, cycle, space, flag_array[cur_line], times_array[cur_line]);
}
cur_line ++;
if (cur_line >= numRows) {
break;
}
}
result[s_len] = '\0';
return result;
}
使用了这个算法,再来看看时间以及内存的消耗
对比之前的算法有了极大的提升,当然了,这道题目可能还有很多更优的解法,这等我后续有时间在考虑考虑,最近会在leet_code上刷刷题,会把一些题目记录下来,也欢迎大神们交流交流想法。
ps: 之前在提交这部分代码上去的时候,报了内存检查的错误,实际上是因为没有加result[s_len] = ''\0"这一行,虽然在自己的编译环境下测试通过,但是在线上提交的时候,还是要注意这种细节!!!