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

本篇目录

前言

题目

解法一

1.分析

2.代码

解法二

1.分析

2.代码

3.后记

解法三

1.分析

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

3.后记

这段代码在我电脑上的VSCode中测试成功,但是后来经过网友提醒关于申请空间的问题(十分感谢),我去请教了牛人,他对这段代码给出了如下建议。

这段代码看起来没有明显的语法错误,但是需要注意以下几点:

  1. 在函数的参数中,数组应该声明为指针类型,即int *R。虽然在C语言中可以使用数组作为函数参数,在函数中可以直接修改数组的内容,但是这种写法会影响代码的可读性和可维护性。

  2. 在定义辅助数组S时,数组大小应该使用常量表达式或者动态分配内存。这里使用了变量p作为数组大小,虽然在一些编译器中可以编译通过,但是在其他编译器中可能会报错。

  3. 函数中没有对参数进行有效性检查,如果传入的参数不合法,例如p大于n,将会导致数组越界错误。

  4. 函数中没有返回值,如果函数运行出现错误,调用者将无法得知错误原因。

于是我针对这四点进行了优化。关于第二点,其实我原本写的是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;   //将新数组的指针返回
}

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
西南交通大学的考研数据结构C语言真题主要涵盖了数据结构C语言的基本概念、常见算法数据结构的应用,是考研复习中的重点和难点。 数据结构部分的真题主要涉及线性表、栈和队列、链表和树、图和排序等知识点。例如,可能会出现关于数组的插入、删除和查找操作以及对其时间复杂度的分析目,还可能会要求设计和实现单链表、二叉树或图等数据结构,并进行相应的操作和应用。对于这些目,考生需要熟悉各种数据结构的特点、使用方法和算法,能够分析算法的时间复杂度和空间复杂度,并灵活应用到实际问中。 C语言部分的真题主要考察C语言的基本语法、指针和内存管理、函数和库等方面的知识。可能会出现关于函数的声明和定义、指针的使用、内存动态分配和释放等方面的目。考生需要对C语言的语法、特性和常用库函数有一定的掌握,能够理解和分析C语言程序的执行过程和内存管理机制。 对于准备西南交通大学考研的考生来说,要复习数据结构C语言,首先要掌握基础概念和常用算法数据结构的原理和应用。其次,要多做真题和模拟,加深对知识的理解和应用。同时,还要关注最新的考研动态和备考资料,及时调整和完善复习计划。通过系统的学习和不断的练习,相信考生一定能够顺利应对西南交通大学考研数据结构C语言的考试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值