文章目录
注意事项
ACM模式
import java.util.*;
不用return的吗?还是直接用sout?
貌似BufferedReader和BufferedWriter更快
什么时候要while(sc.next)
拿到一个陌生题,如何判断类型或者思考方法?
dp
当后面的结果和前面有关的时候,可以考虑动规。
动规五步:
1.确定dp数组的意义
2.求递推公式
3.初始化
4.确定遍历方向
5.带入模拟验证
动规技巧&注意
学会利用数组初始为0的这个特点
一维不行开二维,一层for不行开二层
动规和贪心都要注意遍历顺序,也可以是从后向前
递推方程的形式:
可以是=若干个前面递推数组元素之和、+=递推数组元素,递推数组的运算和其他运算形式的最大值。
递推公式初始化:
二维数组的第一行第一列比较特殊的话可以考虑单独用循环来初始化。
如果发现0附近不好初始化,可以考虑把递推方程扩大1,说不定就好了
递推公式如何想:
当前状态能由哪些上一个状态得到。
dp[i]的含义也很重要:
往往可以决定递推公式怎么写,所以思考递推公式的时候先构思好dp[i]用来表示什么比较恰当。
整数拆分有个坑: 考虑不全,dp[i]=Math.max(Math.max(dp[i-j]*j,dp[i]),(i-j)*j);,容易把(i-j)*j漏掉
不同的二叉搜索树: 找规律题的总结: 规律不知道怎么找,看看不同的节点在根节点会是什么情况。盯住最特殊的节点根节点(在这里就是根节点和最大的那个数,但是最大的那个数又不好讨论,也无法遍历,所以盯住根节点) ,dp又要有一个循环遍历的过程,所以我们可以考虑不同的节点在根节点会是什么情况。
背包问题:用物品去填背包
重要性质:如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
标准的背包问题(01背包):
dp[j]: 0-i号物品j容量背包的最大价值
遍历顺序? 先遍历物品再遍历背包比较好理解,一维dp一定要先物品再背包。当物品不能重复的时候,背包应从大到小遍历。当物品可以重复的时候,背包应从小到大遍历。
是否要求有序? 背包大小要求有序,物品重量不要求
01背包问题经典问题:容量为bagSize的背包能容纳的最大价值是多少。
dp[j]表示,容量为j的背包所能装的最大价值
for(int i=0;i<nums.length;i++){
for(int j=bagSize;j>=nums[i];j--){
dp[j]=Math.max(dp[j-nums[i]]+nums[i],dp[j]);
}
}
01背包问题的组合问题(不考虑顺序):
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
for (int i = 0; i < nums.length; i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
二维的01背包
for(int i=0;i<wupin.length;i++)
for (int i = m; i >= zeroNum; i--) {
for (int j = n; j >= oneNum; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
完全背包
经典问题和组合问题:和01背包的转移方程相同,只是遍历顺序不同,完全背包可以重复,所以都是从小到大遍历。j=nums[i],j<=bagSize
排列问题,外层遍历背包,内层遍历物品,都是从小到大(从小到大是由完全背包决定的)。状态转移一样,注意判断。
当爬楼梯爬n级的时候,就是完全背包的排列问题了。
for (int i = 0; i <= target; i++) {
for (int j = 0; j < nums.length; j++) {
if (i >= nums[j]) {
dp[i] += dp[i - nums[j]];
}
}
}
注意完全背包的最小物品数问题的初始化
int max = Integer.MAX_VALUE;
int[] dp = new int[n + 1];
//初始化
for (int j = 0; j <= n; j++) {
dp[j] = max;
}
打家劫舍:比较简单,围成一圈的变种就写一个函数,讨论两种情况取最大值吃就行了 。
树形dp,先跳过。
股票问题:
这个系列不算难
递推公式的意义:
dp[i][0]:第i天持有股票的最大收益(为负值),dp[i][1]:第i天不持有股票的最大收益
原版
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
}
不限次数版
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]-prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
}
只限两次版:1是第一次持有,2是第一次卖出,3是第二次持有,4是第二次卖出取dp[len-1][4];
dp[i][0]是不操作,维护一个不操作的状态会使整体看起来比较工整
for (int i = 1; i < len; i++) {
dp[i][1] = Math.max(dp[i - 1][1], dp[i][0]-prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4], dp[i][3] + prices[i]);
}
k次版本
奇数持有,持有的初始化是-prices[0];
为什么第n次的初始化都用-prices[0]呢?
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
for(int i=1;i<2*k+1;i+=2){
dp[0][i]=-prices[0];
}
for(int i=1;i<prices.length;i++){
for(int j=1;j<2*k;j+=2){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]+prices[i]);
}
}
冷冻期
dp[i][0],买入/持有
dp[i][1],不持有,不在冷冻期
dp[i][2],不持有,在冷冻期
dp[0][0]=-prices[0];
for(int i=1;i<prices.length;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][2]);
dp[i][2]=dp[i-1][0]+prices[i];
}
子序列问题
最长递增子序列
for(int i=1;i<nums.length;i++){
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
res = Math.max(res, dp[i]);
}
}
最长重复子数组
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
最长公共子序列
for(int i=1;i<=s1.length;i++){
for(int j=1;j<=s2.length;j++){
if(s1[i-1]==s2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}
else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
编辑距离问题
判断子序列,编辑距离中的删除
如果初始化不方便,也可以对dp数组进行位移
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。
有同学问了,为啥要表示下标i-1为结尾的字符串呢,为啥不表示下标i为结尾的字符串呢?
用i来表示也可以!
但我统一以下标i-1为结尾的字符串来计算,这样在下面的递归公式中会容易理解一些,如果还有疑惑,可以继续往下看。
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];
}
}
不同的子序列
初始化比较抽象
dp[i][0]表示什么呢?
dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。
那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。
再来看dp[0][j],dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。
for(int i=1;i<=s.length();i++){
for(int j=1;j<=t.length();j++){
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}
else{
dp[i][j]=dp[i-1][j];
}
}
}
两个字符串的删除操作
for (int j = 1; j <= word2.size(); j++) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
}
}
编辑距离
一个删除和另一个添加是等效操作,替换只需要+1
int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 因为dp数组有效位从1开始
// 所以当前遍历到的字符串的位置为i-1 | j-1
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
}
}
}
回文串问题
dp[i][j],以i开头j结尾的子串是否是回文串
递推公式为dp[i][j]=dp[i+1][j-1]+2
i是从后往前遍历,j是从前往后遍历。
贪心
局部最优推出全局最优。手动模拟,举不出反例。
动规和贪心都要注意遍历顺序。
贪心技巧&注意
双指针&滑动窗口
直接看甜姐说的刷题论
二分查找
for和while的选择
while比for更灵活
循环嵌套需谨慎,大循环里面经常用if来改变判断条件,一步一个脚印。如果大循环里面想用while来改变循环条件就需要谨慎了,经常会越界。
当要改变一个数组的时候,有时需要new一个新数组,有时直接覆盖就行了(能直接覆盖就优先直接覆盖)。
指针的滑动方向:双指针可以从头快慢滑,也可以一头一尾滑,也可以中心扩展滑。
左右指针:左右往中间滑动的题目特点:只需要看两个元素(乘最多水的容器)
while(right > left){
left++;
right–;
}
快慢指针:快指针直接遍历,慢指针条件遍历
while(right < len){
right++;
条件left++;
}
找到符合条件的子区间
子区间计数问题
条件判断 :条件判断需谨慎!当出现且的条件时,越界判断要在逻辑判断之前。
区间问题也可以考虑中心扩展算法
改变值类问题
前缀和
一般适用于数组的问题
前缀和数组是从nums[0]到nums[i]的累计和。
差分数组的前缀和是原数组,可以用来进行区间相加,eg教室同时在线学生数,航班预定
构造前缀和数组注意数组长度+1,第一个元素为0
栈与队列
对于逆序处理应该想到栈
栈经常可以使用辅助栈或者说双栈
用队列实现栈和用栈实现队列的异同。
用栈实现队列:一定要等用来输出的栈的那批数据输出完了才能载入新数据,要不然顺序就会乱。不需要拷贝交换操作,因为满足前面的规则数据就是有序的。
用队列实现栈:q2是作为添加元素的辅助队列,常年为空。添加元素的时候,先放进空的q2,再把q1全部poll到q2,交换q1q2,这样q1每次拿出来都是新的元素。
单调队列
维护了一个队列,每次取出来都是最大值。
优先队列
经常可以考虑用两个栈或者队列存储不同类型的元素,可以简化运算。栈也不一定需要存字符串,当字符串确定的时候,也可以选择存下标等标记元素。
链表
在遍历链表的时候我老是忘记node=node.next这一步!
链表的含义: 变量里面存储的是内存地址。
记录链表结点的技巧:在链表的题目中可能会需要记录链表的节点,eg用pre记录当前节点之前的节点,用next记录当前节点之后的节点。定义pre,cur,next是很常见的处理操作
遍历的时候除了对本身进行操作,还可以对next进行操作。当对本身操作感觉有问题的时候,可以尝试用next进行操作。
dummy的使用
dummy的2个作用:
1.构造链表并且需要返回链表的头节点的时候,可以使用dummy。
2.统一链表构造的逻辑
主逻辑都是从dummyNode.next开始的,如果遍历链表的时候发现从主逻辑开始遍历有点困难,那不妨试着从dummyNode开始遍,当可能要对head进行操作,则多半要选择从dummy开始处理了。
dummy的两种用法(仅个人总结,不一定对)
如果是需要构造新链表,则head=dummy(合并两个有序链表)。
ListNode dummy=new ListNode();
ListNode head=dummy;
//构造链表
head.next=x;
head=head.next;
return dummy.next;
如果需要辅助遍历则dummy.next=head( 反转链表 II)。
ListNode dummy=new ListNode();
dummy.next=head;
return dummy.next;
共同点是主逻辑的头节点都是dummy.next。dummynode的位置应根据主逻辑来设置。
对于双链表,dummy是空的头尾节点。
链表的切割
切割前面的一段&中间的一段&后面的一段通用
1.遍历到切割点的前面
2.记录下切割点后面的节点
3.把切割点前面节点的next置为null
不一定要在原链表上操作
如果没有要求完全可以构造新的链表
边界条件
头节点是否为空
遍历过程中检查next是否为空,next.next是否为空(fast.next!=null&&fasft.next.hext!=null)
快慢指针
递归&回溯
递归什么时候要回溯什么时候不要
回溯vs记忆化递归
回溯是没有返回值的,递归有。
递归三步:
1.确定递归函数的参数和返回值
2.确定终止条件 注意一定要返回,有何没有都要,没返回值就return
3.确定一层递归的逻辑
记忆化递归:
dfs之岛屿问题(网格类问题的 DFS 遍历方法)
岛屿问题
回溯:
回溯一般解决组合问题
1.回溯函数模板返回值以及参数(一般都是void?)
2.回溯函数终止条件
3.单层回溯搜索的逻辑(注意回溯有一步很重要的撤回操作)
可以解决n个for循环问题:for循环横向遍历,递归纵向遍历
在思考的时候可以用树抽象
递归&回溯技巧&注意
回溯法模板&剪枝
组合问题
排序对于剪枝和去重都有用
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res= new ArrayList<>();
void backtracking(参数) {
if (终止条件) {
res.add(new ArrayList<>(path));
return;
}
//求和问题时,要判断是否已经大于目标和
//固定路径长度时,要判断路径成不成立
for ((剪枝)选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(startIndex,条件); // 递归
回溯,撤销处理结果
}
}
回溯法去重
包含有序的树层去重和无序的树层去重,这里先是有序的,后面还有无序的例子
无序去重(Set)是通用的方法,但有序去重在有序场景下会更快
used数组去重:要去重的是“同一树层上的使用过”
同一树层指的是在for循环中经过回溯的的下一个而不是递归下去的,递归下去的不用去重。used[i-1]=false;
startIndex去重(start可以保证在同一层)
for(int i=start;i<candidates.length&& sum + candidates[i] <= target;i++){
// 要对同一树层使用过的元素进行跳过
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
//该层处理逻辑
}
子集问题
很简单,不用判断,直接加到路径里。去重也和排列组合一样。
无序去重(Set)是通用的方法,但有序去重在有序场景下会更快
但是“递增子序列”有一个无序的每一层去重,需要用到hashSet(创建在for循环外面),而且不需要撤回操作
排列问题
每层都是从0开始搜索而不是startIndex
需要used数组记录path里都放了哪些元素了
排列问题ii要标记使用过的,也要同一层去重,这两个used居然是可以共用的,因为他们的意义是一样的,都是记录位置为i的元素是否用过,并且不会互相影响
切割问题
注意s.substring(left,right)函数是左闭右开的
二叉树
层序遍历
深度和高度的联系与区别:根节点的高度就是二叉树的深度。前序求的是深度,后序求的是高度。
第二遍需要重点关注什么时候需要返回值,怎么递归,每层需要什么操作:
返回值选择
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。
如何同时返回数字和bool类型?尝试用数字和bool映射
树的遍历方式选择:
构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。
因为通过递归函数的返回值来做下一步计算,后序遍历。
二叉搜索树的性质:
中序遍历的结果是有序数组
二叉树的几种递归函数处理:
return
什么都不return
构造:if(){root.left=递归函数} 像插入节点和删除节点就可以考虑这种
累加二叉树,直接把sum放在外面,不需要return(root=null return)全局变量可以替代返回值或者参数。
终止条件
好像一般都是root==null,不过都可以考虑考虑。
留坑 18
单调栈
位运算
并查集
亦或
拓扑排序
课程表i ii
图论
模拟
排序算法
数组排序:
不用创建新的,直接改变原来的
arrays.sort(arr)
默认升序
集合排序:
Collections.sort(lst);
Collections.sort(lst, Collections.reverseOrder());
冒泡排序:
思想:通过比较相邻的值每次内循环都把最大的放到无序部分的最后面
for(int i=0;i<nums.length;i++){
for(int j=0;j<nums.length-i-1;j++){
if(nums[j]>nums[j+1]){
int temp=nums[j];
nums[j]=nums[j+1];
nums[j+1]=temp;
}
}
}
选择排序:
思想:每次找到最小值放到,记录最小值的index,放到无序部分的最前面
for(int i=0;i<nums.length-1;i++){
int minIndex=i;
for(int j=i+1;j<nums.length;j++){
if(nums[j]<nums[minIndex]){
minIndex=j;
}
}
int temp=nums[i];
nums[i]=nums[minIndex];
nums[minIndex]=temp;
}
插入排序:
思想:把第一个无序的数据插入前面有序的序列。
for(int i=0;i<nums.length-1;i++){
int next=nums[i+1];
for(int j=i;j>=0;j--){
if(nums[j]>next){
nums[j+1]=nums[j];
}else{
break;
}
nums[j]=next;
}
}
快速排序
思想:递归分治
快速排序的主要思想是通过划分将待排序的序列分成前后两部分,其中前一部分的数据都比后一部分的数据要小,然后再递归调用函数对两部分的序列分别进行快速排序,以此使整个序列达到有序。
关键:
用if不能用while,left<right,如果=就没有意义了
(right-left)+left
特别注意j是从left开始的
//手撕快排开始
class Solution {
public int[] sortArray(int[] nums) {
randomizeQuicksort(nums, 0, nums.length - 1);
return nums;
}
//递归函数,每次找到分割点,将数组一分为二递归
public void randomizeQuicksort(int[] nums, int left, int right) {
//递归条件,一直递归到左右指针重合
if (left < right) {
//进行一趟排序并返回分割点(即枢轴位置)
int partition = randomizePartition(nums, left, right);
//递归每一趟下来分割得到的两数组
randomizeQuicksort(nums, left, partition - 1);
randomizeQuicksort(nums, partition + 1, right);
}
}
//1.随机化选择枢轴
//2.以选择的枢轴为基准,小值放左,最后将枢轴放中间,此时大值默认均在枢轴右边
//3.返回当前枢轴位置,作为递归函数的分割点
public int randomizePartition(int[] nums, int left, int right) {
//随机化选出枢轴位置
int pos = new Random().nextInt(right - left) + left;
//将枢轴放于右边界
swap(nums, pos, right);
int pivot = nums[right];
int patition = left;
//规定left和right边界,真正的指针移动仅仅是partition
for (int i = left; i < right; i++) {
if (nums[i] <= pivot) {
swap(nums, i, patition);
++patition;
}
}
//将第一个比枢轴大的值放于最右端,枢轴放中间
swap(nums, patition, right);
//返回分割点
return patition;
}
//无脑将两元素交换的函数
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
归并排序
思想:递归分治,把前后两段都变成有序的,合并
边界条件看清楚。
注意归并排序的递归是包含mid的,而快排是不包含的,注意区分
class Solution {
int[] tmp;
int[] sortArray(int[] nums) {
tmp = new int[nums.length];
mergeSort(nums, 0, nums.length - 1);
return nums;
}
public void mergeSort(int[] nums, int l, int r) {
if (l >= r) {
return;
}
int mid = (l + r) /2;
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
int i = l, j = mid + 1;
int cnt = l;
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) {
tmp[cnt++] = nums[i++];
} else {
tmp[cnt++] = nums[j++];
}
}
while (i <= mid) {
tmp[cnt++] = nums[i++];
}
while (j <= r) {
tmp[cnt++] = nums[j++];
}
for (int k = l; k <= r ; ++k) {
nums[k] = tmp[k];
}
}
}
堆排序
思想:建堆,交换(把堆首的元素也就是最大的元素放到数组尾部),调整(重新建堆,这时建堆的数组是0到i-1)
class Solution {
int[] sortArray(int[] nums) {
//开始为第一个非叶子节点
//建堆,>=0,根叶需要调整
for(int i=(nums.length-1)/2;i>=0;i--){
maxHeap(nums,i,nums.length);
}
//交换堆顶(最大的)和末尾的元素(把最大的元素放到末尾,末尾-1),并进行调整
for(int i=nums.length-1;i>0;i--){
int temp=nums[0];
nums[0]=nums[i];
nums[i]=temp;
//从堆顶开始调整,末尾每次都-
maxHeap(nums,0,i);
}
return nums;
}
void maxHeap(int[] nums,int adjustIndex,int length){
//暂存要调整的元素的值
int temp=nums[adjustIndex];
//逐层调整循环遍历
for(int k=2*adjustIndex+1;k<length;k=2*k+1){
if(k+1<length&&nums[k]<nums[k+1]){
//找到左右子树最大值的下标
k++;
}
//注意这里不是nums[adjustIndex],因为会变
if(temp<nums[k]){
nums[adjustIndex]=nums[k];
//下次调整从k开始
adjustIndex=k;
}
else{
break;
}
}
nums[adjustIndex]=temp;
}
}
单调栈
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
这里我们要使用递增循序(再强调一下是指从栈顶到栈底的顺序),因为只有递增的时候,加入一个元素i,才知道栈顶元素在数组中右面第一个比栈顶元素大的元素是i。
栈顶元素的特点:因为是后进去的,所以下标比较大,因为是递增的,所以值比较小。
语言基础&数据结构
创建整形数组
int[] dp = new int[cost.length];
length和size的区别
数组元素个数:.length 不带括号
字符串获取字符个数:.length() 带括号
集合获取元素个数: .size()
集合类关系图:集合类不能装基本数据类型
但是可以调用.intValue()取到基本数据类型
JAVA集合类
new集合类的时候等号前面必须要加类型,等号后面可以不加
List< Integer >=new ArrayList<>();
List常用操作
正常的插入操作ArrayList更快
数组访问高效,链表扩容高效
但LinkedList可以实现Deque
一般用ArrayList比较多,要用队列就用LinkedList,否则用ArrayList
统一操作:
ArrayList移除最后一个元素:path.remove(path.size()-1);
ArrayList添加元素:path.add();
使用下标遍历:用get访问下标
for(int i=1;i<arr.size();i++){
min = Math.min(min,(arr.get(i)-arr.get(i-1)));
}
递归模板创建二维的List
创建二维的List(三数之和)
List<List< Integer >> result = new ArrayList<>();
List的自定义排序:(排序默认都是升序)
List里面装的是int[] 按照int[0] 排序,
Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0])->x[0]-y[0]);
List转数组: res.toArray(new int[res.size()][]); 记得数组要加长度
数组转List: List resultList= new ArrayList<>(Arrays.asList(array));
HashSet常用操作
Set每个元素都是唯一的,不能添加相同的元素
数组也是一个哈希表
import java.util.HashSet;
import java.util.Set;
创建 Set< Integer > set1 = new HashSet<>();
添加元素,返回true or false: set1.add(i);
是否包含: set1.contains(i);
*set的遍历: *entris是要遍历的集合,Map.Entry< Integer,Integer >是集合中单个元素的数据类型。entry是集合中的单个元素。
for (Map.Entry<Integer, Integer> entry : entries) {
queue.offer(entry);
}
TreeSet是有序的集合
HashMap常用操作
Map特点:key必须唯一,后面加入的会对前面的覆盖
创建: Map<Integer, Integer> map = new HashMap<>();
是否包含key: map.containsKey(temp)
根据key获得value: res[0] = map.get(temp);
放入新的kv: map.put(nums[i], i);
统计数字频率并把kv放进map的操作
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
大小: size和isEmpty()
拿到kv对的集合: entrySet()
拿到key的集合: keySet()
返回该value的集合。 Collection values():
删除指定key所对应的键值对,返回可以所关联的value,如果key不存在,返回null Object remove(Object key)
返回该Entry里包含的key值: Object getKey():
返回该Entry里包含的value值: Object getValue()。
设置该Entry里包含的value值,并返回新设置的value值: Object setValue(V value):
Deque(栈和队列)
双端队列可以实现队列和栈的功能,直接用Deque来替代一般的队列
双端队列
创建:Deque< Character > st = new LinkedList< Character >();
在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于 Deque 方法
栈的操作:push pop peek
队列的操作:add(用offer更好) poll peek
判断大小&是否为空:.size() isEmpty()
poll和peek的区别:
poll:将首个元素从队列中弹出,如果队列是空的,就返回null
peek:查看首个元素,不会移除首个元素,如果队列是空的就返回null
为什么Deque作为栈和队列的时候都可以用peek呢?
因为push方法是把数据塞到Deque前面,offer是塞到后面。这样就正好可以保证peek=peekFirst对于栈和队列都生效。
add和offer的区别
add()和offer()都是向队列中添加一个元素。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,调用 add() 方法就会抛出一个 unchecked 异常,而调用 offer() 方法会返回 false。因此就可以在程序中进行有效的判断!
优先队列
PriorityQueue< Integer > queue = new PriorityQueue<>((x,y)->x-y);
Arrays.sort(intervals,(x,y)->x[0]-y[0]);
String字符串操作
把String转化成字符串:
char[] cs=s.toCharArray();
不过似乎还是charAt[i]速度要快一些。
String也有length不过要加括号string.length()
String和int互转
Integer.parseInt(s);
int+“”
反转字符串:sbuilder和sbuffer都可以
public static String reverseTestOne(String s) {
return new StringBuffer(s).reverse().toString();
}
判断是否是数字
char ch = s.charAt(i);
if (Character.isLetterOrDigit(ch))
尽量用StringBuilder,效率最高
sb.append();
sb.toString();
取子串
s.substring(i,j)左闭右开
取最大(小)值
Math.min(dp[cost.length - 1], dp[cost.length - 2]);
优先队列&堆
特点:维护一个有序队列,可以保证出队的元素是最小的or最大的
优先队列和堆的关系:优先队列的底层是用堆构建的
大顶堆(也叫大根堆):每个结点的值都大于或等于其左右孩子结点的值,
小顶堆(也叫小根堆):每个结点的值都小于或等于其左右孩子结点的值
//默认是小根堆,出队最小的元素
PriorityQueue<Integer> maxHeap=new PriorityQueue<>(new Comparator<Integer>()
//大根堆
PriorityQueue<Integer> maxHeap=new PriorityQueue<>((o1,o2)->o2-o1);
小技巧
分类讨论
有的分类讨论可以使用循环交换归为一类。
循环
两层循环也可以用一层循环+自增代替
更新和判断的顺序
有的时候出现bug是更新和判断的顺序不对。
边界条件的判断
大于,大于等于,n,n-1?
这些很容易错,应该特别注意,优先检查
边界条件的判断顺序也可能影响结果
警惕判断之后处理的结果会对后面程序产生影响的形态。
遍历方向
有的时候改变遍历方向可以使逻辑变得简单