第一周的代码刷题(PTA基础篇)

32 阅读14分钟

大致思路:

笔者刷代码主要还是为了锻炼代码能力,好应对考研复试和工作,所以会使用PTA(目标学校要求)和力扣进行刷题,主要使用C与python,以周为单位进行计划和总结,考虑到以前的知识遗忘许多,所以可能还需要穿插着进行网课学习。

时间:

诸多杂事打扰,还有因为我本身有点计划狂,开始的有点晚12/31晚上才开始刷,惭愧啊,效率较低,毕竟也不止有着一件事,所以付出时间有限,排版不太精美,读者见谅(<(_ _)>)。

PTA基础刷题:

因为好久没刷题,写代码了,就从基础开始:

image.png 果然生疏了,慢慢练吧! 6-1:简单题:简单利用for循环,和使用C语言的printf()打印就可,唯一的遗漏是居然忘了printf的换行符是“/n”,也算是收获 6-2:多项式求值f(x)=∑i=0n​(a[i]×^i)也是很容易写出来了

double f(int n,double a[],double x){
    double f=0;
    double m=1;
    for(int i=0;i<=n;i++){
        for (int j=0;j<i;j++){
            m*=x;
        }
        f+=a[i]*m;
        m=1;
    }
    return f;
}

毫无疑问--错了!习惯了408考试的暴力解了,时间复杂度太高!

image.png 所以,再仔细思考:既然是数学题,就从数学角度思考,让我想起以前在计算机组成红皮书上看到的一个解法,仔细搜了一下还是个著名算法:秦九韶算法:P(x)=(...((an​x)+an−1​)x+...)x+a0​从高次项到底次方项计算,将最高次方项系数作为初值,类似递归的想法,每次将上一次的结果乘x加上新的临近的底次方的系数,然后依次到0次方项!

double f(int n,double a[],double x){
    double result=a[n];
    for(int i=n-1;i>=0;i--){
        result=result*x+a[i];
    }
    return result;
}

image.png 简洁而高效啊!(实际上,实现过程中还是犯了许多错误:第一次把系数整倒了,把a[0]当成最高次方项处理了,第二次for循环多写了一次从n到0了,/(ㄒoㄒ)/~~ ,第三次终于对了,都快哭了┭┮﹏┭┮,不过解出来很是蛮爽的(●'◡'●)这也是为数不多的代码乐趣) 6-3:简单求和 6-4:求平均值:都很简单,使用一个for循环就能出来的事就不赘述了 6-5:求最大值:我直接一个冒泡排序,简简单单解决了,果然算法学习是程序猿必修课啊。 6-6:求链表结点的阶乘:

image.png 刚开始以为很简单,求L所指的结点的数据部分的值的阶乘呗,简单使用个for循环不就解决了

int FactorialSum( List L ){
    if(L==NULL){return -1;}
    int sum=1;
    int n=L->Data;
    if (n==0){return 1;}
    for(int i=1;i<n+1;i++){
        sum*=i;
    }
    return sum;
}

结果:

image.png 好吧,是我想简单了,再看一眼测试案例

image.png 好家伙:846=5!+3!+6!原来是求整个链表所有结点的数据的阶乘之和啊! 是我大意了,上面还提示我的程序不能通过空链表,可是我已经做L=NULL的措施了,难道,他不想让我返回-1作为这个情况的值?嗯,试一下改成0:

image.png 我擦,还真是,哈哈哈,被自己的粗心气笑了! 重新改:仔细看看错误: 1.sum初值应该为0 2.因为是多个阶乘和,所以要依次处理结点数值,需要每次重置n,并利用一个中间变量记录单个结点的阶乘,命名为jie,每次计算完后将其加在sum上:

int FactorialSum( List L ){
    if(L==NULL){return 0;}
    int sum=0;
    struct Node *p=L;
    int jie=0;
    while(p!=NULL){
        int n=p->Data;
        jie=1;
        for(int i=1;i<n+1;i++){
            jie*=i;
        }
        sum+=jie;
        p=p->Next;
    }
    return sum;
}

然后,结果就是:

image.png o( ̄▽ ̄)ブ
6-7:统计某类完全平方数:本题要求实现一个函数,判断任一给定整数N是否满足条件:它是完全平方数,又至少有两位数字相同,如144、676等。 首先,笔者怀着对不起数学老师的心情复习一下完全平方数:确定了就是可以作为一个数的平方结果的数 所以,不能为负,为负的数可以直接排除了。 然后,判断N是否为完全平方数:将N开平方,结果赋值给n,如果N不是完全平方数,开平方时结果就会有取舍,所以n的平方和就会和N不一样,以此判断N是否为完全平方数。 最后,判断第二个条件:数字N中至少有两个数相同,如果没学过算法的话,还真被你难住了:利用空间换时间的策略--定义一个长度10数组,记录各个数字(0-9)在N中的出现次数,利用除法和阶乘依次取各位的数。

int IsTheNumber ( const int N ){
    if(N<0)return 0;
    int n=(int)sqrt(N);
    if(n*n!=N)return 0;
    int N1=N;
    //使用计数法,计算0-9各个数字出现的次数,一旦出现2就跳出,以空间换时间
    int count[10]={0};
    int dig,s2=0;
    while(N1>0){
        dig=N1%10;//依次统计各个位置上的数字
        count[dig]++;
        if(count[dig]>=2){
            s2=1;
            break;//一旦出现2就跳出
        }
        N1=N1/10;//因为要依次处理,所以,已经处理的低位数字需要消掉
    }
    if(s2==1){return 1;}
    return 0;
}

再满足第二个条件的时候,我也在想有没有不那么浪费空间的方法,短时间没想到,隔网上搜搜,嘿,还真有大神有妙招:本质上计算机存储数字时是二进制的形式,如十进制的8就表示为二进制的100,所以一个变量,如int型变量,本身就可以当作一个数组,这个时候将该数的二进制形式的底10为当作0-9的出现与否,当0-9出现时,就令对应位置为1,然后提前判断:给对应位置是否已经被置为1,如果是,那么说明这个数已经是第二次出现,那么就可以判断第二个条件满足,若不是,则继续循环处理高位数字!

int IsTheNumber ( const int N ){
    if(N<0)return 0;
    int n=(int)sqrt(N);
    if(n*n!=N)return 0;
    int N1=N;
    //使用计数法,计算0-9各个数字出现的次数,一旦出现2就跳出,以空间换时间
    int count=0;
    int dig,s2=0;
    while(N1>0){
        dig=N1%10;//依次统计各个位置上的数字
         if (count & (1 << dig)) {
            return 1;
        }
        // 置第digit位为1
        count |= (1 << dig);
        N1=N1/10;//因为要依次处理,所以,已经处理的低位数字需要消掉
    }
    return 0;
}

but,这个方法只可以处理2次及以上的情况,如果想处理多次,其实比较难,因为涉及到把一个数二进制表示均匀分割成许多部分,以此表示多次,但是,不得不说,太过复杂了得不偿失!还是建议采用第一种数组的方法,有时候最简单的方法就是最有效的方法1 6-8 简单阶乘计算:本题要求实现一个计算非负整数阶乘的简单函数---简简单单一个for循环解决了 6-9 统计个位数字:本题要求实现一个函数,可统计任一整数中某个位数出现的次数。例如-21252中,2出现了3次,则该函数应该返回3。 (不对劲啊,哈哈哈,6-8和6-9都是做6-6的基础,这出的题顺序反了吧,先打BOSS后刷小怪啊,😄。) 不过这个6-9也是有坑的:1.当出现负数时,进行取余数时可能得出一个负数,而我们统计的只是数字,不带符号,所以要对待统计的数取其绝对值(就是负数取反)2.统计的数为0时要特殊处理。

int Count_Digit ( const int N, const int D ){
    int count=0;
    int n=N,s;
    if(n<0)n=-n;
    if(n==0&&D==0)count=1;
    while(n!=0){
        s=n%10;
        if(s==D)count++;
        n=n/10;
    }
    return count;
}

6-10:本题要求实现一个打印非负整数阶乘的函数。 好家伙,还是在一个系列,那不简简单单,我直接给出答案:

void Print_Factorial ( const int N ){
    if(N<0||N>1000){printf("Invalid input");return ;}
    int jie=1;
    for(int i=1;i<=N;i++){
        jie*=i;
    }
    printf("%d",jie);
}

结果:错了!啊不,部分错误

image.png 果然不能大意啊,要认真对待每一题啊。于是我左思右想,我的逻辑哪里错了,想不通啊(挠头--条条青丝掉落)上个厕所,我忽然灵感爆发:好像输入大于12时就不行,等一下是不是我的载体--int不行?当输入变大时,int表示的范围不够了,实践一下,把int换为double 结果:

image.png 对啊,double无法保证精度,还不如原来的int,那怎么办? 有问题,自己解决不了,绝不死磕,咱就别人那里取取经呗,不丢人(😏) 于是,咱就去豆包老师那里学习了,哎嘿,获得一个好思路:题目本身只让我们输出所给数字的阶乘,但是人家也没要求必须是个数字啊--而且,目前也没有啥类型的变量能够表示这样的结果,那咱用传统的思路,也就是咱现实中算乘法的思路:
把要求的输出看作是一个数被1到N(输入)乘了个遍所得到的数字,在现实中我们计算乘法时也是一位位的算,那咱就把要求的输出分解为一个个单独的数字(0-9)然后化为一个很大的数组存储(一个数组元素储存一个数位上的数字),然后一位位的计算阶乘,对于每个位上的结果--保留结果的个位作为这个位置上的数,把十位及以上的数作为进位,传递给上一层,如果进位的数很大,也没事,以此取模,再把进位的数剩余的高位接着向上传递呗!就跟套娃似的。
(这个进位数字的处理看似复杂,其实很简单:具体原理就像一个很长的队列,有人从队尾给出一个装满饼干的袋子,接过袋子的人拿出饼干直到自己吃饱,如果饼干还有剩余就往前传)
(整个过程的处理,就像计算机组成里介绍的加法器那样运行,毕竟,它的本质也是模仿人类的运算逻辑。)

正式开始写: 整个逻辑就是:将最终结果和整个计算过程放在一个数组里,这里就设为A[3000]--肯定够了,然后从A[0]开始利用循环依次阶乘,利用取余得到每次乘法的保留位,利用除法除以10得到单个位置上的进位数字,然后在下次计算隔壁的高位数的阶乘时,把上次得到的进位加上,然后再取余,再除以10得到新的进位数字! 然而,要考虑的是,计算过程中,我们是A[0]依次向高位计算的,一开始数组全是0,A[0]=1,但是计算过程中通过进位,A[]中的高位1、2、3...N都会变为非0,我们没必要每次都计算数组中所有位置的乘积,因为0乘以任何数都是0嘛!所以我们需要通过一个数位指示变量(len)指引我们,告诉我们目前得到的阶乘结果是几位数!
那么一开始len肯定是1,毕竟是从个位数计算(常识嘛),然后什么时候增加呢?那就是在上面计算过程中的阶乘的那个循环结束时,如果进位还大于0,也就是说进位还有富裕,那么就可以利用这个富裕的进位数字向高位那些0去开拓了!
最终,经过整理,我写出来是这样滴:

void Print_Factorial ( const int N ){
    if(N<0||N>1000){printf("Invalid input\n");return ;}
    int A[3000]={0};
    A[0]=1;//做阶乘的准备
    int len=1;//用作记录计算的位数
    int jin=0;//进位数字
    for(int i=2;i<=N;i++){//这个循环就是为了计算当前所有的非零数的阶乘中的一环,比如都乘以3
        jin=0;//每次循环时将进位置为0,避免误差
        for(int j=0;j<len;j++){
            int chengji=A[j]*i+jin;//计算单个位置上数字的乘积
//由小学知识:单个位置上乘积为两个乘数相乘加上地位进位
            A[j]=chengji%10;//保留个位数字作为该位置的数
            jin=chengji/10;//更新进位数字
        }
        while(jin>0){//这个循环就是:当进位还大于0==>也就是说出现新的高位非零数字
            A[len]=jin%10;
            jin=jin/10;
            len++;
        }
    }
    for(int z=len-1;z>=0;z--){
        printf("%d",A[z]);//为了符合人类习惯,还是要从高向底输出的
    }
    printf("\n");//这行纯属强迫症
}

不过!这居然还只是简单题!OMG!看来是我太弱了呀,还得练!不过,今天也就这样了,一天就磨出来这一题┭┮﹏┭┮相信各位读者肯定比我更牛,能秒了这题✪ ω ✪
6-11:

image.png 好家伙,这不撞我枪口上了吗,熟读考研408数据机构排序算法的我,直接就是一个快速排序,给A[]排个序,然后返回中间数,不就是中位数吗,(●'◡'●) But,我又高兴早了:程序运行超时了!,看来是我被刷题的模式害了,太过轻敌了! 那怎么办呢?我想来想去,还是感觉快速排序应该可以,我们已知:快速排序每次排序的过程中都至少能确认一个元素的最终位置,那么如果我在确认了这个最终位置后,加一个判断逻辑,判断这个最终位置是否刚好是中间位置,不就可以找到中位数了吗!说干就干:

int Partition(ElementType A[],int left,int right){//划分,或者说“分治”
    ElementType pivot=A[left];
    while(left<right){
        while(left<right&&A[right]>pivot)right--;
        A[left]=A[right];
        while(left<right&&A[left]<pivot)left++;
        A[right]=A[left];
    }
    A[left]=pivot;
    return left;
}
ElementType Quick(ElementType A[],int l,int r,int k){//快速排序
    if(l==r)return A[l];
    int mid=Partition(A,l,r);
    if(mid==k)return A[mid];
    else if(mid<k){
        return Quick(A,mid+1,r,k);
    }
    else 
        return Quick(A,l,mid-1,k);
}
ElementType Median( ElementType A[], int N ){
    int k=(N+1)/2-1;
    return Quick(A,0,N-1,k);
}

然而,还是没有全对

image.png 那么,问题出在哪里?可以看到还是有部分是对的,那是不是我的中位数指针K定的有些毛病? 找中位数,已知N个元素,理所当然的,中间位置就是(N+1)/2,但是这是数组啊!从0开始的数组啊!所以,我取得是(N+1)/2,这个逻辑应该是对的啊,算了,改改再说:设为N/2吧

int Partition(ElementType A[],int left,int right){
    ElementType pivot = A[left];
    while(left<right){
        while(left<right&&A[right]>pivot)right--;
        A[left]=A[right];
        while(left<right&&A[left]<pivot)left++;
        A[right]=A[left];
    }
    A[left]=pivot;
    return left;
}
ElementType Quick(ElementType A[],int l,int r,int k){
    if(l==r)return A[l];
    int mid=Partition(A,l,r);
    if(mid==k)return A[mid];
    else if(mid<k){
        return Quick(A,mid+1,r,k);
    }
    else 
        return Quick(A,l,mid-1,k);
}
ElementType Median( ElementType A[], int N ){
    int k=N/2;
    return Quick(A,0,N-1,k);
}

image.png 什么!!!居然对了!哈哈哈,看来真是因为中位数位置没有定对 知道答案后,再看看,感觉都清晰了一点(有点拿着答案讲题的高中老师的感觉,哈哈哈),我反思原来的错是因为:我原来取得中位数位置是(N+1)/2-1,当N=5时,就是取数组A[2],没啥问题,所以原来的奇数测试用例对了,当N=6时,由于C语言向下取整,取A[2],还是数组中第三个元素,这个地方就比较暧昧了——当数组有偶数个元素时,中间位置有两个值,我这种取法,默认取左边的那个,但是这个题目看来是默认取右边的那个,所以,当测试用例为偶数时,我错了。这次,我选择甩锅给出题方,(●ˇ∀ˇ●)。