To my CSDN

算法 | 【分治策略与递归】——求阶乘、整数数位输出、最大公约数

分治策略:是将规模比较大的问题可分割成规模较小的相同问题。问题不变,规模变小。这自然导致递归过程的产
生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
递归:若一个函数直接地或间接地调用自己,则称这个函数是递归的函数。

分治法所能解决的问题一般具有以下四个特征:

  • 该问题的规模缩小到一-定的程度就可以容易地解决。
  • 该问题可以分解为若干个规模较小的相同问题。
  • 使用小规模的解,可以合并成,该问题原规模的解。
  • 该问题所分解出的各个子规模是相互独立的。

分治法步骤
在分治策略中递归地求解一个问题,在每层递归中应用如下三个步骤:

  1. 分解:将问题划分成一些子问题,子问题的形式与原问题一样,只是规模更小。
  2. 解决:递归地求解子问题。如果子问题的规模足够小,则停止递归,直接求解。
  3. 合并:将小规模的解组合成原规模问题的解。

示例1.1:求解n的阶乘。(不考虑 int 溢出)

分析:

n! = 1 * 2 * 3 * 4 * 5 * 6 . . . . . . . . . . . . . . . . . . . . . (n-2) * (n-1) * n
递推:
 n!    = (n-1)! * n  							欲求得    n! ,需先求得 (n-1)!
(n-1)! = (n-2)! * (n-1)  						欲求得(n-1)! ,需先求得 (n-2)!
(n-2)! = (n-3)! * (n-2)  						欲求得(n-2)! ,需先求得 (n-3)!
(n-x)! = (n-x-1) * (n-x) 						欲求得(n-x)! ,需先求得 (n-x-1)!
... ...											... ...
1! = 1 											最终求得 1= 1
回归:
2! = 1! * 21! 可得 2!
3! = 2! * 32! 可得 3!
... ...											... ...
n! = (n-1)! * n									最终求得 n! 

因此,此题的阶乘可用递归的方式定义为:
fac(n)={1;n=1fac(n1)nn>1 fac(n)=\left\{ \begin{array}{rcl} 1; & & {当 n = 1 时}\\ fac(n-1)*n & & {当 n > 1 时} \end{array} \right.

C++代码实现:

#include <iostream>
using namespace std;

/*
题目:求解n的阶乘。(不考虑 int 溢出)
*/

// 循环实现	O(n)S(1)
int fac(int n)
{
	int sum = 1;
	for (int i = 1; i <= n; ++i)
	{
		sum *= i;
	}
	return sum;
}

// 递归实现	O(n)S(n)
int fac_r(int n)
{
	if (n <= 1)	return 1;
	else return  fac(n - 1) * n;
}

int main()
{
	// 十组测试用例
	for(int i = 0; i < 10; ++i)
	{
		int res = fac(i);
		int res_r = fac(i);
		cout << i << "! = " << res << endl;
		cout << i << "! = " << res_r << endl;
	}
	return 0;
}

递归函数的执行分为“递推"和"回归”两个过程,这两个过程由递归终止条件控制,即逐层递推,直至递归终止
条件满足,终止递归,然后逐层回归。
递归调用同普通的函数调用一样,每当调用发生时,就要分配新的栈帧(形参数据,现场保护,局部变量) ;而与普通的函数调用不同的是,由于递推的过程是一个逐层调用的过程, 因此存在一个逐层连续的分配栈帧过程, 直至遇到递归终止条件时,才开始回归,这时才逐层释放栈帧空间,返回到上一层,直至最后返回到主调函数。

注意:善用递归,我们经常会用到递归函数,但是如果递归深度太大时,往往导致栈溢出。因此要避免过度使用递归函数,一般来说递归问题是可以转化为循环问题的,在进行一些大规模递归运算时可以使用循环代替递归。

示例1.2:求解n的阶乘。(考虑 int 溢出)

int 类型的在C/C++ 中默认是有符号整型,它的范围在 -2147483648 ~ 2147483647 。下面是int类型范围的计算方法:

#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
	/* 
	int 类型范围
	分析:
	4字节  ==> 8位十六进制数  ==> 32位二进制位
	有符号 ==> 二进制位高位占一个符号位 
				正数: 0,000... -- 0,111...
				负数: 1,000... -- 1,111...
	最大   0x7fff ffff		0,1111111 11111111 11111111 11111111 
	最小   0x8000 0000		1,0000000 00000000 00000000 00000000
	*/

	// 使用宏 INT_MIN、INT_MAX 检验
	cout << hex;		// 输出十六进制格式
	cout << INT_MIN << " ~ " << INT_MAX <<  endl;
	cout << dec;		// 输出十进制
	cout << INT_MIN << " ~ " << INT_MAX << endl;
	// 另一种计算方法
	cout << (1<<31) << " ~ " << (1<<31)-1 << endl;		
	return 0;
}

/*
输出:
80000000 ~ 7fffffff
-2147483648 ~ 2147483647
-2147483648 ~ 2147483647
*/

我们在计算阶乘的方法中,使用了数据类型 int 。如果说我们计算的结果超出了 int 的范围,那么计算的结果就将是错误的。如果我们把 int 类型换成 unsigned int 类型确实可以增大我们的计算上限,可是我们一般的计算是用不到 unsigned int 这么大的数据类型的的,并且unsigned int 也存在上限,依然会存在溢出问题。

在设计算法时,应提前避免可能发生的错误,增加程序的容错性也就是我们常说的代码的健壮性。因此我们可以在示例1代码的基础上增加一个判断机制,在判断出计算的结果出错时我们就没必要再进行下去了。

/*
int 类型
正数上界限	0,1111111 11111111 11111111 11111111
加1 导致上届溢出为
			1,0000000 00000000 00000000 00000000
分析:
	最大正数 + 1 = 最小负数
结论:
	当结果为负数时,已经发生了上届溢出,且该数值无效
*/

int fac(int n)
{
	int sum = 1;
	for (int i = 1; i <= n; ++i)
	{
		if (sum < 0) return -1;		// 判断数值是否有效
		sum *= i;
	}
	return sum;
}

// 递归方法
int fac_r(int n)
{
	if (n <= 1)	return 1;
	else
	{
		int tmp = fac(n - 1) * n;
		if (tmp > 0)				// 判断数值是否有效
			return  fac(n - 1) * n;
		else
			return -1;
	} 
}

以上的程序还是不够完善,程序本身的逻辑结构已经没有明显的错误了。但是我们还要注意一点,回到我们所求的问题,阶乘
阶乘是什么?
百科上给出的定义是 一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!
也就是说,负数没有阶乘。
而我们的程序并没有针对这个问题作出处理。因此,我们还需要修改程序,使其只接受正数和0的输入。

if(n >= 0)
	fac(n);
/* 
这里只在调用处进行了判断,
建议在 fac() 函数中添加判断条件 
*/

示例2.1:输入一个整数(无符号整型),用递归算法将整数倒序输出。

这个算法非常简单,倒序输出非常容易实现。在实现算法之前我们先来了解一下 /% 这两个运算符的特点。
根据我们小学三年级学的除法知识可知,如果在不涉及小数的情况下,一个完整的除法算术应由 被除数,除数,商和余数组成:
例如 10÷5=2......(0)10 ÷ 5 = 2 ......(0) 10 除以 5 余 2,或是5÷2=2......(1)5 ÷ 2 = 2 ......(1) 5 除以 2 余 1。不过这不是我们讨论的重点,重点是以下这两种运算。

  • / 整除运算:10/5=2, 5/2=2。这是整除的特点,商永远是一个整数。
    而当“整除”与“10” 结合在一起后就会有一个新的特点:
    比如 100/10=10,10/10=1;123/10=12,12/10=1。
    我们可以看到,当一个数对10进行整除时,会消除尾部的一个数字,我们暂且把这种性质命名为消尾
  • % 在C/C++中为取余运算,在python中为取模运算 。传送门:取余与取模
    10%5=0,5%2=1。这是取余运算的特点,计算结果为除法算式的余数。
    而当“取余”与“10” 结合在一起后也会有一个新的特点:
    比如 123%10=3,12%10=2,1%10=1。也就是说,对10取余可以得到一个数尾部的数字,或者说个位上的数字,我们暂且把这种性质命名为取尾

因此,对于一个 12345 的数来说,我们想要倒序输出其每一位其实非常简单,只需要不断地 取尾,消尾,取尾,消尾 … 即可。
那么我们很容易就能设计出他的递归式和循环式。C++代码实现如下:

#include <iostream>
using namespace std;

/*
题目:输入一个整数(无符号整型),用递归算法将整数倒序输出。
*/

// 循环实现
void fun(unsigned int n)
{
	while (n)
	{
		cout << n % 10 << " ";
		n /= 10;
	}
	cout << endl;
}

// 递归实现
void fun_r(unsigned int n)
{
	if (n > 0)
	{
		cout << n % 10 << " ";
		fun(n / 10);
	}
	cout << endl;
}

int main()
{
	int n = 12345;
	fun(n);
	fun_r(n);
	return 0;
}
示例2.2:输入一个整数(无符号整型),用递归算法将整数正序输出。

这个算法也不是很难,而且针对他有很多中不同的解法。
解法1:利用 /% 运算符进行 “取头” 、“消头”操作。
比如 123

  • 123 / 100 = 1     得到 1
    123%100 = 23
  • 23 / 10 = 2     得到 2
    23%10 = 3
  • 3 / 1 = 3      得到 3
    3%1 = 0
    通过这种方式,我们得到了正序的 1 2 3 。关键点在与我们需要事先知道输入的整数的长度。代码实现如下:
#include <iostream>
#include <cmath>
using namespace std;

/*
题目:输入一个整数(无符号整型),正序输出。
算法:先整除,在取余
*/

void fun1(unsigned int n)
{
	int m = n;
	int len = 0;
	while (m)		// 计算整数长度
	{
		m /= 10;
		len++;
	}
	while (n)
	{
		int div = pow(10.0, len - 1.0);		// 除数
		cout << n / div << " ";
		n %= div;
		len--;
	}
	cout << endl;
}


int main()
{
	int n = 12345;
	fun1(n);

	return 0;
}

解法二:利用数组将示例2.1中的结果存储下来,逆序输出数组内容。代码如下:

#include <iostream>
using namespace std;

/*
题目:输入一个整数(无符号整型),正序输出。
算法:保存逆向的结果与数组中,反向输出数组。
*/

#define MAX_LEN 10		// unsigend int 最大值  ==》 (unsigned)-1 = 4294967295

void fun2(unsigned int n)
{
	int i = 0;
	int arr[MAX_LEN] = {};
	while (n)
	{
		arr[i++] = n % 10;
		n /= 10;
	}
	while (i > 0)		// 反向输出arr[] 
	{
		cout << arr[--i] << " ";
	}
	cout << endl;
}


int main()
{
	int n = 12345;
	fun2(n);
	return 0;
}

解法三:将原整数逆置,再逆向输出。
这个解法看起来有些多此一举,但也可以视为一个解法。其中核心是的整数逆置算法。

对于一个整数 123 ,对其进行逆置操作。

  • 第一步: 取其尾数值。将原数进行消尾操作。
    取得 3,原数变为 12
  • 第二步:取得的尾数值。将原数进行消尾操作。运算:将累积的尾数值 * 10 + 当前取得尾数值
    取得 2,原数变为 1 ,运算 3*10+2=32
  • 第三步:取得的尾数值。将原数进行消尾操作。运算:将累积的尾数值 * 10 + 当前取得尾数值
    取得 1,尾数变为0,运算 32*10+1=321

我们发现,除了第一步之外,余下的n步都有相同的步骤。因此,我们设计第一步为 0 * 10 + 3 = 3 ,可以让所有的步骤相同,接下来使用循环就可以完成整数逆置。

#include <iostream>
using namespace std;

/*
题目:输入一个整数(无符号整型),正序输出。
算法:整数逆置。调用整数的倒序输出函数
*/

void fun(unsigned int n)
{
	while (n)
	{
		cout << n % 10 << " ";
		n /= 10;
	}
	cout << endl;
}

// 整数逆置
int Reverse_int(unsigned int n)
{
	int sum = 0;
	while ((sum = sum * 10 + n % 10, n /= 10));
	/* 等同于
	while (n)
	{
		sum = sum * 10 + n % 10;
		n /= 10;
	}
	*/
		
	return sum;
}

int main()
{
	int n = 12345;
	int res = Reverse_int(n);
	fun(res);		// 调用整数倒序输出函数
	return 0;
}

解法四:我们很容易就可以设计出倒序输出的函数,而正序输出可以看做是对倒序输出的一种倒序输出。我们知道有一种名为栈的数据结构,它的特点就是“先进后出”。可以通过栈的特点来完成正序输出。(实际上这和用数组逆序输出的方式很相似)

#include <iostream>
using namespace std;

/*
题目:输入一个整数(无符号整型),正序输出。
算法:使用数据结构栈
*/
#define MAX_LEN 10

struct Stack		// 定义栈结构
{
	int data[MAX_LEN];
	int top;
};

void fun(unsigned int n)
{
	Stack st;
	st.top = 0;		// 初始化栈

	while (n)
	{
		st.data[st.top++] = n % 10;
		n /= 10;
	}				// 输出
	while (st.top != 0)
	{
		cout << st.data[--st.top] << " ";
	}
	cout << endl;
}

int main()
{
	int n = 12345;
	fun(n);
	return 0;
}

注:实际上,这里举例子使用栈是为了引出其他几种数据结构,在C++中除了有stack外,还有vector、deque等都可以使用。也就是说,当我们分析问题时,常规的方法不足以或者说不容易解决问题时,我们可以通过增加其他的数据结构参与其中。如果对于一个问题来说,当你感到没有思路,无处下手时,可以试着引入更多的变量或数据结构来实现。

在计算机领域有人说过有人说过一句名言:

“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
“Any problem in computer science can be solved by anther layer of indirection.”


示例3:输入两个正整数,求它们的最大公约数

对于这一类问题,我们通常第一反应就是采用暴力法求解,也就是暴力枚举其所有的因数(或约数),找出其中最大的即可。
或者我们也可以从最大的因数找起,找到的第一个满足的公因数即为最大公因数。
解法一:暴力枚举。

#include <iostream>
using namespace std;

/*
问题:求两正整数(非0)的最大公约数
算法:暴力枚举
*/

int GCD(int a, int b)
{
	if (a <= 0 || b <= 0)	return -1;
	int i = a > b ? b : a;
	for ( ; i > 0; --i)
	{
		if (a % i == 0 && b % i == 0)
		{
			break;
		}
	}
	return i;
}

int main()
{
	int a = 125, b = 225;
	int res = GCD(a, b);
	if (res != -1)
	{
		cout << a << "与" << b << "最大公约数为 " << res << endl;
	}
	else
	{
		cout << "输入格式错误,亲输入正整数" << endl;
	}

	return 0;
}

解法二:辗转相除法
除数余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数。

根据辗转相除的方法,我们模拟计算a = 1025,b = 625两数的最大公约数。其中公式可表示为 gcd(a,b)=gcd(b,a mod b)gcd(a,b) = gcd(b, a\ mod\ b)

a b = b a mod b
1025 625 625 400
625 400 400 225
400 225 225 175
225 175 175 50
175 50 50 25
50 25 25 0
25 0

最终通过六步运算就得到了a、b的最大公约数。我们按照此方法设计函数,实现求两正整数的最大公约数。

#include <iostream>
using namespace std;

/*
问题:求两正整数(非0)的最大公约数
算法:辗转相除法
*/


int GCD(int a,int b)
{
	if (a <= 0 || b <= 0)	return -1;
	while (b != 0)
	{
		int tmp = b;
		b = a % b;
		a = tmp;
	}
	return a;
}

int main()
{
	int a = 1025, b = 625;
	int res = GCD(a, b);
	if (res != -1)
	{
		cout << a << "与" << b << "最大公约数为 " << res << endl;
	}
	else
	{
		cout << "输入格式错误,亲输入正整数" << endl;
	}

	return 0;
}

步骤三:更相减损法

“可半者半之,不可半者,副置分母、子之数,以少减多,更相减损,求其等也。以等数约之。” ——《九章算术》

更相减损法求最大公约数主要分为三步:

  • 第一步:判断所给的两个正整数是否都是偶数。若是,则用2约简;若不是则执行第二步。
  • 第二步:以较大的数减较小的数,接着把所得的差与较小的数比较并以大数减小数。继续这个操作直到所得的减数和差相等为止。否则重复步骤1、2 。
  • 第三步:在第一步中约掉的若干个2与第二步中等数乘积就是所求的最大公约数。
    其中所说的“等数”,就是最大公约数。求“等数”的办法是“更相减损”法。所以更相减损法也叫等值算法。

例如 a = 28 ,b = 42 。

  1. 判断偶数
    • 判偶:a,b 是偶数,用2约简。 a = 14,b = 21;
  2. 大数减小数
    • 大减小:b - a = 21 - 14 = 7。
    • 差与较小数比较:max( a , 7 )
    • 大数减小数:a - 7 = 14 - 7 = 7
    • 判等:减数 == 差 ? --Yes
  3. 若干2与差的乘积: 2 × 7 = 14 。(28,42)= 14。

例如 a = 63, b = 98

  1. 判断偶数
    • 判偶:a,b 不都是偶数,下一步。
  2. 大数减小数
    • 大减小:b - a = 98 - 63 = 35。
    • 差与较小数比较:max( a , 35 )
    • 大数减小数:a - 35 = 63 - 35 = 28
    • 判等:减数 == 差 ? NO
  • 继续 1、2循环,a = 35 ,b = 28
  1. 判断偶数
    • 判偶:a,b 不都是偶数,下一步。
  2. 大数减小数
    • 大减小:a - b = 35 - 28 = 7。
    • 差与较小数比较:max( b , 7 )
    • 大数减小数:b - 7 = 28 - 7 = 21
    • 判等:减数 == 差 ? NO
  • 继续 1、2循环,a = 7 ,b = 21
  1. 判断偶数
    • 判偶:a,b 不都是偶数,下一步。
  2. 大数减小数
    • 大减小:b - a = 21 - 7 = 14。
    • 差与较小数比较:max( a , 14 )
    • 大数减小数:14 - a = 14 - 7 = 7
    • 判等:减数 == 差 ? --Yes
  3. 若干2与差的乘积: 20 × 7 = 7 。(63,98)= 7。
    这个过程可简写为: (98,63) = (35,63) = (35,28) = (7,28) = (7,21) = (7,14) = (7,7) = 7 。
    即 98-63、63-35、35-28、28-7、21-7、14-7

依据上述两个例子,我们可以着手设计算法。C++代码如下:

#include <iostream>
#include <vector>
using namespace std;

/*
问题:求两正整数(非0)的最大公约数
算法:更相减损法
*/

void Max_Swap(int& a, int& b)	// 使 a > b
{
	if (a < b)
	{
		int tmp = a;
		a = b;
		b = tmp;
	}
}

int GCD(int a, int b)
{
	if (a <= 0 || b <= 0)	return -1;
	int count = 0;		// 2约简的次数
	while (a != b)
	{
		if (a % 2 == 0 && b % 2 == 0)
		{
			a /= 2;
			b /= 2;
		}
		Max_Swap(a, b);
		int tmp = b;
		b = a - b;
		a = tmp;
	}
	int result = 1;
	while (count--)
	{
		result *= 2;
	}
	return result * a;
}

int main()
{
	int a = 1025;
	int b = 625;
	int res = GCD(a, b);
	cout << res << endl;
	return 0;
}

解法四:质因数分解法
把每个数分别分解质因数,再把各数中的全部公有质因数提取出来连乘,所得的积就是这几个数的最大公约数。

例如 求24和60的最大公约数,先分解质因数,得24=2×2×2×3,60=2×2×3×5,24与60的全部公有的质因数是2、2、3,它们的积是2×2×3=12,所以,(24,60)=12。

分析:对于一个合数来说,其质因数为有2、3、5、7 … 等多种可能。那么对于两个数 a,b来说,欲求得其最小公约数可分为以下步骤:

  • 步骤一:先求得其中一个数的所有可能的质因数列表。
    如:对于20来说,我们列出 (1 ~ 20] 之内的所有质因数,ar=[2,3,5,7,11,13,17,19]。
  • 步骤二:分别让两个数对各质因数做除法(能整除)运算,直至商为 1 。
    令 a = 20,b = 45。在表中记录质因数出现的次数。
a当前的值 质因数判断 2 3 5 7 11 13
20 2是因数 1次
10 2是因数 2次
5 3不是因数 2次 0次
5 5是因数 2次 0次 1次
1 终止判断,得到质因数集 2次 0次 1次
b当前的值 质因数判断 2 3 5 7 11 13
45 2不是因数 0次
45 3是因数 0次 1次
15 3是因数 0次 2次
5 5是因数 0次 2次 1次
1 终止判断,得到质因数集 0次 2次 1次
  • 步骤三:对两个数的相同质因数做乘法运算。
质因数表 2 3 5 7 11 13
a的质因数次数表 2次 0次 1次
b的质因数次数表 0次 2次 1次
a与b相同的质因数次数 0次 0次 1次

则可得算式 GCD(20,45)=20×30×51=5GCD(20,45) = 2^0 × 3^0 × 5^1 = 5

根据上面总结的规律我们可以设计算法:
这里我选择使用vector 容器作为存放质数表和次数表的数据结构。C++代码如下:

#include <iostream>
#include <vector>
using namespace std;

/*
问题:求两正整数(非0)的最大公约数
算法:质因数分解法
*/

bool IsPrimr(int n)		// 判断是否素数
{
	int i = 2;
	for ( ; i <= sqrt(n * 1.0); ++i)
	{
		if (n % i == 0)	return false;
	}
	return true;
}

void GetPrimer(vector<int>& vec, int n)
{
	for (int i = 2; i <= n; ++i)
	{
		if (IsPrimr(i))
		{
			vec.push_back(i);
		}
	}
}

// 在质因数列表中计算质因数次数
void GetTimes(const vector<int>& vec, vector<int>& vi,int n)
{
	vector<int>::const_iterator it = vec.begin();
	for (; it != vec.end(); ++it)
	{
		if (*it > n)	// n == 1
		{
			//vec.erase(it, vec.end());	// 删除其后的素数表
			break;
		}
		int count = 0;
		while (n % *it == 0)
		{
			count++;
			n /= *it;
		}
		vi.push_back(count);
	}
}

// 相同质因数求积
int GetResult(const vector<int>& vec, const vector<int>& va,const vector<int>& vb)
{
	vector<int>::const_iterator ia = va.begin();
	vector<int>::const_iterator ib = vb.begin();
	vector<int>::const_iterator it = vec.begin();
	int res = 1;
	while (ia != va.end() && ib != vb.end())
	{
		int n = *ia > * ib ? *ib : *ia;	// 取小的
		while (n--)
		{
			res *= *it;
		}
		ia++;
		ib++;
		it++;
	}
	return res;
}

int GCD(int a, int b)
{
	if (a <= 0 || b <= 0)	return -1;
	int min = a < b ? a : b;
	vector<int> vec;			// 存放a,b的所有可能质因数
	GetPrimer(vec, min);		// 获取质因数列表

	// a的质因数
	vector<int> va;				// va 存放质因数的个数
	GetTimes(vec, va, a);		// 计算 a 的各质因数次数

	//  b的质因数
	vector<int> vb;				// 存放质因数的个数
	GetTimes(vec, vb, b);		// 计算 b 的各质因数次数 

	// 质因数相乘
	return 	GetResult(vec, va, vb);

}

int main()
{
	int a = 1025;
	int b = 625;
	int res = GCD(a, b);
	cout << res << endl;
	return 0;
}

在求最大公约数的过程中,我们用到了许多数学推导,以及引用了几个有名的数学公式定理。可见算法的设计与数学也有着密不可分关系。另外,示例3全部是用循环方法做的,理论上使用递归去解决此类问题会比较方便。感兴趣同学的可以尝试将循环改成递归算法。



写在最后:
博主目前依然是个编程路上的小学生😃,如果大家发现博文中有错误的地方,请私信或评论告诉我,我会抽时间修改✍🏻。如果有不同意见也可以在评论区讨论,如果觉得这篇博文对你有帮助的不妨帮忙点个赞👍。你们的鼓励就是我学习的动力,我也希望可以为大家带来跟多优质的博文,虽然现在还是菜鸟,但就像马云所说的。“只有菜鸟才能飞向千家万户,笨鸟先飞。飞了半天还是笨鸟,菜鸟有机会变成好鸟,我们希望自己成为一只勤奋、努力、不断学习、对未来有敬畏、对昨天又感恩的鸟。”      最后:学习路上,望你我共勉。

posted @ 2020-03-28 00:54  我叫RT  阅读(905)  评论(0编辑  收藏  举报