前言
如何准备面试手撕代码题
如果你是转专业,或者计算机专业但之前没有参加过acm这种赛事,对于算法代码就比较普通的选手(没错 说的就是我,应该也是大部分选手现状了)。我推荐你可以按我整理的这12篇博客的顺序来刷。时间充足的话,每个类型里面所有题目都刷完,时间不充足的话,确保每个类型的题目至少能会个两三道。
本篇专栏的作用:主要是将高频的面试手撕代码真题进行了一个总结,并划分了11个分类,大家可以根据我这个专栏的1~11博客来依次刷,帮助大家节省了去找题目的一个过程,同时题目后面配有我自己写的Java版的代码解析,部分重点内容还有我自己学习过程中参考的一些学习视频链接。如果学有余力,可以再刷leetcode hot 100中不包含在本专栏中的题目。
刷题总的思路:
1 没思路的直接去看我题解(Java选手的话),题解没理解,就把题目和题解扔给gpt让他给你详细解释一下,基本就能懂了,因为后面一题不止刷一遍,请相信自己的脑子,它会记住这些。
2 从第二天开始,每天上午复习前一天刷过的新题,复习完后再开始刷接下来新的题 (比如我第二天,复习了第一天刷的题,然后同时新刷了10道题。那再第三天我只需要复习第二天新刷的那十道题,然后再接着刷新题就行了)
我是如何准备
准备时间:10月16日 - 10月21日 共经历五天
手撕代码准备:我是参考一些评论,qq群,牛客上的帖子,把题目整理了一下,大概得有个六十来道的样子,然后按顺序刷,刷完后按照题型进行一个分类,按照分类又刷了一遍。
八股文准备:[Java面试题介绍 | 小林coding (xiaolincoding.com)](https://xiaolincoding.com/interview/spring.html) 直接看这个八股文,然后我也把牛客上od面经常问到的八股总结了一下 ,两个搭配使用
day1: 一整天下来大概刷了13道题左右,第一天效率不是很高 可以理解
day2:上午把day1刷的题重刷一遍,下午和晚上接着刷剩下的题(下午和晚上加起来刷了得有14道题左右)
day3:上午把day2新刷的题重刷一遍,下午和晚上开始刷剩下的题(下午和晚上加起来刷了得有18道题左右),
day4:上午把day3新刷的题重刷一遍,下午和晚上开始刷剩下的题(下午和晚上加起来刷了得有12道题左右),刷完还有点时间开始看八股,八股文用的是小林八股文,并且搭配牛客上一些华为od面经来使用(这个我也有总结,后续可以分享出来)
day5:花一整天时间,从头到尾,把所有题目刷一遍,这个刷的时候就很快了,并且刷完一个按分类来记录下来(这个记录过程就是我这个专栏博客的雏形)。同时晚上看八股
day6:上午用整理的笔记,按题型分类把手撕代码的真题都看一遍。下午有空的时候看看八股,然后就是静候面试
以上的准备过程只是我个人的一个过程,针对像我这样的普通人,如果acm选手肯定不用这样!!!
字符串,String,char[]
将string转成char[] 方便根据下标进行反转操作
然后再将反转后的char[] 通过new String(arr)的方式 构成成一个新的String
char[] arr = s.toCharArray();
for (int i = 0; i < n; i += 2 * k){
if (i + k <= n){ // 反转i到i+k 字符
reverse(arr, i, i + k - 1); // [] 区间
}else{ // 否则则到末尾 且小于k个字符 将剩余字符全部反转
reverse(arr, i, n - 1);
}
}
return new String(arr);
常规字符串,数组,矩阵
1 实现超长数字减1
- 思路:Java中用BigInteger类
public String subOne(String s){
BigInteger bi = new BigInteger(s);
bi = bi.subtract(BigInteger.ONE);
return bi.toString();
}
2 十八进制数比较大小
任意进制的字符串a,转成十进制的数 : Integer aTen = Integer.valueOf(a, 任意进制)
十进制的数aTen,转成任意进制的字符串 :String str = Integer.toString(aTen, 任意进制)
public String compareTenEight20241020(String a, String b){
Integer aten = Integer.valueOf(a, 18);
Integer bten = Integer.valueOf(b, 18);
return aten > bten ? a : b;
}
3 最长公共前缀
思想:取第一个字符串作为前缀,然后遍历数组中的每个字符串,不断缩小前缀长度,直到它匹配字符串的前缀为止
public String longestCommonPrefix(String[] strs) {
int n = strs.length;
String prefix = strs[0];
for (int i = 1; i < n; i ++){
String cur = strs[i];
//如果当前字符串不是以prefix开头,则不断缩小prefix的长度,直到它匹配当前字符串的前缀为止
while (cur.indexOf(prefix) != 0){
prefix = prefix.substring(0, prefix.length() - 1);
}
}
return prefix;
}
4 八进制求和
不难想到用Integer类提供的方法,但当a,b过长超出Integer型范围时会报错。
public String addBinary(String a, String b) { //会超时
int aten = Integer.valueOf(a,2);
int bten = Integer.valueOf(b,2);
int sum = aten + bten;
return Integer.toString(sum,2);
}
还得用常规运算
//二进制求和
public String addBinary(String a, String b) {
int aLen = a.length();
int bLen = b.length();
int i = aLen - 1;
int j = bLen - 1;
StringBuilder sb = new StringBuilder();
int jw = 0; //表示进位
//从a,b末尾开始运算
while (i >= 0 || j >= 0){
int sum = jw;
//若a还有数字
if (i >= 0){
sum += a.charAt(i) - '0';
i --;
}
//若b还有数字
if (j >= 0){
sum += b.charAt(j) - '0';
j --;
}
//考虑进位
jw = sum / 2;
sb.insert(0,sum % 2);
if (i < 0 && j < 0 && jw > 0){
sb.insert(0,jw);
}
}
return sb.toString();
}
八进制求和代码类似上面二进制求和
//八进制求和
public static String addEightJZ(String a, String b){
int aLen = a.length();
int bLen = b.length();
int i = aLen - 1;
int j = bLen - 1;
int jw = 0; //表示进位
StringBuilder sb = new StringBuilder();
while (i >=0 || j >= 0){
int sum = jw;
if (i >= 0){
sum += a.charAt(i) - '0';
i --;
}
if (j >= 0){
sum += b.charAt(j) - '0';
j --;
}
//sum % 8表示当前位置值, sum / 8表示进位
jw = sum / 8; //进位
sb.insert(0, sum % 8);
//如果当前是最高位,且进位大于0 则把进位也加进去
if (i < 0 && j < 0 && jw > 0){
sb.insert(0,jw);
}
}
return sb.toString();
}
5 O(n)时间内求和为target两个数
思路:用hashmap
public int[] twoSum(int[] nums, int target) {
int n = nums.length;
//map存放<nums[i],i> 也就是<值,下标>
HashMap<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < n; i ++){
int cur = nums[i];
int other = target - nums[i];
//看other是否在map中 在则直接返回
if (map.containsKey(other)){
return new int[]{i,map.get(other)};
}
map.put(cur, i);
}
return new int[2];
}
6 判断回文串
public boolean isPalindrome(String s) {
//将所有大写字母转为小写字母
s = s.toLowerCase();
//知识点:s.toUpperCase();//将所有小写字母转为大写字母
//将所有非小写字母 数字字符替换成空字符(移除所有非小写字母 数字字符)
s = s.replaceAll("[^0-9a-z]","");
int n = s.length();
int left = 0, right = n - 1;
while(left < right){
char lc = s.charAt(left);
char rc = s.charAt(right);
if (lc != rc) return false;
left ++;
right --;
}
return true;
}
7 字母异位词分组
public List<List<String>> groupAnagrams(String[] strs) {
int n = strs.length;
//map中 key为排序后的字符串,value排序前的字符串
Map<String, List<String>> map = new HashMap<>();
for (int i = 0; i < n; i ++ ){
String str = strs[i];
//将str进行排序变成key的过程
char[] chars = str.toCharArray();
Arrays.sort(chars);
// String key = chars.toString(); 这个不行
String key = new String(chars); //这个可以
//若map包含key 则将str直接加入
if (map.containsKey(key)){
map.get(key).add(str);
}
else{ //否则新建一个list,将str加到list 组成键值对<key ,list>加到map中
List<String> list = new ArrayList<>();
list.add(str);
map.put(key, list);
}
}
return new ArrayList<>(map.values());
}
8分发糖果
思路:两次循环,第一次从左到右遍历 若右边的孩子比左边孩子评分高,则右孩子糖数为左孩子糖+1,否则为1。第二次从右到左遍历 若左边孩子比右边孩子评分高,则左边孩子糖 = max(左孩子第一轮糖,右孩子糖 + 1)
public int candy(int[] ratings) {
int n = ratings.length;
int[] candy = new int[n]; //candy[i] 表示第i个孩子收到的糖数
candy[0] = 1; //初始化
int cnt = 0; //用来统计糖总数
//从左往右遍历 若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1,否则右孩子糖数为1
for (int right = 1; right < n; right ++){
int leftCandy = candy[right - 1];
int leftRate = ratings[right - 1];
int rightRate = ratings[right];
if (rightRate > leftRate) {
candy[right] = leftCandy + 1; //若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1
}else{
candy[right] = 1;//否则右孩子糖数为1
}
}
//从右往左遍历 若左孩子评分比右孩子高 左孩子糖数 = max(左孩子糖数,右孩子糖数 + 1)
for (int left = n - 2; left >= 0; left --){
int rightRate = ratings[left + 1];
int rightCandy = candy[left + 1];
int leftRate = ratings[left];
if (leftRate > rightRate){
candy[left] = Math.max(candy[left], rightCandy + 1);
}
}
//统计糖总数
for (int a : candy){
cnt += a;
}
return cnt;
}
9 计算字符串的数字和
2243. 计算字符串的数字和 - 力扣(LeetCode)
public String digitSum(String s, int k) {
//递归终止条件
if (s.length() <= k) return s;
int n = s.length();
StringBuilder sb = new StringBuilder(); //记录变化中的字符串
//i以k位间隔
for (int i = 0; i < n; i += k){
int num = 0;
for (int j = 0; j < k && i + j < n; j ++){
char c = s.charAt(i + j);
num += c - '0';
}
sb.append(num);
}
return digitSum(sb.toString(), k);
}
2 在排序数组中查找元素的第一个和最后一个位置
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
public int[] searchRange(int[] nums, int target) {
int n = nums.length;
int start = binsearch(nums, target);
if (start == n || nums[start] != target){
return new int[]{-1, -1};
}
int end = binsearch(nums, target + 1) - 1;
return new int[]{start,end};
}
//闭区间二分查找
public int binsearch(int[] nums, int target){
int n = nums.length;
int left = 0, right = n - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] < target){
left = mid + 1;
}else{
right = mid - 1;
}
}
return left;
}
10 字符串压缩
面试题 01.06. 字符串压缩 - 力扣(LeetCode)
public String compressString(String S) {
int n = S.length();
//用来记录字符以及次数
Map<Character, Integer> map = new HashMap<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i ++){
char c = S.charAt(i);
map.put(c, map.getOrDefault(c, 0) + 1);
if (i > 0){ //i > 0确保左边有字符
char lc = S.charAt(i - 1); //拿到左边字符
if (lc != c){ //若c左边的字符不等于c 则将左边的字符以及个数存到sb中
sb.append(lc).append(map.get(lc));
map.put(lc,0);
}
}
//若到了最后字符
if (i == n - 1){
sb.append(c).append(map.get(c));
}
}
//sb S 哪个短返回哪个
return sb.length() < n ? sb.toString() : S;
}
11 判断是ipv4还是ipv6地址
正常解法
public String validIPAddress(String queryIP) {
if (isIPv4(queryIP)){
return "IPv4";
}else if (isIPv6(queryIP)){
return "IPv6";
}else{
return "Neither";
}
}
public boolean isIPv4(String ip){
String[] split = ip.split("\\.");
//若分割后的数组长度不为4,则不是ipv4地址
if (split.length != 4 || ip.charAt(ip.length() - 1) == '.'){
return false;
}
//遍历分割后的每个数,是否符合Ipv4的要求
for (String s : split) {
//IP为172.16.254.1 则s分别代表172 16 254 1
//1、若s的长度为0或者大于3,则不是ipv4地址
if (s.length() == 0 || s.length() > 3){
return false;
}
//2、判断首位0是否合理。172.16.254.01 不是ipv4地址,但172.0.0.1是ipv4地址
if (s.charAt(0) == '0' && s.length() != 1){
return false;
}
//3、判断是否为数字
for (int i = 0; i < s.length(); i++) {
if (!Character.isDigit(s.charAt(i))){
return false;
}
}
//4、判断是否在0~255之间(经3之后得知是数字)
int sint = Integer.parseInt(s);
if (sint < 0 || sint > 255){
return false;
}
}
//若上述都满足则返回true
return true;
}
public boolean isIPv6(String ip){
if (ip.contains("::")){
return false;
}
String[] split = ip.split(":");
//若分割后的数组长度不为8,则不是ipv6地址
if (split.length != 8 || ip.charAt(ip.length() - 1) == ':'){
return false;
}
//遍历分割后的每个数,判断每个数的长度是否小于等于4,是否为16进制数
for (String s : split) {
//"2001:0db8:85a3:0:0:8A2E:0370:7334" 是ipv6地址
//s分别代表2001 0db8 85a3 0 0 8A2E 0370 7334
//1、若s的长度大于4,则不是ipv6地址
if (s.length() > 4){
return false;
}
//2、判断是否为16进制数
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (!Character.isDigit(c) && (c < 'a' || c > 'f') && (c <'A' || c > 'F')){
return false;
}
}
}
return true;
}
正则表达式解法 (如何写出来的待学习)
public String validIPAddress(String queryIP) {
String ipv4Pattern = "((2(5[0-5]|[0-4]\\d))|(1([0-9][0-9]))|[1-9][0-9]?|[0-9])(.((2(5[0-5]|[0-4]\\d))|(1([0-9][0-9]))|[1-9][0-9]?|[0-9])){3}";
String ipv6Pattern = "([0-9a-fA-F]{1,4})(:[0-9a-fA-F]{1,4}){7}";
if(queryIP.indexOf(".") > 0 && (queryIP.matches(ipv4Pattern))){
return "IPv4";
}
if(queryIP.indexOf(":") > 0 && (queryIP.matches(ipv6Pattern))){
return "IPv6";
}
return "Neither";
}
12旋转矩阵旋转90度
面试题 01.07. 旋转矩阵 - 力扣(LeetCode)
思路:先转置,再沿着中间列翻转
public void rotate(int[][] matrix) {
int n = matrix.length;
//1转置 a[i][j] = a[j][i]
for (int i = 0; i < n; i ++){
for (int j = 0; j < i; j ++){
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
//2 沿着中间列倒转 a[i][j] = a[i][n - j - 1] 当j=0时 a[i][0] = a[i][n - 1]
for(int i = 0; i < n; i ++){
for (int j = 0; j < n / 2; j ++){
int tmp = matrix[i][j];
matrix[i][j] = matrix[i][n - j - 1];
matrix[i][n - j - 1] = tmp;
}
}
}
数学题
0 求1~n的最小公倍数
思路:先将1~n的数存入数组arr中,遍历arr数组,将arr数组中的数进行约分,将约分后的arr数组中所有数相乘存放到BigInteger
public static BigInteger getMinGBNumberSelf(int n){
//定义数组arr,存放1~n的数
int[] arr = new int[n + 1];
//初始化arr数组,将1~n的数存入arr数组
for (int i = 1; i <= n ; i++) {
arr[i] = i;
}
//遍历arr数组,将arr数组中的数进行约分
for (int i = 1; i < n; i++) {
for (int j = i + 1; j <= n; j++) {
int a = arr[i];
int b = arr[j];
if (b % a == 0){
arr[j] = b / a;
}
}
}
//使用BigInteger存储最小公倍数,防止越界
BigInteger minGB = new BigInteger("1");
for (int i = 1; i <= n; i++) {
minGB = minGB.multiply(BigInteger.valueOf(arr[i]));
}
return minGB;
}
1 求两数a,b的最小公倍数
- 思路: a,b的最大公倍数 = a * b / gcd(a, b) 这里gcd(a,b)表示a,b的最大公因数。gcd通过辗转相除法代码很简单如下
public int maxGGNum(int a, int b){
return a * b / gcdHere(a,b);
}
public int gcd(int a, int b){ //可令a=12 b=8代入模拟下
if (b == 0) return a;
else return gcd(b, a % b);
}
2 最简分数
public List<String> simplifiedFractions(int n) {
List<String> list = new ArrayList<>();
//分母
for (int i = 2; i <= n; i ++){
//分子
for (int j = 1; j < i; j ++){
if (gcd(i, j) == 1){
String str = j + "/" + i;
list.add(str);
}
}
}
return list;
}
public int gcd(int a, int b){
if (b == 0) return a;
else return gcd(b, a % b);
}
3 求n中素数的个数
题目:给定整数n,返回 所有小于非负整数的素数的数量。
注意:素数是只能被1和自身整除的正整数
public static int countPrimes20241020(int n){
if (n < 2) return 0; //当n小于等于2时,没有素数
int cnt = 0; //记录素数个数
for (int i = 2; i < n; i ++ ) {
if (isPrime20241020(i)){
cnt ++;
}
}
return cnt;
}
//判断num是否是素数
public static boolean isPrime20241020(int num){
if (num < 2) return false;
for (int i = 2; i * i <= num; i ++){
if (num % i == 0) return false;
}
return true;
}
4 判断一个数能否分解为几个连续自然数之和
思路:
若存在数a 是的 a+0 a+1 a+2 … a +(n-1) = num
则推出 num = n * a + n * (n-1)/2
则推出 n * a = num - n * (n-1)/2 把n*a记为remain
我们的思路就是计算式子中 n * a的值称为remain 然后判断remain能否整出n, 能说明存在数a使得num可以分解为n个连续自然数之和
public boolean isPossibleDivide20241020(int num) {
if (num <= 2) return false; //当num小于等于2时无法分解为多个连续自然数之和
//奇数一定可以分解成连续两个数之和 如15 = 7 + 8
if (num % 2 == 1) return true;
for (int n = 3; n < num ; n ++){
int remain = num - (n * (n - 1)) / 2;
if (remain % n == 0) {
System.out.println(remain / n + " " + n); //表示从 remain/n开始 有n个连续自然数之和为num
return true;
}
}
return false;
}
二分查找
1 非减序列查找目标值
思路:直接使用闭区间二分查找,闭区间二分查找优点,若查找的数不在,返回的是插入位置
public int searchBin20241020(int[] nums, int target){
int n = nums.length;
int left = 0, right = n - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] < target){
left = mid + 1;
}else {
right = mid - 1;
}
}
return left;
}
2 在排序数组中查找元素的第一个和最后一个位置
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
这里可以看一下:二分查找 红蓝染色法_哔哩哔哩_bilibili 这位up的讲解,讲的很到位 ,看完能知道 >= > < <= 之间的转换。对这题至关重要
public int[] searchRange(int[] nums, int target) {
int start = binsearch(nums, target); //>= 注意整数中 >x 可转成 >=x+1;<x 可转成 (>=x)-1; <=x 可转成 (>x)-1 ==> (>=x+1)-1
if (start == nums.length || nums[start] != target){
return new int[]{-1,-1};
}
int end = binsearch(nums, target + 1) - 1; //<=
return new int[]{start, end};
}
public int binsearch(int[] nums, int target){
int left = 0, right = nums.length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] < target){
left = mid + 1;
}else{
right = mid - 1;
}
}
return left;
}
链表
1 单链表相交
解法一 指针 pA 指向 A 链表,指针 pB 指向 B 链表,依次往后遍历。如果 pA 到了末尾,则 pA = headB 继续遍历、如果 pB 到了末尾,则 pB = headA 继续遍历。如此 长度差就消除了
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA;
ListNode q = headB;
while(p != q){
if (p == null) {
p = headB;
}else{
p = p.next;
}
if (q == null){
q = headA;
}else{
q = q.next;
}
}
return p == null ? null : p;
}
解法二 先得到headA,headB的长度,让长的先走长度差的单位,然后再一起走并判断节点是否相同。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode p = headA;
ListNode q = headB;
int lenA = 0;
int lenB = 0;
//获取链表长度
while (p != null){
p = p.next;
lenA ++;
}
while (q != null){
q = q.next;
lenB ++;
}
//p,q重新指向链表头
p = headA;
q = headB;
if (lenA >= lenB){
int n = lenA -lenB;
while (n -- > 0){ //让p先走n步
p = p.next;
}
}else {
int n = lenB -lenA;
while (n -- > 0){ //让q先走n步
q = q.next;
}
}
//然后一起走
while (p != null && q != null){
if (p == q) return p;
p = p.next;
q = q.next;
}
return null;
}
2 判断链表是否有环
思路:双指针
876 题解
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
//fast != null && fast.next != null 最后slow指的就是中点(偶数的话中偏右)
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
141 题解
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
142题解
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if (slow == fast){ //达到相交点后 head到环头的长度等于快慢指针相交点到环头的距离
while (head != slow){
head = head.next;
slow = slow.next;
}
return slow;
}
}
return null;
}
二叉树
1 二叉树层次遍历
思路:用一个队列,队列中保存的是同层所有节点,先让root进队列(第一层只有root),然后while(队列不为空的情况下),一直从队列中取出同层节点,再将该节点对应的下次节点入队。由于一开始拿的个数是int size = qu.size(); 规定好了个数,所以不会发生跨层拿节点的问题。
public List<List<Integer>> levelOrder(TreeNode root){
List<List<Integer>> lists = new ArrayList<>();
if (root == null) return lists;
Queue<TreeNode> qu = new LinkedList<>();
qu.add(root);
while (qu.size() > 0){
//当前层的节点个数
int size = qu.size();
List<Integer> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = qu.poll();
list.add(node.val);
if (node.left != null) qu.add(node.left);
if (node.right != null)qu.add(node.right);
}
lists.add(list);
}
return lists;
}
可用层次遍历解决的题目
- 107.二叉树的层次遍历II(opens new window)
- 199.二叉树的右视图(opens new window)
- 637.二叉树的层平均值(opens new window)
- 429.N叉树的层序遍历(opens new window)
- 515.在每个树行中找最大值(opens new window)
- 116.填充每个节点的下一个右侧节点指针(opens new window)
- 117.填充每个节点的下一个右侧节点指针II(opens new window)
- 104.二叉树的最大深度(opens new window)
- 111.二叉树的最小深度
2 二叉树中的最大路径和
124. 二叉树中的最大路径和 - 力扣(LeetCode)
class Solution {
int sum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return sum;
}
public int dfs(TreeNode root){
if (root == null) return 0;
//递归计算左右子节点的最大贡献值,只有在最大贡献值大于0时,才会选取对应子节点
int leftV = Math.max(dfs(root.left), 0);
int rightV = Math.max(dfs(root.right),0);
//更新最大路径
sum = Math.max(root.val + leftV + rightV, sum);
//返回当前节点的最大贡献值
return root.val + Math.max(leftV, rightV);
}
}
传统双指针
1 三数之和
public List<List<Integer>> threeSum(int[] nums) {
//对数组nums排序
Arrays.sort(nums);
//lists保存结果
List<List<Integer>> lists = new ArrayList<>();
int n = nums.length;
for (int i = 0;i < n; i ++){
int cur = nums[i];
//如果cur大于0,则直接退出循环
if (cur > 0) break;
//如果cur与之前一个元素相同,则跳过
if (i > 0 && cur == nums[i - 1]) continue;
//转成在[i+1,n-1]中求两数之和为target的问题
int target = -nums[i];
//定义左右指针
int left = i + 1, right = n - 1;
while (left < right){
int lNum = nums[left];
int rNum = nums[right];
if (lNum + rNum == target){
List<Integer> list = new ArrayList<>();
list.add(cur);list.add(lNum);list.add(rNum);
lists.add(list);
//接着左指针右移 右指针左移 同时要跳过相同的数
while(left + 1 < right && nums[left + 1] == lNum) left ++;
while(left < right - 1 && nums[right - 1] == rNum) right --;
left ++;
right --;
}else if (lNum + rNum > target){
right --;
}else{
left ++;
}
}
}
return lists;
}
2 最长回文字符串
思路**:两重for**循环 拿到所有可能长度的子串 根据下标判断子串是否回文串 若是判断是否需要更新 最长回文子串的[begin,end)
暴力解,用到了双指针
public String longestPalindrome(String s) {
int n = s.length();
char[] chars = s.toCharArray();
//记录最长回文子串的长度
int max = 0;
//记录最长回文子串的起始下标
int begin = 0;
int end = 0;
//i遍历所有起始位置 [i,j]
for(int i = 0; i < n; i ++) {
//j遍历所有结束位置
for (int j = i; j < n; j ++){
//若s下标[i,j]是回文串
if (isHW(chars,i,j)){
//判断是否需要更新
if (j - i + 1 > max){
max = j - i + 1;
begin = i;
end = j + 1;
}
}
}
}
return s.substring(begin, end);
}
public boolean isHW(char[] chars,int left,int right){
while(left <= right){
if (chars[left] != chars[right]) return false;
left ++;
right --;
}
return true;
}
3 判断回文串
public boolean isPalindrome(String s) {
//将所有大写字母转为小写字母
s = s.toLowerCase();
//将所有非小写字母 数字字符替换成空字符(移除所有非小写字母 数字字符)
s = s.replaceAll("[^0-9a-z]","");
int n = s.length();
int left = 0, right = n - 1;
while(left < right){
char lc = s.charAt(left);
char rc = s.charAt(right);
if (lc != rc) return false;
left ++;
right --;
}
return true;
}
4 回文子串
本人二面手撕代码原题,最优解要用到动态规划思想,我没有想出来,本人最后写出暴力解如下,暴力解思路就是拿到所有子串,判断子串是否是回文串,是数量就+1
public int getSumHw(String s){
int n = s.length();
char[] chars = s.toCharArray();
int cnt = 0; //记录回文子串的数量
for(int start = 0; start < n; start ++){
for (int end = start + 1; end <= n; end++) {
//判断s中[start,end) 是否是回文串
if (isHW(chars, start, end)){
cnt ++;
}
}
}
return cnt;
}
//判断chars数组中[start,end-1]下标组成的字符串是否是回文串
private boolean isHW(char[] chars, int start, int end) {
int left = start, right = end - 1;
while (left < right){
if (chars[left] != chars[right]) {
return false;
}
left ++;
right --;
}
return true;
}
滑动窗口
思路: 连续 最大 长度 要想起滑动窗口
可以去看下这个up讲滑动窗口的视频,我就是看了他讲的,讲的很不错:滑动窗口【基础算法精讲 03】_哔哩哔哩_bilibili
1 最长不重复子串
public int lengthOfLongestSubstring(String s) {
int n = s.length();
//左指针
int left = 0;
//最长子串的长度
int maxLen = 0;
//定义map存放[left,right]区间中各个字符出现的个数
HashMap<Character, Integer> map = new HashMap<>();
for (int right = 0; right < n; right++) {
//右指针指向的字符
char rightChar = s.charAt(right);
//存到map中,该字符出现的次数加1
map.put(rightChar, map.getOrDefault(rightChar, 0) + 1);
//map.get(rightChar) > 1 表示右字符在前面接上的字符串中已经出现过,
//此时需要移动左指针
while (map.get(rightChar) > 1){
//左指针指向的字符
char leftChar = s.charAt(left);
//将左指针指向的字符在map中的个数减1
map.put(leftChar, map.get(leftChar) - 1);
//同时左指针左移一个单位
left++;
}
//上述while之后,得到的[left,right]区间中的字符串是不重复的
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
2 长度最小的子数组
public int minSubArrayLen20241016(int target, int[] nums) {
int n = nums.length;
int minLen = Integer.MAX_VALUE;
int sum = 0;
int left = 0;
for (int right = 0; right < n; right ++){
//加入当前right值到sum中
sum += nums[right];
//若此时sum大于target,则向右移动左指针尝试找到长度最小的总和大于等于target的子数组
while (sum >= target){
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left];
left ++;
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
3 乘积小于 K 的子数组
713. 乘积小于 K 的子数组 - 力扣(LeetCode)
public int numSubarrayProductLessThanK(int[] nums, int k) {
if (k <= 1) return 0;
int n = nums.length;
//左指针
int left = 0;
//乘积初始化时要为1而不是0
int sum = 1;
//符合要求的数组个数
int cnt = 0;
for (int right = 0; right < n; right++) {
sum *= nums[right];
//当乘积大于等于k时,左指针右移,直到符合要求(乘积小于k)
while (sum >= k){
//当不符合要求时,左指针右移
sum /= nums[left];
left++;
}
//当右节点为right,此时符合要求的数组个数如下式
cnt += right - left + 1;
}
return cnt;
}
4 最长的连续绿色衣服士兵
题目(华为可信科目二):一队士兵排成一排,身穿绿色跟黑色衣服,如果可以随便将一个士兵移除队伍,那么求最长的连续绿色衣服的士兵队伍长度,示例:绿色表示1,黑色表示0
输入:10111
输出: 4
表示移出第二个位置的黑色士兵,最大连续长度为4.
public static int longestOnes(int[] nums){
int n = nums.length;
int cnt0 = 0; //表示滑动窗口里的0的个数
int max1 = 0; //表示最大连续1的个数
int left = 0; //左指针
//最长可翻转1 也就是变成了求滑动窗口内0的数量小于等于1时最大滑动窗口大小
for (int right = 0; right < n; right++) {
int rNum = nums[right];
if (rNum == 0) cnt0 ++;
//当cnt0 滑动窗口左移
while (cnt0 > 1){
int lNum = nums[left];
if (lNum == 0) cnt0 --;
left ++;
}
max1 = Math.max(max1, right - left + 1);
}
return cnt0 == 1 ? max1 - 1 : max1;
}
5 最大连续1的个数|||
1004. 最大连续1的个数 III - 力扣(LeetCode)
思路:转换为求滑动窗口内0的个数小于等于k时,滑动窗口的最大值
public int longestOnes(int[] nums, int k) {
//思路 转换成当cnt0 <= k时,滑动窗口长度最大值
int n = nums.length;
int cnt0 = 0; //表示当前滑动窗口内0的个数
int maxlen = 0; //表示当前最长的滑动窗口
int left = 0;
for (int right = 0; right < n; right ++){
int rNum = nums[right];
if (rNum == 0) cnt0 ++;
//若前面操作使得cnt0 > k,则需要让left右移,直到cnt0 <= k
while (cnt0 > k){
int lNum = nums[left];
if (lNum == 0) cnt0 --;
left ++;
}
//经上面操作后 cnt0 <= k
//判断是否更新maxlen。 right - left + 1为当前滑动窗口大小与max1比较,取最大值
maxlen = Math.max(maxlen, right - left + 1);
}
return maxlen;
}
6 加油站
思路:目的 找到以left开始 长度为n的滑动窗口即是正确的流程,用rest来表示剩余油量
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
int rest = 0; //表示剩余汽油量
int left = 0; //左指针
//目的 找到以left开始 长度为n的滑动窗口即是正确的流程
for (int right = 0; right - left < n; right ++){
rest += gas[right % n] - cost[right % n]; //因为right可能回调 从 4调到1
//若当前滑动窗口 剩余油量小于0说明 不能以该left为起点 让left右移
while (rest < 0 && left < n){
rest -= gas[left] - cost[left];
left ++;
}
}
return rest >= 0 ? left : - 1;
}
7 求字符串中同一字母连续出现的最大次数
题目描述: 给你一个字符串,只包含大写字母,求同一字母连续出现的最大次数。
例如”AAAABBCDHHH”,同一字母连续出现的最大次数为4,因为一开始A连续出现了4次。
ADDDDBDDDCAAAAAAA 7
这是本人一面手撕代码原题
public static int maxLX(String s){
int n = s.length();
int max = 0;
int left = 0;
char win = s.charAt(0); //初始 将滑动窗口内的字符设置为最左边的字符
for (int right = 0; right < n; right++) {
char rChar = s.charAt(right);
while (rChar != win){ //若右字符与滑动窗口内字符不一致
char lChar = s.charAt(left);
if (lChar == win) left ++; //若当前左指针指向的字符和win相同 则让左指针右移
else { //否则更新滑动窗口字符以及左指针位置
win = rChar;
left = right;
}
}
max = Math.max(right - left + 1, max);
}
return max;
}
8 找最大连续子串 (遇到换题)
题目(华为可信科目二):一串字符串由"1-10"“A-Z"组成,各字符之间由”"隔开,找到最大连续的子串。(10后面的字符是A)
输入:1,2,3,4,5,7,8 输出1 2 3 4 5
这题,我做了挺久但苦于没有输入输出用例,所以想着如果遇到了就换题
栈
1 Linux路径处理
思路:
1.使用栈来存储路径
2.遍历路径,遇到**…则弹出栈顶元素,遇到.**或者空字符串则跳过,其他情况则入栈
3.最后将栈中元素用**/*连接起来
public String simplifyPath(String path) {
//将path用"\"切割
String[] sp = path.split("/");
Deque<String> stack = new ArrayDeque<>();
for(String str : sp){
if (str.equals("..")){ //当前字符串为".." 若栈不为空则栈顶出栈一个元素
if(!stack.isEmpty())stack.pop();
}else if(str.equals(".") || str.equals("")){ //若当前字符为"." 或是""空字符串(切割可能产生空字符串) 则跳过
continue;
}else{ //入栈
stack.push(str);
}
}
//用栈底开始出栈,拼接到sb中
StringBuilder sb = new StringBuilder();
while(!stack.isEmpty()){
sb.append("/").append(stack.pollLast());
}
return sb.length() == 0 ? "/" : sb.toString();
}
2 小行星碰撞
public int[] asteroidCollision(int[] asteroids) {
ArrayDeque<Integer> stack = new ArrayDeque<>();
int n = asteroids.length;
for(int i = 0; i < n; i ++){
int cur = asteroids[i];
boolean alive = true; //表示cur是否存在,默认为true
//栈不为空,且cur与栈顶元素方向相反 发生碰撞
while (alive && !stack.isEmpty() && (cur < 0 && stack.peek() > 0)){
int absCur = Math.abs(cur);
int absPeek = Math.abs(stack.peek());
if (absCur < absPeek){//若cur的绝对值< peek,则碰撞后cur不存在, peek仍然存在
alive = false;
}
else if(absCur > absPeek){ //若cur的绝对值 > peek 则碰撞后cur存在,peek不存在
alive = true;
stack.pop();
}else{ //若cur绝对值 == peek 则两者都不存在
alive = false;
stack.pop();
}
}
if (alive){
stack.push(cur);
}
}
int[] ans = new int[stack.size()];
int j = 0;
while (!stack.isEmpty()){
ans[j] = stack.pollLast();
j ++;
}
return ans;
}
3 括号内字符串翻转
1190. 反转每对括号间的子串 - 力扣(LeetCode)
思路:
左括号,则将之前准备好的sb入栈,并重置sb
右括号 将当前sb翻转,并出栈一个字符串插到翻转后的sb最前面
public String reverseParentheses(String s) {
int n = s.length();
StringBuilder sb = new StringBuilder();
Deque<String> stack = new ArrayDeque<>();
for (int i = 0; i < n; i ++){
char c = s.charAt(i);
if (c == '('){ //入栈'('之前的sb,然后重置sb
stack.push(sb.toString());
sb = new StringBuilder();
}else if (c == ')'){//翻转')'之前的sb,并将栈顶元素出栈插到翻转后sb最前面
sb.reverse();
sb.insert(0, stack.pop());
}else{ //普通字符 则加入sb
sb.append(c);
}
}
return sb.toString();
}
4 有效的括号
思路:遇到左括号则入栈,遇到右括号则出栈(出栈时需进行括号匹配)
public boolean isValid(String s) {
int n = s.length();
Deque<Character> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++){
char c = s.charAt(i);
//当c为左括号 入栈
if (c == '(' || c == '{' || c =='['){
stack.push(c);
}else{//当c为右括号,进行括号匹配
//如果此刻栈为空,则没有左括号与该右括号匹配 返回false
if (stack.isEmpty()) return false;
//先将栈顶元素拿到(不是出栈)
char peek = stack.peek();
//括号匹配方法1,左右括号是同一类型时,匹配失败,返回false
if (peek == '(' && c != ')') return false;
if (peek == '[' && c != ']') return false;
if (peek == '{' && c != '}') return false;
//到这就是匹配成功 出栈
stack.pop();
//括号匹配方法2,左右括号是同一类型时,匹配成功,栈顶元素(左括号)出栈,若一直没匹配到最后返回false
// if (peek == '(' && c == ')'){
// //匹配成功,栈顶元素出栈
// stack.pop();
// }
// if (peek =='{' && c == '}'){
// stack.pop();
// }
// if (peek == '[' && c == ']'){
// stack.pop();
// }
// return false;
}
}
//最后栈是空 才表明括号匹配全部成功,否则表示有括号匹配失败 返回false
return stack.isEmpty() ? true : false;
}
单调栈
1 移除k位数字,求最小数 (遇到换题)
思路:通过单调栈,保证每次新加入的字符都比前面的字符小,遇到大的字符则移除,这样可以形成最小的数。代码通过遍历和判断来移除尽量大的数字,并在必要时移除队列尾部的元素。
public String removeKdigits(String num, int k) {
if (num.length() <= k) return "0";
//使用双端队列来保存可能的最小数字
LinkedList<Character> deque = new LinkedList<>();
int n = num.length();
//遍历num中的每个字符
for (int i = 0; i < n; i++) {
char c = num.charAt(i);
//当队列不空且 k > 0 且队尾字符大于当前字符时,移除队尾字符
while (!deque.isEmpty() && k >0 && deque.peekLast() > c){
deque.pollLast(); //移除队尾元素
k --; //移除的字符数减少一个
}
//将当前字符添加到队列的末尾
deque.offerLast(c);
}
//如果还没够删够k位数字,继续删除队列尾部的字符
for (int i = 0; i < k; i++) {
deque.pollLast();
}
//构建最终结果
StringBuilder sb = new StringBuilder();
boolean leadingZero = true; //是否遇到前导零
while (!deque.isEmpty()){
char digit = deque.pollFirst(); //取出队列中的第一个字符
//如果遇到前导零,则跳过
if (leadingZero && digit == '0'){
continue;
}
leadingZero = false; //一旦遇到非零字符,就不再是前导零
sb.append(digit); //将非前导零字符添加到结果中
}
//如果结果为空,则返回0
return sb.length() == 0 ? "0" : sb.toString();
}
深搜dfs,广搜bfs
1 岛屿数量
思路:若当前是岛屿,岛屿数+1,然后调用dfs进行深搜并置为0
public int numIslands(char[][] grid) {
int n = grid.length; //行
int m = grid[0].length; //列
int cnt = 0; //记录岛屿数
for (int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if (grid[i][j] == '1'){ //当前岛屿为1时
cnt ++;
dfs(grid,i,j);
}
}
}
return cnt;
}
public void dfs(char[][]grid, int i, int j){
int n = grid.length; //行
int m = grid[0].length; //列
grid[i][j] = 0; //把该岛屿置为0
//上 grid[i - 1][j]
if (i - 1 >= 0 && grid[i - 1][j] == '1'){
dfs(grid, i - 1, j);
}
//下 grid[i + 1][j]
if (i + 1 <= n - 1 && grid[i + 1][j] == '1'){
dfs(grid, i + 1, j);
}
//左 grid[i][j - 1]
if(j - 1 >= 0 && grid[i][j - 1] == '1'){
dfs(grid, i, j - 1);
}
//右 grid[i][j + 1]
if (j + 1 <= m - 1 && grid[i][j + 1] == '1'){
dfs(grid, i ,j + 1);
}
}
2 岛屿的周长
思路:依次判断每个岛的上下左右四边是否临海,临海则周长加1,本人觉得这样也很好理解
public int islandPerimeter(int[][] grid) {
int n = grid.length; //行
int m = grid[0].length; //宽
int cnt = 0;//临近海的总边数 == 周长
for (int i = 0; i < n; i ++){
for (int j = 0; j < m; j ++){
int cur = grid[i][j]; //当前
if (cur == 1){ //若当前是岛屿
int lines = 4; //一个岛屿最多能贡献4个边长(四面临海的情况下)
//上 grid[i - 1][j]
if (i - 1 >= 0 && grid[i - 1][j] == 1) lines --; //若上面是岛则贡献边减1
//下 grid[i + 1][j]
if(i + 1 <= n - 1 && grid[i + 1][j] == 1) lines--; //若下面是岛则贡献边减1
//左 grid[i][j - 1]
if(j - 1 >= 0 && grid[i][j - 1] == 1) lines--;
//右 grid[i][j + 1]
if(j + 1 <= m - 1 && grid[i][j + 1] ==1) lines --;
cnt += lines;
}
}
}
return cnt;
}
贪心
局部最优 推出全局最优
比如从一堆钱中拿十张,求拿到的总和最大。 局部最优就是每次都拿最大面值的钱 --> 凑一起就是全局最优总和最大
题目如下
动态规划,回溯,贪心
这类题型是我的薄弱点,我好多也不会,感觉需要花比较久的时间才能掌握,时间允许的话读者可以看从这里开始看下去 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili
但我因为时间有限,有的一下理解不了的,我就想着遇到了就换题。
1 最大数组和
(lc 53 最大子数组和 https://leetcode.cn/problems/maximum-subarray/description/)
-
思路:一开始看题目求最大,连续,想到滑动窗口。
但实际上该题不能直接通过滑动窗口的方法来解决,原因在于这道题,我们不明确知道最佳的窗口长度是多少,且子数组的大小和位置是动态变化的,而滑动窗口通常适用于固定长度的窗口,或者窗口的变化与某些条件相关。
a 贪心解法:若当前指针所指元素之前的和小于0,则丢弃当前元素之前的数列
public int maxSubArray(int[] nums) {
//贪心思想 若当前指针所指元素之前的和小于0,则丢弃当前元素之前的数列
int n = nums.length;
int preSum = 0; //之前元素的和
int maxSum = Integer.MIN_VALUE;
for(int i = 0; i < n; i ++){
int cur = nums[i];
if(preSum < 0){
preSum = cur;
}else{
preSum += cur;
}
maxSum = Math.max(preSum, maxSum);
}
return maxSum;
}
b 动态规划解法
核心思想是:
- 维护一个变量
currentSum
来记录当前的子数组和; - 每次遍历到一个新的元素时,判断是要将其加入现有子数组,还是重新开始一个新的子数组;
- 维护另一个变量
maxSum
来记录全局最大子数组和。
动态规划的具体步骤:
- 初始化
currentSum = nums[0]
和maxSum = nums[0]
。 - 从数组第二个元素开始,逐个遍历数组:
- 对于每个元素,更新
currentSum = max(currentSum + nums[i], nums[i])
,即判断是继续加入之前的子数组,还是从当前位置重新开始计算子数组。 - 更新
maxSum = max(maxSum, currentSum)
,记录当前出现的最大和。
- 对于每个元素,更新
- 返回
maxSum
作为结果。
public int maxSubArray(int[] nums) {
int currentSum = nums[0];
int maxSum = nums[0];
for (int i = 1; i < nums.length; i++) {
// 更新当前的子数组和,决定是继续加还是从当前元素重新开始
currentSum = Math.max(currentSum + nums[i], nums[i]);
// 更新全局最大子数组和
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}
2 换零钱
思路:dp[i] 表示凑i元钱需要的最少硬币数, 当j硬币可以用来凑i元钱时,dp[i] = Math.min(dp[i], dp[i - coinNum] + 1);
public int coinChange(int[] coins, int amount) {
int k = coins.length;
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0; //表示凑0元钱最少需要0个硬币
//遍历从1到amout元钱 的凑法
for (int i = 1; i <= amount; i ++){
for (int j = 0; j < k; j ++){
int coinNum = coins[j]; //表示j硬币的面值
//如果j硬币的面值小于i元钱,则说明可以尝试用j硬币来凑i元钱
if (coinNum <= i){
//i - coinNum表示用j硬币凑i元 剩下的钱,dp[i - coinNum]表示凑剩下的钱需要的硬币数,+ 1是因为用了一个j硬币
//因此dp[i - coinNum] + 1表示为凑剩下来的钱需要的硬币数加上用的一个j硬币
dp[i] = Math.min(dp[i], dp[i - coinNum] + 1);
}
}
}
//若dp[amount] > amount 则表示凑不成
return dp[amount] > amount ? -1 : dp[amount];
}
2 硬币兑换 (遇到换题)
这题就是本人一面遇到的第一个题目,然后本人一下想不起来就让面试官换题了
题目:在一个国家仅有1分,2分,3分硬币,将钱N兑换成硬币有很多种兑法。请你编程序计算出共有多少种兑法。
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] coins = {1, 2, 3};
int Max = 32768; //题目规定的N的上限
//dp[i] 表示凑成i元钱需要的最少硬币数
int[] dp = new int[Max + 1];
//初始化
dp[0] = 1;// 表示凑0元钱的方法只有1种,就是什么也不凑
//动态规划计算所有可能的兑换方式
for (int coin : coins) {
for (int i = coin; i <= Max; i ++){
dp[i] = dp[i] + dp[i - coin];
}
}
//处理输入
while (sc.hasNextInt()){
int N = sc.nextInt();
//输出换N元钱有多少个兑换方式
System.out.println(dp[N]);
}
sc.close();
}
时间复杂度:O(N * m)
N是要换的钱,m是硬币的种类数
空间复杂度:O(N)
,只需要一个大小为 N
的数组来存储 dp 值。
3 给定一个字符串,判断是否能够分为三个回文子串 (遇到换题)
时间原因,没有理解这道题,想着遇到就换题。
4 切割回文串最小切割次数 (遇到换题)
时间原因,没有理解这道题,想着遇到就换题。
5 救生艇
贪心思想:先对数字排序,然后尝试让最轻的人和最重的人坐一个船,若能坐下则一起坐一个船,否则最重的人单独坐一条船
public int numRescueBoats(int[] people, int limit) {
Arrays.sort(people);
int n = people.length;
int left = 0, right = n - 1;
int cnt = 0; //cnt用来记录要用多少搜救生艇
while(left <= right){
int lv = people[left];
int rv = people[right];
if (lv + rv <= limit){//若两个人的重量小于等于船最大承载
cnt ++; //两个人用一条船
left ++;
right --;
}else{ //若两个人的重量大于船最大承载
cnt ++; //右边重量大的人用一条船
right --;
}
}
return cnt;
}
6 单词拆分 (遇到换题,考前可看看)
public boolean wordBreak(String s, List<String> wordDict) {
//思路 用dp[i]表示s中[0,i-1] i前面字符能否由字典中的单词组成
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; //空字符串能由字典中的单词组成
Set<String> set = new HashSet<>(wordDict);
//由子串[start,end) 推出 dp[end]
for (int end = 1; end <= n; end ++){
for (int start = 0; start < end; start ++){
String substr = s.substring(start, end);
//检查子串s[j,i]是否在字典中,并且dp[j]是否为true
if (dp[start] == true && set.contains(substr)){
dp[end] = true;
break;
}
}
}
return dp[n];
}
7分发糖果
思路:两次循环,第一次从左到右遍历 若右边的孩子比左边孩子评分高,则右孩子糖数为左孩子糖+1,否则为1。第二次从右到左遍历 若左边孩子比右边孩子评分高,则左边孩子糖 = max(左孩子第一轮糖,右孩子糖 + 1)
public int candy(int[] ratings) {
int n = ratings.length;
int[] candy = new int[n]; //candy[i] 表示第i个孩子收到的糖数
candy[0] = 1; //初始化
int cnt = 0; //用来统计糖总数
//从左往右遍历 若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1,否则右孩子糖数为1
for (int right = 1; right < n; right ++){
int leftCandy = candy[right - 1];
int leftRate = ratings[right - 1];
int rightRate = ratings[right];
if (rightRate > leftRate) {
candy[right] = leftCandy + 1; //若右孩子评分比左孩子高则右孩子糖数 = 左孩子糖数 + 1
}else{
candy[right] = 1;//否则右孩子糖数为1
}
}
//从右往左遍历 若左孩子评分比右孩子高 左孩子糖数 = max(左孩子糖数,右孩子糖数 + 1)
for (int left = n - 2; left >= 0; left --){
int rightRate = ratings[left + 1];
int rightCandy = candy[left + 1];
int leftRate = ratings[left];
if (leftRate > rightRate){
candy[left] = Math.max(candy[left], rightCandy + 1);
}
}
//统计糖总数
for (int a : candy){
cnt += a;
}
return cnt;
}
8 字符串所有排列(遇到换题)
输入一个字符串,打印出该字符串中字符的所有排列。可以任意顺序返回这个字符串数组,但里面不能有重复元素。
LCR 157. 套餐内商品的排列顺序 - 力扣(LeetCode)
场景题
1 座位预约管理系统
-
思路: 用最小堆(优先队列来解决)
-
步骤1:在SeatManager中加入一个优先队列queue;
-
步骤2:SeatManager(n)方法将1~n个数放入最小堆,表示初始n个座位都可以预约;
-
步骤3:预约reserve()就是拿出最小堆堆头的元素(座位编号),归还unreserve(seatNumber)就是将seatNumber重新插到最小堆中
#Java
class SeatManager {
PriorityQueue<Integer> queue;
public SeatManager(int n) {
queue = new PriorityQueue<>();
//将1到n加入优先队列(最小堆)
for(int i = 1; i <= n; i ++){
queue.offer(i);
}
}
public int reserve() {
//预约目前可用最小位置编号座位
return queue.poll();
}
public void unreserve(int seatNumber) {
//将seatNumber重新插入到优先队列
queue.offer(seatNumber);
}
}
2 完美答案 (遇到换题)
暂时没思路 遇到直接让面试官换题吧
3 乘电梯最小的花费时间 (遇到换题)
暂时没思路 遇到直接让面试官换题吧
4 实现一个简易版计算器
public int calculate(String s) {
ArrayDeque<Integer> stack = new ArrayDeque<>();
int preSign = '+'; //首个数字前的加号
int n = s.length();
int num = 0;
for (int i = 0; i < n; i++){
char c = s.charAt(i);
//若当前字符为数字 则计算
if (Character.isDigit(c)){
num = num * 10 + c - '0';
}
//若c是运算符 或是最后一个字符
if (c == '+' || c == '-' || c == '*' || c == '/' || i == n - 1){
if (preSign == '+'){//若num前的运算符为+ 则把num加入栈中
stack.push(num);
}else if (preSign == '-'){//若num前的运算符为- 则把-num加入栈中
stack.push(-num);
}else if (preSign == '*'){
stack.push(stack.pop() * num);
}else{
stack.push(stack.pop() / num);
}
preSign = c;
num = 0;
}
}
int sum = 0;
while(!stack.isEmpty()){
sum += stack.pop();
}
return sum;
}
5 实现备忘录 (遇到换题)
6 LRU缓存
2025/3/5 不用换了
用双端队列加map实现
class LRUCache {
int capacity;
// 队列中存放key 和map中的key完全保持一致
Deque<Integer> deque = new LinkedList<>();
Map<Integer,Integer> map = new ConcurrentHashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if (map.containsKey(key)){
// 进行LRU操作 deque.remove(key)时间复杂度On
deque.remove(key);
deque.offer(key);
// 返回结果
return map.get(key);
}
return -1;
}
public void put(int key, int value) {
// 若key已存在,则更新value就行
if (map.containsKey(key)){
map.put(key,value);
//LRU
deque.remove(key);
deque.offer(key);
}else{ // 若是新键值对
// 先判断是否超大小
if (deque.size() == capacity){
// 若队列满了 则移除队首 即最近最少使用的key
int delKey = deque.poll();
map.remove(delKey);
}
map.put(key,value);
deque.offer(key);
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
使用双端队列加map实现的缺点
deque.remove(key)
操作性能问题:在 get
和 put
方法中,使用了 deque.remove(key)
来移除队列中的元素 以保正符合LRU。LinkedList
实现的 Deque
中,remove(Object o)
方法的时间复杂度是 O(n),因为它需要遍历链表来找到要移除的元素。这会导致在频繁调用 get
和 put
方法时,性能下降。
改进方案
可以使用 LinkedHashMap
来替代 Deque
和 HashMap
的组合,LinkedHashMap
可以维护元素的插入顺序或访问顺序,并且在访问元素时可以将其移动到链表尾部,删除元素时可以直接删除链表头部元素,时间复杂度都是 O(1)。
用LinkedHashMap实现
class LRUCache {
private LinkedHashMap<Integer, Integer> cache;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
// 初始化 LinkedHashMap,0.75表示负载因子
// 访问顺序标志,true 表示按访问顺序排序,false 表示按插入顺序排序
this.cache = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
// 当缓存大小超过容量时,移除最旧的元素
return size() > capacity;
}
};
}
public int get(int key) {
// 若 key 存在于缓存中,则返回其值
return cache.getOrDefault(key, -1);
}
public void put(int key, int value) {
// 若 key 不存在或已存在,都将其放入缓存中
cache.put(key, value);
}
}
7 带过期时间的LRU缓存
字节技术二面
实现方式:双端队列+map+线程池 设置过期时间为5秒
class LRUCache {
// LRU格子数
int capacity;
// 双端队列 ,队头表示最先移除的 队尾表示最近使用的
// offer(队尾进) offerFirst offerLast
// poll(队头出) pollFirst pollLast
// remove(key) 删除双端队列中指定元素
Deque<Integer> deque = new LinkedList<>();
// hashmap存储键值对
Map<Integer, Integer> map = new HashMap<>();
// 存储每个键的过期时间
Map<Integer, Long> expirationMap = new HashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
// 启动一个定时任务,每秒检查一次过期数据
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleAtFixedRate(this::removeExpiredEntries, 1, 1, TimeUnit.SECONDS);
}
// 移除过期的条目
private void removeExpiredEntries() {
long currentTime = System.currentTimeMillis();
List<Integer> expiredKeys = new ArrayList<>();
for (Map.Entry<Integer, Long> entry : expirationMap.entrySet()) {
int key = entry.getKey();
long expirationTime = entry.getValue();
if (currentTime >= expirationTime) {
expiredKeys.add(key);
}
}
for (int key : expiredKeys) {
deque.remove(key);
map.remove(key);
expirationMap.remove(key);
}
}
public int get(int key) {
// 若key存在map中
if (map.containsKey(key)) {
// 检查是否过期
if (System.currentTimeMillis() < expirationMap.get(key)) {
// 在队列中移除key
deque.remove(key);
// 再将key添加到最后,表示最近使用过
deque.offer(key);
// 更新过期时间
expirationMap.put(key, System.currentTimeMillis() + 5000);
// 返回key的值
return map.get(key);
} else {
// 若已过期,移除该键值对
deque.remove(key);
map.remove(key);
expirationMap.remove(key);
}
}
// 若不存在或已过期 直接返回-1
return -1;
}
public void put(int key, int value) {
// 若map中包含该key 则更新key
if (map.containsKey(key)) {
deque.remove(key);
deque.offer(key);
} else {
// 若队列满了 移除队首元素
if (deque.size() == capacity) {
int removedKey = deque.poll();
map.remove(removedKey);
expirationMap.remove(removedKey);
}
}
map.put(key, value);
deque.offer(key);
// 设置过期时间为当前时间加上5秒
expirationMap.put(key, System.currentTimeMillis() + 5000);
}
}
实现方式:LinkedHashMap+线程池 设置过期时间为5秒
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
class LRUCache {
private LinkedHashMap<Integer, CacheEntry> cache;
private int capacity;
private final long EXPIRATION_TIME = 5000; // 过期时间 5 秒
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new LinkedHashMap<Integer, CacheEntry>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, CacheEntry> eldest) {
return size() > capacity;
}
};
// 启动定时任务,每秒检查一次过期数据
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleAtFixedRate(this::removeExpiredEntries, 1, 1, TimeUnit.SECONDS);
}
// 内部类,用于存储值和过期时间
private static class CacheEntry {
int value;
long expirationTime;
CacheEntry(int value, long expirationTime) {
this.value = value;
this.expirationTime = expirationTime;
}
}
// 移除过期的条目
private void removeExpiredEntries() {
long currentTime = System.currentTimeMillis();
// 遍历缓存,找到过期的键并移除
for (Map.Entry<Integer, CacheEntry> entry : cache.entrySet()) {
if (currentTime >= entry.getValue().expirationTime) {
cache.remove(entry.getKey());
}
}
}
public int get(int key) {
CacheEntry entry = cache.get(key);
if (entry != null) {
if (System.currentTimeMillis() < entry.expirationTime) {
// 更新过期时间
entry.expirationTime = System.currentTimeMillis() + EXPIRATION_TIME;
return entry.value;
} else {
// 若已过期,移除该键值对
cache.remove(key);
}
}
return -1;
}
public void put(int key, int value) {
CacheEntry entry = new CacheEntry(value, System.currentTimeMillis() + EXPIRATION_TIME);
cache.put(key, entry);
}
}
测试代码
public class Main {
public static void main(String[] args) throws InterruptedException {
LRUCache cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1)); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
System.out.println(cache.get(2)); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
System.out.println(cache.get(1)); // 返回 -1 (未找到)
System.out.println(cache.get(3)); // 返回 3
System.out.println(cache.get(4)); // 返回 4
// 等待 6 秒,让数据过期
Thread.sleep(6000);
System.out.println(cache.get(3)); // 返回 -1 (已过期)
System.out.println(cache.get(4)); // 返回 -1 (已过期)
}
}