本篇目录
前言
考研408真题(2010年42题,数据结构),参考答案上给了两种方法,我自己另外想到了一种方法,每一种方法都进行了思路分析与代码测试,在此做个记录。
题目
设将n(n>1)个整数存放到一维数组R中。试设计一个在时间和空间两方面都尽可能高效的算法。将R中保存的序列循环左移p(0<p<n)个位置,即将R中的数组由(X0,X1, … Xn-1)变换为(Xp,Xp-1, … Xn-1,X0,X1, … Xp-1)。要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C、C++或JAVA语言描述算法,关键之处给出注释。
(3)说明你所涉及算法的时间复杂度和空间复杂度。
解法一
1.分析
这个方法是参考答案中给出的(包括具体代码),主要原理是调用三次原地逆置函数。
根据p的位置将数组看做ab两部分,
第一次逆置a,数组变为(-a)b,
第二次逆置b,数组变为(-a)(-b),
第三次逆置整个数组,数组变为ba。
由此思路可见,要求考生对原地逆置函数有深刻的理解应用能力,我自认为目前水平不够,实在无法在考场上想到通过数组逆置来实现数组元素循环移动。
时间复杂度:
原地逆置函数的时间复杂度与需要逆置的元素多少成线性关系,总共逆置了三次,第一次的时间复杂度为O(p/2),第二次的时间复杂度为O((n-p)/2),第三次的时间复杂度为O(n/2)。因而总的时间复杂度是 O(n) 。
空间复杂度:
而原地逆置函数既然都已经叫“原地”函数了,空间复杂度自然是常数,因而总的空间复杂度为O(1)。
下面是具体的代码。
2.代码
void Reverse(int R[],int from,int to){
/*功能:将数组R中指定角标范围内的部分原地逆置*/
int i,temp; //i是计数器,temp用于暂存元素以便于逆置交换
for(i=0;i<(to-from+1)/2;i++){
temp = R[from+i];
R[from+i] = R[to-i];
R[to-i] = temp;
}
}
void Converse(int R[],int n,int p){
/*功能:将数组R的元素循环左移p个位次*/
/*实现思路:
利用三次原地逆置实现。
根据p的位置将数组看做ab两部分,
第一次逆置a,数组变为(-a)b,
第二次逆置b,数组变为(-a)(-b)
第三次逆置整个数组,数组变为ba。*/
Reverse(R,0,p-1);
Reverse(R,p,n-1);
Reverse(R,0,n-1);
}
解法二
1.分析
这个方法也是参考答案中给出的(参考答案中只给出了思路,没有给出具体代码),主要原理是利用一个辅助函数S暂存R中的前p个元素,之后将R中后面的元素移位到正确位置,再将S中暂存的R中前p个元素移位到正确位置。
时间复杂度:
角标0至(p-1)的元素一共移动了两次,剩下的角标p至(n-1)的元素移动了一次,因此我认为时间复杂度应当是O(n+p),不过参考答案给的是O(n)。
空间复杂度:
空间方面,因为使用了一个辅助数组,所以空间复杂度自然就是O(p) 。
这个解法虽然在空间复杂度方面不如第一种解法更优,但是这个方法比较容易想到,实现起来也比较容易。我第一次遇到这道题时就用了这种解法。下面是具体代码。
2.代码
请注意,这个代码需要优化,请见下一条标题“后记”。
void Converse2(int R[],int n,int p){
/*实现思路:
利用一个辅助数组S,暂存前p个元素,之后将后面的元素移位,
再将前p个元素移位到正确位置。
*/
/*时间复杂度:O(n) ;空间复杂度: O(p) */
int S[p]; //创建一个辅助数组
for(int i=0;i<=p-1;i++) //将R中前p个元素移到S中
S[i] = R[i];
for(int i=p;i<n;i++) //将R中p及后面的元素移到正确位置
R[i-p] = R[i];
for(int i=0;i<=p-1;i++) //将S中的元素移到R中正确位置
R[n-p+i] = S[i];
}
3.后记
这段代码在我电脑上的VSCode中测试成功,但是后来经过网友提醒关于申请空间的问题(十分感谢),我去请教了牛人,他对这段代码给出了如下建议。
这段代码看起来没有明显的语法错误,但是需要注意以下几点:
在函数的参数中,数组应该声明为指针类型,即int *R。虽然在C语言中可以使用数组作为函数参数,在函数中可以直接修改数组的内容,但是这种写法会影响代码的可读性和可维护性。
在定义辅助数组S时,数组大小应该使用常量表达式或者动态分配内存。这里使用了变量p作为数组大小,虽然在一些编译器中可以编译通过,但是在其他编译器中可能会报错。
函数中没有对参数进行有效性检查,如果传入的参数不合法,例如p大于n,将会导致数组越界错误。
函数中没有返回值,如果函数运行出现错误,调用者将无法得知错误原因。
于是我针对这四点进行了优化。关于第二点,其实我原本写的是Java代码(因为我对Java更熟悉),然后发现改为C程序恰好也能测试通过,就直接用了。这里的确应该考虑到C语言的特性,毕竟近些年的考题已经不能再用Java,只能用C语言来答题了。其它几点我觉得其实问题不大,因为参考答案中给出的解法一,参数类型也是用的int[] 而不是int *R ,函数返回值用的也是void而不是int,以及题目中已经限定了“0<p<n”这个条件。不过为了更加严谨,我还是都进行了优化,优化后的代码如下。
//解法二
int Converse2(int *R,int n,int p){
/*实现思路:
利用一个辅助数组S,暂存前p个元素,之后将后面的元素移位,
再将前p个元素移位到正确位置。
*/
/*时间复杂度:O(n) ;空间复杂度: O(p) */
/*
int S[p]; //创建一个辅助数组
这是Java的写法,在C语言中,
在定义辅助数组S时,数组大小应该使用常量表达式或者动态分配内存。
如果像这样,使用变量p作为数组大小,虽然在一些编译器中可以编译通过,
但是在其他编译器中可能会报错。
*/
//参数合法性检查
if(p>=n) //若参数不合法
return -2; //则返回错误代码
//申请空间
int *S = (int *)malloc(sizeof(int) * p); // 动态分配内存并强制类型转换
if (S == NULL) { // 如果内存分配失败
return -1; // 则返回错误代码
}
//移位
for(int i=0;i<=p-1;i++) //将R中前p个元素移到S中
S[i] = R[i];
for(int i=p;i<n;i++) //将R中p及后面的元素移到正确位置
R[i-p] = R[i];
for(int i=0;i<=p-1;i++) //将S中的元素移到R中正确位置
R[n-p+i] = S[i];
//返回
return 0; // 执行成功,返回0表示执行成功
}
解法三
1.分析
这个解法的出发点是希望空间复杂度为常数,因为我认为既然是每个元素都要逐个进行移位,那么只需要一个额外的变量就够了。这种解法是我在第二次做这道题时想到的,个人认为比解法一容易想到。代码长度与上边两种解法差不多,时空复杂度与解法一相同。
假如p=1,那么移位方法很简单,先用一个额外的变量存储首个元素的值,这时R[0]就相当于数组中的一个空位(因为R[0]的值已经被保存了,此时R[0]就可以被覆盖了),之后将R[1]中的元素复制到R[0]中,这相当于将目标元素移动到空位中。此时R[1]的值也已经被保存,因此R[1]又成为新的空位,而目标元素就是R[2]。以此类推,不断将目标元素移动到空位,即将剩下的元素向前移动一个。最后一步是将之前暂存的R[0]元素复制到数组末尾(即此刻数组的空位处)。
这就像一种滑块拼图文具,只要面板上有一块空位,就可以通过上下左右移动周围的拼图块,最终将所有的拼图块都可以移到正确的位置。
假如p>1,那么道理是一样的。上例中每个元素移动的距离是1,那是因为p=1,此例中,只要让每个元素向前移动的距离是p就可以了。最终当目标元素又变成R[0]时,就意味着只差最后一步移动了:将之前暂存的R[0]复制到此刻的空位,搞定。
不过有一个要考虑到的问题:n有可能能被p整除(如果n是质数那么就没有这种烦恼了)。这会导致,数组中只有一部分元素被移动了,然后R[0]就成为空位了。如果以R[0]是否为空位来作为是否移动完毕的判断依据,就会出错。解决方法就是,设定一个计数器,负责计量一共移动了几个元素。只有移动次数达到n时,这才是真的将所有元素都移位完毕。否则,就再来一轮。
举个例子。想像一个时钟就是一个数组R,R[0]=12,i=1~11时R[i]=i。很显然数组个数n=12。现在假设p=3,那么按照上述思想,第一轮,0 3 6 9 号位的元素都会被移位到正确位置,此时空位在0号位处,然后执行t++将下一轮的首位置设置为1号位。第二轮,就要从1号位开始了(因为0号位已经是正确的元素),这一轮要移动的元素是 1 4 7 10号位的。完成之后,空位就到了1号位处,再执行t++将下一轮的首位置设置为2号位。第三轮,毫无疑问要从2号位开始。以此类推,可知需要设置一个计数器,来记住当前是第几轮,由此计算出当前轮的首元素是几号位的。
时间复杂度:
无论移动几轮,每个元素都只经过一次移动到达正确位置(首元素除外,移动2次),因而时间复杂度为O(n) 。
空间复杂度:
额外定义的变量数量为常数,因此空间复杂度为O(1)。
2.代码
void Converse3(int R[],int n,int p){
/* 接收的参数:
R[] 要重新排序的数组;n 数组R中元素的个数 ;p 要向左移动几位 */
/*功能:
将含有n个元素的int数组R中的元素循环左移p位 */
/*实现思路:
1.将首元素暂存于x,则首元素的位置视为空,接下来逐个移位填空。
移位时,R[i]为目前的空位元素,R[m]为待移动到空位的元素。
当目标元素地址=首地址时,则移位完成。
2.考虑到p可能是n的因数,因此要通过移动次数j判断是否已移动n个元素;
若要移动多轮,则每一轮的移动首元素应+1,
因此,t表示共移动了几轮,R[t]表示本轮移动的首元素
*/
/*时间复杂度:O(n);空间复杂度:O(1) */
/*for循环的参数:
int j=0; //计数 共移动了几个元素
int t=0; //计数 共移动了几轮,R[t]表示本轮移动的首元素
j++; //每移动一次,则j+1
t++; //每移动完1轮,则t+1,t用于计算下一轮的首元素角标
*/
int i,m,x;
for(int j=0,t=0;j<n;j++,t++){ //外层while()执行的次数,即共移动了几轮
i = t; //设置新一轮的首元素角标
m = i + p; //设置新一轮的目标元素角标
x = R[i]; //将本轮首元素暂存于x
for(;m!=t;j++){ //逐个将目标元素移位到空位处,当m=t,则说明本轮移动完成。
R[i] = R[m]; //将目标元素移到空位元素处
i=m; //计算新的空位元素角标
(i+p<=n-1)?(m=i+p):(m=i+p-n); //计算新的目标元素角标
} //移动完一轮
R[i] = x; //本轮收尾:将暂存在x的首元素移到空位元素处
}
}
总结
通过本题,复习了有关数组的知识,也熟悉了C语言的写法。解法一我实在想不到,解法二容易想到但空间复杂度不如另两个方法,解法三的思路比解法二复杂,但可以实现O(1)的空间复杂度。
最后,我把所有的代码都放在这里,三种解法在本地都已测试通过。有需要的可以拿去做个测试。
#include <stdio.h>
#include <malloc.h>
void Converse(int R[],int n,int p); //解法一
void Reverse(int R[],int from,int to);
int Converse2(int *R,int n,int p); //解法二
void Converse3(int R[],int n,int p); //解法三
void myprint001(int R[],int n); //打印数组
int main() {
//给定条件
int R[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21};
int n = 21;
int p = 7;
myprint001(R,n); //打印初始数组
//解法1:原地逆置法
//Converse(R,n,p);
//解法2:辅助数组法
/*
int ret = Converse2(R,n,p);
if(ret!=0)
printf("数组逆置出现错误!\n");
*/
//解法3:逐个填空法
Converse3(R,n,p);
myprint001(R,n); //打印移位后的数组
return 0;
}
//解法一
void Converse(int R[],int n,int p){
/*功能:将数组R的元素循环左移p个位次*/
/*实现思路:
利用三次原地逆置实现。
根据p的位置将数组看做ab两部分,
第一次逆置a,数组变为(-a)b,
第二次逆置b,数组变为(-a)(-b)
第三次逆置整个数组,数组变为ba。*/
Reverse(R,0,p-1);
Reverse(R,p,n-1);
Reverse(R,0,n-1);
}
void Reverse(int R[],int from,int to){
/*功能:将数组R中指定角标范围内的部分原地逆置*/
int i,temp; //i是计数器,temp用于暂存元素以便于逆置交换
for(i=0;i<(to-from+1)/2;i++){
temp = R[from+i];
R[from+i] = R[to-i];
R[to-i] = temp;
}
}
//解法二
int Converse2(int *R,int n,int p){
/*实现思路:
利用一个辅助数组S,暂存前p个元素,之后将后面的元素移位,
再将前p个元素移位到正确位置。
*/
/*时间复杂度:O(n) ;空间复杂度: O(p) */
/*
int S[p]; //创建一个辅助数组
这是Java的写法,在C语言中,
在定义辅助数组S时,数组大小应该使用常量表达式或者动态分配内存。
如果像这样,使用变量p作为数组大小,虽然在一些编译器中可以编译通过,
但是在其他编译器中可能会报错。
*/
//参数合法性检查
if(p>=n || p<0) //若参数不合法
return -2; //则返回错误代码
//申请空间
int *S = (int *)malloc(sizeof(int) * p); // 动态分配内存并强制类型转换
if (S == NULL) { // 如果内存分配失败
return -1; // 则返回错误代码
}
//移位
for(int i=0;i<=p-1;i++) //将R中前p个元素移到S中
S[i] = R[i];
for(int i=p;i<n;i++) //将R中p及后面的元素移到正确位置
R[i-p] = R[i];
for(int i=0;i<=p-1;i++) //将S中的元素移到R中正确位置
R[n-p+i] = S[i];
//返回
return 0; // 执行成功,返回0表示执行成功
}
//解法三
void Converse3(int R[],int n,int p){
/* 接收的参数:
R[] 要重新排序的数组;n 数组R中元素的个数 ;p 要向左移动几位 */
/*功能:
将含有n个元素的int数组R中的元素循环左移p位 */
/*实现思路:
1.将首元素暂存于x,则首元素的位置视为空,接下来逐个移位填空。
移位时,R[i]为目前的空位元素,R[m]为待移动到空位的元素。
当目标元素地址=首地址时,则移位完成。
2.考虑到p可能是n的因数,因此要通过移动次数j判断是否已移动n个元素;
若要移动多轮,则每一轮的移动首元素应+1,
因此,t表示共移动了几轮,R[t]表示本轮移动的首元素
*/
/*时间复杂度:O(n);空间复杂度:O(1) */
/*for循环的参数:
int j=0; //计数 共移动了几个元素
int t=0; //计数 共移动了几轮,R[t]表示本轮移动的首元素
j++; //每移动一次,则j+1
t++; //每移动完1轮,则t+1,t用于计算下一轮的首元素角标
*/
int i,m,x;
for(int j=0,t=0;j<n;j++,t++){ //外层while()执行的次数,即共移动了几轮
i = t; //设置新一轮的首元素角标
m = i + p; //设置新一轮的目标元素角标
x = R[i]; //将本轮首元素暂存于x
for(;m!=t;j++){ //逐个将目标元素移位到空位处,当m=t,则说明本轮移动完成。
R[i] = R[m]; //将目标元素移到空位元素处
i=m; //计算新的空位元素角标
(i+p<=n-1)?(m=i+p):(m=i+p-n); //计算新的目标元素角标
} //移动完一轮
R[i] = x; //本轮收尾:将暂存在x的首元素移到空位元素处
}
}
//打印数组
void myprint001(int R[],int n){
for (int i=0;i<=n-1;i++)
printf("%d ",R[i]);
printf("\n");
}
后记
评论中有网友提出了另一种解法,我用代码实现了,我把思路和代码一并放在这里,做个参考。
这种解法的思路也是建立辅助数组,不过这个辅助数组的大小是n,根据对应规则将原数组中的元素一个一个地复制到辅助数组中。这样这个辅助数组就是移位后的数组,最后只要将辅助数组作为结果返回即可。要注意的一个问题就是,C语言不能返回int数组,只能返回int指针。
时间复杂度:这个解法,每个元素只移动一次,因而时间复杂度为O(n)。
空间复杂度:用到了一个长度为n的辅助数组,因而空间复杂度为O(n)。从这一点来说这种解法不如解法一和三。
具体代码如下,已在本地测试通过。
#include <stdio.h>
#include <malloc.h>
int *Converse4(int R[],int n,int p); //解法四
int main() {
//给定条件
int R[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21};
int n = 21;
int p = 7;
//解法4:辅助数组法2
int *ptr = NULL; //声明一个指针变量
ptr = Converse4(R,n,p);; // 获取移位后的数组指针
if (ptr == NULL) { // 如果指针为NULL,则说明分配失败
printf("Memory allocation failed.\n");
return 1;
}
//打印移位后的数组
for (int i = 0; i < n; i++) { // 使用指针遍历数组并打印
printf("%d ", ptr[i]);
}
free(ptr); // 释放动态分配的内存空间
return 0;
}
//解法四
int *Converse4(int R[],int n,int p){
/*C语言不能返回int数组,可以返回int类型的指针,在函数名前加一个*表示返回的是指针类型。*/
//申请空间
int *B = (int *)malloc(sizeof(int) * n); // 建立一个长度为n的辅助数组 动态分配内存并强制类型转换
if (B == NULL) // 如果内存分配失败
return NULL; // 则返回NULL
//移位
for(int i=0;i<=n-1;i++) // 将原数组的元素按照指定规则一一移到新数组中
B[(i+n-p)%n]=R[i];
//程序返回
return B; //将新数组的指针返回
}