《学姐教我写代码(二)》一行代码一个算法(上)

本文通过梦境故事的形式,讲述了作者从学姐那里学到的四个经典算法:等差数列求和、阶乘递归、部分和差分法及最大公约数递归。每个算法都以简洁的代码呈现,涉及知识点包括整数溢出、三目运算符、递推公式和整数取模等,旨在帮助初学者理解算法和C/C++语言特性。
摘要由CSDN通过智能技术生成

📢博客主页:https://blog.csdn.net/WhereIsHeroFrom
📢欢迎各位 👍点赞 ⭐收藏 📝评论,如有错误请留言指正,非常感谢!
📢本文由 英雄哪里出来 原创,转载请注明出处,首发于 🙉 CSDN 🙉
作者的专栏:
  👉C语言基础专栏《光天化日学C语言》
  👉算法进阶专栏《夜深人静写算法》
  👉C/C++大厂面试专栏《C/C++ 面试 100 例》
  👉奇奇怪怪的专栏《学姐教我写代码》

一、前言

  本文适合对算法处于朦胧期的初学者,文字浅显易懂,并且配有生动有趣的动图,也是作者呕心沥血之作,希望对刚入大学,或者职场上想要涉足算法的青年同僚有所启示。
  学习算法,任何时候都不嫌晚,大不了就是大器晚成而已,所以无论你是30岁,40岁,50岁,甚至60岁,只要下了决心,就已经成功了一半!本文的故事发生在 ❤️学姐教你 10 道题搞定 c 语言❤️ 的两年后,剧情扑朔迷离,作者至今回忆起来还历历在目。

二、朝思暮想

  • 自从上次一别,不知何时才能相见,不免有些感伤。于是,那天晚上,我,辗转反侧,彻夜难眠,寝不安席,食无甘味。
    在这里插入图片描述
    不太聪明的亚子
  • 从来没有一个女孩子可以让我如此朝思暮想,魂牵梦萦。
  • 可能是因为她还没有把她的毕生算法教会给我,怎么一声不吭就人间蒸发了呢!我不甘心!
  • 就算是天涯海角,我也要找到你!
    在这里插入图片描述
  • 终于,在一个夜黑风高的晚上,让我在睡梦中见到了她,梦里的她比现实中还要逗比。竟然给我写下了八行代码!
    在这里插入图片描述
  • 也就是因为那个晚上,成就了我后来的 ❤️《夜深人静写算法》❤️
  • 她告诉我,一行代码代表一个算法!我觉得她在侮辱我的智商!
  • 那天晚上大致是这样的 … …

三、南柯一梦


在这里插入图片描述

四、梦中的梦中

  • 哎,越想越不对劲!
  • 所以我打算继续睡,看看能不能继续梦到学姐。
  • 果然……功夫不负有心人……
  • 学姐出现了!!!

  • 学姐还是像往常一样,心思缜密,替人着想,越来越崇拜她了。

在这里插入图片描述

五、梦中人的梦中

算法一

在这里插入图片描述

【例题1】给定 n ( n ≤ 65535 ) n(n \le 65535) n(n65535),求 ∑ i = 1 n i = 1 + 2 + . . . + n \sum_{i=1}^n i = 1 + 2 + ... + n i=1ni=1+2+...+n

  • 这是一个等差数列!
  • 我直接用等差数列的求和公式就行了。
int sum(int n) {
    return n * (n + 1) / 2;
}

在这里插入图片描述

  • 然后我调试了一下,发现:
    在这里插入图片描述
  • n = 65535 n=65535 n=65535 时,输出的竟然是负数!

原因是因为 n ∗ ( n + 1 ) = 65535 ∗ 65536 = ( 2 16 − 1 ) 2 16 = 2 32 − 2 16 n * (n + 1) = 65535 * 65536 = (2^{16}-1)2^{16} = 2^{32} -2^{16} n(n+1)=6553565536=(2161)216=232216,而 i n t int int 能够表示的最大值为 2 31 − 1 2^{31}-1 2311,所以产生了溢出。就变成了负数。至于为什么溢出会变成负数,可以了解补码相关的知识:c++ 补码详解

  • 这里只需要对 n n n 进行奇偶性判定,将除法放在乘法之前,就可以防止溢出了。即:
  • s u m ( n ) = { ( n + 1 ) / 2 × n n 为 奇 数 n / 2 × ( n + 1 ) n 为 偶 数 sum(n) = \begin{cases} (n+1)/2 \times n & n 为奇数 \\ n/2 \times (n+1) & n 为偶数 \end{cases} sum(n)={(n+1)/2×nn/2×(n+1)nn
  • c++ 实现如下:
int sum(int n) {
    if(n % 2 == 1) {
        return (n + 1) / 2 * n;
    }else {
        return n / 2 * (n + 1);
    }
}
  • 然后我们再通过三目运算符写成一行代码,如下:
int sum(int n) {
    return (n%2) ? (n+1)/2*n : n/2*(n+1);
}

这里的 condition ? a : b是 c/c++ 中的三目运算符,含义是根据表达式condition的值的真或假,选择返回 a还是 b。由于对于一个数 x x x x x x 非0就是真,为0就是假,所以可以直接省略 x == 0的判断。

在这里插入图片描述

  • 通过这段代码,我了解了 32位整数的溢出、补码表示、三目运算符、表达式真值的省略写法。

算法二

在这里插入图片描述

【例题2】给定 n ( n < 16 ) n(n \lt 16) n(n<16),求 ∏ i = 1 n i = 1 × 2 × . . . × n \prod_{i=1}^n i = 1 \times 2 \times ... \times n i=1ni=1×2×...×n

  • 由于 n n n 比较小,所以我打算直接暴力枚举,大概可以写成这样:
int sum(int n) {
    int s = 1;
    for(int i = 1; i <= n; ++i) {
        s *= i;
    }
    return s;
}
  • 但是学姐说的一行代码好像比较难办到,我继续压缩,把 s这个变量放到循环体内和i一起初始化,并且把乘法和循环放到同一行,变成了下面这副样子。
int sum(int n) {
    for(int s = 1, i = 1; i <= n; ++i) s *= i;
    return s;
}
  • 这时候发现编译不过!!!
    在这里插入图片描述
  • 原因是:s的作用域在循环体内,所以无法在循环体外部进行使用,但是我们这个函数有需要有一个返回值,总不能把函数体给返回吧?这可如何是好!

在这里插入图片描述

  • 由于那时候,我对递归还没有什么概念,所以一脸懵逼。

在这里插入图片描述

  • 我还是听不懂……

  • 这下我就懂了!
  • 我们可以定义这么一个函数 f ( x ) = 1 × 2 × 3 × . . . × x f(x) = 1 \times 2 \times 3 \times ... \times x f(x)=1×2×3×...×x,其中 x ≥ 0 x \ge 0 x0
  • x > 0 x > 0 x>0 时,代入 ( x − 1 ) (x-1) (x1),显然有 f ( x − 1 ) = 1 × 2 × 3 × . . . × ( x − 1 ) f(x-1) = 1 \times 2 \times 3 \times ... \times (x-1) f(x1)=1×2×3×...×(x1)
  • 于是,可以得到:
  • f ( x ) f ( x − 1 ) = x \frac {f(x)} {f(x-1)} = x f(x1)f(x)=x
  • 由于 f ( x − 1 ) > 0 f(x-1) > 0 f(x1)>0, 等式两边可以同时乘上 f ( x − 1 ) f(x-1) f(x1),很容易得出递推公式如下:
  • f ( x ) = { 1 ( x = 0 ) f ( x − 1 ) × x ( x > 0 ) f(x) = \begin{cases} 1 & (x = 0) \\ f(x-1) \times x & (x > 0)\end{cases} f(x)={1f(x1)×x(x=0)(x>0)
  • 所以,翻译成 c/c++ 的语言,就可以写成这样:
int f(int x) {
    if(x == 0) {
        return 1;
    }else {
        return f(x-1) * x;
    }
}
  • 然后利用三目运算符,改成一行代码,得到:
int f(int x) {
    return x ? f(x-1) * x : 1;
}

在这里插入图片描述

  • 通过这段代码,我了解了 递推公式 和 递归调用。

算法三


【例题3】现在有一个 n ( n ≤ 10000 ) n(n \le 10000) n(n10000) 个元素的数组 a [ i ] a[i] a[i],但是我们已知的是前 i i i 个元素的和 f [ i ] ( 1 ≤ i ≤ n , 1 ≤ f [ i ] ≤ 100000 ) f_[i](1 \le i \le n, 1 \le f[i] \le 100000) f[i](1in,1f[i]100000),然后给出 Q ( Q ≤ 1000000 ) Q(Q \le 1000000) Q(Q1000000) 次询问 ( l , r ) ( 1 ≤ l ≤ r ≤ n ) (l, r) (1 \le l \le r \le n) (l,r)(1lrn),求 ∑ i = l r a [ i ] \sum_{i=l}^r a[i] i=lra[i]

  • 首先根据题意,得知: a [ i ] = f [ i ] − f [ i − 1 ] a[i] = f[i] - f[i-1] a[i]=f[i]f[i1]
  • 可以用一个for循环计算出所有a[i]的值。
const int maxn = 100005;
void preCalculate(int f[maxn]) {
    int a[maxn];
    for(int i = 1; i <= n; ++i) {
        a[i] = f[i] - f[i-1];
    }
}
  • 然后对于每次循环,循环统计 a [ i ] a[i] a[i] 的累加和,返回答案。
int get(int a[], int l, int r) {
    int s = 0;
    for(int i = l; i <= r; ++i) {
        s += a[i];
    }
    return s;
}

  • 事实的确如此,如果每个询问都是 1 到 n n n 的话,时间复杂度就会变成 O ( n Q ) O(nQ) O(nQ),总的数量级在 1 0 10 10^{10} 1010,比较难承受了。
  • 再仔细想想,不难发现。我们可以将要求的结果定义为函数 g ( l , r ) g(l ,r) g(l,r),则有:
  • g ( l , r ) = a [ l ] + a [ l + 1 ] + . . . + a [ r ] = ( f [ l ] − f [ l − 1 ] ) + ( f [ l + 1 ] − f [ l ] ) + . . . + ( f [ r ] − f [ r − 1 ] ) = f [ r ] − f [ l − 1 ] \begin{aligned}g(l, r) &= a[l] + a[l+1] + ... + a[r] \\ &= (f[l]-f[l-1]) + (f[l+1]-f[l]) + ... + (f[r]-f[r-1]) \\ &= f[r] - f[l-1]\end{aligned} g(l,r)=a[l]+a[l+1]+...+a[r]=(f[l]f[l1])+(f[l+1]f[l])+...+(f[r]f[r1])=f[r]f[l1]
  • f [ l ] f[l] f[l] f [ r ] f[r] f[r] 都是题目给出的,神奇!
  • 直接得到一行代码求解:
int g(int l, int r) {
    return f[r] - f[l-1];
}

  • 学姐估计是太激动,本来想夸我 “数学底子不错”,结果说成了 “数学底子不过” …… 只要她不尴尬,尴尬的就是我 ……
  • 通过这段代码,我了解了 前缀和 和 差分法。

算法四

【定义1】对于一个数 a a a,如果有数 b b b 能够整除 a a a,则称 a a a b b b 的倍数, b b b a a a 的约数。
【定义2】如果一个数 c c c 同时是数 a a a 和 数 b b b 的约数,则称 c c c a a a b b b 的公约数。
【定义3】如果 c c c a a a b b b 中最大的公约数,则称 c c c a a a b b b 的最大公约数,记为 c = g c d ( a , b ) c = gcd(a, b) c=gcd(a,b)

  • 例如,1,2,4 均为 8 和 12 的公约数,最大的公约数就是 4。
    在这里插入图片描述

  • 我可以枚举所有 a a a 的约数,然后再判断它是不是 b b b 的约数,从而找到最大的那个满足条件的约数就是答案了。算法实现如下:
int gcd(int a, int b) {
    int maxret = 1;
    for(int i = 2; i <= a; ++i) {
        if(a % i == 0 && b % i == 0)
            maxret = max(maxret, i);
    }
}
  • 这个算法的时间复杂度是 O ( n ) O(n) O(n) 的。

  • 以下这段话出自当年学姐的口中:

  首先,当 b ≠ 0 b \neq 0 b=0 时,我们令 a = k b + r a = kb + r a=kb+r,其中 k = ⌊ a b ⌋ k = \lfloor \frac a b \rfloor k=ba r = a   m o d   b r = a \ mod \ b r=a mod b,并且满足 ( 0 ≤ r < b ) (0 \le r < b) (0r<b),当一个数 c c c,是 a a a 的约数,也是 b b b 的约数,则必然也是 a − k b a-kb akb 的约数,即 r r r 的约数。自然 a a a b b b 的公约数 也就是 b b b r r r 的公约数。
  所以, a a a b b b 的最大公约数 = = = b b b r r r 的最大公约数。表示为: g c d ( a , b ) = g c d ( b , a   m o d   b ) gcd(a, b) = gcd(b, a \ mod \ b) gcd(a,b)=gcd(b,a mod b)

  • 但是我们的假设是建立在 b ≠ 0 b \neq 0 b=0 上的,而 b = 0 b=0 b=0 的情况,答案显然就是 a a a 了。于是对于上面的 g c d gcd gcd 函数,我们可以表示成如下递归式:
  • g c d ( a , b ) = { a b = 0 g c d ( b , a   m o d   b ) b ≠ 0 gcd(a,b) = \begin{cases} a & b=0\\ gcd(b, a \ mod \ b) & b \neq 0 \end{cases} gcd(a,b)={agcd(b,a mod b)b=0b=0
  • 写成 c++ 代码就是:
int gcd(int a, int b) {
    return !b ? a : gcd(b, a % b);
}
  • 这里 !是 c/c++ 中的表达式取非的意思,即 真变假,假变真,且 c/c++ 中 0 为假,非0为真;%是取模,即 m o d mod mod 的程序语法。
  • 学姐真是太装逼牛逼了!又是一行代码让我学到了这么多知识,满满的干货!

在这里插入图片描述

  • 这时候,天上掉下来一座长城!
  • 果然是说曹操,曹操就到啊!
  • 难道,这个梦是在暗示我赶紧把学姐的算法学完吗???


六、梦不到,被吹散

  • 充满求知欲的我不甘心,还想继续睡。可再也梦不到学姐了。
    在这里插入图片描述
  • 我逐渐陷入沉思,等我反应过来的时候,已然到了晚上,今天的课又没去上!
  • 我不甘心啊! 学姐快把算法传授给我吧!

七、往事如风

  • 十年后的今天再回首,恍如隔世,学姐当时托梦,的确让我受益匪浅,这也是我后来誓要写成❤️《夜深人静写算法》❤️系列最大的动力。这个系列目前还在紧锣密鼓的更新中,适合高中IOer,大学Acmer,以及职场的有志青年学习算法之用。
  • 至于学姐后来到底有没有托梦于我,我会在 《❤️学姐说她用 8 行代码写了 8 个算法(下)❤️》 里继续更新,如果有想帮我想剧情的,亦或是对学姐的钦慕之情,都可以在评论区留言告诉我, 学姐是我的,也是你的。

  • 本文通过一些简单的算法,对 c/c++ 的语言性质进行了一些温故而知新,希望对各位初学者有所帮助。
  • 以下是本文涉及到的知识点,最后再进行一个总结归纳。

1、知识点回顾

知识点难度
整除★☆☆☆☆
倍数★☆☆☆☆
约数★☆☆☆☆
前缀和★★★☆☆
差分法★★★☆☆
递推公式★★★☆☆
递归调用★★★★★
整数取模★☆☆☆☆
最大公约数★★★☆☆
表达式取反★☆☆☆☆
整数的溢出★★☆☆☆
三目运算符★★☆☆☆
变量的作用域★★☆☆☆
整数的补码表示★★☆☆☆
表达式真值的省略写法★☆☆☆☆

2、温故而知新

// 算法1:求和公式
int sum(int n) {
    return (n%2) ? (n+1)/2*n : n/2*(n+1);
}
// 算法2:递归求阶乘
int f(int x) {
    return x ? f(x-1) * x : 1;
}
// 算法3:差分法求部分和
int g(int l, int r) {
    return f[r] - f[l-1];
}
// 算法4:递归求最大公约数
int gcd(int a, int b) {
    return !b ? a : gcd(b, a % b);
}
  • 49
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论
好的,下面是一个简单的进程创建的C代码,并结合Round Robin进程调度算法: ``` #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<signal.h> #include<sys/types.h> #include<sys/wait.h> #define MAX 10 typedef struct PCB{ pid_t pid;//pid int priority;//进程优先级 int cpu_time;//占用CPU时间 int run_time;//已运行时间 int remain_time;//剩余运行时间 int status;//进程状态 struct PCB* next;//指向下一个进程的指针 }PCB; int process_num = 0; PCB* ready_list;//就绪队列的头指针 PCB* p;//指向当前进程的指针 void scheduling();//进程调度函数 void create_process(int priority, int cpu_time){//创建进程函数 PCB* new_pcb = (PCB*)malloc(sizeof(PCB)); new_pcb->pid = fork(); if(new_pcb->pid == 0){//子进程 signal(SIGSTOP, SIG_DFL);//子进程去掉SIGSTOP信号的处理函数 while(1);//占用CPU } else if(new_pcb->pid < 0){//创建进程失败 printf("Create process failed!\n"); free(new_pcb); return; } else{//父进程 new_pcb->priority = priority; new_pcb->cpu_time = cpu_time; new_pcb->run_time = 0; new_pcb->remain_time = cpu_time; new_pcb->status = 0;//新建进程状态为就绪 new_pcb->next = ready_list; ready_list = new_pcb;//将新建进程加入就绪队列 process_num++;//进程数量加1 printf("Process[%d] created! priority=%d cpu_time=%d\n", new_pcb->pid, priority, cpu_time); } } void time_out(int sig){//设置时间片到时的处理函数 p->remain_time -= 1; p->run_time += 1; if(p->remain_time == 0){ printf("Process[%d] finished!\n", p->pid); waitpid(p->pid, NULL, 0);//等待进程结束 p->status = -1;//进程状态改为退出 process_num--;//进程数量减1 free(p);//释放进程的PCB所占的空间 if(ready_list != NULL) p = ready_list;//指向下一个进程的PCB } else{ printf("Process[%d] time out!\n", p->pid); p->status = 1;//进程状态改为就绪 p->next = ready_list; ready_list = p;//将进程加入就绪队列尾部 p = ready_list;//指向就绪队列的头部 ready_list = ready_list->next;//就绪队列指向下一个进程 } } void scheduling(){ int time_slice = 2;//时间片长度为2 p = ready_list; signal(SIGALRM, time_out);//设置时间片到时的处理函数 while(process_num > 0){ if(p != NULL){//p不为空,说明就绪队列不为空 if(p->status == 0){//进程状态为就绪 p->status = 2;//进程状态改为运行 kill(p->pid, SIGCONT);//恢复运行 alarm(time_slice);//设置时间片长度 pause();//等待时间片结束 } else if(p->status == 1){//进程状态为阻塞 p = p->next; } else if(p->status == -1){//进程状态为退出 if(ready_list != NULL) p = ready_list; } else if(p->status == 2){//进程状态为运行 kill(p->pid, SIGSTOP);//暂停运行 } } else{//p为空,说明就绪队列为空 printf("Ready list is empty!\n"); sleep(1); } } } int main(){ ready_list = NULL; int i, priority, cpu_time; for(i=0; i<MAX; i++){ printf("Enter priority and cpu time of process[%d]:", i); scanf("%d%d", &priority, &cpu_time); create_process(priority, cpu_time); } scheduling(); return 0; } ``` 在上面的代码中,我们进行了如下操作: - 定义了进程控制块(PCB)结构,包含进程的pid、优先级、占用CPU时间、已运行时间、剩余运行时间和状态,以及指向下一个进程的指针。 - 定义了进程数量和就绪队列的头指针。 - 创建了进程的函数`create_process`,其中使用`fork()`系统调用创建子进程,并将子进程PID保存到新建PCB中,同时子进程占用CPU,父进程将新建PCB加入就绪队列中。 - 设置了时间片到时的处理函数`time_out`,当时间片到时时,将进程的剩余运行时间减1,已运行时间加1,如果剩余运行时间为0,就结束进程,将进程状态改为退出,进程数量减1,释放PCB的空间;否则,将进程状态改为就绪,将进程加入就绪队列尾部,就绪队列指向下一个进程。 - 设置了进程调度函数`scheduling`,在函数中定义了时间片长度,将当前进程指针p指向就绪队列的头部,设置时间片到时的处理函数和信号,循环执行进程调度操作,当进程数量为0时退出循环。在循环中,如果当前进程不为空,就根据进程状态选择不同的操作,如果进程状态为就绪,就将进程状态改为运行,恢复进程运行并设置时间片长度;如果进程状态为阻塞,就继续指向下一个进程;如果进程状态为退出,就将p指向就绪队列的头部;如果进程状态为运行,就暂停进程运行。如果当前进程为空,就输出就绪队列为空并休眠1秒。注:这里的调度算法是Round Robin算法
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值