【刷题日记】笔试经典编程题目(六)

大家好,我是白晨,一个不是很能熬夜,但是也想日更的人✈。如果喜欢这篇文章,点个赞关注一下白晨吧!你的支持就是我最大的动力!

文章目录

  • 前言
  • 笔试经典编程题目(六)
    • 1.分解因数
    • 2.美国节日
    • 3.淘宝网店
    • 4.斐波那契凤尾
    • 5.剪花布条
    • 6.客似云来
    • 7.收件人列表
    • 8.养兔子
    • 9.年会抽奖
    • 10.抄送列表
    • 11.Rational Arithmetic
    • 12.Pre-Post
  • 后记

前言


虽然还有很多课,但是也不能忘了写编程题呀。

白晨总结了大厂笔试时所出的经典题目,本周题型包括动态规划,条件控制,数学归纳,算法等,其中数据结构和条件控制题目难度比上周有很大增长。这次的题目平均难度比以前的题都难,特别是最后还出现了英文题目,如果一时间没有想到,不用怀疑自己,试着看完解析再试试。

都是很有代表性的经典题目,适合大家复习和积累经验。

这里是第六周,大家可以自己先试着自己挑战一下,再来看解析哟!

笔试经典编程题目(六)


1.分解因数


【刷题日记】笔试经典编程题目(六)_第1张图片

原题链接:分解因数

算法思想

  • 这道题的意思就是求一个数的全部的因子,所以我们可以直接使用求质数的方法进行求解。

代码实现

#include 
#include 
#include 
using namespace std;

int main()
{
    int n;
    while (cin >> n)
    {
        vector<int> factor;// 存放因子
        int tmp = n;
        // 求因子
        for (int i = 2; i <= sqrt(n); ++i)
        {
            if (n % i == 0)
            {
                // 一次去掉重复的因子
                while (n % i == 0)
                {
                    factor.push_back(i);
                    n /= i;
                }
            }
        }
        // 当n不为1时,说明这是一个大于sqrt(n)的质数
        // eg.26 = 2*13,sqrt(26)<13,所以无法在上面求得。
        if (n != 1)
            factor.push_back(n);
        
        cout << tmp << " = ";
        for (int i = 0; i < factor.size() - 1; ++i)        
            cout << factor[i] << " * ";        
        cout << factor.back() << endl;
    }
    return 0;
}

2.美国节日


【刷题日记】笔试经典编程题目(六)_第2张图片

原题链接:美国节日

这道题虽然看着平平无奇,但是实现起来是相当复杂。我这里提供两种思路:

  • 如果知道蔡勒公式,用蔡勒公式可以简化代码。
  • 如果不知道,将2000年当作参考,按照日期加减以及年与年的天数关系求解(麻烦死)。

算法思想

  • 首先,来介绍一下蔡勒公式:

蔡勒(Zeller)公式,是一个计算星期的公式,随便给一个日期,就能用这个公式推算出是星期几。

  • D = [ c / 4 ] − 2 c + y + [ y / 4 ] + [ 13 ( m + 1 ) / 5 ] + d − 1 D=[c/4]−2c+y+[y/4]+[13(m+1)/5]+d−1 D=[c/4]2c+y+[y/4]+[13(m+1)/5]+d1
  • W = D % 7 W=D \% 7 W=D%7

其中:

  • W W W 是星期数。注意 W W W为0时,这一天是周日。
  • c c c 是世纪数减一,也就是年份的前两位。
  • y y y 是年份的后两位。
  • m m m 是月份。 m m m 的取值范围是 3 至 14,所以某年的 1、2 月要看作上一年的 13、14月,比如 2019 年的 1 月 1 日要看作 2018 年的 13 月 1 日来计算。
  • d d d 是日数。
  • [ ] [] [] 是取整运算。
  • % \% % 是求余运算。
  • 通过蔡勒公式,我们可以得知任意一天的日期,结合题目都是第x月的第y个星期n这种形式。所以,我们可以先计算出第x月的1号是星期几。
  • 以每个月1号的星期数作为参考,求得题目要求的日期。
  • 具体实现细节见代码实现:

代码实现

#include 
using namespace std;

// 蔡勒公式,求某一天的星期数
int zeller(int year, int month, int day = 1)
{
    // m 的取值范围是 3 至 14,所以某年的 1、2 月要看作上一年的 13、14月
    if (month == 1 || month == 2)
    {
        month += 12;
        year--;
    }
    // 按照公式求解
    int c = year / 100;
    int y = year % 100;
    int D = c / 4 - 2 * c + y + y / 4 + 13 * (month + 1) / 5 + day - 1;
    int W = D % 7;
    // w==0,说明这一天是周日
    return W == 0 ? 7 : W;
}

// 求xxxx年y月的第x个星期n 是 几号
int theDayOfDemand(int year, int month, int cnt, int weekDay)
{
    int first_day = zeller(year, month);
    int day = 1 + (cnt - 1) * 7 + (weekDay + 7 - first_day) % 7;
    return day;
}

void newYear(int year)
{
    printf("%d-01-01\n", year);
}

void martinDay(int year)
{
    printf("%d-01-%02d\n", year, theDayOfDemand(year, 1, 3, 1));
}

void presidentDay(int year)
{
    printf("%d-02-%02d\n", year, theDayOfDemand(year, 2, 3, 1));
}

void soldiersDay(int year)
{
    // 因为一个月最多有5个周一,所以直接求第五个周一的日期,如大于31减7。
    int day = theDayOfDemand(year, 5, 5, 1);
    while (day > 31)
        day -= 7;
    printf("%d-05-%02d\n", year, day);
}

void nationalDay(int year)
{
    printf("%d-07-04\n", year);
}

void workerDay(int year)
{
    printf("%d-09-%02d\n", year, theDayOfDemand(year, 9, 1, 1));
}

void thanksgiving(int year)
{
    printf("%d-11-%02d\n", year, theDayOfDemand(year, 11, 4, 4));
}

void Christmas(int year)
{
    printf("%d-12-25\n", year);
}

int main()
{
    int n;
    while (cin >> n)
    {
        newYear(n);
        martinDay(n);
        presidentDay(n);
        soldiersDay(n);
        nationalDay(n);
        workerDay(n);
        thanksgiving(n);
        Christmas(n);
        cout << endl;
    }
    return 0;
}

3.淘宝网店


【刷题日记】笔试经典编程题目(六)_第3张图片

原题链接:淘宝网店

算法思想

  • 这道题的思路其实并不是很难,本质上就是算两个日期之间间隔的天数,但是这道题中每一天的权重不再都是1了,而是根据月份不同而不同,素月的每一天天权重为1,其他月的每一天权重为2。
  • 我们用逐步逼近的策略,让起始日期不断逼近结束日期,随着起始日期的逼近,总收益也不断随着起始日期的增加而增加,直到最后起始日期等于结束日期。
  • 注意:结束日期也在营业,不能忘记最后加上结束日期的收益。
  • 这里描述的可能比较抽象,但是代码还是比较好懂的,这道题主要考的就是对于边界的控制,以及思路的实现,实现细节见代码。

代码实现

#include 
using namespace std;

// 获取year年month月的天数
int getMonthDay(int year, int month)
{
    static int monthDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    int ret = monthDay[month];
    if (month == 2 && (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)))
        ret++;
    return ret;
}

// 判断是否为素月
bool is_poor_month(int month)
{
    if (month == 2 || month == 3 || month == 5 || month == 7 || month == 11)
        return true;
    return false;
}

int main()
{
    int sy, sm, sd, ey, em, ed;// 初始年,月,日,结束年,月,日
    while (cin >> sy >> sm >> sd >> ey >> em >> ed)
    {
        int w = 0;// 总利润
        int y1 = sy + 1, y2 = ey - 1;
        // 将初始年和结束年中间的年份的收益加起来
        while (y2 - y1 >= 0)
        {
            // 闰年每年的收益为580,素年为579,闰年比平常年2月多一天,2月为素月
            if (y1 % 400 == 0 || (y1 % 100 != 0 && y1 % 4 == 0))
                w += 580;
            else
                w += 579;
            y1++;
        }
        // 通过上一个函数的逼近,初始年和结束年就是前后两年了,现在逼近月
        while (sm != em || sy != ey)
        {
            // sm等于13代表已经到了结束年的一月
            if (sm == 13)
            {
                sy = ey;
                sm = 1;
            }
            // 按照月份加总收益
            if (is_poor_month(sm))
                w += getMonthDay(sy, sm);
            else
                w += 2 * getMonthDay(sy, sm);
            sm++;
        }
        // 天数逼近
        if (sd <= ed)
        {
            if (is_poor_month(sm))
                w += ed - sd + 1;
            else
                w += 2 * (ed - sd + 1);
        }
        else
        {
            if (is_poor_month(sm))
                w -= ed - sd + 1;
            else
                w -= 2 * (ed - sd + 1);
        }
        cout << w << endl;
    }
    return 0;
}

4.斐波那契凤尾


【刷题日记】笔试经典编程题目(六)_第4张图片

原题链接:斐波那契凤尾

算法思想

  • 这道题不能直接计算斐波那契数列,因为在计算到几百位的时候会超过整形最大的存储范围。所以,我们只用存储后6位即可。
  • 特别注意:当n > 24时,斐波那契数会大于等于6位,输出时,必须输出6位。
  • eg. 1000001 % 1000000 = 1 1000001 \% 1000000 = 1 1000001%1000000=1 ,存储了1,但是其实原数的后六位是: 000001 000001 000001 ,所以要进行补位。

代码实现

#include 
#include 
using namespace std;

int main()
{
    vector<int> v(100001);
    v[0] = 1;
    v[1] = 1;
    for (int i = 2; i <= 100000; ++i)
    {
        v[i] = (v[i - 1] + v[i - 2]) % 1000000;
    }

    int n;
    while (cin >> n)
    {
        if (n > 24)
            printf("%06d\n", v[n]);
        else
            printf("%d\n", v[n]);
    }
    return 0;
}

5.剪花布条


【刷题日记】笔试经典编程题目(六)_第5张图片

原题链接:剪花布条

  • 法一:strstr

算法思想

  • 使用 strstr 函数进行搜索,将返回的字符指针 + 短串(待匹配串)的长度,也就是下一个搜索的位置。
  • 如果上面得到的位置小于长串(匹配串),继续进行搜索,每搜索成功一次cnt++。
  • 直到搜索完匹配串或者找不到待匹配串结束搜索。

代码实现

#include 
#include 
using namespace std;

int main()
{
    string s, t;
    while (cin >> s >> t)
    {
        // 进行搜索
        char* tmp = strstr(s.c_str(), t.c_str());
        int cnt = 0;
        // 当搜索成功时,进入循环
        while (tmp)
        {
            cnt++;
            // 字符指针 + 短串(待匹配串)的长度,也就是下一个搜索的位置
            tmp += t.size();
            // 匹配串搜索结束
            if (tmp >= s.c_str() + s.size())
                break;
            // 搜索
            tmp = strstr(tmp, t.c_str());
        }
        cout << cnt << endl;
    }
    return 0;
}

  • 法二:BF算法

算法思想

BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。

  • BF算法C语言详细代码实现:
# define _CRT_SECURE_NO_WARNINGS
#include 
#include 
#include 

// str是主串,也就是要被查找的字符串
// tar是子串,也就是目标字符串
int BF(const char* str, const char* tar)
{
	assert(str && tar);
	if (str == NULL || tar == NULL)
		return -1;

	int lenStr = strlen(str);
	int lenTar = strlen(tar);
	int i = 0, j = 0;

	while (i < lenStr && j < lenTar)
	{
		// 两个字符相等,一起向后走,比较下一个字符
		if (str[i] == tar[j])
		{
			++i;
			++j;
		}
		// 不相等,i 跳到上一次开始的后一个位置,i - j + 1中 j 相当于向后走的步数,因为j每次从0开始
		// i - j就为回到str上一次的起始位置,+1就是向后跳一个位置,也即回到上一次开始的后一个位置
		// 不相等,j回到0位置继续匹配
		else
		{
			i = i - j + 1;
			j = 0;
		}
	}
	// 当子串走完时,说明找到了,返回str上一次的起始位置
	if (j == lenTar)
		return i - j;
	// 子串没走完,主串走完了,就说明没找到,返回-1
	return -1;
}

代码实现

#include 
#include 
using namespace std;

int bf(string& s, string& t)
{
    int n1 = 0, n2 = 0;
    int cnt = 0;
    // 将传统BF算法进行调整,此时只有当匹配串走完才算走完
    while (n1 < s.size())
    {
        // 当匹配串走完时,说明成功找到一个,归零继续找
        if (n2 == t.size())
        {
            n2 = 0;
            cnt++;
        }
        if (s[n1] == t[n2])
        {
            n1++;
            n2++;
        }
        else
        {
            n1 = n1 - n2 + 1;
            n2 = 0;
        }
    }
    if (n2 == t.size())
        cnt++;
    return cnt;
}

int main()
{
    string s, t;
    while (cin >> s >> t)
    {
        cout << bf(s, t) << endl;
    }
    return 0;
}
  • 法三:KMP算法

算法思想

  • 以下实现就是KMP算法,如果没有接触过KMP算法就可以忽略了,感兴趣的可以看看。

代码实现

#include 
#include 
using namespace std;

void SupGetNext(const char* sub, int* next, int lenSub)
{
    if (lenSub == 1)
    {
        next[0] = -1;
        return;
    }
    // next[i]是sub[i]匹配不到以后跳到的位置
    // sub[0 , next[i] - 1] 和 [i - k,i - 1]是相等的
    // 但是sub[i]和sub[next[i]]不一定相等,只是它们前面有一段相等
    next[0] = -1;
    next[1] = 0;
    if (sub[0] == sub[1])
    {
        next[1] = -1;
    }
    int i = 2;// 从2开始遍历
    int k = 0;// 上一个数的next值,当前也就是next[1],也就是0

    while (i < lenSub)
    {
        // 当sub[i-1]和sub[k]相同时,说明正好可以和前面相同的续上
        // 已知next[i-1] = k,则next[i] = k + 1
        if (k == -1 || sub[i - 1] == sub[k])
        {
            next[i] = k + 1;
            // 如果回退的字符和当前的字符相等,说明一旦这个字符不等于主串的字符,回退的字符也不等于主串的字符
            // 还要回退,这样是很浪费时间的,所以我们直接让这个字符回退到回退字符的回退位置
            if (sub[k + 1] == sub[i])
            {
                next[i] = next[k + 1];
            }
            ++i;
            ++k;
        }
        else
        {
            k = next[k];
        }
    }
}

int KMP(const char* str, const char* sub, int pos = 0)
{
    int cnt = 0;
    int lenStr = strlen(str);
    int lenSub = strlen(sub);

    if (lenStr == 0 || lenSub == 0)
        return -1;
    if (pos < 0 || pos >= lenStr)
        return -1;

    int i = pos;// 记录主串遍历下标
    int j = 0;// 记录模式串的下标
    // 获取next数组
    int* next = (int*)malloc(sizeof(int) * lenSub);
    if (next == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }

    SupGetNext(sub, next, lenSub);

    while (i < lenStr)
    {
        if (j == lenSub)
        {
            j = 0;
            cnt++;
        }
        if (j == -1 || str[i] == sub[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j = next[j];
        }
    }
    free(next);
    if (j == lenSub)
        cnt++;
    return cnt;
}

int main()
{
    string s, t;
    while (cin >> s >> t)
    {
        cout << KMP(s.c_str(), t.c_str()) << endl;
    }
    return 0;
}

6.客似云来


【刷题日记】笔试经典编程题目(六)_第6张图片

原题链接:客似云来

算法思想

  • 本题就是斐波那契数列的变种,直接求数列即可。

代码实现

#include 
#include 
using namespace std;

int main()
{
    int from, to;
    vector<long long> v(81);
    v[1] = 1;
    v[2] = 1;
    for (int i = 3; i <= 80; ++i)
    {
        v[i] = v[i - 1] + v[i - 2];
    }

    while (cin >> from >> to)
    {
        long long sum = 0;
        for (int i = from; i <= to; ++i)
            sum += v[i];
        cout << sum << endl;
    }
    return 0;
}


7.收件人列表


【刷题日记】笔试经典编程题目(六)_第7张图片

原题链接:收件人列表

算法思想

  • 这个题没有思想上的难度,具体实现细节见下面代码:

代码实现

#include 
#include 
#include 
using namespace std;

int main()
{
    int n;
    while (cin >> n)
    {
        vector<string> s(n);
        getchar();// 这一步是避免geiline读到空串
        for (int i = 0; i < n; ++i)
            getline(cin, s[i]);
        for (int i = 0; i < n; ++i)
        {
            int flag = 0;
            for (auto& ch : s[i])
            {
                // 串中有空格和,,进行标记
                if (ch == ' ' || ch == ',')
                    flag = 1;
            }
            // 当串中有空格和, ,并且不为最后一个串时
            if (flag && i != n - 1)
                cout << '"' << s[i] << "\", ";
            // 当串中有空格和, ,并且为最后一个串时
            else if (flag && i == n - 1)
                cout << '"' << s[i] << '"' << endl;
            // 最后一个普通串时
            else if (!flag && i == n - 1)
                cout << s[i] << endl;
            // 普通串
            else
                cout << s[i] << ", ";
        }

    }
    return 0;
}

8.养兔子


【刷题日记】笔试经典编程题目(六)_第8张图片

原题链接:养兔子

算法思想

  • 这个题其实就是一道找规律+动态规划的题目,我们可以现写出前7天月兔子的变化情况。
时间/月份 1 2 3 4 5 6 7
兔子数量 1 2 3 5 8 13 21
  • 观察图表,我们可以得到这样一个动态转移方程: f ( x ) = f ( x − 1 ) + f ( x − 2 ) f(x) = f(x-1)+f(x-2) f(x)=f(x1)+f(x2)
  • 这就是斐波那契数列的变式题。

代码实现

#include 
#include 
using namespace std;

int main()
{
    int n;
    vector<long long> v(91);
    v[0] = 1;
    v[1] = 1;
    for (int i = 2; i <= 90; ++i)
        v[i] = v[i - 1] + v[i - 2];
    while (cin >> n)
    {
        cout << v[n] << endl;
    }
    return 0;
}

9.年会抽奖


【刷题日记】笔试经典编程题目(六)_第9张图片

原题链接:年会抽奖

算法思想

  • 这是一道考验排列组合的题目,这里我们可以使用元素法:无人获奖的概率 = 无人获奖的全部情况数 / 抽奖的所有情况数

  • 设一共有n个人参与抽奖。

  • 抽 奖 的 所 有 情 况 数 = A n n 抽奖的所有情况数 = A_n^n =Ann ,也就是排列数。

  • n个人中无人获奖的情况数我们设为f(n),可以分为以下情况讨论:

    • 前提条件:m的名字没有被m拿到,其他n - 1个人都有可能拿到,即有n - 1种情况。
    • 假设k拿到了m的名字,那么对于k的名字有两种情况:
  • case1 :是k的名字被m拿到了,也就是mk互相拿到了对方的名字,那么对于其他n - 2个人互相拿错又是一个子问题f(n - 2)

  • case2 :是k的名字没有被m拿到,这时要保证在除了kn - 1个人中m不能拿到k的名字,其他人也不能拿到自己的名字,则剩下的问题是子问题f(n - 1)。(此时,在n - 1个人中,相当于m的名字变成了k,而规定自己不能拿自己的名字,所以问题又变成了f(n - 1)。)

  • 因此可得递推公式 f ( n ) = ( n − 1 ) ∗ ( f ( n − 1 ) + f ( n − 2 ) ) f(n) = (n - 1) * (f(n - 1) + f(n - 2)) f(n)=(n1)(f(n1)+f(n2))

  • 易得,f(1) = 0, f(2) = 1

  • 所以 P = f ( n ) / A n n P = f(n) / A_n^n P=f(n)/Ann

代码实现

  • 递归:
#include 
using namespace std;
// 求无人获奖的情况数
long long getFunc(int n)
{
    if (n == 2)
        return 1;
    if (n == 1)
        return 0;
    return (n - 1) * (getFunc(n - 1) + getFunc(n - 2));
}
// 求排列数
long long getSum(int n)
{
    if (n == 1)
        return 1;
    return n * getSum(n - 1);
}

int main()
{
    int n;
    while (cin >> n)
    {
        printf("%.2f%%\n", 1.0 * getFunc(n) / getSum(n) * 100);
    }
    return 0;
}
  • 迭代:
#include 
#include 
using namespace std;

int main()
{
    int n;
    vector<long long> sum(21), v(21);
    sum[1] = 1;
    sum[2] = 2;
    v[1] = 0;
    v[2] = 1;
    for (int i = 3; i <= 20; ++i)
    {
        sum[i] = i * sum[i - 1];
        v[i] = (i - 1) * (v[i - 1] + v[i - 2]);
    }
    while (cin >> n)
    {
        printf("%2.2f%%", 1.0 * v[n] / sum[n] * 100);
    }
    return 0;
}

10.抄送列表


【刷题日记】笔试经典编程题目(六)_第10张图片

原题链接:抄送列表

  • 法一:逐个遍历,控制条件切割

算法思想

  • 思想上没有难度,主要是实现上的细节,具体细节见代码:

代码实现

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    string s, tar;
    while (getline(cin, s))
    {
        getline(cin, tar);
        vector<string> v;// 记录名字
        int flag = 0;// 判断语句是否在""中,0代表不在其中,1代表在其中
        int len = 0;// 记录一个名字的长度
        int begin = 0;// 记录一个名字的起始位置
        for (int i = 0; i < s.size(); ++i)
        {
            // 当在""中,并且此时到了""名字的结尾,遇到了另一个"
            if (flag && s[i] == '"')
            {
                // 将名字保存
                v.push_back(s.substr(begin, len));
                // 重置len和flag
                len = 0;
                flag = 0;
            }
            // 当在""且没有遇到另一个"时,直接名字长度++即可
            else if (flag)
                len++;
            // 当遇到"时,进入""语句
            else if (s[i] == '"')
            {
                // 记录名字开始位置
                begin = i + 1;
                // 切换状态
                flag = 1;
            }
            // 当遇到,并且此时记录的名字长度不为0,说明这个名字到结尾了
            else if (s[i] == ',' && len != 0)
            {
                // 储存
                v.push_back(s.substr(begin, len));
                // 重置len
                len = 0;
                // 调整begin位置
                begin = i + 1;
            }
            // 当遇到,并且此时记录的名字长度为0,说明一个新名字刚刚开始
            else if (s[i] == ',' && len == 0)
            {
                // 调整begin位置
                begin = i + 1;
                continue;                
            }
            // 在两个,间,并且不在""中时
            else
            {
                len++;
            }
        }
        // 当名字长度不为0时,说明此时最后一个名字没有被存储
        if (len != 0)
            v.push_back(s.substr(begin, len));

        if (find(v.begin(), v.end(), tar) != v.end())
            cout << "Ignore" << endl;
        else
            cout << "Important!" << endl;
    }
    return 0;
}
  • 法二:查找切割

算法思想

  • 上一个思路思想较难理解而且代码较长,下面我们换个好理解的思路:

    1. 遇到"直接找下一个"的位置,将""中的子串切割下来,就是一个名字。

    2. 不然,就找下一个,的位置,将两个,间的子串切割,也是一个名字。

代码实现

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    string s, tar;
    while (getline(cin, s))
    {
        getline(cin, tar);
        vector<string> v;
        for (int i = 0; i < s.size(); ++i)
        {
            if (s[i] == '"')
            {
                // 找下一个"
                size_t pos = s.find('"', i + 1);
                // 切割""中间的子串
                v.push_back(s.substr(i + 1, pos - i - 1));
                // 调整i的位置
                i = pos + 1;
            }
            else
            {
                // 找,
                size_t pos = s.find(',', i);
                // 找到了
                if (pos != string::npos)
                {
                    // 切割子串
                    v.push_back(s.substr(i, pos - i));
                    // 调整i的位置
                    i = pos;
                }
                // 到字符串末尾了
                else
                {
                    // 直接切割上一个,到末尾的内容
                    v.push_back(s.substr(i));
                    break;
                }
            }
        }
        if (find(v.begin(), v.end(), tar) != v.end())
            cout << "Ignore" << endl;
        else
            cout << "Important!" << endl;
    }
    return 0;
}

11.Rational Arithmetic


【刷题日记】笔试经典编程题目(六)_第11张图片

原题链接:Rational Arithmetic

算法思想

  • 这个题目的意思就是要实现分数之间的加减乘除,但是有几点要求:
    1. 题目给的数如果为假分数,那么输出时要按照带分数输出。eg. 5 3 − − > 1 2 3 \frac{5}{3} --> 1 \frac{2}{3} 35>132,转化为题目要求输出格式就是 3/5 --> 1 2/3
    2. 所有输出的分数必须为最简形式。eg. 8/6 --> 1 1/3
    3. 负数必须带括号。eg.-7/6 --> (-1 1/6)
    4. 如果除数等于0,那么结果输出Inf。eg. 3/4 / 0 = Inf
  • 这道题我的方法是创建一个类,来实现上述功能,具体实现比较复杂,应该是本系列的题目中最复杂的一个了。
  • 具体实现细节见代码。

代码实现

#include 
using namespace std;

// 分数类
struct rnum
{
    friend ostream& operator<<(ostream& out, const rnum& n);
    // 加法
    void add(rnum& n)
    {
        cout << *this << " + " << n << " = ";
        // 通分
        long comdeno = getMinmul(deno, n.deno);// 公分母
        if (!zero && !n.zero)
        {
            // 通分直接加,构造一个类输出
            rnum ans(fnumer * (comdeno / deno) + n.fnumer * (comdeno / n.deno), comdeno);
            cout << ans << endl;
        }
        else
        {
            if (zero)
                cout << n << endl;
            else
                cout << *this << endl;
        }
    }
	// 乘法
    void mul(rnum& n)
    {
        cout << *this << " * " << n << " = ";
        if (!zero && !n.zero)
        {
            // 直接分数乘法,构造类输出
            rnum ans(fnumer * n.fnumer, deno * n.deno);
            cout << ans << endl;
        }
        else
            cout << 0 << endl;
    }
	// 减法
    void sub(rnum& n)
    {
        cout << *this << " - " << n << " = ";
        // 和加法逻辑差不多
        long comdeno = getMinmul(deno, n.deno);
        if (!zero && !n.zero)
        {
            rnum ans(fnumer * (comdeno / deno) - n.fnumer * (comdeno / n.deno), comdeno);
            cout << ans << endl;
        }
        else
        {
            if (zero)
                cout << rnum(-n.fnumer, n.deno) << endl;
            else
                cout << *this << endl;;
        }
    }
	// 除法
    void div(rnum& n)
    {
        cout << *this << " / " << n << " = ";
        if (!zero && !n.zero)
        {
            // 除法时,特别注意一个点,题目要求符号必须在分子上
            // 所以,新分母如果为负数,必须分子,分母交换符号
            long newfnumer = fnumer * n.deno;
            long newdeno = deno * n.fnumer;
            if (newdeno < 0)
            {
                newfnumer *= -1;
                newdeno *= -1;
            }
            rnum ans(newfnumer, newdeno);
            cout << ans << endl;
        }
        else
        {
            if (zero)
                cout << 0 << endl;
            else
                cout << "Inf" << endl;
        }
    }
	// 获取最小公倍数
    long getMinmul(long a, long b)
    {
        return a * b / getComdiv(a, b);
    }
	// 获取最大公因数
    long getComdiv(long a, long b)
    {
        if (a < b)
            swap(a, b);
        if (a < 0)
            a = -a;
        if (b < 0)
            b = -b;
        long c = 0;
        c = a % b;
        while (c != 0)
        {
            a = b;
            b = c;
            c = a % b;
        }
        return b;
    }
	// 构造函数
    rnum(const long& n1, const long& n2)
        :fnumer(n1)
        , deno(n2)
    {
        zero = fnumer == 0;
        // 非0时
        if (!zero)
        {
            negative = fnumer < 0;
            inter = fnumer / deno;
            rnumer = fnumer - inter * deno;
            // 约分化简
            if (rnumer)
            {
                // 最大公因数
                long com = getComdiv(rnumer, deno);
                if (com != 1)
                {
                    // 化简
                    fnumer /= com;
                    rnumer /= com;
                    deno /= com;
                }
            }
        }
        else
        {
            inter = 0;
            rnumer = 0;
            negative = false;
        }
    }
    
	long fnumer;// 化简前假分数的分子
    long rnumer;// 化简后真分数的分子
    long deno;// 分母
    long inter;// 整数部分
    bool negative;// 是否为负
    bool zero;// 是否为0
};
// 重载<<操作符,输出有理分数
ostream& operator<<(ostream& out, const rnum& n)
{
    // 如果这个数是0,直接输出0
    if (n.zero)
    {
        out << 0;
        return out;
    }
	// 如果这个数是负数
    if (n.negative)
    {
        out << "(-";
        // 如果这个数整数部分和分数部分都不为0
        if (n.inter && n.rnumer)
            out << -n.inter << " " << -n.rnumer << "/" << n.deno << ")";
        // 整数部分为0
        else if (n.rnumer)
            out << -n.rnumer << "/" << n.deno << ")";
        // 分数部分为0
        else
            out << -n.inter << ")";

    }
    // 正数
    else
    {
        // 如果这个数整数部分和分数部分都不为0
        if (n.inter && n.rnumer)
            out << n.inter << " " << n.rnumer << "/" << n.deno;
        // 分数部分为0
        else if (n.inter)
            out << n.inter;
        // 整数部分为0
        else
            out << n.rnumer << "/" << n.deno;
    }
    return out;
}

int main()
{
    int num1, num2;
    scanf("%d/%d", &num1, &num2);
    rnum n1(num1, num2);
    scanf("%d/%d", &num1, &num2);
    rnum n2(num1, num2);
    n1.add(n2);
    n1.sub(n2);
    n1.mul(n2);
    n1.div(n2);
    return 0;
}


12.Pre-Post


【刷题日记】笔试经典编程题目(六)_第12张图片

原题链接:Pre-Post

原题链接:Pre-Post

这道题对于数据结构要求能力比较高,第一次做没有思路很正常。但是其实只要理解了思路,这道题还算比较好实现(比上一道题好实现)。

算法思想

  • 众所周知,二叉树只有给出中序遍历序列 + 前序遍历序列/后序遍历序列才能确定唯一一棵二叉树,但是这道题目就是不给你中序遍历,而只给你前序遍历序列 + 后序遍历序列,并且更变态的一点是,题目要求这棵树是一棵多叉树。输入分叉数 前序遍历序列 后序遍历序列,求满足所给前序遍历序列和后序遍历序列的多叉树所有可能的构型数。
  • 首先,我们得了解为什么中序遍历序列 + 前序遍历序列/后序遍历序列可以唯一构成一棵二叉树

前序遍历 ---- 头节点 左子树 右子树

后序遍历 ---- 左子树 右子树 头节点

中序遍历 ---- 左子树 头节点 右子树

观察上面的遍历顺序,我们发现只有中序遍历的左右子树是在头节点的两侧的,而前序和后序都是头节点在一侧,左右子树在另一侧。

  1. 所以,我们可以在前序遍历序列中找到头节点(就是序列中第一个元素,如果是后序序列,则是最后一个元素)。
  2. 找到头节点后,再在中序序列中找到这个元素,左右两边就是左右子树。
  3. 确定左子树元素个数n后,在前序序列中,头节点后n个元素就是左子树的前序序列,同理可得到右子树的前序序列。
  4. 这样,就得到了左右子树的前序序列和中序序列,再重复1,2,3过程,就可以得到一棵二叉树。
  • 所以,中序序列起着划分二叉树左右子树的作用,现在没有中序序列(当然,多叉树没有中序序列),就得依靠前序序列和后序序列的划分子树以及位置。
  • 那么前序序列和后序序列如何划分子树呢?

前序遍历 ---- 头节点 左子树 右子树

后序遍历 ---- 左子树 右子树 头节点

一定要记住上面的遍历顺序,这很关键!

我们用一道例题来讲解:

输入:13 abejk jkeba

【刷题日记】笔试经典编程题目(六)_第13张图片

  • 总结一下:
    • 前序序列确定根,到后序序列中找根元素,确定此根元素子树的所有结点(也可以说确定这棵子树的长度)。
    • 通过子树的排列组合确定树的可能构型。
#include 
#include 
#include 
using namespace std;

// 获取组合数,使用动态规划获取组合数
// 使用的递推规则是组合数公式:C[n][m] = c[n - 1][m - 1] + c[n - 1][m]
void getCombNum(vector<vector<int>>& c)
{
	c[1][0] = c[1][1] = 1;
	for (int i = 2; i < 21; ++i)
		c[i][0] = 1;
	for (int i = 2; i < 21; ++i)
	{
		for (int j = 1; j < 21; ++j)
		{
			c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
 		}
	}
}

// 计算树的可能构型
int calTreeNum(string pre, string post, int n, vector<vector<int>>& c)
{
    // 当序列为空或者为1(叶子节点),返回1
	if (pre.size() == 1 || pre.empty())
		return 1;
	int prei = 1, posti = 0;// 前序遍历指针,后序遍历指针
	int num = 1;// 构型数
	int cnt = 0;// 子树数量
	
    // 遍历完前序序列
	while (prei < pre.size())
	{
        // 在后序序列中找根结点
		int pos = posti;
		while (pos < post.size() - 1)
		{
			if (pre[prei] == post[pos])
				break;
			else
				pos++;
		}
		cnt++;// 子树数量+1
		int len = pos - posti + 1;// 确定子树长度
		num *= calTreeNum(pre.substr(prei, len), post.substr(posti, len), n, c);// 递归计算子树可能构型数
		prei += len;// 下一个根结点
		posti += len;// 下一个查找位置
	}
	return num * c[n][cnt];
}

int main()
{
	vector<vector<int>> c(21, vector<int>(21, 0));
	getCombNum(c);
	int n;
	string pre, post;
	while (cin >> n >> pre >> post)
	{
		cout << calTreeNum(pre, post, n, c) << endl;
	}
	return 0;
}

后记


本次题目难度有提升很多,这次题目主要集中在数据结构条件控制这两个面试最爱考的题目,比较考验大家的逻辑思维以及代码实现能力,相信大家做完会有所收获。

这个是一个新的系列——《笔试经典编程题目》,隶属于【刷题日记】系列,白晨开这个系列的目的是向大家分享经典的笔试编程题,以便于大家参考,查漏补缺以及提高代码能力。如果你喜欢这个系列的话,不如关注白晨,更快看到更新呀。

本文是笔试经典编程题目的第六篇,如果喜欢这个系列的话,不如订阅【刷题日记】系列专栏,更快看到更新哟


如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。

如果大家喜欢这个系列,还请大家多多支持啦!

如果这篇文章有帮到你,还请给我一个大拇指小星星 ⭐️支持一下白晨吧!喜欢白晨【刷题日记】系列的话,不如关注白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。

你可能感兴趣的