【考研·数据结构】408真题 (2010年42题) 的三种解法

本篇目录

前言

题目

解法一

1.分析

2.代码

解法二

1.分析

2.代码

解法三

1.分析

扫描二维码关注公众号,回复: 14727584 查看本文章

2.代码

总结


前言

考研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];
}

解法三

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号位处。第二轮,就要从1号位开始了(因为0号位已经是正确的元素),这一轮要移动的元素是 1 4 7 10号位的。完成之后,空位又到了0号位处。第三轮,毫无疑问要从2号位开始。以此类推,可知需要设置一个计数器,来记住当前是第几轮,由此计算出当前轮的首元素是几号位的。

时间复杂度:

无论移动几轮,每个元素都只经过一次移动到达正确位置(首元素除外,移动2次),因而时间复杂度为O(n) 。

空间复杂度:

额外定义的变量数量为常数,因此空间复杂度为O(1)。

2.代码

void myMove(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>
void Converse(int R[],int n,int p);
void Reverse(int R[],int from,int to);
void myprint001(int R[],int n);
void myMove(int R[],int n,int p);
void Converse2(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;
    myprint001(R,n);    //打印初始数组
    int p = 7;
    //解法1:原地逆置法
    //Converse(R,n,p);
    //解法2:辅助数组法
    //Converse2(R,n,p);
    //解法3:逐个填空法
    //myMove(R,n,p);
    myprint001(R,n);    //打印移位后的数组

	return 0;
}
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];
}
void myMove(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");
}
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);
}

猜你喜欢

转载自blog.csdn.net/Dr_Cheeze/article/details/127983021
今日推荐