目录
- *001* [PAT (Basic Level) 1002 写出这个数(20 分) ](https://blog.csdn.net/weixin_45027619/article/details/116149052)
- *002* [PAT (Basic Level)1005 继续(3n+1)猜想 (25 分)](https://blog.csdn.net/weixin_45027619/article/details/116193820)
- *003* [PAT (Basic Level)1007 素数对猜想 (20 分)](https://blog.csdn.net/weixin_45027619/article/details/116208211)
- *004* [PAT (Basic Level) 1008 数组元素循环右移问题 (20 分)](https://blog.csdn.net/weixin_45027619/article/details/116228018)
- *005* [逆置字符串](https://pintia.cn/problem-sets/994805260223102976/problems/994805314941992960)
- *006* [1010 一元多项式求导 (25 分)](https://pintia.cn/problem-sets/994805260223102976/problems/994805313708867584)
- *007* [1013 数素数 (20 分)](https://pintia.cn/problem-sets/994805260223102976/problems/994805309963354112)
- *008* [PAT (Basic Level)1015 德才论 (25 分)](https://blog.csdn.net/weixin_45027619/article/details/116297203)
- *009* [1016 部分A+B (15 分)](https://pintia.cn/problem-sets/994805260223102976/problems/994805306310115328)
- *010* ==重点== [1017 A除以B (20 分)](https://blog.csdn.net/weixin_45027619/article/details/116304735)
- *011* ==重点==[1018 锤子剪刀布 (20 分)](https://pintia.cn/problem-sets/994805260223102976/problems/994805304020025344)
- *011* ==重点==[1019 数字黑洞 (20 分)](https://blog.csdn.net/weixin_45027619/article/details/116372870)
- 012 双指针同时移动
- 1.双指针专题
- 2.链表
- 3.二分查找
001 PAT (Basic Level) 1002 写出这个数(20 分)
收获:
1、对于求一个整数的各个位置上数字的两种思路:
- 用%/来求,将结果保存在数组中;
- 也可以直接将其转换成字符串,字符串相当于一个只读char数组,又因为char和int可以隐式转换,所以用字符串数组中的元素减去‘0’即为其元素值
2、对于与整数存在一一对应的关系,可以将其用数组存储起来利用下标进行访问;也可以采用流程控制语句进行判断。
002 PAT (Basic Level)1005 继续(3n+1)猜想 (25 分)
收获:
**1、如果题目中的条件中有 正整数 n (1<n≤100)的值这样的字眼,即既规定了正整数,又限定了其范围,这种情况下可以考虑用空间换时间按的方法,用辅助数组。
2、最后一个输出的数字不加空格:**
先输出空格,再输出元素,第一个元素之前不加空格,对 i 进行判断即可。**
3、用双数组可以实现字典,字典中的key存放在arr1中,其对应的value存放在以key为下标的arr2中,遍历arr1,在arr2中下标属于arr1的值中查找符合条件的值。形如arr2[ arr1[ i ] ]
003 PAT (Basic Level)1007 素数对猜想 (20 分)
收获:
1、直接用运算符运算要比调用库函数效率高。例如本题中判断素数用j * j <= i的效率要高于j <= Math.Pow(i,0.5),要注意这个细节,正是这个细节,有一个测试就超时了。
2、能不申请新变量尽量不要申请,降低空间复杂度。
004 PAT (Basic Level) 1008 数组元素循环右移问题 (20 分)
收获:
因为逆置之前并没有判断右移位数和数组长度的关系,导致部分测试没有通过,所以要注意这个细节:在逆置之前应该首先判断右移的位数,如果右移位数是数组长度的整数倍,则直接将数组元素输出即可。
005 逆置字符串
1、可以用split函数将其分割成字符串数组,然后通过交换实现元素逆置
2、可以借助栈的先进后出的特性,先压栈再出栈
006 1010 一元多项式求导 (25 分)
1、关于特殊情况的判断,可以在一般情况判断前or后进行判断。
007 1013 数素数 (20 分)
判断素数
static bool IsPrime(int n)
{
for (int i = 2; i * i <= n; i++)
{
if (n % i == 0)
{
return false;
}
}
return true;
}
008 PAT (Basic Level)1015 德才论 (25 分)
1、掌握两个用于集合排序的接口的使用情况;
2、注意比较逻辑的写法:
每类考生按总分降序排列,当某类考生中有多人总分相同时,按其德分降序排列;若德分也并列,则按准考证号的升序输出。
先写总分不同的情况下的处理逻辑,然后写德分不同的情况下的处理逻辑,最后写准考证号不同的处理逻辑。
if (x.De + x.Cai != y.De + y.Cai)
return (y.De + y.Cai) - (x.De + x.Cai);
else if (x.De != y.De)
return y.De - x.De;
else
return x.Num - y.Num;
009 1016 部分A+B (15 分)
1、注意将char类型当作对应的整型数值进行加减乘除时,要减去’0’
2、处理整数的各位元素时,可以将其变成字符串,遍历每一位时相当于char类型,更加方便快捷。
010 重点 1017 A除以B (20 分)
1、思路很重要,因为所给的数值过大,不可能直接除或者模,所以要想到除法的计算过程;
2、边界情况一定要注意。
011 重点1018 锤子剪刀布 (20 分)
实现这个题并不难,但是写的代码有些冗余,造成时间复杂度和空间复杂度比较高,一定要搞清楚题目的核心需求,减少变量的申请,用最少的资源来实现功能可以进行如下优化:
1、降低空间复杂度,因为甲乙的输赢是对立的,所以只用设立两个变量,jiaWin和yiWin,就可以推出甲乙各自的输赢平手次数,又因为我们只需要得到甲乙获胜的次数和获胜的手势,所以我们就只需要考虑6种可能的情况(算上平手总共有九种可能的情况);
2、用数组来模拟字典:如果让我们求出现频率最高的元素,设置两个变量,一个记录最大值,一个记录最大值下标;然后可以将所有元素放入数组的对应位置,通过之前求出的最大值下标来找出目标元素。
011 重点1019 数字黑洞 (20 分)
1、一定要考虑每一种可能出现的情况;
2、想办法将不同的情况整合到一起,避免代码冗余。例如本题中的do-while设计将输入是6174和其他数值整合,这样就不用另外对6174进行判断;while中的判断条件是s != “6174”&& s!=“0000”,将0000和其他结果整合,不用对结果0000做另外判断。
012 双指针同时移动
获取两个字符串中最大相同子串。比如:
str1 = "abcwerthelloyuiodef“;str2 = “cvhellobnm”
提示:将短的那个串进行长度依次递减的子串与较长的串比较。
长度一定,双指针同时移动可以找出所有符合情况的结果
如果存在多个长度相同的最大相同子串。 此时先返回String[],后面可以用集合中的ArrayList替换,较方便
public String[] findSame1(String str1, String str2) {
if (str1 == null || str2 == null)
return null;
if (str1.length() == 0 || str2.length() == 0)
return new String[]{};
String minStr = str1.length() >= str2.length() ? str2 : str1;
String maxStr = str1.length() < str2.length() ? str2 : str1;
StringBuilder s = new StringBuilder();
// 每一轮减少一个字母,内层循环判断所有的组合是否符合题意
for (int i = 0; i < minStr.length(); i++) {
for (int start = 0, end = minStr.length() - i; end <= minStr.length(); start++, end++) {
String str = minStr.substring(start, end);
if (maxStr.contains(str)) {
s.append(str + ",");
}
}
// 如果存在多个长度相同的最大相同子串,内层for循环可以全部找出来,找出来之后就不用再进行下一轮外层循环了
if (s.length() > 0)
break;
}
String[] split = s.delete(s.length() - 1, s.length()).toString().split(",");
return split;
}
1.双指针专题
1.1解题思路
- 确定两个指针的作用;
- 确定两个指针的移动方向:两侧向内,一前一后向后;
1.2 快慢指针
1.2.1 核心思想
如果数组中慢指针slow指向当前结果序列的下一个元素,此时比较的是slow-1指向的元素和fast指向的元素,满足条件会将fast指向的元素赋值给slow指向的位置;如果数组中慢指针slow指向当前结果序列的末尾元素,此时比较的是slow指向的元素和fast指向的元素,满足条件会将fast指向的元素赋值给slow+1指向的位置。(一般来说数组中的慢指针指向的都是当前结果序列的下一个元素,因为初始值一般设为0;链表中指向的是当前结果序列的末尾元素);
快指针fast指向的是当前正在判断的元素;
无论fast指向的元素是否满足要求,fast指针每次都会向后移动一位;而slow指针只有在满足条件时向后移动。
1.2.2 相关题目
80. 删除有序数组中的重复项 II
26. 删除有序数组中的重复项
82. 删除排序链表中的重复元素 II
83. 删除排序链表中的重复元素
1.2.3 删除有序数组重复项通解(此解法采用的是slow指向当前结果序列的下一个元素)
注:如果是无序数组,可以先进行排序。
参考文章
k表示保留 k 个相同数字
int process(int[] nums, int k) {
int u = 0;
for (int x : nums) {
// 利用了短路或的特性,将前k个元素先复制到数组中,然后再进行比较去重;
// 也可以直接将快慢指针的初始值设为k。
if (u < k || nums[u - k] != x) nums[u++] = x;
}
return u;
}
1.3 滑动窗口
1.3.1 使用场景
子串问题
1.3.2 核心思想 参考文章
初始时,两个指针都位于索引为0的位置,快指针向右移动,一直到符合题意时停止,然后左指针向右移动,不符合题意时停止,然后重复以上两个步骤,直到右指针到达边界。
右指针移动是为了寻找一个「可行解」,左指针移动是为了优化这个「可行解」,最终找到最优解。
1.3.3 解题步骤
需要考虑的4个问题:
- 移动快指针扩大窗口时,需要更新哪些数据?(移动快慢指针更新数据是对称的)
- 找到「可行解」后,窗口应该暂停扩大,开始移动慢指针缩小窗口?(此处需要维护一个变量res来充当窗口缩小的条件)
- 移动慢指针缩小窗口时,需要更新哪些数据?(移动快慢指针更新数据是对称的)
- 应该在扩大窗口时还是缩小窗口时更新结果值?(只需要考虑扩大窗口和缩小窗口两种情况)
1.3.4 标识找到「可行解」的变量 res
使用场景:题目中有两个字符串,如果题目中只有一个字符串,不需要使用变量res。
扩大窗口时,如果滑动窗口中的元素记录的次数和needs中一样,就让res++;
缩小窗口时,如果滑动窗口中的元素记录的次数和needs中一样,就让res–,然后将滑动窗口中的元素记录的次数-1。
1.3.5 模板
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 增大窗口
right++;
// 进行滑动窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 缩小窗口
left++;
// 进行滑动窗口内数据的一系列更新
...
}
}
}
2.链表
2.1 技巧
- 如果head会被移除,那么一定要设置一个虚拟节点指向head,题目如果要求返回的是head,那么只要返回虚拟节点的next节点即可;
- 链表中可以使用一个变量来充当两个指针,cur是慢指针,cur.next是快指针。写代码时不要关注cur和cur.next的关系,只需将cur.next当作右指针,通过比较cur.next和cur.next.next来确定cur的值。82. 删除排序链表中的重复元素 II
- 如果要删除所有的重复元素,重复数字可以使用一个变量保存。82. 删除排序链表中的重复元素 II
- 链表中的慢指针指向的结果序列的末尾元素,故初始位置应该是一个空节点或者是虚拟节点; 反转链表中使用的是空节点,因为链表反转后末尾元素指向的是null。
2.1.1 相关题目
19.删除链表的倒数第 N 个结点
82. 删除排序链表中的重复元素 II
3.二分查找
如果是有序数组且要求时间复杂度是O(log(n)),可以使用二分查找。
- 如果元素至多出现1次,例如寻找唯一元素,搜索插入点,推荐使用模板3;
- 如果存在重复元素找边界或者是寻找峰值,查找左边界采用模板1,查找右边界采用模板2。
- 模板1和模板2需要先确定下一轮搜索区间,再去确定计算mid时需不需要加1.
3.1 整数二分算法模板
3.1.1 模板1
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
public int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + ((r - l) >> 1); // 此处注意>>的优先级低于+
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
使用场景: 第一个满足条件的mid一种情况可能是左端点,另一种情况一定是在左端点右边。所以满足条件时,将mid赋值给r,继续向左寻找,直至找到左端点为止。
注意: 返回值可以是left,也可以是right,因为最后退出循环时left一定等于right;
3.1.2 模板2
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l +((r - l + 1) >> 1);
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
使用场景: 第一个满足条件的mid一种情况可能是右端点,另一种情况一定是在右端点左边。所以满足条件时,将mid赋值给l,下一轮搜索区间是[mid,right],继续向右寻找,直至找到右端点为止。
注意:
- 此处mid = l +((r - l + 1) >> 1),因为当r = l + 1时,如果mid = l + ((r - l) >> 1),此时mid = l,就会陷入死循环中。
- 返回值可以是left,也可以是right,因为最后退出循环时left一定等于right;
3.1.3 模板3
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right){
mid = left + (right - left) /2;
if(nums[mid] == target){
return mid;
}else if(nums[mid] > target){
right = mid -1;
}else{
left = mid + 1;
}
}
return left;//如果在数组中没有找到target,则返回target在数组中对应的插入位置left,left位置的元素大于target,而left-1(right)位置的元素小于target。
}
使用场景: 目标元素如果在数组中至多出现一次,那么使用此模板;如果数组中存在目标元素,则返回其对应的下标;如果不存在,则返回目标元素在数组中的插入位置。
注意:如果在数组中没有找到target,则返回target在数组中对应的插入位置left,left位置的元素大于target,而left-1(或者是right)位置的元素小于target。
3.1.4 模板1和模板2使用步骤
- 寻找左边界
先考虑nums[mid]=target
时,接下来的搜索区域是哪里,因为寻找的是左边界,所以应该去mid的左边寻找,所以就可以和target<nums[mid]
合并,又因为nums[mid]可能是左边界,所以令right = mid,然后else中一定是left = mid + 1
。因为left不可能等于right或者right+1,所以mid不可能等于right,所以mid向下取整。
如果数组中不一定存在目标值,循环结束后需要判断nums[right]是否等于target。
public int searchFirstTarget(int[] nums, int target) {
int left = 0, right = nums.length - 1, mid;
while (left < right) {
mid = left + (right - left ) / 2;
if (nums[mid] >= target) {
right = mid;
} else {
left = mid + 1;
}
}
if (nums[left] == target)
return left;
return -1;
}
- 寻找右边界
右边界的思路与左边界同理。因为right可能等于left+1,所以mid可能等于left,为了防止死循环,mid向上取整。
public int searchLastTarget(int[] nums, int target) {
int left = 0, right = nums.length - 1, mid;
while (left < right) {
mid = left + (right - left + 1) / 2;
if (nums[mid] <= target) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}