乘法快速幂 如果需要求x^n，那么最简单的办法就是对x进行n次相乘，此时时间复杂度为O（n），而乘法快速幂可以是O（logn）的复杂度 举例 假设需要求10^75,首先对75转为二进制，为 对于该例子，x=10，设置每次x = x*x 设置一个ans变量，每次有有选择地乘以x，具体选择方式如下： x=10^1，发现1对应的二进制那一位为1，则ans = 1*x= 1*10^1 然后x=x*x，即x=10^2，发现对应的二进制那一位为1，则ans = 1*10^1*10^2 然后x=x*x，即x=10^4，发现对应的二进制那一位为0，则ans = 1*10^1*10^2，ans不变 以此类推，总结下来就是先把n变成二进制，然后x每次都与自己相乘，遇到二进制对应的位为1就乘进ans里面，否则ans不动 对应的java代码如下： public static long power(long x, long n,long mod) { long res = 1; while (n > 0) { if ((n & 1)!= 0) res = (res * x) % mod; n = n >> 1; x = (x * x) % mod; } return res; }
实验五 一. 7-1 部分排序 对于一组数据，我们可以只对原先处在中间位置的那些元素进行排序。 输入格式: 在一行内输入n r a1 a2 ... an 其中，不大于200的正整数n表示该组数据的个数；不大于200的非负整数r表示该组数据两端各自留有r个数不参与排序，若r+r>=n，则该组数据无需排序。 整数a1 a2 ... an是该组的n个数据，且都在8位以内。 输出格式: 排序之后的序列，元素之间用一个空格间隔，最后一个元素之后不加空格。 输入样例: 5 1 6 5 4 3 2 输出样例: 6 3 4 5 2 参考答案： #include<iostream> #include<vector> using namespace std; int main(){ int n,r; cin>>n>>r; vector<int>ivec; int num; for(int i=0;i<n;i++){ cin>>num; ivec.push_back(num); } if(r+r<n){ sort(ivec.begin()+r,ivec.begin()+n-r); } bool isHead = true; for(auto &c:ivec){ if(isHead){ cout<<c; isHead = !isHead; } else{ cout<<" "<<c; } } cout<<endl; return 0; } 二. 7-2 组最大数 设有n个正整数，将他们连接成一排，组成一个最大的多位整数。 如:n=3时，3个整数13,312,343连成的最大整数为34331213。 如:n=4时，4个整数7,13,4,246连接成的最大整数为7424613。 输入格式: 有多组测试样例，每组测试样例包含两行，第一行为一个整数N（N<=100），第二行包含N个数(每个数不超过1000，空格分开)。 输出格式: 每组数据输出一个表示最大的整数。 输入样例: 2 12 123 4 7 13 4 246 输出样例: 12312 7424613 参考答案： #include<iostream> #include<vector> using namespace std; bool compare(int a,int b){ string sa = to_string(a); string sb = to_string(b); return sa + sb > sb + sa; } int main(){ int n; while(cin>>n){ vector<int>ivec; int num; for(int i=0;i<n;i++){ cin>>num; ivec.push_back(num); } sort(ivec.begin(),ivec.end(),compare); for(auto &s:ivec){ cout<<s; } ivec.clear(); cout<<endl; } return 0; } 三. 7-3 办事大厅排队 在郑州大学综合办事大厅，每天陆陆续续有很多人来排队办事。现在你能否写程序帮助老师时刻了解当前办理业务的情况。 请同学们学习C++ STL中 list相关内容后，编程实践。 输入格式: 第一行一个数字N，表示排队信息或者查询信息条目的数量。 以下N行，每行的内容有以下3种情况 (1) in name 表示名字为name的人员新来到办事大厅，排在队伍的最后。（in和name间存在一个空格，name是名字对应字符串，长度不超过10）。 (2) out 表示当前排在最前面的人已经办理完业务，离开了。 (3) q 表示一次查询，请输出当前正在办理业务的人，也就是队伍的第1个人。如果当前无人办理业务，则输出“NULL”，不包括引号。 输出格式: 请根据以上信息，每次遇到查询时，对应一行输出。如果这时队伍有人，则输出第一个人的姓名，否则输出NULL。 输入样例: 5 in A out q in B q 输出样例: 在这里给出相应的输出。例如： NULL B 参考答案： #include<iostream> #include<vector> #include<deque> using namespace std; int main(){ int n; cin>>n; deque<string>person; string op,name; while(n-->0){ cin>>op; if(op=="q"){ if(person.size()>0){ cout<<person.front()<<endl; } else{ cout<<"NULL"<<endl; } } else if(op=="out"){ person.pop_front(); } else{ cin>>name; person.push_back(name); } } return 0; } 四. 7-4 括号匹配 给定仅包含“()[]{}”六种括号的字符串，请你判断该字符串中，括号的匹配是否是合法的，也就是对应括号的数量、嵌套顺序完全正确。 输入格式: 第一行一个整数T（T<=10） 其后T行每行一个字符串只包含[{()}]六种字符（字符串长度2e5以内） 输出格式: 对于每个字符串，匹配输出Yes,否则输出No 输入样例: 2 {()[]} ([)] 输出样例: Yes No 参考答案： #include<iostream> #include<stack> using namespace std; int main(){ stack<char>st; int n; cin>>n; string s; while(n-->0){ cin>>s; bool flag = false; for(auto &c:s){ if(c=='{'||c=='['||c=='('){ st.push(c); } else if(c=='}'){ if(st.empty()||st.top()!='{'){ cout<<"No"<<endl; flag = true; break; } if(!st.empty())st.pop(); } else if(c==']'){ if(st.empty()||st.top()!='['){ cout<<"No"<<endl; flag = true; break; } if(!st.empty())st.pop(); } else if(c==')'){ if(st.empty()||st.top()!='('){ cout<<"No"<<endl; flag = true; break; } if(!st.empty())st.pop(); } } if(!flag&&st.size()>0){ cout<<"No"<<endl; } else if(!flag){ cout<<"Yes"<<endl; } while(!st.empty()){ st.pop(); } } return 0; } 五. 7-5 找出不是两个数组共有的元素 给定两个整型数组，本题要求找出不是两者共有的元素。 输入格式: 输入分别在两行中给出两个整型数组，每行先给出正整数N（≤20），随后是N个整数，其间以空格分隔。 输出格式: 在一行中按照数字给出的顺序输出不是两数组共有的元素，数字间以空格分隔，但行末不得有多余的空格。题目保证至少存在一个这样的数字。同一数字不重复输出。 输入样例: 10 3 -5 2 8 0 3 5 -15 9 100 11 6 4 8 2 6 -5 9 0 100 8 1 输出样例: 3 5 -15 6 4 1 参考答案： #include<iostream> #include<unordered_map> #include<deque> using namespace std; int main(){ int n1,n2; unordered_map<int,int>imap; deque<int>row2; cin>>n1; int num; while(n1-->0){ cin>>num; imap[num] = 1; } cin>>n2; while(n2-->0){ cin>>num; if(imap.count(num)){ imap[num] = 0; } else{ if(find(row2.begin(),row2.end(),num)==row2.end()){ row2.push_back(num); } } } for(const auto& pair:imap){ if(pair.second==1){ if(find(row2.begin(),row2.end(),pair.first)==row2.end()){ row2.push_front(pair.first); } } } bool head = true; for(auto& c:row2){ if(head){ cout<<c; head = false; } else{ cout<<" "<<c; } } return 0; }
实验四 一. 7-1 简单求阶乘问题 本题要求编写程序，计算N的阶乘。 输入格式: 输入在一行中给出一个不超过12的正整数N。 输出格式: 在一行中输出阶乘的值。 输入样例: 4 输出样例: 24 参考答案： #include<iostream> using namespace std; int f(int n){ if(n==1||n==0){ return 1; } return n*f(n-1); } int main(){ int n; cin>>n; cout<<f(n); return 0; } 二. 7-2 出生年 以上是新浪微博中一奇葩贴：“我出生于1988年，直到25岁才遇到4个数字都不相同的年份。”也就是说，直到2013年才达到“4个数字都不相同”的要求。本题请你根据要求，自动填充“我出生于y年，直到x岁才遇到n个数字都不相同的年份”这句话。 输入格式： 输入在一行中给出出生年份y和目标年份中不同数字的个数n，其中y在[1, 3000]之间，n可以是2、或3、或4。注意不足4位的年份要在前面补零，例如公元1年被认为是0001年，有2个不同的数字0和1。 输出格式： 根据输入，输出x和能达到要求的年份。数字间以1个空格分隔，行首尾不得有多余空格。年份要按4位输出。注意：所谓“n个数字都不相同”是指不同的数字正好是n个。如“2013”被视为满足“4位数字都不同”的条件，但不被视为满足2位或3位数字不同的条件。 输入样例1： 1988 4 输出样例1： 25 2013 输入样例2： 1 2 输出样例2： 0 0001 参考答案： #include<iostream> #include<set> using namespace std; bool isValid(int Y,int num){ string s = to_string(Y); set<char>mySet; if(s.size()<4){ for(char c:s){ mySet.insert(c); } mySet.insert('0'); return mySet.size()==num; } for(char c:s){ mySet.insert(c); } return mySet.size()==num; } string toString(int num){ string s = "0000"; string origin = to_string(num); int i = origin.size()-1; int j = s.size()-1; while(i>=0){ s[j--] = origin[i--]; } return s; } int main(){ int Y,num; cin>>Y>>num; int index = 0; while(true){ if(isValid(Y+index,num)){ cout<<index<<" "<<toString(Y+index); return 0; } index++; } return 0; } 三. 7-3 验证“哥德巴赫猜想” 数学领域著名的“哥德巴赫猜想”的大致意思是：任何一个大于2的偶数总能表示为两个素数之和。比如：24=5+19，其中5和19都是素数。本实验的任务是设计一个程序，验证20亿以内的偶数都可以分解成两个素数之和。 输入格式： 输入在一行中给出一个(2, 2 000 000 000]范围内的偶数N。 输出格式： 在一行中按照格式“N = p + q”输出N的素数分解，其中p ≤ q均为素数。又因为这样的分解不唯一（例如24还可以分解为7+17），要求必须输出所有解中p最小的解。 输入样例： 24 输出样例： 24 = 5 + 19 参考答案： #include<iostream> #include<cmath> using namespace std; bool isValid(long long n){ if(n<2)return false; for(long long i=2;i*i<=n;i++){ if(n%i==0)return false; } return true; } int main(){ long long n; cin>>n; for(long long i=2;i<=n/2;i++){ if(isValid(i)&&isValid(n-i)){ cout<<n<<" = "<<i<<" + "<<n-i; return 0; } } return 0; } 四. 7-4 谷歌的招聘 2004 年 7 月，谷歌在硅谷的 101 号公路边竖立了一块巨大的广告牌（如下图）用于招聘。内容超级简单，就是一个以 .com 结尾的网址，而前面的网址是一个 10 位素数，这个素数是自然常数 e 中最早出现的 10 位连续数字。能找出这个素数的人，就可以通过访问谷歌的这个网站进入招聘流程的下一步。 自然常数 e 是一个著名的超越数，前面若干位写出来是这样的：e = 2.718281828459045235360287471352662497757247093699959574966967627724076630353547594571382178525166427427466391932003059921... 其中粗体标出的 10 位数就是答案。 本题要求你编程解决一个更通用的问题：从任一给定的长度为 L 的数字中，找出最早出现的 K 位连续数字所组成的素数。 输入格式： 输入在第一行给出 2 个正整数，分别是 L（不超过 1000 的正整数，为数字长度）和 K（小于 10 的正整数）。接下来一行给出一个长度为 L 的正整数 N。 输出格式： 在一行中输出 N 中最早出现的 K 位连续数字所组成的素数。如果这样的素数不存在，则输出 404。注意，原始数字中的前导零也计算在位数之内。例如在 200236 中找 4 位素数，0023 算是解；但第一位 2 不能被当成 0002 输出，因为在原始数字中不存在这个 2 的前导零。 输入样例 1： 20 5 23654987725541023819 输出样例 1： 49877 输入样例 2： 10 3 2468001680 输出样例 2： 404 参考答案： #include<iostream> using namespace std; bool isValid(long long n){ if(n<2)return false; for(long long i=2;i*i<=n;i++){ if(n%i==0)return false; } return true; } int main(){ int L,N; cin>>L>>N; if(L<N){ cout<<"404"; return 0; } string s; cin>>s; int pos = 0; while(pos<=s.size()-N){ string sub = s.substr(pos++,N); if(isValid(stol(sub))){ cout<<sub; return 0; } } cout<<"404"; return 0; } 五. 7-5 估值一亿的AI核心代码 以上图片来自新浪微博。 本题要求你实现一个稍微更值钱一点的 AI 英文问答程序，规则是： 无论用户说什么，首先把对方说的话在一行中原样打印出来； 消除原文中多余空格：把相邻单词间的多个空格换成 1 个空格，把行首尾的空格全部删掉，把标点符号前面的空格删掉； 把原文中所有大写英文字母变成小写，除了 I； 把原文中所有独立的 can you、could you 对应地换成 I can、I could—— 这里“独立”是指被空格或标点符号分隔开的单词； 把原文中所有独立的 I 和 me 换成 you； 把原文中所有的问号 ? 换成惊叹号 !； 在一行中输出替换后的句子作为 AI 的回答。 输入格式： 输入首先在第一行给出不超过 10 的正整数 N，随后 N 行，每行给出一句不超过 1000 个字符的、以回车结尾的用户的对话，对话为非空字符串，仅包括字母、数字、空格、可见的半角标点符号。 输出格式： 按题面要求输出，每个 AI 的回答前要加上 AI: 和一个空格。 输入样例： 6 Hello ? Good to chat with you can you speak Chinese? Really? Could you show me 5 What Is this prime? I,don 't know 输出样例： Hello ? AI: hello! Good to chat with you AI: good to chat with you can you speak Chinese? AI: I can speak chinese! Really? AI: really! Could you show me 5 AI: I could show you 5 What Is this prime? I,don 't know AI: what Is this prime! you,don't know 参考答案： #include<iostream> using namespace std; string process(string s){ while(s[0]==' ') s.erase(s.begin());//去掉句首的空格 while(s.back()==' ') s.erase(s.end()-1);//去掉句尾的空格 for(int i=0;i<s.size();i++) { if(s[i]==' ') { while(s[i+1]==' ') s.erase(s.begin()+i+1);//句中多个空格，只保留一个 if(!isalnum(s[i+1])) s.erase(s.begin()+i);//去掉符号前面的空格 } } for(auto &x:s) { if(x>='A'&&x<='Z'&&x!='I') x=x-'A'+'a';//大写转化为小写 } for(int i=0;;i++) { i=s.find("can you",i);//查找"can you"出现的位置 if(i==-1) break;//没有找到 //位置为0或者当前位置前不能为字母或者数字，并且后面一个字符也不能是字母或者数字 if((i==0||!isalnum(s[i-1]))&&(i==s.size()-7||!isalnum(s[i+7]))) s.replace(i,7,"@ can");//将i后7个位置替换成"@ can" } for(int i=0;;i++) { i=s.find("could you",i); if(i==-1) break; if((i==0||!isalnum(s[i-1]))&&(i==s.size()-9||!isalnum(s[i+9]))) s.replace(i,9,"@ could"); } for(int i=0;;i++) { i=s.find("I",i); if(i==-1) break; if((i==0||!isalnum(s[i-1]))&&(i==s.size()-1||!isalnum(s[i+1]))) s.replace(i,1,"you"); } for(int i=0;;i++) { i=s.find("me",i); if(i==-1) break; if((i==0||!isalnum(s[i-1]))&&(i==s.size()-2||!isalnum(s[i+2]))) s.replace(i,2,"you"); } for(auto &x:s) { if(x=='@') x='I';//因为I会换成you所以先换成@然后再换回来 if(x=='?') x='!'; } return s; } int main(){ int n; cin>>n; getchar(); while(n-->0){ string s; getline(cin,s); cout<<s<<endl; cout<<"AI: "<<process(s)<<endl; } return 0; }
实验三 一. 7-1 求最大值及其下标 本题要求编写程序，找出给定的n个数中的最大值及其对应的最小下标（下标从0开始）。 输入格式: 输入在第一行中给出一个正整数n（1<n≤10）。第二行输入n个整数，用空格分开。 输出格式: 在一行中输出最大值及最大值的最小下标，中间用一个空格分开。 输入样例: 6 2 8 10 1 9 10 输出样例: 10 2 参考答案： #include<iostream> #include<climits> using namespace std; int main(){ int n; int index = 0; cin>>n; int max = INT_MIN; for(int i=0;i<n;i++){ int num; cin>>num; if(num>max){ index = i; max = num; } } cout<<max<<" "<<index; return 0; } 二. 7-2 统计字符出现次数 本题要求编写程序，统计并输出某给定字符在给定字符串中出现的次数。 输入格式： 输入第一行给出一个以回车结束的字符串（少于80个字符）；第二行输入一个字符。 输出格式： 在一行中输出给定字符在给定字符串中出现的次数。 输入样例： programming is More fun! m 输出样例： 2 参考答案： #include<stdio.h> #include<string.h> int main() { int i,len,count=0; char str[80]; char ch; gets(str);//输入str ch=getchar();//输入ch len=strlen(str);//str字符串的长度 for(i=0;i<len;i++) { if(str[i]==ch) count++; } printf("%d",count); return 0; } 三. 7-3 出租 下面是新浪微博上曾经很火的一张图： 一时间网上一片求救声，急问这个怎么破。其实这段代码很简单，index数组就是arr数组的下标，index[0]=2 对应 arr[2]=1，index[1]=0 对应 arr[0]=8，index[2]=3 对应 arr[3]=0，以此类推…… 很容易得到电话号码是18013820100。 本题要求你编写一个程序，为任何一个电话号码生成这段代码 —— 事实上，只要生成最前面两行就可以了，后面内容是不变的。 输入格式： 输入在一行中给出一个由11位数字组成的手机号码。 输出格式： 为输入的号码生成代码的前两行，其中arr中的数字必须按递减顺序给出。 输入样例： 18013820100 输出样例： int[] arr = new int[]{8,3,2,1,0}; int[] index = new int[]{3,0,4,3,1,0,2,4,3,4,4}; 参考答案： #include<iostream> #include<climits> #include<vector> #include<set> using namespace std; void print(vector<int>vec){ std::for_each(vec.begin(), vec.end(), [](int val) { std::cout << val << " "; }); cout<<endl; } int main(){ string tel; cin>>tel; set<int,greater<int>>mySet; for(char c:tel){ mySet.insert(c-'0'); } vector<int>index(10,0); int pos = 0; for(int val:mySet){ index[val] = pos; pos++; } vector<int>ivec; for(char c:tel){ ivec.push_back(index[c-'0']); } cout<<"int[] arr = new int[]{"; bool isHead = true; for(auto i:mySet){ if(isHead){ cout<<i; isHead = !isHead; } else{ cout<<","<<i; } } cout<<"};"<<endl; cout<<"int[] index = new int[]{"; isHead = true; for(auto i:ivec){ if(isHead){ cout<<i; isHead = !isHead; } else{ cout<<","<<i; } } cout<<"};"; return 0; } 四. 7-4 找鞍点 一个矩阵元素的“鞍点”是指该位置上的元素值在该行上最大、在该列上最小。 本题要求编写程序，求一个给定的n阶方阵的鞍点。 输入格式： 输入第一行给出一个正整数n（1≤n≤6）。随后n行，每行给出n个整数，其间以空格分隔。 输出格式： 输出在一行中按照“行下标 列下标”（下标从0开始）的格式输出鞍点的位置。如果鞍点不存在，则输出“NONE”。题目保证给出的矩阵至多存在一个鞍点。 输入样例1： 4 1 7 4 1 4 8 3 6 1 6 6 2 0 7 8 9 输出样例1： 2 1 输入样例2： 2 1 7 4 1 输出样例2： NONE 参考答案： #include<iostream> #include<climits> #include<vector> using namespace std; int main(){ int n; cin>>n; vector<int>rowMax(n,INT_MIN); vector<int>colMin(n,INT_MAX); vector<vector<int>>arr(n,vector<int>(n)); for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ int num; cin>>num; arr[i][j] = num; rowMax[i] = num>rowMax[i]?num:rowMax[i]; colMin[j] = num<colMin[j]?num:colMin[j]; } } for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ if(arr[i][j]==rowMax[i]&&arr[i][j]==colMin[j]){ cout<<i<<" "<<j; return 0; } } } cout<<"NONE"; return 0; } 五. 7-5 小偷踩点 俗话说不怕贼偷，就怕贼惦记。 小偷在作案前有时会在居民家的门、墙上做一些标记，每一种记号代表一个含义，一般人看不懂，但同行一看便知道这个家庭的情况。不过派出所干警也不是吃素的，很快破译了这些记号的含义（如上图），并且在辖区内广为张贴，告知居民。 随后小偷们又改变了方法，将这些记号从 1 到 N 编号，然后将这些编号按照某种规则重新打乱再做标记，标记变成了一串数字。不过这种新的编号方法又被破译了！干警们发现这些数字的规律可以用一个二维矩阵来表示：矩阵有 10 列，顺序对应数字 0 到 9；矩阵一般不超过 10 行，每行对应一个 0 到 9 之间的数字，这些数字保证不重复。小偷的新标记由若干个两位数组成，每个数字的十位对应行、个位对应列，而对应位置上的数字就是原始标记的编号。 如上图 40 种标记从上到下、从左到右顺序编号后，按下图所示的规律打乱，则如果我们看到标记“71”，就是行标记为 7，列标记为 1 的单元格对应的数字 11，对应原始标记中第 11 个，即“很有钱”。那么标记“71 78 57”就表示原始标记的第 11、8、7 号，意思是“很有钱”、“没有防范”、“计划行动”。 本题就请你编写程序，自动破译小偷的新标记。 输入格式： 输入第一行给出 2 个正整数：N（≤100）为小偷的原始标记个数，M（≤10）为新标记对照矩阵的行数。 随后 N 行，第 i 行给出第 i 个标记的解释，由不超过 100 个英文字母和空格组成。 接下来一行给出 M 个数字，为 0 到 9 之间的数字，保证不重复，其中第 i 个数对应矩阵第 i 行。 接下来 M 行，每行给出 10 个数字，或者是 1 到 N 之间的一个编号，或者是 −1 表示没有对应的编号。 最后一行给出小偷留在墙上的数字标记，格式为： k t[1] ... t[k] 其中 k 是数字个数（不超过 N），后面跟着 k 个数字。 输出格式： 对小偷留下的每个数字，在一行中输出其对应的意义，顺序与输入顺序相同。如果没有对应的意义，则在对应行中输出 ?。 输入样例： 10 2 jia li you ren kong fang zi jia you e gou dan shen hen you qian xiao xin lin ju you bao jing qi jin kuai dong shou xia ci zai lai bu bi jin ru 6 2 -1 6 5 1 -1 10 3 4 -1 9 2 4 7 -1 3 -1 5 -1 8 2 5 20 64 61 22 13 输出样例： kong fang zi ? xiao xin lin ju you bao jing qi ? 参考答案： #include<iostream> #include<climits> #include<vector> using namespace std; int main(){ int N,M; cin>>N>>M; getchar(); vector<string>original; string line; for(int i=0;i<N;i++){ getline(cin,line); original.push_back(line); } vector<int>index(10,-1); for(int i=0;i<M;i++){ int num; cin>>num; index[num] = i; } vector<vector<int>>arr(M,vector<int>(10)); for(int i=0;i<M;i++){ for(int j=0;j<10;j++){ cin>>arr[i][j]; } } int k; cin>>k; for(int i=0;i<k;i++){ int sign; cin>>sign; int row = sign/10; int col = sign%10; if(index[row]==-1){ cout<<"?"<<endl; continue; } if(arr[index[row]][col]==-1){ cout<<"?"<<endl; } else{ cout<<original[arr[index[row]][col]-1]<<endl; } } return 0; }
实验二 一. 7-1 奇偶分家 给定N个正整数，请统计奇数和偶数各有多少个？ 输入格式： 输入第一行给出一个正整N（≤1000）；第2行给出N个非负整数，以空格分隔。 输出格式： 在一行中先后输出奇数的个数、偶数的个数。中间以1个空格分隔。 输入样例： 9 88 74 101 26 15 0 34 22 77 输出样例： 3 6 参考答案： #include<iostream> using namespace std; int main(){ int n; int odd = 0; //奇数 int even = 0; //偶数 cin>>n; for(int i=0;i<n;i++){ int num; cin>>num; if(num%2==0){ even++; } else{ odd++; } } cout<<odd<<" "<<even; return 0; } 二. 7-1 水仙花数 水仙花数是指一个N位正整数（N≥3），它的每个位上的数字的N次幂之和等于它本身。例如：153=13+53+33。 本题要求编写程序,计算所有N位水仙花数。 输入格式: 输入在一行中给出一个正整数N（3≤N≤7）。 输出格式: 按递增顺序输出所有N位水仙花数，每个数字占一行。 输入样例: 3 输出样例: 153 370 371 407 参考答案: #include<iostream> #include<cmath> using namespace std; bool isValid(int num,int N){ int sum = 0; int tmp = num; while(tmp!=0){ int left = tmp%10; sum+=pow(left,N); tmp/=10; } return sum == num; } int main(){ int N; cin>>N; for(int i=pow(10,N-1);i<pow(10,N);i++){ if(isValid(i,N)){ cout<<i<<endl; } } return 0; } 三. 7-2 猜数字游戏 猜数字游戏是令游戏机随机产生一个100以内的正整数，用户输入一个数对其进行猜测，需要你编写程序自动对其与随机产生的被猜数进行比较，并提示大了（“Too big”），还是小了（“Too small”），相等表示猜到了。如果猜到，则结束程序。程序还要求统计猜的次数，如果1次猜出该数，提示“Bingo!”；如果3次以内猜到该数，则提示“Lucky You!”；如果超过3次但是在N（>3）次以内（包括第N次）猜到该数，则提示“Good Guess!”；如果超过N次都没有猜到，则提示“Game Over”，并结束程序。如果在到达N次之前，用户输入了一个负数，也输出“Game Over”，并结束程序。 输入格式: 输入第一行中给出两个不超过100的正整数，分别是游戏机产生的随机数、以及猜测的最大次数N。最后每行给出一个用户的输入，直到出现负数为止。 输出格式: 在一行中输出每次猜测相应的结果，直到输出猜对的结果或“Game Over”则结束。 输入样例: 58 4 70 50 56 58 60 -2 输出样例: Too big Too small Too small Good Guess! 参考答案： #include<iostream> using namespace std; int main(){ int target,N; cin>>target>>N; int num; cin>>num; int cnt = 1; while(num>=0){ if(cnt>N){ cout<<"Game Over"; return 0; } if(num==target){ if(cnt == 1){ cout<<"Bingo!"; return 0; } else if(cnt<=3){ cout<<"Lucky You!"; return 0; } else if(cnt<=N){ cout<<"Good Guess!"; return 0; } } else if(num>target){ cout<<"Too big"<<endl; cnt++; }else { cout<<"Too small"<<endl; cnt++; } cin>>num; } cout<<"Game Over"; return 0; } 四. 7-4 阶梯电价 为了提倡居民节约用电，某省电力公司执行“阶梯电价”，安装一户一表的居民用户电价分为两个“阶梯”：月用电量50千瓦时（含50千瓦时）以内的，电价为0.53元/千瓦时；超过50千瓦时的，超出部分的用电量，电价上调0.05元/千瓦时。请编写程序计算电费。 输入格式: 输入在一行中给出某用户的月用电量（单位：千瓦时）。 输出格式: 在一行中输出该用户应支付的电费（元），结果保留两位小数，格式如：“cost = 应付电费值”；若用电量小于0，则输出"Invalid Value!"。 输入样例1: 10 输出样例1: cost = 5.30 输入样例2: 100 输出样例2: cost = 55.50 参考答案： #include<iostream> #include<iomanip> using namespace std; int main(){ int elec; cin>>elec; double cost = 0; if(elec<0){ cout<<"Invalid Value!"; return 0; } if(elec<=50){ cost+=elec*0.53; } else{ cost+=0.53*50; cost+=(elec-50)*0.58; } cout<<fixed<<setprecision(2)<<"cost = "<<cost; return 0; } 五. 7-5 阅览室 天梯图书阅览室请你编写一个简单的图书借阅统计程序。当读者借书时，管理员输入书号并按下S键，程序开始计时；当读者还书时，管理员输入书号并按下E键，程序结束计时。书号为不超过1000的正整数。当管理员将0作为书号输入时，表示一天工作结束，你的程序应输出当天的读者借书次数和平均阅读时间。 注意：由于线路偶尔会有故障，可能出现不完整的纪录，即只有S没有E，或者只有E没有S的纪录，系统应能自动忽略这种无效纪录。另外，题目保证书号是书的唯一标识，同一本书在任何时间区间内只可能被一位读者借阅。 输入格式： 输入在第一行给出一个正整数N（≤10），随后给出N天的纪录。每天的纪录由若干次借阅操作组成，每次操作占一行，格式为： 书号（[1, 1000]内的整数） 键值（S或E） 发生时间（hh:mm，其中hh是[0,23]内的整数，mm是[0, 59]内整数） 每一天的纪录保证按时间递增的顺序给出。 输出格式： 对每天的纪录，在一行中输出当天的读者借书次数和平均阅读时间（以分钟为单位的精确到个位的整数时间）。 输入样例： 3 1 S 08:10 2 S 08:35 1 E 10:00 2 E 13:16 0 S 17:00 0 S 17:00 3 E 08:10 1 S 08:20 2 S 09:00 1 E 09:20 0 E 17:00 输出样例： 2 196 0 0 1 60 参考答案： #include<iostream> #include<cmath> using namespace std; int parseTime(string time){ int res = 0; int tmp = 0; for(char c:time){ if(c==':'){ res = tmp*60; tmp = 0; } else{ tmp = tmp*10+c-'0'; } } return res+tmp; } void clear(int arr[1001][2]){ for(int i=0;i<1001;i++){ for(int j=0;j<2;j++){ arr[i][j] = 0; } } } int main(){ int N; cin>>N; int days = 1; int homeNum; char op; string time; int chs[1001][2]; // 清空数组 clear(chs); int cnt = 0; int totalM = 0; while(days<=N){ cin>>homeNum>>op>>time; // 当前输入转换后的分钟数; int curM = parseTime(time); // cout<<"curM = "<<curM<<endl; // 设S为1，E为2 if(op=='S'){ chs[homeNum][0]=1; chs[homeNum][1] = curM; } else{ if(chs[homeNum][0]==1){ cnt++; totalM += curM - chs[homeNum][1]; chs[homeNum][0]=0; chs[homeNum][1]=0; } } // 结束一天的标志，开始统计并输出结果; if(homeNum == 0){ days++; cout<<cnt<<" "<<(cnt==0?0:round(totalM*1.0/cnt))<<endl; // 清除数据 cnt = 0; totalM = 0; clear(chs); } } return 0; }
实验一 一. 7-1 3人分糖果 幼儿园里，有3个小朋友编号分别是1、2、3，他们按自己的编号顺序围坐在一张圆桌旁，每个小朋友的面前分别有8、9、10颗糖果。现在做一个分糖果游戏，从1号小朋友开始，将自己的糖果平均分成三份（如果有多余的糖果，则自己立刻吃掉），自己留一份，其余两份分给相邻座位的两个小朋友。接着，2号、3号小朋友也同样这么做。请问一轮后，每个小朋友面前分别有多少糖果？ 输入格式: 本题目没有输入。 输出格式: 在一行内显示以下结果。注意行末除了换行符不能有任何多余字符。 10 8 5 输入样例: 本题目没有输入。 输出样例: 10 8 5 参考答案： #include<iostream> using namespace std; int main(){ cout<<10<<" "<<8<<" "<<5; return 0; } 二. 什么是机器学习 什么是机器学习？上图展示了一段面试官与“机器学习程序”的对话： 面试官：9 + 10 等于多少？ 答：3 面试官：差远了，是19。 答：16 面试官：错了，是19。 答：18 面试官：不，是19。 答：19 本题就请你模仿这个“机器学习程序”的行为。 输入格式： 输入在一行中给出两个整数，绝对值都不超过 100，中间用一个空格分开，分别表示面试官给出的两个数字 A 和 B。 输出格式： 要求你输出 4 行，每行一个数字。第 1 行比正确结果少 16，第 2 行少 3，第 3 行少 1，最后一行才输出 A+B 的正确结果。 输入样例： 9 10 输出样例： 3 16 18 19 参考答案： #include<iostream> using namespace std; int main(){ int a,b; cin>>a>>b; int sum = a+b; cout<<sum-16<<endl; cout<<sum-3<<endl; cout<<sum-1<<endl; cout<<sum; return 0; } 三. 7-3 鸡兔同笼 《孙子算经》中记载了一个有趣的问题：“今有雉兔同笼，上有三十五头，下有九十四足，问雉兔各几何？”根据上述描述可列出如下形式的二元一次方程组。 {x+y=352x+4y=94​ 那么求解鸡兔同笼问题就转化成了二元一次方程组求解问题。本题要求编写程序对任意给定的头数和足数判断鸡和兔各有多少。 输入格式: 在一行中给出任意两个非负整数，整数之间用空格分开，第一个整数表示头数，第二个整数表示足数。 输出格式: 在一行中按照chickens = mm; rabbits = nn的格式输出解，如果无解则输出No solution! 。其中mm与nn都是非负整数。 输入样例1: 35 94 输出样例1: chickens = 23; rabbits = 12 输入样例2: 94 35 输出样例1: No solution! 参考答案 #include<iostream> using namespace std; int main(){ int head,foots; cin>>head>>foots; if((foots-head*2)%2!=0 || (head==0&&foots!=0)||(foots==0&&head!=0)||head<foots/4){ cout<<"No solution!"; return 0; } int rabbits = (foots-head*2)/2; int chickens = head - rabbits; cout<<"chickens = "<<chickens<<"; rabbits = "<<rabbits; return 0; } 四. 7-4 位数和 输入一个4位整数，求各个位上的数字之和。 输入格式: 一个四位数的整数。 输出格式: 一个整数，表示各位之和。 输入样例: 1234 输出样例: 10 样例解释：1+2+3+4=10 参考答案 #include<iostream> using namespace std; int main(){ int num; cin>>num; int sum = 0; while(num>0){ int left = num%10; num/=10; sum+=left; } cout<<sum; return 0; } 五. 7-5 逆序的三位数 程序每次读入一个正3位数，然后输出按位逆序的数字。注意：当输入的数字含有结尾的0时，输出不应带有前导的0。比如输入700，输出应该是7。 输入格式： 每个测试是一个3位的正整数。 输出格式： 输出按位逆序的数。 输入样例： 123 输出样例： 321 参考答案 #include<iostream> using namespace std; int main(){ int num; cin>>num; int sum = 0; while(num>0){ int left = num%10; sum = sum*10+left; num/=10; } cout<<sum; return 0; } 六. 7-6 算时间 夏老板在跑步机上锻炼身体。他低头一看，跑步机上记录的跑步时间是以秒为单位，屏幕上显示夏老板跑步的总时间是x秒。 夏老板想用“时分秒”的形式来表示跑步时间，他向你提问：x秒是多少个小时 多少分钟 多少秒？ 输入格式: 一行，一个整数x（0≤x≤2∗1012）。 输出格式: 一行，三个整数a,b,c分别表示时分秒，其间以":"作为间隔。 输入样例1: 8000 输出样例1: 2:13:20 输入样例2: 1234 输出样例2: 0:20:34 样例1说明：8000秒等于2小时13分20秒。 参考答案 #include<iostream> using namespace std; int main(){ long long time; cin>>time; long long hour = time/3600; time -=hour*3600; long long minite = time/60; time-=minite*60; long long second = time; cout<<hour<<":"<<minite<<":"<<second; return 0; }
Lua脚本解决高并发与库存超卖 尽管从Redis6开始，网络I/O可以开启多线程模式（通过io-threads配置项），但主命令执行仍然保持单线程。因此，利用主命令执行单线程的性质，可以模拟高并发环境下加锁的处理，加上redis本身具有的超快处理优势，可以完美替代程序中加锁的逻辑。 具体流程图如下所示： 1.编写controller用例： package com.example.demo; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.*; @RestController public class LuaController { private static final String Lua_SCRIPT = """ if tonumber(redis.call('exists',KEYS[1])) == 0 then redis.call('set',KEYS[1],'10') end if tonumber(redis.call('exists',KEYS[2])) == 0 then redis.call('sadd',KEYS[2],'-1') end if tonumber(redis.call('get',KEYS[1]))>0 and tonumber(redis.call('sismember',KEYS[2],ARGV[1])) == 0 then redis.call('incrby',KEYS[1],'-1') redis.call('sadd',KEYS[2],ARGV[1]) return 1 else return 0 end """; private final StringRedisTemplate stringRedisTemplate; LuaController(StringRedisTemplate redisTemplate){ this.stringRedisTemplate = redisTemplate; } @GetMapping("sk") public Map<String,String> seckill(String pid){ Map<String,String> resp = new HashMap<>(); String uid = String.valueOf(new Random().nextInt(10000000)); List<String> keys = new ArrayList<>(); keys.add("P"+pid); keys.add("U"+pid); DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(Lua_SCRIPT,Long.class); Long result = stringRedisTemplate.execute(redisScript,keys,uid); resp.put("uid",uid); resp.put("result",String.valueOf(result)); return resp; } } 其中，以productid为“P1001”为例： if tonumber(redis.call('exists',KEYS[1])) == 0 then redis.call('set',KEYS[1],'10') end if tonumber(redis.call('exists',KEYS[2])) == 0 then redis.call('sadd',KEYS[2],'-1') end 这一段lua脚本表示如果当前redis没有存入“P1001”的商品，就初始化“P1001”=10，表示“P1001”的商品库存有10个，等待秒杀。 KEYS[2]，即uid的列表，比如“U1001”，值是一个set集合，存储秒杀成功的用户id，比如{“1009099”，“90909009”，“68676889” .......}，如果不存在这个列表就初始化这个列表，存一个占位符“-1”进去。 if tonumber(redis.call('get',KEYS[1]))>0 and tonumber(redis.call('sismember',KEYS[2],ARGV[1])) == 0 then redis.call('incrby',KEYS[1],'-1') redis.call('sadd',KEYS[2],ARGV[1]) return 1 else return 0 end 这段代码模拟加锁的场景，即如果先前的PID存在，即“P1001”存在并初始化了并且值是>0的，表示仍然还有余额，当前试图秒杀的用户如果已经在列表里了表示已经秒杀成了，禁止重复秒杀，只有没有秒杀成功的且还有余额的情况下返回1.如果“P1001”的值是负数，即“-1”，表示余额已经没有了或者试图重复秒杀就返回0. 2.编写测试用例 模拟50个并发用户的秒杀场景 package com.example.demo; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.web.client.RestTemplate; import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.LongAdder; @SpringBootTest class Demo1ApplicationTests { @Test void AD() { LongAdder ones = new LongAdder(); // 统计秒杀成功用户数量 ExecutorService pool = Executors.newFixedThreadPool(50); CyclicBarrier barrier = new CyclicBarrier(50); // 利用CyclicBarrier循环屏障确保50个线程同时开始 CountDownLatch cdl = new CountDownLatch(50); // 利用CountDownLatch确保50个线程都调用了Restful接口后主进程才结束 Runnable task = new Runnable() { @Override public void run() { try { barrier.await(); } catch (InterruptedException e){ throw new RuntimeException(e); } catch (BrokenBarrierException e){ throw new RuntimeException(e); } String result = new RestTemplate().getForObject("http://localhost:8080/sk?pid=1080",String.class); System.out.println(result); ObjectMapper mapper = new ObjectMapper(); Map map = null; try { map = mapper.readValue(result, Map.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } if(map.get("result").equals("1")){ ones.increment(); } cdl.countDown(); } }; for(int i=0;i<50;i++){ pool.submit(task); } try { cdl.await(); System.out.println("秒杀成功用户数量: "+ones); } catch (InterruptedException e){ throw new RuntimeException(e); } } } 运行程序后，控制台打印输出 此时查看数据库： 发现最终只有10个用户秒杀成功，有效地实现了高并发下秒杀场景的任务.
权限操作：RBAC+ACL搭配 1. RBAC(基于角色的访问控制) RBAC将权限分配给角色，用户通过关联角色间接获得权限。系统通过“用户-角色-权限”的层级关系实现权限管理。 这种经典的权限控制设计有一个缺点，就是难以处理特殊个体的细粒度权限，比如我作为管理员，我需要将某个文件的访问权限交给不是管理员，但是信任的人(比如张三)进行访问。 2. ACL(访问控制列表) ACL直接在资源上定义允许操作的主体，每条记录明确指定"谁可以对某资源做什么" 这种划分权限类似于我们平常使用的windows文件管理的方式，其主要优势在于灵活性强，每条权限规则都明确可见。但是缺点也很明显，那就是当用户数量或者资源数量很大的时候，ACL条目呈现指数级增长。 3. RBAC 与 ACL 联合使用的优势 灵活性与效率的平衡 RBAC 处理通用权限（如部门角色、职级权限） ACL 处理例外场景（如特殊用户需要访问特定资源） 比如，在企业系统中，普通员工通过角色获得基础权限，但某个机密文件可通过ACL单独授权给少数高管。 减少角色爆炸 通过ACL补充RBAC，避免在RBAC中因为个别的特殊权限去创建冗余角色 精细化权限管理 RBAC管理粗粒度权限（如”访问财务模块“），ACL控制细粒度操作（如”仅能查看报表，不能导出“） 冲突解决与优先级 当RBAC和ACL权限有冲突的时候，可以设定优先级，比如ACL优先于RBAC 4. RBAC + ACL模型举例 RBAC（基于角色的访问控制）经典模型中通常包含以下5张核心表： -- 1. 用户表（3个用户） CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) ); INSERT INTO users (username, email) VALUES ('alice', 'alice@example.com'), -- admin ('bob', 'bob@example.com'), -- user ('charlie', 'charlie@example.com'); -- user -- 2. 角色表（admin 和 user） CREATE TABLE roles ( role_id INT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL UNIQUE ); INSERT INTO roles (role_name) VALUES ('admin'), ('user'); -- 3. 权限表（只包含读和写） CREATE TABLE permissions ( permission_id INT PRIMARY KEY AUTO_INCREMENT, permission_key VARCHAR(50) NOT NULL UNIQUE ); INSERT INTO permissions (permission_key) VALUES ('article:read'), ('article:write'); -- 4. 用户-角色关联表 CREATE TABLE user_roles ( user_id INT, role_id INT, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ); -- 分配角色（1个admin，2个user） INSERT INTO user_roles (user_id, role_id) VALUES (1, 1), -- alice 是 admin (2, 2), -- bob 是 user (3, 2); -- charlie 是 user -- 5. 角色-权限关联表 CREATE TABLE role_permissions ( role_id INT, permission_id INT, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ); -- 权限分配：admin有读写，user只有读 INSERT INTO role_permissions (role_id, permission_id) VALUES (1, 1), -- admin -> article:read (1, 2), -- admin -> article:write (2, 1); -- user -> article:read 然后如果需要对权限进行更加细粒度的控制，有如下ACL表可供参考： CREATE TABLE acl_entry ( id INT PRIMARY KEY AUTO_INCREMENT, resource_type VARCHAR(50) NOT NULL, -- 资源类型，如 article、order、comment resource_id INT NOT NULL, -- 具体资源ID grantee_type ENUM('USER', 'ROLE') NOT NULL, -- 授权对象类型：用户或角色 grantee_id INT NOT NULL, -- 被授权的用户或角色的ID permission VARCHAR(20) NOT NULL, -- 权限类型，如 read、write created_by INT NOT NULL, -- 授权人ID created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_acl_entry(resource_type, resource_id, grantee_type, grantee_id) ); -- admin (user_id = 1) 给用户 (user_id = 2) 授权了对文章101的读权限 -- admin 给角色（role_id = 2，可能是 editor）授权了对文章101的写权限 INSERT INTO acl_entry (resource_type, resource_id, grantee_type, grantee_id, permission, created_by) VALUES ('article', 101, 'USER', 2, 'read', 1), ('article', 101, 'ROLE', 2, 'write', 1);
微服务架构-Outbox发件箱模式 背景：在现代微服务架构中，服务之间常常需要通过异步消息传递来解耦。例如，一个订单服务创建订单后，需要通知库存服务扣减库存。如何在不同服务间传递事件的同时保持数据一致性，成为了分布式系统设计的核心挑战之一。 本篇将介绍一种经典的一致性设计模式：Outbox（发件箱）模式。它能有效解决“写库成功但消息发送失败”所带来的数据不一致问题。 1. 为什么需要Outbox模式？ 考虑一个场景的场景：订单服务先写入数据库，再发送消息的MQ通知库存服务扣减库存。 public void createOrder(Order order) { orderRepository.save(order); // 写数据库 messageQueue.send("order_created", order); // 发消息 } 如果第2步消息发送失败（比如MQ挂掉了、网络波动），订单已经创建了，但是库存不会减少，就会导致业务不一致，可能这时我们就需要进行回滚或者补偿机制，但是我们这次介绍的Outbox设计模式提供了一个全新的思路，确保在第一次处理时就保证“本地事务+跨服务通信”下的数据一致性问题。 2. Outbox模式架构详解 2.1. 核心思想 在数据库中新增一个outbox表，用于保存待发送的事件。 在本地事务中，同时写入业务数据和outbox事件。 通过后台轮询工具或者定时脚本读取outbox表中的事件，找到状态为待发送的事件，发送消息。 2.2. Outbox模式流程图 3. 代码示例 3.1. 创建订单代码（写入Outbox） @Transactional public void createOrder(Order order) { // 1. 创建订单记录 orderRepository.save(order); // 2. 写入发件箱表 OutboxEvent event = new OutboxEvent("ORDER_CREATED", JsonUtil.toJson(order)); outboxRepository.save(event); } 3.2. 消息发送器（定时轮询版） @Scheduled(fixedDelay = 5000) public void dispatchEvents() { List<OutboxEvent> events = outboxRepository.findPendingEvents(); for (OutboxEvent event : events) { try { messageQueue.send(event.getType(), event.getPayload()); event.markAsSent(); outboxRepository.save(event); } catch (Exception e) { // 这里可以放补偿方法或者最终的回滚机制，也可以放重试机制 } } } 4. 适用场景与总结 Outbox模式适用于： 核心业务链路必须保证事件可靠传递 有本地数据库，且可控制写入事务（如 MYSQL） 可以接受最终一致性，不追求强一致性 不适合： 无本地数据库的场景（如纯计算服务） 要求毫秒级响应的一致性事务 总结 Outbox模式是解决分布式事务一致性的一个经典方法。通过“写数据库 + 发消息”组合的原子化改造，把事件的可靠投递交由后台发送器完成，极大地提升了系统的容错性和可靠性。 在电商订单、支付系统、库存调整等典型的微服务场景中，Outbox都是一个值得考虑的模式。
设计模式原则分析 1. 单一职责原则（Single Responsibility Principle） 1.1 定义 单一职责原则是指一个类应该只有一个责任（即一个功能），一个类应该只负责一项任务。换句话说，如果一个类承担了多个职责，这些职责之间会有不同的变化原因，改变其中某个职责时，可能会影响到其他职责，导致系统更加复杂和难以维护。 1.2 不符合SRP原则举例 假设有一个类同时处理数据存储和报告生成的功能。当存储方式变化时（如从数据库变为文件存储），我们需要修改这个类的存储相关代码；而当报告生成的需求变化时（如格式从 PDF 变为 Excel），我们也需要修改这个类的报告生成代码。此时，这个类有两个职责，它们各自有不同的变化原因，导致这个类的维护变得复杂。 // 违反 SRP 原则：一个类同时负责数据存储和报告生成 public class UserManager { public void saveUser(User user) { // 保存用户到数据库 System.out.println("Saving user to database"); } public void generateUserReport(User user) { // 生成用户报告 System.out.println("Generating report for user"); } } 1.3 遵守SRP原则举例 // 遵守 SRP 原则：将职责分离到不同的类中 public class UserRepository { public void saveUser(User user) { // 保存用户到数据库 System.out.println("Saving user to database"); } } public class UserReportGenerator { public void generateUserReport(User user) { // 生成用户报告 System.out.println("Generating report for user"); } } 在这个例子中，UserRepository 只负责数据存储，而 UserReportGenerator 只负责生成报告。这样，每个类只负责一个功能，系统的职责划分更加清晰，修改某一功能时只需要修改对应的类，而不影响其他部分。 2. 开闭原则（Open/Closed Principle） 2.1 定义 开放-封闭原则指的是软件实体（类、模块、函数等）应该对扩展开放，对修改封闭。这意味着当我们需要为系统添加新功能时，应该通过扩展已有的代码，而不是直接修改已有的代码。 2.2 不符合OCP原则举例 假设我们有一个计算薪水的程序，原本只考虑基本工资和奖金两个因素。如果以后需要加入更多的薪酬计算因素（如补贴、扣款等），如果我们每次都修改原有的计算逻辑，就会违反开放-封闭原则。 // 违反 OCP 原则：修改原有类以支持新的薪资计算方式 public class SalaryCalculator { public double calculateSalary(Employee employee) { // 计算基本工资和奖金 return employee.getBaseSalary() + employee.getBonus(); } } 假设现在我们需要新增一个“补贴”因素，每次添加新因素时都需要修改 SalaryCalculator 类的 calculateSalary 方法。随着需求变化，SalaryCalculator 类会变得越来越复杂，而且容易出错。这就是违反了开放-封闭原则。 2.3 符合OCP原则举例 为了遵守开放-封闭原则，我们可以引入抽象类或接口，将不同的薪资计算策略封装成不同的类，而不是修改 SalaryCalculator 类。 // 遵守 OCP 原则：将薪资计算逻辑通过策略模式封装，便于扩展 public interface SalaryCalculatorStrategy { double calculateSalary(Employee employee); } public class BaseSalaryAndBonusCalculator implements SalaryCalculatorStrategy { @Override public double calculateSalary(Employee employee) { // 计算基本工资和奖金 return employee.getBaseSalary() + employee.getBonus(); } } public class BaseSalaryBonusAndSubsidyCalculator implements SalaryCalculatorStrategy { @Override public double calculateSalary(Employee employee) { // 计算基本工资、奖金和补贴 return employee.getBaseSalary() + employee.getBonus() + employee.getSubsidy(); } } public class SalaryCalculator { private SalaryCalculatorStrategy strategy; public SalaryCalculator(SalaryCalculatorStrategy strategy) { this.strategy = strategy; } public double calculateSalary(Employee employee) { return strategy.calculateSalary(employee); } } 我们将计算薪水的逻辑提取到不同的策略类中，并定义了一个共同的 SalaryCalculatorStrategy 接口。每个薪资计算策略都实现了这个接口，分别处理不同的计算逻辑（如仅包含基本工资和奖金，或包含补贴等）。通过策略模式，我们能够灵活地扩展薪资计算方式，而无需修改现有的代码。如果以后还需要新增其他薪资计算方式，只需要新建一个实现 SalaryCalculatorStrategy 接口的类，而不需要修改 SalaryCalculator 类。 3. 里氏替换原则 (LSP) 3.1 定义 里氏替换原则是指子类对象应该能够替换父类对象，且程序的行为不受影响。换句话说，子类必须完全继承父类的行为，并且在替换父类实例时不会破坏父类的行为和功能。 3.2 符合LSP原则举例 假设我们有一个形状类 Shape，其中有一个方法 area() 来计算图形的面积。然后我们有两个子类：Rectangle 和 Square。根据几何学，矩形的面积是 宽 * 高，而正方形的面积是 边长 * 边长。但是，假如我们在正方形类中重写了 setWidth 和 setHeight 方法，使其不再符合父类 Rectangle 的行为，那么就违反了里氏替换原则。 // 违反 LSP 原则：正方形类破坏了矩形类的预期行为 class Rectangle { private double width; private double height; public void setWidth(double width) { this.width = width; } public void setHeight(double height) { this.height = height; } public double area() { return width * height; } } class Square extends Rectangle { @Override public void setWidth(double width) { super.setWidth(width); super.setHeight(width); // 强制宽度和高度相等，违反矩形的行为 } @Override public void setHeight(double height) { super.setHeight(height); super.setWidth(height); // 强制宽度和高度相等，违反矩形的行为 } } 正方形是矩形的一种特殊情况，但正方形并不等同于矩形。矩形的宽度和高度是独立的，而正方形的宽度和高度始终相等。如果我们让正方形继承矩形类，并且让 setWidth 和 setHeight 强制设置相等的值，这将破坏矩形类的预期行为。当我们将一个 Square 对象作为 Rectangle 类型的对象传递时，程序的行为会不符合预期，setWidth 和 setHeight 会强制设置相等的值，而这在矩形的情况下并不应该这样。 3.3 符合LSP原则举例 为了遵守里氏替换原则，我们应该避免让 Square 直接继承 Rectangle，因为它违反了矩形的行为。相反，我们可以通过创建一个新的抽象类 Shape 来统一接口，Rectangle 和 Square 各自继承并实现具体的行为。 // 遵守 LSP 原则：通过抽象类和接口来正确设计 abstract class Shape { public abstract double area(); } class Rectangle extends Shape { private double width; private double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double area() { return width * height; } } class Square extends Shape { private double side; public Square(double side) { this.side = side; } public double area() { return side * side; } } Shape 类作为一个抽象类，定义了所有图形的公共接口 area() 方法。 Rectangle 和 Square 都继承 Shape 类，但它们分别根据自己的特点实现 area() 方法，而不再强行使用不合适的继承关系。 正确的继承：Rectangle 和 Square 的行为各自独立，满足了不同的需求。正方形和矩形之间的差异得到了合理的隔离。 4. 接口隔离原则（ISP) 4.1 定义 接口隔离原则指的是不应该强迫一个类去依赖它不需要的接口。换句话说，接口应该尽可能地小，避免设计过于庞大和臃肿的接口。客户端不应该被迫实现它不使用的方法。 简单理解： 如果一个类依赖了一个接口，并且这个接口中有一些方法是该类不需要的，那么就违反了接口隔离原则。 应该将一个大接口拆分成多个小接口，每个接口承担一个独立的职责，这样类只需要实现它需要的接口，而不是实现整个庞大的接口。 4.2 不符合ISP原则举例 假设我们有一个 Worker 接口，包含了多种不同的功能方法，像是 work、eat、sleep 等。然而，某些具体的实现类（比如 Robot）并不需要所有这些方法，比如它不需要 eat 和 sleep 方法。这个接口设计就违反了接口隔离原则。 // 违反 ISP 原则：接口包含了不需要的方法 public interface Worker { void work(); void eat(); void sleep(); } public class Human implements Worker { @Override public void work() { System.out.println("Human working..."); } @Override public void eat() { System.out.println("Human eating..."); } @Override public void sleep() { System.out.println("Human sleeping..."); } } public class Robot implements Worker { @Override public void work() { System.out.println("Robot working..."); } @Override public void eat() { // Robot doesn't need to implement this method } @Override public void sleep() { // Robot doesn't need to implement this method } } 在这个例子中，Worker 接口定义了 eat 和 sleep 方法，但对于 Robot 类而言，这些方法并不适用，因此需要实现这些无用的方法，违反了接口隔离原则。Robot 类并不需要 eat 和 sleep 方法，但是由于接口设计不合理，它不得不提供这些空实现，从而导致了不必要的代码。 4.3 符合ISP原则举例 为了解决这个问题，我们可以将接口拆分成多个小接口，每个接口仅包含客户端需要的方法。Human 类实现包含 work、eat、sleep 的接口，而 Robot 类只实现 work 方法相关的接口。 // 遵守 ISP 原则：将接口拆分成多个小接口 public interface Workable { void work(); } public interface Eatable { void eat(); } public interface Sleepable { void sleep(); } public class Human implements Workable, Eatable, Sleepable { @Override public void work() { System.out.println("Human working..."); } @Override public void eat() { System.out.println("Human eating..."); } @Override public void sleep() { System.out.println("Human sleeping..."); } } public class Robot implements Workable { @Override public void work() { System.out.println("Robot working..."); } } 5. 依赖倒置原则（Dependency Inversion Principle） 5.1 定义 依赖倒置原则是指高层模块不应该依赖低层模块，二者都应该依赖于抽象；抽象不应该依赖细节，细节应该依赖于抽象。 简单来说，依赖倒置原则强调的是通过依赖于抽象（如接口或抽象类），而不是依赖于具体实现，从而实现高层模块和低层模块的解耦。 5.2 不符合DIP原则举例 假设我们有一个 UserService 类，它依赖于 UserRepository 类来进行数据存储。如果 UserService 直接依赖于 UserRepository 类，这样就违反了依赖倒置原则，因为高层模块（UserService）直接依赖于低层模块（UserRepository）。 // 违反 DIP 原则：高层模块直接依赖低层模块 class UserRepository { public void save(User user) { // 保存用户到数据库 System.out.println("User saved to database."); } } class UserService { private UserRepository userRepository; public UserService() { userRepository = new UserRepository(); // 直接依赖具体实现 } public void registerUser(User user) { // 注册用户 userRepository.save(user); } } UserService 直接依赖于 UserRepository 的实现。这样，如果我们需要更改 UserRepository 的实现，比如从数据库改为从文件中保存数据，UserService 就需要修改其代码，违反了高层模块不应该依赖低层模块的原则。 如果以后需要实现不同类型的存储方式，比如 FileRepository 或 MongoDBRepository，每次都需要修改 UserService，导致代码不容易维护和扩展。 5.3 符合DIP原则举例 为了遵守依赖倒置原则，我们应该引入一个抽象接口，UserRepository 应该是一个接口，而 UserService 只依赖这个接口，而不依赖具体实现类。具体的实现类可以通过依赖注入的方式传入。 // 遵守 DIP 原则：通过接口隔离高层模块和低层模块 interface UserRepository { void save(User user); } class UserRepositoryImpl implements UserRepository { @Override public void save(User user) { // 保存用户到数据库 System.out.println("User saved to database."); } } class FileRepository implements UserRepository { @Override public void save(User user) { // 保存用户到文件 System.out.println("User saved to file."); } } class UserService { private UserRepository userRepository; // 通过构造器注入依赖 public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public void registerUser(User user) { // 注册用户 userRepository.save(user); } } 6. 合成复用原则（Composite Reuse Principle） 6.1 定义 合成复用原则是指优先使用对象的合成（组合）而非继承来实现代码复用。这意味着，通过将已有的类作为成员对象嵌入到新类中，来增强新类的功能，而不是通过继承扩展原有类的功能。 组合优于继承：当需要复用某些功能时，尽量通过组合来实现，而不是通过继承。这是因为继承在很多情况下会导致类之间的耦合性增加，而组合则更灵活且易于扩展。 6.2 不符合CRP原则举例 假设我们有一个 Bird 类和一个 Penguin 类，Penguin 是 Bird 的子类。我们在 Bird 类中定义了一些行为（如飞行），但是企鹅是不能飞的。如果我们用继承来实现复用，这样就会导致不合理的设计。 // 违反合成复用原则：企鹅类继承了鸟类，导致不适用的行为 class Bird { public void fly() { System.out.println("Bird is flying."); } } class Penguin extends Bird { // Penguin 是鸟类的一种，但企鹅不能飞 @Override public void fly() { // 企鹅不能飞，但继承了飞行行为，导致设计不合理 System.out.println("Penguins can't fly."); } } 6.3 符合CRP原则举例 为了遵守合成复用原则，我们可以将飞行行为抽象出来，并将其作为一个接口或者类的成员。这样，企鹅类就不再需要继承鸟类，而是根据实际需要来组合不同的行为。 // 遵守合成复用原则：通过组合来复用功能 interface Flyable { void fly(); } class Bird implements Flyable { @Override public void fly() { System.out.println("Bird is flying."); } } class Penguin { // 企鹅并不具备飞行能力，不需要继承 Bird 类 // 如果需要飞行，可以通过组合一个 Flyable 对象 private Flyable flyable; public Penguin(Flyable flyable) { this.flyable = flyable; } public void tryToFly() { // 企鹅不能飞，但如果需要，可以选择其他飞行方式 if (flyable != null) { flyable.fly(); } else { System.out.println("Penguins can't fly."); } } } class Test { public static void main(String[] args) { // 创建一个可以飞的鸟类 Bird bird = new Bird(); Penguin penguin = new Penguin(null); // 企鹅没有飞行能力 penguin.tryToFly(); // 输出：Penguins can't fly. // 如果需要，可以通过组合飞行的能力 Penguin flyingPenguin = new Penguin(bird); flyingPenguin.tryToFly(); // 输出：Bird is flying. } } 7. 最少知识原则（迪米特法则） 7.1 定义 最少知识原则，又称迪米特法则，其核心思想是一个对象应该对其他对象有尽可能少的了解。也就是说，一个对象应该只与自己直接的朋友（即直接相关的对象）进行交互，而不应该与“陌生人”或“陌生人的朋友”进行交互。 7.2 不符合迪米特法则举例 假设我们有一个 Customer 类，它有一个 Order 类属性，而 Order 类又包含 Product 类。如果 Customer 直接访问 Product 类的属性，这样就违反了最少知识原则。 class Product { public String name; public double price; } class Order { private Product product; public Order(Product product) { this.product = product; } public Product getProduct() { return product; } } class Customer { private Order order; public Customer(Order order) { this.order = order; } // 违反最少知识原则：直接访问了 Product 对象的属性 public void printProductName() { System.out.println(order.getProduct().name); // 访问了 order -> product -> name } } 7.3 符合迪米特法则举例 class Product { private String name; private double price; public Product(String name, double price) { this.name = name; this.price = price; } public String getName() { return name; } } class Order { private Product product; public Order(Product product) { this.product = product; } public String getProductName() { return product.getName(); } } class Customer { private Order order; public Customer(Order order) { this.order = order; } // 遵守最少知识原则：只通过 Order 类与 Product 交互 public void printProductName() { System.out.println(order.getProductName()); } }