本节所有题目、思路、代码参考了左程云老师“算法与数据结构大厂刷题班”的部分内容,特此说明。
题目1:LongestSubstringWithoutRepeatingCharacters
问题描述
求一个字符串中,最长无重复字符子串长度
思路
【字符串dp】
- 字符串问题,考虑以i位置结尾的字符串往前推,推到一个最长无重复的串的最长长度
- 也就是推到不能再推
- i位置的答案有两个因素决定
- 一个是i位置的字符上次出现的位置
- 一个是i-1位置的字符最远能推到哪
- 第一个因素用hash表(数组就可以)记录
- 第二个直接取出i-1位置的答案
- 由于i位置的答案只需要用到i-1位置的答案
- 所以没必要记录所有的答案,直接用一个pre变量滚动接收每次的答案当作下一位的i-1位置的答案即可
代码
package class03;
// 本题测试链接 : https://leetcode.com/problems/longest-substring-without-repeating-characters/
// 最长不重复字串
// 字串:开头结尾
public class Code01_LongestSubstringWithoutRepeatingCharacters {
//决定因素1,上次出现的位置
//决定因素2,前一个推不到的地方,这个位置也推不到
//谁离得近,取谁作为能推出来的长度
//当前字符上次出现的位置怎么维护? 很简单,直接用一张hashmap
public static int lengthOfLongestSubstring(String s) {
if (s == null || s.equals("")) {
return 0;
}
char[] str = s.toCharArray();
int[] map = new int[256];
for (int i = 0; i < 256; i++) {
map[i] = -1;
}
map[str[0]] = 0;
int N = str.length;
int ans = 1;
int pre = 1;
for (int i = 1; i < N; i++) {
pre = Math.min(i - map[str[i]], pre + 1);
ans = Math.max(ans, pre);
map[str[i]] = i;
}
return ans;
}
public static void main(String[] args) {
System.out.println(lengthOfLongestSubstring("xialinxiarbw"));
}
}
题目2:HowManyTypes
问题描述
只由小写字母(a~z)组成的一批字符串,都放在字符类型的数组String[] arr中,如果其中某两个字符串所含有的字符种类完全一样 就将两个字符串算作一类,比如baacbba和bac就算作一类,返回arr中有多少类
思路
【位图】
- 一个整数表示一个摘要
- 一个整数26位,用位图实现
- 如果字符串出现A,这个整数的第0位标1
- 如果出现字符串B,这个整数的第1位标1
- 一个字符串处理完,必定会生成一个整数,存进去hashset
- 怎么标?准备一个32位都为0的整数,某一位标1的方法是,把1左移到这一位,或进去
代码
package class03;
import java.util.HashSet;
public class Code02_HowManyTypes {
/*
* 只由小写字母(a~z)组成的一批字符串,都放在字符类型的数组String[] arr中,
* 如果其中某两个字符串,所含有的字符种类完全一样,就将两个字符串算作一类 比如:baacba和bac就算作一类
* 虽然长度不一样,但是所含字符的种类完全一样(a、b、c) 返回arr中有多少类?
*/
public static int types1(String[] arr) {
HashSet<String> types = new HashSet<>();
for (String str : arr) {
char[] chs = str.toCharArray();
boolean[] map = new boolean[26];
for (int i = 0; i < chs.length; i++) {
map[chs[i] - 'a'] = true;
}
String key = "";
for (int i = 0; i < 26; i++) {
if (map[i]) {
key += String.valueOf((char) (i + 'a'));
}
}
types.add(key);
}
return types.size();
}
public static int types2(String[] arr) {
HashSet<Integer> types = new HashSet<>();
for (String str : arr) {
char[] chs = str.toCharArray();
int key = 0;
for (int i = 0; i < chs.length; i++) {
key |= (1 << (chs[i] - 'a'));
}
types.add(key);
}
return types.size();
}
// for test
public static String[] getRandomStringArray(int possibilities, int strMaxSize, int arrMaxSize) {
String[] ans = new String[(int) (Math.random() * arrMaxSize) + 1];
for (int i = 0; i < ans.length; i++) {
ans[i] = getRandomString(possibilities, strMaxSize);
}
return ans;
}
// for test
public static String getRandomString(int possibilities, int strMaxSize) {
char[] ans = new char[(int) (Math.random() * strMaxSize) + 1];
for (int i = 0; i < ans.length; i++) {
ans[i] = (char) ((int) (Math.random() * possibilities) + 'a');
}
return String.valueOf(ans);
}
public static void main(String[] args) {
int possibilities = 5;
int strMaxSize = 10;
int arrMaxSize = 100;
int testTimes = 500000;
System.out.println("test begin, test time : " + testTimes);
for (int i = 0; i < testTimes; i++) {
String[] arr = getRandomStringArray(possibilities, strMaxSize, arrMaxSize);
int ans1 = types1(arr);
int ans2 = types2(arr);
if (ans1 != ans2) {
System.out.println("Oops!");
}
}
System.out.println("test finish");
}
}
题目3:Largest1BorderedSquare
问题描述
给定一个只有0和1组成的二维数组,返回边框全是1(内部无所谓)的最大正方形面积
思路
【预处理数组方法】
- 几个心里有数的结论:
- NN区域中,长方形数量级是N4(N2N2,选两个点的可能性)
- NN区域中,正方形的数量级是N3(N2N,选一个点和边长的可能性)
- 法1,枚举所有正方形,往右下方向,两个for枚举点的可能性,边长枚举时候,右、下哪个距离短选哪个当作边长最大值,可以枚举所有正方形。如何验证正方形边框是不是1,最好O(1)?
- 维护一个表,(i,j)位置右边有多少个连续的1,下边有多少个连续的1。判断的时候调三个点的信息即可。
- 表怎么生成?两个二维数组,一个生成每个点右边连续的1;一个生成每个点下边连续的1;遍历一边生成右信息,从右到左遍历,如果原数组这个位置是1,++就可,如果是0,归零。下信息同理。
代码
package class03;
// 本题测试链接 : https://leetcode.com/problems/largest-1-bordered-square/
public class Code03_Largest1BorderedSquare {
public static int largest1BorderedSquare(int[][] m) {
int[][] right = new int[m.length][m[0].length];
int[][] down = new int[m.length][m[0].length];
setBorderMap(m, right, down);
for (int size = Math.min(m.length, m[0].length); size != 0; size--) {
if (hasSizeOfBorder(size, right, down)) {
return size * size;
}
}
return 0;
}
public static void setBorderMap(int[][] m, int[][] right, int[][] down) {
int r = m.length;
int c = m[0].length;
if (m[r - 1][c - 1] == 1) {
right[r - 1][c - 1] = 1;
down[r - 1][c - 1] = 1;
}
for (int i = r - 2; i != -1; i--) {
if (m[i][c - 1] == 1) {
right[i][c - 1] = 1;
down[i][c - 1] = down[i + 1][c - 1] + 1;
}
}
for (int i = c - 2; i != -1; i--) {
if (m[r - 1][i] == 1) {
right[r - 1][i] = right[r - 1][i + 1] + 1;
down[r - 1][i] = 1;
}
}
for (int i = r - 2; i != -1; i--) {
for (int j = c - 2; j != -1; j--) {
if (m[i][j] == 1) {
right[i][j] = right[i][j + 1] + 1;
down[i][j] = down[i + 1][j] + 1;
}
}
}
}
public static boolean hasSizeOfBorder(int size, int[][] right, int[][] down) {
for (int i = 0; i != right.length - size + 1; i++) {
for (int j = 0; j != right[0].length - size + 1; j++) {
if (right[i][j] >= size && down[i][j] >= size && right[i + size - 1][j] >= size
&& down[i][j + size - 1] >= size) {
return true;
}
}
}
return false;
}
}
题目4:MaxPairNumber
问题描述
给定一个数组arr,代表每个人的能力值。再给定一个非负数k,如果两个人能力差值正好为k,那么可以凑在一起比赛 一局比赛只有两个人,返回最多可以同时有多少场比赛
思路
代码
package class03;
import java.util.Arrays;
// 给定一个数组arr,代表每个人的能力值。再给定一个非负数k。
// 如果两个人能力差值正好为k,那么可以凑在一起比赛,一局比赛只有两个人
// 返回最多可以同时有多少场比赛
public class Code04_MaxPairNumber {
// 暴力解
public static int maxPairNum1(int[] arr, int k) {
if (k < 0) {
return -1;
}
return process1(arr, 0, k);
}
public static int process1(int[] arr, int index, int k) {
int ans = 0;
if (index == arr.length) {
for (int i = 1; i < arr.length; i += 2) {
if (arr[i] - arr[i - 1] == k) {
ans++;
}
}
} else {
for (int r = index; r < arr.length; r++) {
swap(arr, index, r);
ans = Math.max(ans, process1(arr, index + 1, k));
swap(arr, index, r);
}
}
return ans;
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 时间复杂度O(N*logN)
//排序+窗口
// 差值广义上递增,用左右两个指针代表当时选的两个人
//贪心的点在于,让小值的人先凑比赛,最终一定不会有情况比这种策略下更多
//看L位置的人能不能被满足
//R不会重复,因为R一直往右跑
public static int maxPairNum2(int[] arr, int k) {
if (k < 0 || arr == null || arr.length < 2) {
return 0;
}
Arrays.sort(arr);
int ans = 0;
int N = arr.length;
int L = 0;
int R = 0;
//用过没有
boolean[] usedR = new boolean[N];
//正式的寻找过程
while (L < N && R < N) {
//L使用过,右移
if (usedR[L]) {
L++;
} else if (L >= R) {//没有或者一个人
R++;
} else { // 不止一个数,而且都没用过!
//差值是多少
int distance = arr[R] - arr[L];
if (distance == k) {//如果刚好是K
ans++;
usedR[R++] = true;
L++;
} else if (distance < k) {//距离小,L++
R++;
} else {//距离大
L++;
}
}
}
return ans;
}
// 为了测试
public static int[] randomArray(int len, int value) {
int[] arr = new int[len];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * value);
}
return arr;
}
// 为了测试
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 为了测试
public static int[] copyArray(int[] arr) {
int[] ans = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
ans[i] = arr[i];
}
return ans;
}
public static void main(String[] args) {
int maxLen = 10;
int maxValue = 20;
int maxK = 5;
int testTime = 1000;
System.out.println("功能测试开始");
for (int i = 0; i < testTime; i++) {
int N = (int) (Math.random() * (maxLen + 1));
int[] arr = randomArray(N, maxValue);
int[] arr1 = copyArray(arr);
int[] arr2 = copyArray(arr);
int k = (int) (Math.random() * (maxK + 1));
int ans1 = maxPairNum1(arr1, k);
int ans2 = maxPairNum2(arr2, k);
if (ans1 != ans2) {
System.out.println("Oops!");
printArray(arr);
System.out.println(k);
System.out.println(ans1);
System.out.println(ans2);
break;
}
}
System.out.println("功能测试结束");
}
}
题目5:Code05_BoatsToSavePeople
问题描述
给定一个正数数组arr,代表若干人的体重,再给定一个正数limit,表示所有船共同拥有的载重量,每艘船最多坐两人,且不能超过载重 想让所有的人同时过河,并且用最好的分配方法让船尽量少,返回最少的船数 Leetcode链接 : leetcode.com/problems/bo…
思路
- 边界判断,有人体重比limit大,返回无穷大
- 排序
- 卡<=(limit / 2)最右的位置,L指向它,R指向后一个
- 整体贪心思想:极大极小相互吞噬
- 构建例子去想边界吧,有点麻烦
- 中间分界点开始往左右两边划
代码
package class03;
import java.util.Arrays;
// 给定一个正数数组arr,代表若干人的体重
// 再给定一个正数limit,表示所有船共同拥有的载重量
// 每艘船最多坐两人,且不能超过载重
// 想让所有的人同时过河,并且用最好的分配方法让船尽量少
// 返回最少的船数
// 测试链接 : https://leetcode.com/problems/boats-to-save-people/
public class Code05_BoatsToSavePeople {
public static int numRescueBoats1(int[] arr, int limit) {
if (arr == null || arr.length == 0) {
return 0;
}
int N = arr.length;
Arrays.sort(arr);
if (arr[N - 1] > limit) {
return -1;
}
int lessR = -1;
for (int i = N - 1; i >= 0; i--) {
if (arr[i] <= (limit / 2)) {
lessR = i;
break;
}
}
if (lessR == -1) {
return N;
}
int L = lessR;
int R = lessR + 1;
int noUsed = 0;
while (L >= 0) {
int solved = 0;
while (R < N && arr[L] + arr[R] <= limit) {
R++;
solved++;
}
if (solved == 0) {
noUsed++;
L--;
} else {
L = Math.max(-1, L - solved);
}
}
int all = lessR + 1;
int used = all - noUsed;
int moreUnsolved = (N - all) - used;
return used + ((noUsed + 1) >> 1) + moreUnsolved;
}
// 首尾双指针的解法
public static int numRescueBoats2(int[] people, int limit) {
Arrays.sort(people);
int ans = 0;
int l = 0;
int r = people.length - 1;
int sum = 0;
while (l <= r) {
sum = l == r ? people[l] : people[l] + people[r];
if (sum > limit) {
r--;
} else {
l++;
r--;
}
ans++;
}
return ans;
}
}
题目6:ClosestSubsequenceSum
问题描述
给定整数数组nums和目标值goal,需要从nums中选出一个子序列,使子序列元素总和最接近goal
也就是说如果子序列元素和为sum ,需要最小化绝对差abs(sum - goal),返回 abs(sum - goal)可能的最小值
注意数组的子序列是通过移除原始数组中的某些元素(可能全部或无)而形成的数组。
数据范围:
0<=nums.length<=40
0<=nums[i]<=108
0<=goal<=108
思路
【分治】
- 数组长度少,用分治
- 具体做法是
- 把40拆成左右两半:
- 左边暴力枚举出所有的累加和的情况,每一个位置可以选和不选,一共是220的复杂度
- 同理,右边暴力枚举出所有的累加和也是220复杂度
- 目标是让累加和与给定了target最接近
- 那么有这么几种情况,左边的某一个累加和有一个根target最接近的
- 右边也有一个累加和与target最接近的
- 或者,左边给我一个累加和,右边选哪一个累加和加起来更近,这个交叉计算的时间复杂度是多少呢?
- 其实左边任选一个累加和x是220,选定这个的情况下,问题就变成了
- 在右侧找出距离target - x最近的数。最好的做法是右边用有序表存,查询距离某个值最近的数 直接二分得log2220=20
- 因此交叉计算的时间复杂度能做到20*220
- 总的时间复杂度就是220+220+20220=11221能过
代码
package class03;
import java.util.Arrays;
// 本题测试链接 : https://leetcode.com/problems/closest-subsequence-sum/
// 本题数据量描述:
// 1 <= nums.length <= 40
// -10^7 <= nums[i] <= 10^7
// -10^9 <= goal <= 10^9
// 通过这个数据量描述可知,需要用到分治,因为数组长度不大
// 而值很大,用动态规划的话,表会爆
public class Code06_ClosestSubsequenceSum {
public static int[] l = new int[1 << 20];
public static int[] r = new int[1 << 20];
public static int minAbsDifference(int[] nums, int goal) {
if (nums == null || nums.length == 0) {
return goal;
}
int le = process(nums, 0, nums.length >> 1, 0, 0, l);
int re = process(nums, nums.length >> 1, nums.length, 0, 0, r);
Arrays.sort(l, 0, le);
Arrays.sort(r, 0, re--);
int ans = Math.abs(goal);
for (int i = 0; i < le; i++) {
int rest = goal - l[i];
while (re > 0 && Math.abs(rest - r[re - 1]) <= Math.abs(rest - r[re])) {
re--;
}
ans = Math.min(ans, Math.abs(rest - r[re]));
}
return ans;
}
public static int process(int[] nums, int index, int end, int sum, int fill, int[] arr) {
if (index == end) {
arr[fill++] = sum;
} else {
fill = process(nums, index + 1, end, sum, fill, arr);
fill = process(nums, index + 1, end, sum + nums[index], fill, arr);
}
return fill;
}
}
题目7:FreedomTrail
题目描述
电子游戏“辐射4”中,任务“通向自由”要求玩家到达名为“Freedom Trail Ring”的金属表盘,并使用表盘拼写特定关键词才能开门 给定一个字符串 ring,表示刻在外环上的编码;给定另一个字符串 key,表示需要拼写的关键词。您需要算出能够拼写关键词中所有字符的最少步数 最初,ring 的第一个字符与12:00方向对齐。您需要顺时针或逆时针旋转 ring 以使 key 的一个字符在 12:00 方向对齐,然后按下中心按钮,以此逐个拼写完 key 中的所有字符 旋转 ring 拼出 key 字符 key[i] 的阶段中: 您可以将 ring 顺时针或逆时针旋转一个位置,计为1步。旋转的最终目的是将字符串 ring 的一个字符与 12:00 方向对齐,并且这个字符必须等于字符 key[i] 。 如果字符 key[i] 已经对齐到12:00方向,您需要按下中心按钮进行拼写,这也将算作 1 步。按完之后,您可以开始拼写 key 的下一个字符(下一阶段), 直至完成所有拼写。 Leetcode题目:leetcode.com/problems/fr…
思路
- 先处理几个小函数:
- 第一个是从i位置拨到j位置最省的代价,返回;这个很简答,正和逆就能搞定,取最小值
- 第二是处理之前预处理出一张表,比如字符串“afaxacf”,哪些位置都是a,哪些位置都是f……
- 然后dfs,看拨到当前字符有多少种选择,递归就行,水
- 参数:
- preButton:指针指向的上一个按键是几
- keyIndex:此时要转到谁(搞定哪个字符)
- map:任何一种字符在电话里哪些位置有(HashMap<Character,ArrayList>)
- N:电话机有多少个字符
- str:要搞定的串
- 这个dfs直接改记忆化搜索,应该就是最优解
- dfs过程:
- 终止位置:return 0
- 当前要搞定的字符拿出来,哪些位置可选拿出来
- 初始话这一步的全局最大值
- 选每一个位置,更新代价是拨号代价+确认代价+后续搞定
- 然后改成记忆化搜索直接提速
- 总结一下这个题,最重要的是在写之前想好具体过程,写一个大致例子串一串;想想要卡的边界,不然dfs很容易错
代码
package class03;
o
import java.util.ArrayList;
import java.util.HashMap;
// 本题测试链接 : https://leetcode.com/problems/freedom-trail/
public class Code07_FreedomTrail {
public static int findRotateSteps(String r, String k) {
char[] ring = r.toCharArray();
int N = ring.length;
HashMap<Character, ArrayList<Integer>> map = new HashMap<>();
for (int i = 0; i < N; i++) {
if (!map.containsKey(ring[i])) {
map.put(ring[i], new ArrayList<>());
}
map.get(ring[i]).add(i);
}
char[] str = k.toCharArray();
int M = str.length;
int[][] dp = new int[N][M + 1];
// hashmap
// dp[][] == -1 : 表示没算过!
for (int i = 0; i < N; i++) {
for (int j = 0; j <= M; j++) {
dp[i][j] = -1;
}
}
return process(0, 0, str, map, N, dp);
}
// 电话里:指针指着的上一个按键preButton
// 目标里:此时要搞定哪个字符?keyIndex
// map : key 一种字符 value: 哪些位置拥有这个字符
// N: 电话大小
// f(0, 0, aim, map, N)
public static int process(int preButton, int index, char[] str, HashMap<Character, ArrayList<Integer>> map, int N,
int[][] dp) {
if (dp[preButton][index] != -1) {
return dp[preButton][index];
}
int ans = Integer.MAX_VALUE;
if (index == str.length) {
ans = 0;
} else {
// 还有字符需要搞定呢!
char cur = str[index];
ArrayList<Integer> nextPositions = map.get(cur);
for (int next : nextPositions) {
int cost = dial(preButton, next, N) + 1 + process(next, index + 1, str, map, N, dp);
ans = Math.min(ans, cost);
}
}
dp[preButton][index] = ans;
return ans;
}
public static int dial(int i1, int i2, int size) {
return Math.min(Math.abs(i1 - i2), Math.min(i1, i2) + size - Math.max(i1, i2));
}
}
题目8
题目描述
给定三个参数,二叉树的头节点head,树上某个节点target,正数K。从target开始,可以向上走或者向下走,返回与target的距离是K的所有节点
思路
【二叉树的递归】【图的bfs】【树的图处理化】
- 本质就是一个节点往三个方向走
- 核心就是怎么获取三个方向的节点
- 很简单,左右子节点通过左右指针获取即可
- 怎么获取父节点呢?
- 很好办,在bfs之前先去生成一张hash表,记录一下每个节点的父节点,递归就行
- 这样通过查表就能知道第三个方向的节点了!
- 下面是距离问题,
- 具体其实就是层数问题
- 怎么在bfs中抓层数呢?
- 很简单,一次处理一层的节点,这样留在queue中的必定是一层的!
- 只需要用一个变量记录一下当前是第几层就行了;
- 发现满足K了把满足的节点记在list里边返回就行了!
代码
package class03;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Code08_DistanceKNodes {
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int v) {
value = v;
}
}
//bfs队列中标记层数的方法:队列中元素的个数(因为知道个数)
public static List<Node> distanceKNodes(Node root, Node target, int K) {
HashMap<Node, Node> parents = new HashMap<>();
parents.put(root, null);
//获得父亲节点列表
createParentMap(root, parents);
Queue<Node> queue = new LinkedList<>();
//有没有进入过队列
HashSet<Node> visited = new HashSet<>();
//进过队列就登记
queue.offer(target);
visited.add(target);
//当前层数,到达第K层的时候收集节点就可以
int curLevel = 0;
//收集到这里
List<Node> ans = new ArrayList<>();
while (!queue.isEmpty()) {
//这一批有多少个节点,一定是上一次的某个的周围
//一批就是同一层
int size = queue.size();
//处理一批节点(同一层)
while (size-- > 0) {
Node cur = queue.poll();
//如果是第K层,加入到答案数组里边去
if (curLevel == K) {
ans.add(cur);
}
//三种情况放队列
if (cur.left != null && !visited.contains(cur.left)) {
visited.add(cur.left);
queue.offer(cur.left);
}
if (cur.right != null && !visited.contains(cur.right)) {
visited.add(cur.right);
queue.offer(cur.right);
}
if (parents.get(cur) != null && !visited.contains(parents.get(cur))) {
visited.add(parents.get(cur));
queue.offer(parents.get(cur));
}
}
curLevel++;
//收集完答案
if (curLevel > K) {
break;
}
}
return ans;
}
//建一张父亲表,用于找到父亲
public static void createParentMap(Node cur, HashMap<Node, Node> parents) {
if (cur == null) {
return;
}
if (cur.left != null) {
parents.put(cur.left, cur);
createParentMap(cur.left, parents);
}
if (cur.right != null) {
parents.put(cur.right, cur);
createParentMap(cur.right, parents);
}
}
public static void main(String[] args) {
Node n0 = new Node(0);
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
Node n4 = new Node(4);
Node n5 = new Node(5);
Node n6 = new Node(6);
Node n7 = new Node(7);
Node n8 = new Node(8);
n3.left = n5;
n3.right = n1;
n5.left = n6;
n5.right = n2;
n1.left = n0;
n1.right = n8;
n2.left = n7;
n2.right = n4;
Node root = n3;
Node target = n5;
int K = 2;
List<Node> ans = distanceKNodes(root, target, K);
for (Node o1 : ans) {
System.out.println(o1.value);
}
}
}
如有问题,欢迎各位大佬指正
�
� �