学习视频:一周刷爆LeetCode,算法大神左神(左程云)耗时100天打造算法与数据结构基础到高级全家桶教程,直击BTAJ等一线大厂必问算法面试题真题详解(马士兵)_哔哩哔哩_bilibili
目录
3.将单链表按照某值划分成左边小于,中间等于,右边大于的形式
给定一个二叉树的头结点,返回其中包含的最大二叉搜索树的子树的节点个数
计算字符串公式的结果(套路:所有括号、优先级的题都可以使用)
添加最少字符的情况下,让字符串整体都是回文子串(DP-范围查询)
将字符串全部切成回文子串的最小分割数(DP-从右往左+范围查询)
时间复杂度
一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的一个指标。常用O(读作big O)来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中发生了多少常数操作,进而总结出常数操作数量的表达式。
在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度就位O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数项时间”.
常数操作:例如数组根据索引寻值,加、减、乘、除、比较
排序
选择排序
每次选择 i 到 n-1 范围内值最小的索引位置 t ,每一次内层循环结束之后交换 i 和 t 索引位置上的值,每一次外层循环 i 的值加 1
private static void selectSort(Integer[] nums) {
for(int i=0;i<nums.length-1;i++){
int t=i;
for(int j=i+1;j<nums.length;j++){
if(nums[t]>nums[j]){
t=j;
}
}
swap(nums,i,t);
}
}
时间复杂度:O(n^2) ,空间复杂度:O(1)
冒泡排序
每次从 0 到 i 的范围中将相邻的值进行两两比较,将大的一个值移到后面,这样每一次内层循环结束后,i 位置的值就是0 到 i 范围内最大的值,每一次外层循环 i 的值减 1.
private static void BubbleSort(Integer[] nums) {
int n=nums.length;
for(int i=n-1;i>0;i--){
for(int j=0;j<i;j++){
if(nums[j]>nums[j+1]){
swap(nums, j, j+1);
}
}
}
}
时间复杂度:O(n^2) ,空间复杂度:O(1)
插入排序
i 从 0 开始,维护一个 0 到 i 范围的有序数组,每一次 i++ ,令 j 等于 i,j 所在位置的元素 和 j-1 所在位置的元素进行比较 因为前 j-1 个元素是有序的,所以 j 就往前进行比较,如果 j 所在位置 比 j-1 小,就进行交换,再往前进行比较,知道比前面的大为止。
private static void MySort(Integer[] nums) {
if(nums==null || nums.length<2){
return;
}
int l=nums.length;
for(int i=1;i<l;i++){
for(int j=i;j>0;j--){
if(nums[j]<nums[j-1]){
swap(nums,j,j-1);
}else{
break;
}
}
}
}
最坏情况下 时间复杂度为 O(n^2),空间复杂度为O(1)
归并排序
将一个数组分为左右两个子数组,先将这两个子数组分别排好序,再融合,使得父数组也有序。
public static void mergeSort(int[] arr,int l,int r){
if(l==r) return;
process(arr,l,r);
}
private static void process(int[] arr, int l, int r) {
if(l==r) return;
int mid=l+((r-l)>>1);
process(arr,l,mid);
process(arr,mid+1,r);
merge(arr,l,mid,r);
}
private static void merge(int[] arr, int l, int m, int r) {
int[] help=new int[r-l+1];
int p1=l;
int p2=m+1;
int i=0;
while(p1<=m && p2<=r){
help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=m){
help[i++]=arr[p1++];
}
while (p2<=r){
help[i++]=arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[l+j]=help[j];
}
}
根据master公式可以得到:
T(N) = 2T(N/2)+O(N) --> 每一次将父函数分成两个数据量为父函数一半的子函数,mergn函数的时间复杂度为O(N) 所以:log(a,b)==d-->log(2,2)==1 所以归并排序的时间复杂度为:O(N*logN),空间复杂度:O(N)
归并排序的扩展
1)小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和
例如:[1,3,4,2,5]:左边比1小的数,没有,为0;3左边比3小的数,1个1;4左边比4小的数:1 * 1+1 * 3=4;2左边比2小的数:1个1;5左边比5小的数:1+3+4+2=10;所以小和为:0+1+4+1+10=16。
也可以看一个数右边有几个比他大的数:1右边有4个数比他大,1 * 4=4;3右边有两个数比他大,2 * 3=6;4右边有1个数比他大,1 * 4=4;2右边有1个数比他大,1 * 2=2;5右边没有比他大的数 ,0;所以小和为:4+6+4+2+0=16。
代码实现:
public static int smallSum(int[] arr,int l,int r){
if(l==r) return 0;
return process(arr,l,r);
}
private static int process(int[] arr, int l, int r) {
if(l==r) return 0;
int mid=l+((r-l)>>1);
return process(arr,l,mid) + process(arr,mid+1,r) + merge(arr,l,mid,r);
}
private static int merge(int[] arr, int l, int m, int r) {
int[] help=new int[r-l+1];
int p1=l;
int p2=m+1;
int i=0;
int res=0;
while(p1<=m && p2<=r){
//判断右边数组中有几个比左边数组当前p1下标下的数大,计算和
res+=arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
//在merge的时候,当右边数组p2下标对应的数跟左边数组p1下标对应的数相等的时候,先把右边数组的数加入到临时数组中,因为在去掉了右边与左边相等的数之后,才能得到右边比左边大的数的数量。
help[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=m){
help[i++]=arr[p1++];
}
while (p2<=r){
help[i++]=arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[l+j]=help[j];
}
return res;
}
流程图:
2)逆序对问题
在一个数组中,左边的数如果比右边的数大,则拆两个数构成一个逆序对,请打印所有逆序对。
代码:
//int[] arr={3,2,4,5,1};
private static int merge(int[] arr, int l, int m, int r) {
int[] help=new int[r-l+1];
int p1=l;
int p2=m+1;
int i=0;
int res=0;
while(p1<=m && p2<=r){
//在 1)的代码上加入这一段判断逻辑,就可以打印出所有的逆序对
if(arr[p1]>arr[p2]){
int p11=p1;
while(p11<=m){
System.out.println("["+arr[p11++]+" "+arr[p2]+"]");
}
}
res+=arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
//这里与 1)有差别,当左边和右边相等的时候,移动左边的
help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=m){
help[i++]=arr[p1++];
}
while (p2<=r){
help[i++]=arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[l+j]=help[j];
}
return res;
}
运行结果:
[3 2]
[5 1]
[2 1]
[3 1]
[4 1]
快速排序
思考问题
1)给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。
思路:定义一个小于等于区,初始下标为-1,i 从0开始遍历数组,可以分为两种情况 (1)arr[i] <= num,nums[i]和小于等于区的下一个数交换,小于区右扩,i++。(2)arr[i] > num,i++。
代码:
//arr={3,4,6,7,5,3,5,8},num=5
public static void func1(int[] arr,int num){
int i=0;
int left=-1;
while(i<arr.length){
if(arr[i]<=num){
swap(arr,++left,i++);
}else if(arr[i]>num){
i++;
}
}
System.out.println(Arrays.toString(arr));
}
运行结果:
[0, 0, 5, 3, 5, 7, 6, 8]
2)荷兰国旗问题:给定一个数组arr,和一个数num,请把小于num的数放在数组的左边。等于num的数放在数组的中间,大于num的数放在数组的右边,要求额外空间复杂度为O(1),时间复杂度O(N)。
思路:定义一个小于区,初始下标为-1;定义一个大于区,初始下标为arr.length,i 从0开始遍历数组,有三种情况:(1)arr[i] < num ,nums[i]和小于区的下一个数交换,小于区右扩,i++。(2)arr[i] = num ,i++。(3)arr[i] > num ,交换 num[i] 和大于区前一个数,大于区左扩。
代码:
//arr={3,4,6,7,5,3,5,8},num=5
public static void func2(int[] arr,int num){
int i=0;
int left=-1;
int right=arr.length;
while (i<right){
if(arr[i]<num){
swap(arr,++left,i++);
}else if(arr[i]==num){
i++;
}else{
swap(arr,--right,i);
}
}
System.out.println(Arrays.toString(arr));
}
运行结果:
[0, 0, 3, 5, 5, 7, 8, 6]
快排1.0
使用思考1)的方式,每次确认一个数的位置
public static void quickSort(int[] arr){
if(arr==null || arr.length<2){
return;
}
quickSort(arr,0,arr.length-1);
}
public static void quickSort(int[] arr,int l,int r){
if(l<r){
//r所在位置的元素作为基准点
int i = partition(arr, l, r);
quickSort(arr,l,i);
quickSort(arr,i+1,r);
}
}
public static int partition(int[] arr, int l, int r){
int left=l-1;
while(l<r){
if(arr[l]<=arr[r]){
swap(arr,++left,l++);
}else if(arr[l]>arr[r]){
l++;
}
}
swap(arr,left+1,r);
//left是小于区的边界
return left;
}
对于数组:arr={1,2,3,4,5,6,7,8,9},最坏时间复杂度为O(N^2)
快排2.0
使用 思考 2) 的方式,每次确认一种数的位置
代码:
public static void quickSort(int[] arr){
if(arr==null || arr.length<2){
return;
}
quickSort(arr,0,arr.length-1);
}
//选择数组中最后一位为 基准点
public static void quickSort(int[] arr,int l,int r){
if(l<r){
//p的大小为2,是与最后一位数相等的数的左右边界
int[] p=partition(arr,l,r);
//左边继续
quickSort(arr,l,p[0]-1);
//右边继续
quickSort(arr,p[1]+1,r);
}
}
private static int[] partition(int[] arr, int l, int r) {
int[] p=new int[2];
//小于区域边界
int less=l-1;
//大于区域边界
int more=r;
//当l达到大于边界时说明已经分类完成
while(l<more){
//l所在位置比基准点位置小,arr[l]和小于区的下一个数交换,小于区右扩,l++
if(arr[l]<arr[r]){
swap(arr,++less,l++);
}else if(arr[l]==arr[r]){
l++;
}else{
//arr[l]>arr[r],l位置上的值与大于区前一个位置上的值进行交换,大于区左扩
swap(arr,--more,l);
}
}
//基准点所在当前数组的最后一位r,和大于arr[r]上的第一个值的位置(也就是大于区的边界)进行交换。
swap(arr,r,more);
p[0]=less+1;//等于基准点的数的左边界
p[1]=more;//等于基准点的数的有边界
//此时:l~less 就是比基准点小的数,more+1~r就是比基准点大的数
return p;
}
如果我们选择的数组为 arr={1,2,3,4,5,6,7,8,9},那么最坏的时间复杂度为O(N^2),
快排3.0
就是在快排2.0的基础上添加了一个随机交换的操作,保证不能人为的列出最坏执行情况
代码:
//int[] arr={3,4,6,7,5,3,5,8};
public static void quickSort(int[] arr){
if(arr==null || arr.length<2){
return;
}
quickSort(arr,0,arr.length-1);
}
//选择数组中最后一位为 基准点
public static void quickSort(int[] arr,int l,int r){
if(l<r){
//随机选择一个位置的值与最后一位交换
swap(arr,r,l+(int)(Math.random()*(r-l+1)));
//p的大小为2,是与最后一位数相等的数的左右边界
int[] p=partition(arr,l,r);
//左边继续
quickSort(arr,l,p[0]-1);
//右边继续
quickSort(arr,p[1]+1,r);
}
}
private static int[] partition(int[] arr, int l, int r) {
int[] p=new int[2];
//小于区域边界
int less=l-1;
//大于区域边界
int more=r;
//当l达到大于边界时说明已经分类完成
while(l<more){
//l所在位置比基准点位置小,arr[l]和小于区的下一个数交换,小于区右扩,l++
if(arr[l]<arr[r]){
swap(arr,++less,l++);
}else if(arr[l]==arr[r]){
l++;
}else{
//arr[l]>arr[r],l位置上的值与大于区前一个位置上的值进行交换,大于区左扩
swap(arr,--more,l);
}
}
//基准点所在当前数组的最后一位r,和大于arr[r]上的第一个值的位置(也就是大于区的边界)进行交换。
swap(arr,r,more);
p[0]=less+1;//等于基准点的数的左边界
p[1]=more;//等于基准点的数的有边界
//此时:l~less 就是比基准点小的数,more+1~r就是比基准点大的数
return p;
}
运行结果:
[3, 3, 4, 5, 5, 6, 7, 8]
时间复杂度:O(N*log(N))
堆排序
代码:
/**
int[] arr={4,2,3,1,7};
heapSort(arr);
System.out.println(Arrays.toString(arr));
**/
/**
* 堆排序
* @param arr 待排序的数组
* 时间复杂度:O(N*log(N))
* 空间复杂度:O(1)
*/
public static void heapSort(int[] arr){
if(arr==null || arr.length<2){
return;
}
//先调用heapInsert将数组变为大根堆的结构
for (int i = 0; i < arr.length; i++) { //O(N)
heapInsert(arr,i);//O(log(N))
}
//如果仅仅是将数组变成大根堆结构的话,这段代码要快一些
// for(int i=arr.length-1;i>=0;i--){
// heapify(arr,i,arr.length);
// }
/**
* 然后每一次将堆的最大值(所在位置为0)与最后一个元素互换,堆的长度减1,然后使用heapify调整堆结构,
* 这样,数组就从后往前依次有序起来了
**/
int heapSize=arr.length;
swap(arr,0, --heapSize);
while (heapSize>0){ //O(N)
heapify(arr,0,heapSize);//O(log(N))
swap(arr,0,--heapSize);//O(1)
}
}
/**
* index上的值满足大顶堆的条件从下往上移动
* @param arr 数组
* @param index 要移动的元素
* 时间复杂度取决于树的高度:O(logN)
*/
public static void heapInsert(int[] arr,int index){
//index和index的父节点进行比较,如果arr[index]>arr[(index-1)/2],说明它比父节点的值要大,就交换这父子元素的位置。否则,while循环结束
//当index=0的时候,(index-1)/2 =0,此时的条件不满足,while循环就结束
while(arr[index]>arr[(index-1)/2]){
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
/**
* index 上的值满足大顶堆的条件从上往下移动
* @param arr 数组
* @param index 要移动的元素
* @param heapSize 堆的大小
* 时间复杂度取决于树的高度:O(logN)
*/
public static void heapify(int[] arr,int index,int heapSize){
//当前节点的左孩子节点
int left=2*index+1;
//当左孩子节点存在的时候
while (left<heapSize){
//选择左孩子和右孩子中大的一个
int largest=(left+1) < heapSize && arr[left+1]>arr[left]?(left+1):left;
//选择父节点和孩子节点之间大的一个
largest=arr[largest]>arr[index]?largest:index;
//如果父节点是大的一个,说明已经满足大顶堆的条件,break
if(largest==index){
break;
}
//交换父节点和孩子节点的值
swap(arr,index,largest);
//移动到交换后的孩子节点
index=largest;
//当前节点的左孩子节点
left=index*2+1;
}
}
public static void swap(int[] arr,int i,int j){
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
运行结果:
[1, 2, 3, 4, 7]
桶排序(基数排序)
代码:
/**
* int[] arr={1,21,32,14,57,83,64,100,101};
* radixSort(arr);
* System.out.println(Arrays.toString(arr));
**/
public static void radixSort(int[] arr){
if(arr==null || arr.length<2){
return;
}
//maxbits(arr) 获取数组中最大数的长度
radixSort(arr,0,arr.length-1,maxbits(arr));
}
private static void radixSort(int[] arr, int L, int R, int digit) {
//10进制
final int radix=10;
int i=0,j=0;
//有多少个数就准备多少个辅助空间
int[] bucket=new int[R-L+1];
for(int d=1;d<=digit;d++){//有多少位就进出几次
/**
*10个空间
* count[0] 当前(d位)是0的数字有多少个
* count[1] 当前(d位)是(0~1)的数字有多少个
* count[2] 当前(d位)是(0~2)的数字有多少个
* count[3] 当前(d位)是(0~3)的数字有多少个
* count[i] 当前(d位)是(0~i)的数字有多少个
**/
int[] count=new int[radix]; //count[0....9]
for(i=L;i<=R;i++){
//j=获取当前arr[i]的第d位(个、十、百、千.....)数的值
j=getDigit(arr[i],d);
//对应第j位上的数加1。(0~9)
count[j]++;
}
for(i=1;i<radix;i++){
//count[i]=数组中的数在第d位上小于等于i的有多少
count[i]+=count[i-1];
}
//从数组的右边开始遍历
for(i=R;i>=L;i--){
j=getDigit(arr[i],d);
//进桶
bucket[count[j]-1]=arr[i];
count[j]--;
}
for(i=L,j=0;i<=R;i++,j++){
arr[i]=bucket[j];
}
}
}
private static int getDigit(int i, int d) {
int res=0;
while(d--!=0){
res=i%10;
i/=10;
}
return res;
}
private static int maxbits(int[] arr) {
int max=Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max=Math.max(max,arr[i]);
}
int res=0;
while (max!=0){
res++;
max/=10;
}
return res;
}
运行结果:
[1, 14, 21, 32, 57, 64, 83, 100, 101]
时间复杂度:O(N),空间复杂度:O(N)
排序算法的稳定性
稳定性:同样的个体之间,如果不因为排序而改变相对次序,那这个排序就是具有稳定性的,否则就没有。
稳定性对于基本数据类型的数据没有什么作用,但是对于引用类型的数据在排序的时候就有用了,比如一个商品具有价格与好评这两个属性,我们先根据价格排序,然后根据好评排序,如果排序的算法是具有稳定性的,那么就可以形成一个物美价廉的商品排序。
排序算法 | 时间复杂度 | 空间复杂度 | 是否可以实现稳定性 |
---|---|---|---|
选择排序 | O(N^2) | O(1) | 否 |
冒泡排序 | O(N^2) | O(1) | 可 |
插入排序 | O(N^2) | O(1) | 可 |
归并排序 | O(N*log(N)) | O(N) | 可 |
快排(随机) | O(N*log(N)) | O(log(N)) | 否 |
堆排 | O(N*log(N)) | O(1) | 否 |
桶排 | O(N) | O(N) | 可 |
目前还没有找到时间复杂度O(N*log(N)),额外空间复杂度O(1),有可以实现稳定性的排序算法。
异或运算
异或运算 也叫做无进位运算,当两个操作数相同时,异或运算的结果为 0。当两个操作数不同时,异或运算的结果为 1。
1011 ^ 1001 = 0010 , 0 ^ N = N , N ^ N = 0
1 0 1 1
1 0 0 1
= 0 0 1 0
//异或操作满足交换律和结合率:
a^b=b^a
a^bc=a^(b^c)
当交换两个数的值时,可以使用异或操作,这样就不会产生额外的空间
@Test
public void swap(){
int a=1;
int b=2;
a=a^b;
b=a^b;
a=a^b;
System.out.println("a="+a+"\n"+"b="+b);
}
输出:
a=2
b=1
原因:
a=1,b=2 第一次异或: a=a^b,b=b ---> a=1^2,b=1 第二次异或: a=a^b,b=a^b^b=a ---> a=1^2,b=1^2^2-->因为2^2=0,所以b=1^0=1 第三次异或: a=a^b^a,b=a ---> a=1^2^1(原本是a^b^b,因为后面一个b在第二步异或的时候变成了a的值,所以这里是1)-->a=1^1^2=0^2=2 所以a和b就完成了交换。
但是:使用异或操作完成的交换功能在程序中只能作用于地址位置不同的两个对象,如果是同一个地址位置来进行异或,会把值抹为0
例:存在一个int类型的数组
(1)其中有一种数出现了奇数次,其余都出现了偶数次,求这个奇数次的数 (使用时间复杂度为 (O(n),空间复杂度为O(1))。
使用一个整数0,对数组中的每一个数进行异或运算,因为异或运算不在乎数的顺序,所以就是 0 ^ 所有的偶数次的数(为0)^ 奇数次的数(剩一个) ---> 结果就是 0 ^ 一个奇数次的数 = 奇数次的数。
//int[] nums=new int[]{0,0,1,3,1,2,3};
public static void printOddTimesNum1(int[] nums){
int eor=0;
for (int num : nums) {
eor=eor^num;
}
System.out.println(eor);
}
/**输出为:
2
**/
(2)有两种数出现了奇数次,其余都出现了偶数次,求这个偶数次的数
//int[] nums=new int[]{0,1,3,1,2,3};
public static void printOddTimesNum2(int[] nums){
int eor=0;
/**
eor = 奇数1 ^ 奇数2,
因为奇数1和奇数2不相同,所以eor一定不等于0,这表示eor的32位二进制中至少有一位为1.
那么在为1的这个二进制位置上,奇数1和奇数2的值一定不同(一定是其中一个为1,另一个为0),所以我们就可以先求出这个位 置,然后再去与数组中的值进行异或,得到其中一个奇数
**/
for (int num : nums) {
eor=eor^num;
}
/**
这里就是来求出最右边为1的位置
例如:
eor = 10100
~eor = 01011
~eor+1 = 01100
eor & ~eor+1 : 10100
01100
---> 00100
所以最右边为1的位置就是00100(4)
**/
int rightIndex=eor & ~eor+1;
int onlyOne=0;
for (int num : nums) {
if((num & rightIndex)==1){
//如果右边第4为为1才进行异或运算,onlyOne最后得到的就是奇数1或者奇数2的值
onlyOne = onlyOne^num;
}
}
System.out.println("num1:"+onlyOne+"\n"+"num2:"+(eor ^ onlyOne));
}
//输出: num1:0
// num2:2
位运算
比较两个数的大小
/**
*
* @param n 0或1
* @return n位0,return 1,n为1,return 0
*/
public static int flip(int n){
return n^1;
}
/**
*
* @param n
* @return 如果n为负数,返回0;如果n位非负数,返回1
*/
public static int sign(int n){
//n>>31:表示此时最右边是n的符号位
// return flip((n>>31) & 1 );
//这两种都可以,n>>31是有符号右移,n>>>31是无符号右移
return flip(n>>>31 );
}
public static int getMax(int a,int b){
int c=a-b;
int sa=sign(a);//a>=0 sa=1;a<0 sa=0
int sb=sign(b);
int sc=sign(c);
int difSab=sa ^ sb;//如果sa,sb的符号一样,difSab为0,如果不一样,difSab为1
int sameSab= flip(difSab);//如果sa,sb符号一样,sameSab为1
//返回a的情况:(1)a,b符号一样,并且c>0 (2)a,b符号不一样,且a大于0
int returnA=sameSab * sc + difSab * sa;
//返回b的情况:不是返回A,就返回B
int returnB=flip(returnA);
return a*returnA + b*returnB;
}
判断一个数是不是2的幂、4的幂
/**
* 判断一个数是否是2的幂次方
* @param n
* @return
*/
public static boolean is2Power(int n){
return (n & (n-1))==0;
}
/**
* 判断一个数是否是4的幂次方
* @param n
* @return
*/
public static boolean is4Power(int n){
//0x55555555=...01010101
return is2Power(n) && (n & 0x55555555)!=0;
}
实现两个数的加、减、乘、除
/**
* 加法
* @param a
* @param b
* @return
*/
public static int add(int a,int b){
int sum=0;
while (b!=0){//如果进位信息为0,表示这时的sum就是两个数相加的结果
sum=a^b;//得到无进位相加的结果
b=(a&b)<<1;//得到进位信息
a=sum;
}
return sum;
}
/**
* 减法
* @param a
* @param b
* @return
*/
public static int minus(int a,int b){
/*
* 对于整数 b,~b 表示对 b 的按位取反,
* 即将 b 的每一位 0 变为 1,1 变为 0。
* 然后再加上 1,即 ~b + 1。
* 这个操作相当于对 -b,即 b 的补码取反,得到 -b 的补码形式。
*/
return add(a,add(~b,1));
}
/**
* 乘法
* @param a
* @param b
* @return
*/
public static int multiply(int a,int b){
int res=0;
while (b!=0){
if((b & 1)!=0){
res=add(res,a);
}
a=a<<1;
b=b>>1;
}
return res;
}
/*
判断一个数是否是负数
*/
public static boolean isNeg(int n){
return n<0;
}
/*
返回一个数的相反数
*/
public static int negNum(int n){
return add(~n,1);
}
/*
* 除法
* @param a
* @param b
* @return
*/
public static int divide(int a,int b){
if(b==0){
throw new RuntimeException("divisor is 0");
}
//如果a、b是负数,先将他们都转为正数,方便后面做正数的减法操作
int x= isNeg(a) ? negNum(a):a;
int y= isNeg(b) ? negNum(b):b;
//res是商
int res=0;
//int类型,32位
for(int i=31;i>=0;i=minus(i,1)){
//如果x右移i位之后大于y,这是就可以相减,商就上1
if((x>>i)>=y){
res |=(1<<i);
//x减掉已经减过的部分
x=minus(x,y<<i);
}
}
//如果a、b中有一个为父,结果就为负。
return isNeg(a) ^ isNeg(b)?negNum(res):res;
}
测试:
public static void main(String[] args) {
int a=3;
int b=6;
System.out.println(add(a, b));
System.out.println(minus(a, b));
System.out.println(multiply(a, b));
System.out.println(divide(b, a));
}
结果:
9
-3
18
2
二分查找
1)在一个有序数组中,找某个数是否存在
//int res=binarysearch(nums,target,0,nums.length-1);
private static int binarysearch(Integer[] nums, Integer target,Integer left,Integer right) {
if(left>right) return -1;
int l=left;
int r=right;
int mid=(l+r)>>>1;
if(nums[mid]>target){
return binarysearch(nums,target,left,mid-1);
}else if(nums[mid]<target){
return binarysearch(nums,target,mid+1,right);
}
return mid;
}
2)在一个有序数组中,找 >= 某个数最左侧的位置
/**
Integer[] nums2={1,1,1,1,1,2,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5};
System.out.println(binarysearch2(nums2, 3, 0, nums2.length - 1));
**/
private static int binarysearch2(Integer[] nums, Integer target,Integer left,Integer right) {
if(left>right) return -1;
int l=left;
int r=right;
int t=0;
while(l<=r){
int mid=(l+r)>>>1;
if(nums[mid]>=target){
/**
* 找到了一个 >= target的数,将位置记录下来,
* 不过也可能在这个数的左边还存在 >= target 的数,
* 所以就更新右边界,去左边继续寻找
*/
t=mid;
r=mid-1;
}else{
//这里就是 nums[mid]的值比target要小,更新左边界,继续去数组右边去找
l=mid+1;
}
}
return t;
}
//输出:10
3)局部最小值问题 (数组无序,相邻的两个数不相等 局部最小定义:arr[0] < arr[1] 数组0位置的值就是局部最小,arr[arr.length-1] < arr[arr.length-2] 数组最后一个位置的值就是局部最小,对于数组其他位置上的数 ,满足 arr[i-1] > arr[i] < arr[i+1] i位置上的数就是局部最小)
/**
Integer[] nums3={1,0,1,3,6,5,1,2,3};
System.out.println(binarysearch3(nums3));
**/
private static int binarysearch3(Integer[] nums) {
int l=0;
int r=nums.length-1;
if(nums[l]<nums[l+1]) return l;
if(nums[r]<nums[r-1]) return r;
//如果执行到了这里,表示数组的两个边界都不是局部最小值,那么在这数组之中,一定存在一个局部最小值
while(l<=r){
int mid=(l+r)>>>1;
if(nums[mid]<nums[mid-1] && nums[mid]<nums[mid]+1){
return mid;
}
if(nums[mid]>nums[mid-1]){
r=mid-1;
} else if (nums[mid]>nums[mid+1]) {
l=mid+1;
}
}
return -1;
}
//输出: 1
对数器
有一个你想要测的方法 a,
实现复杂度不好但是容易实现的方法 b ,
实现一个随机样本产生器,
把方法 a 和方法 b 跑相同的随机样本,看看得到的结果是否一样。
如果有一个随机样本是的对比结果不一致,打印样本进行人工干预,改对方法 a 或 方法 b
当样本数量很多时对比测试是否依然正确,如果依然正确,就可以确定方法 a 已经正确
这里以插入排序作为方法a,java中本身提供的sort方法作为方法b
public static void main(String[] args) {
int testTime=500000;
int maxSize=100;
int maxValue=100;
boolean succeed=true;
for(int i=0;i<testTime;i++){
int[] arr1=generateRandomArray(maxSize,maxValue);
int[] arr2=copyArray(arr1);//对arr1数组深拷贝
MySort(arr1);//插入排序 a方法
comparator(arr2);//b方法
if(!isEqual(arr1,arr2)){//如果排序后的两个方法中的值存在差异
succeed=false;
break;
}
}
System.out.println(succeed ? "Nice":"Fucking fucked");
int[] array = generateRandomArray(maxSize, maxValue);
System.out.println(Arrays.toString(array));
MySort(array);
System.out.println(Arrays.toString(array));
}
//随机生成一个长度位maxSize,最大值为maxValue的int类型的数组
public static int[] generateRandomArray(int maxSize,int maxValue){
/**
* Math.random() -> [0,1) 所有的小数,等概率返回一个 因为在计算机中,数的位数是有限制的,所以可以确定所有的小数,在数学上就不行。
* Math.random()*N -> [0,N) 所有的小数,等概率返回一个
* (int)(Math.random()*N) -> [0,N-1] 所有的整数,等概率返回一个
*/
int[] arr=new int[(int)((maxSize+1)*Math.random())];//长度随机
for(int i=0;i<arr.length;i++){
arr[i]=(int) ((maxValue+1)*Math.random())-(int)(maxValue*Math.random());
}
return arr;
}
//深拷贝
private static int[] copyArray(int[] arr1) {
if(arr1==null) return null;
int[] arr=new int[arr1.length];
for (int i = 0; i < arr1.length; i++) {
arr[i]=arr1[i];
}
return arr;
}
//插入排序
private static void MySort(int[] nums) {
if(nums==null || nums.length<2){
return;
}
int l=nums.length;
for(int i=1;i<l;i++){
for(int j=i;j>0;j--){
if(nums[j]<nums[j-1]){
swap(nums,j,j-1);
}else{
break;
}
}
}
}
//交换
private static void swap(int[] nums, int i, int j) {
int tmp= nums[i];
nums[i]= nums[j];
nums[j]=tmp;
}
//java提供的方法
private static void comparator(int[] arr2) {
Arrays.sort(arr2);
}
//比较两个数组中的内容是否完全一致
public static boolean isEqual(int[] arr1,int[] arr2){
for (int i = 0; i < arr1.length; i++) {
if(arr1[i]!=arr2[i]) return false;
}
return true;
}
## 比较器
返回负数的时候,认为第一个参数排在前面 -----> 升序
返回正数的时候,认为第二个参数排在前面 -----> 降序
返回0的时候,谁排在前面无所谓
PriorityQueue<Integer> heap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
递归
递归求数组中的最大值
剖析递归行为何递归行为时间复杂度的估算
用递归方法找一个数组中的最大值,系统上到底是怎么做的
master公式的使用
T(N) = a*T(N/b) + O(N^d)
1)log(b,a) > d --> 时间复杂度为:O(N^log(b,a))
2)log(b,a) = d --> 时间复杂度为:O(N^d * logN)
3)log(b,a) < d --> 时间复杂度为:O(N^d)
递归求数组中的最大值
public static int findMax(int[] arr,int left,int right){
if(left==right){
return arr[left];
}
int mid=left+((right-left)>>1);
int leftMax=findMax(arr,left,mid);
int rightMax=findMax(arr,mid+1,right);
return Math.max(leftMax,rightMax);
}
执行流程
随着每一次findMax函数的调用,都会在java虚拟机栈中创建一个栈帧,等待每一个栈帧中的内容执行完毕后返回结果给上一层调用它的栈帧,这样一层层的调用,到最后,最开始的那一个栈帧对应的函数就可以得到最终的结果。
根据master公式,我们可以得出下面这个数据
T(N) = 2T(N/2)+O(1)
a=2,b=2,d=0
a=2因为一个函数中包含了两次子函数的调用
b=2因为调用一次子函数的数据是父函数的一半
d=0因为除了调用子函数之外的代码的时间复杂度是常数项
所以 log(a,b)>d
所以T(N)=T(N^log(2,2))=T(N),跟直接循环得到数组中的最大值的时间复杂度结果一样
汉诺塔问题
public static void hanoi(int n){ if(n>0){ func(n,"左","右","中"); } } private static void func(int n, String start, String end, String other) { if(n==1){//base case System.out.println("move 1 from "+start+" to "+end); return; } func(n-1,start,other,end); System.out.println("move "+n+" from "+start+" to "+end); func(n-1,other,end,start); }
打印一个字符串的全部子序列,包括空字符串
思路:对于给定的一个字符串,从左到右遍历他的长度,每一位上的字符都有两种选择,选或不选,收集这每一种选择,其结果就是全部子序列
public static void printSubsequence(String str){
if(!Objects.isNull(str)){
process(str.toCharArray(),0,new ArrayList<Character>());
}
}
private static void process(char[] chs, int i, List<Character> list) {
if(i==chs.length){//base case
System.out.println(list.toString());
return;
}
List<Character> addCurChar=copyList(list);
addCurChar.add(chs[i]);
process(chs,i+1,addCurChar);//要当前字符
List<Character> noCurChar=copyList(list);
process(chs,i+1,noCurChar);//不要当前字符
}
private static List<Character> copyList(List<Character> list){
List<Character> copy=new ArrayList<>();
for (Character character : list) {
copy.add(character);
}
return copy;
}
得到一个字符串的全部全排列
public static List<String> fullPermutation(String str){
List<String> res=new ArrayList<>();
if(Objects.isNull(str) || str.length()<2){
res.add(str);
return res;
}
process1(str.toCharArray(),0,res);
return res;
}
/**
* chs[i....]范围上,所有的字符都可以在i位置上,后序都去尝试
* chs[....i-1]范围上,是之前做的选择
* 把所有字符串形成的全排列加入到res中去
* @param chs
* @param i
* @param res
*/
private static void process1(char[] chs, int i, List<String> res) {
if(i==chs.length){
res.add(String.valueOf(chs));
return;
}
//如果字符串中存在重复字符,visit可以防重复,分支限界
boolean[] visit=new boolean[26];
for(int j=i;j<chs.length;j++){
if(!visit[chs[j]-'a']){
visit[chs[j]-'a']=true;
swap(chs,i,j);
process1(chs,i+1,res);
swap(chs,j,i);
}
}
}
private static void swap(char[] chs, int i, int j) {
char t=chs[i];
chs[i]=chs[j];
chs[j]=t;
}
选择纸牌
题目描述:
代码:
public static int win1(int [] arr){
if(arr==null || arr.length==0){
return 0;
}
//比较先拿牌和后拿牌哪一个的结果要大一些
return Math.max(f(arr,0,arr.length-1),s(arr,0,arr.length-1));
}
//先拿
public static int f(int[] arr,int l,int r){
if(l==r){
return arr[l];
}
return Math.max(arr[l]+s(arr,l+1,r),arr[r]+s(arr,l,r-1));
}
//后拿
private static int s(int[] arr, int l, int r) {
if(l==r){
return 0;
}
return Math.min(f(arr,l+1,r),f(arr,l,r-1));
}
逆序一个栈
题目:
/**
* 不借助额外的空间,将栈中的数据逆序
* @param stack
*/
public static void reverse(Stack<Integer> stack){
if(stack.isEmpty()){
return;
}
//每一次返回栈底的元素
int i=f(stack);
reverse(stack);
stack.push(i);
}
/**
* 每一次弹出当前栈底的元素,其他位置的元素相对位置保持不变
* @param stack
* @return
*/
private static int f(Stack<Integer> stack) {
int result=stack.pop();
if(stack.isEmpty()){
return result;
}else{
int last=f(stack);
stack.push(result);
return last;
}
}
数字字符串转化结果
public static int strCombination(String str){
if(str==null){
return 0;
}
return process(str.toCharArray(),0);
}
public static int process(char[] chs,int i){
if(i==chs.length){
return 1;
}
if(chs[i]=='0'){
return 0;
}
if(chs[i]=='1'){
int res=process(chs,i+1);//i自己作为单独部分,后续有多少种方法
if(i+1<chs.length){
res+=process(chs,i+2);// (i和i+1)作为单独部分,后续有多少种方法
}
return res;
}
if(chs[i]=='2'){
int res=process(chs,i+1);//i自己作为单独部分,后续有多少种方法
//(i和i+1)作为单独的部分,且没有超过26,后续有多少种方法
if(i+1<chs.length && chs[i+1]>='0' && chs[i+1]<='6'){
res+=process(chs,i+2);// (i和i+1)作为单独部分,后续有多少种方法
}
return res;
}
return process(chs,i+1);
}
最大价值
public static int maxValue(int[] weight,int[] values,int bag){
return process(weight,values,0,0,0,bag);
}
private static int process(int[] weight, int[] values, int i, int alreadyWeight,int alreadyValue, int bag) {
if(alreadyWeight>bag){
return 0;
}
if(i==weight.length){
return alreadyValue;
}
return Math.max(
//不选择当前i号位置的商品
process(weight,values,i+1,alreadyWeight,alreadyValue,bag),
//选择当前i号位置的商品
process(weight,values,i+1,alreadyWeight+weight[i],alreadyValue+values[i],bag)
);
}
N皇后问题
public static int N1(int n){
if(n<1){
return 0;
}
//record[i]=j-->表示第i行的皇后再第j列
int[] record=new int[n];
return process1(record,0,n);
}
/**
*
* @param record
* @param i 当前为第i行的皇后寻找合适的位置
* @param n 一共有多少行
* @return 可能的次数
*/
private static int process1(int[] record, int i, int n) {
if(i==n){
return 1;
}
int res=0;
for (int j = 0; j < n; j++) {
//前i-1行的皇后都已经放到了合适的位置上
//当前行的皇后是否可以放在第j列
if(isValid(record,i,j)){
record[i]=j;
res+=process1(record,i+1,n);
}
}
return res;
}
private static boolean isValid(int[] record, int i, int j) {
for(int k=0;k<i;k++){
/**
* record[k]==j:当前列已经安排了第k行的皇后
* Math.abs(record[k]-j)==Math.abs(i-k):当前第i行第j列的位置放皇后,与第k行第record[k]列的皇后在一条斜线上
* 满足一个条件,当前j列就不能放第i行的皇后
*/
if(record[k]==j || Math.abs(record[k]-j)==Math.abs(i-k)){
return false;
}
}
return true;
}
优化版本
public static int N2(int n){
if(n<1 || n>32){
return 0;
}
//如果是8皇后问题,就将32位的后8位设置为1
int limit=n==32?-1:(1<<n)-1;
return process(limit,0,0,0);
}
/**
*
* @param limit 一共可以放多少个皇后
* @param colLim 列的限制,1的位置不能放皇后,0的位置可以
* @param LeftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以
* @param rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以
* @return 有多少种放置的方法
*/
private static int process(int limit, int colLim, int LeftDiaLim, int rightDiaLim) {
//如果所有列上都放满了皇后(即32位的后n位上的二进制值都为1),表示这是一次成功的方法,返回1.
if(limit==colLim){
return 1;
}
//还可以放置皇后的所有位置
int pos=0;
//最右边的1的位置
int mostRightOne=0;
//pos的结果是:pos上为1的位置就是可以正确放置皇后的位置(这里跟上面的限制要求有差异,是为了后面找位置的时候要好找一些)
pos=limit & (~(colLim | LeftDiaLim | rightDiaLim));
int res=0;
while (pos!=0){
//取出pos中最右边的1的位置(取最右边为1的方法我要熟悉一些,所以才让pos中为1的位置表示可以放皇后)
mostRightOne=pos & (~pos+1);
//当前位置已经放置了皇后,设置为0,表示当前位置不能再放皇后
pos=pos-mostRightOne;
/**
* 这里就要加限制,因为当前mostRightOne位置已经加了皇后,
* 1.所以colLim列限制就要加上这一个位置,因为colLim中1表示已经放置的皇后,所以就可以直接和mostRightOne进行&运算
* 2.LeftDiaLim&mostRightOne之后往左移动1位
* 3.rightDiaLim&mostRightOne之后往右移动1位
* >> 和 >>> 的区别:
* (1)、‘>>’ 是带符号右移运算符,左侧空出的位用原始数的最高位(符号位)填充,
* (2)、‘>>>’ 是无符号右移运算符,左侧空出的位都用 0 填充
*/
res+=process(limit,colLim | mostRightOne,
(LeftDiaLim | mostRightOne)<<1,
(rightDiaLim | mostRightOne)>>>1);
}
return res;
}
时间对比:
int n=13;
long starTime = System.currentTimeMillis();
System.out.println(N1(n));
long endTime = System.currentTimeMillis();
System.out.println("普通N皇后解法的实现"+n+"皇后问题用时:"+(endTime-starTime)+" ms");
long starTime1 = System.currentTimeMillis();
System.out.println(N2(n));
long endTime1 = System.currentTimeMillis();
System.out.println("优化N皇后解法的实现"+n+"皇后问题用时:"+(endTime1-starTime1)+" ms");
堆
堆结构
堆结构就是用数组实现的完全二叉树结构,完全二叉树是一种特殊的二叉树,其中除了最后一层外,每一层的节点都被完全填满,并且最后一层的节点都靠左对齐。
完全二叉树中如果每颗子树的最大值都在顶部就是大根堆
完全二叉树中如果每颗子树的最小值都在顶部就是小根堆
堆结构的heapInsert与heapify操作
/**
* index上的值满足大根堆的条件从下往上移动
* @param arr 数组
* @param index 要移动的元素
* 时间复杂度取决于树的高度:O(logN)
*/
public static void heapInsert(int[] arr,int index){
//index和index的父节点进行比较,如果arr[index]>arr[(index-1)/2],说明它比父节点的值要大,就交换这父子元素的位置。否则,while循环结束
//当index=0的时候,(index-1)/2 =0,此时的条件不满足,while循环就结束
while(arr[index]>arr[(index-1)/2]){
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
/**
* index上的值满足大根堆的条件从上往下移动
* @param arr 数组
* @param index 要移动的元素
* @param heapSize 堆的大小
* 时间复杂度取决于树的高度:O(logN)
*/
public static void heapify(int[] arr,int index,int heapSize){
//当前节点的左孩子节点
int left=2*index+1;
//当左孩子节点存在的时候
while (left<heapSize){
//选择左孩子和右孩子中大的一个
int largest=(left+1) < heapSize && arr[left+1]>arr[left]?(left+1):left;
//选择父节点和孩子节点之间大的一个
largest=arr[largest]>arr[index]?largest:index;
//如果父节点是大的一个,说明已经满足大顶堆的条件,break
if(largest==index){
break;
}
//交换父节点和孩子节点的值
swap(arr,index,largest);
//移动到交换后的孩子节点
index=largest;
//当前节点的左孩子节点
left=index*2+1;
}
}
堆结构的增大和减少:
堆结构的增大:将增加的值放到数组的最后一位,然后调用heapInsert函数
堆结构的减少:将要删除的元素所在位置记录为index,并且和最后一位互换,然后数组长度减1,调用Heapify函数
所以也可以说堆结构就是优先级队列结构。
在Java中封装了一个优先级队列PriorityQueue
PriorityQueue<Integer> heap = new PriorityQueue<>();
PriorityQueue默认的就是使用小根堆实现的,也可以使用比较器自己定义。
如果在我们的需求中仅仅只是对堆中的数据进行add和poll的话,就可以使用PriorityQueue,这样的效率也比较高。
比如:
已知一个几乎有序的数组(几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离不超过k,并且k相对于数组的长度来说比较小),请选择一个合适的排序算法针对这个数据进行排序。
代码:
/**
* int[] arr2={2,3,1,4,6,5};
* heapSortByK(arr2,2);
* System.out.println(Arrays.toString(arr2));
**/
public static void heapSortByK(int[] arr,int k){
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index=0;
/**
* 先将数组中前k的元素加入到堆中,这前k的元素中,一定包含数组0位置上正确的数字,就是数字中的最小值
*/
for(;index<=Math.min(k,arr.length);index++){
heap.add(arr[index]);
}
int i=0;
/**
* 每一次将数组中k位置后面的元素加入堆中,并弹出堆顶的位置,
* 弹出的元素就是数字i位置上应该放的元素。
*/
for(;index<arr.length;index++){
heap.add(arr[index]);
arr[i++]=heap.poll();
}
while (!heap.isEmpty()){
arr[i++]=heap.poll();
}
}
运行结果:
[1, 2, 3, 4, 5, 6]
不过如果我们的业务需要修改堆中的结构,比如删除或修改一个堆中的数据,那么使用PriorityQueue就不是一个最好的选择,因为PriorityQueue它不能确定我们所修改的位置,只能一个一个的遍历堆中的数据,判断这个数据是否需要进行heapify,这样PriorityQueue的效率就不高。如果我们有这样的需求,就需要我们自己手写堆,我们手写的堆在修改堆结构时可以知道是那里发生了修改,直接在修改的位置进行heapify即可。
排序一个几乎有序的数组
已知一个几乎有序的数组(几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离不超过k,并且k相对于数组的长度来说比较小),请选择一个合适的排序算法针对这个数据进行排序。
代码:
/**
* int[] arr2={2,3,1,4,6,5};
* heapSortByK(arr2,2);
* System.out.println(Arrays.toString(arr2));
**/
public static void heapSortByK(int[] arr,int k){
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index=0;
/**
* 先将数组中前k的元素加入到堆中,这前k的元素中,一定包含数组0位置上正确的数字,就是数字中的最小值
*/
for(;index<=Math.min(k,arr.length);index++){
heap.add(arr[index]);
}
int i=0;
/**
* 每一次将数组中k位置后面的元素加入堆中,并弹出堆顶的位置,
* 弹出的元素就是数字i位置上应该放的元素。
*/
for(;index<arr.length;index++){
heap.add(arr[index]);
arr[i++]=heap.poll();
}
while (!heap.isEmpty()){
arr[i++]=heap.poll();
}
}
运行结果:
[1, 2, 3, 4, 5, 6]
不过如果我们的业务需要修改堆中的结构,比如删除或修改一个堆中的数据,那么使用PriorityQueue就不是一个最好的选择,因为PriorityQueue它不能确定我们所修改的位置,只能一个一个的遍历堆中的数据,判断这个数据是否需要进行heapify,这样PriorityQueue的效率就不高。如果我们有这样的需求,就需要我们自己手写堆,我们手写的堆在修改堆结构时可以知道是那里发生了修改,直接在修改的位置进行heapify即可。
一个数据流中,随时可以取得中位数
在将数组中的数据加入大根对和小根堆的时候,首先把数组的第一个元素加入大根堆,然后每一次加入判断两个条件:
(1)当前值是否 小于等于 大根堆的堆顶值,如果是,加入大根堆,否则加入小根堆
(2)如果两个堆的长度大小之差等于2,就把长的一个的堆顶值加入短的一个
将数组中的数据加入完成之后,大根堆就存放小于中位数的N/2数据,小根堆就存放大于中位数的N/2数据(N为数组长度),最后如果数组长度为奇数,中位数就是长度大的一个堆的堆顶,为偶数,中位数就是两个堆的堆顶加起来除以2。
public static int median(int[] arr){
PriorityQueue<Integer> smallHeap=new PriorityQueue<>();
PriorityQueue<Integer> bigHeap=new PriorityQueue<>((o1, o2) -> o2-o1);
bigHeap.add(arr[0]);
for (int i = 1; i < arr.length; i++) {
int cur=arr[i];
if(cur<=bigHeap.peek()){
bigHeap.add(cur);
}else{
smallHeap.add(cur);
}
if(bigHeap.size()-smallHeap.size()==2){
smallHeap.add(bigHeap.poll());
}else if(smallHeap.size()-bigHeap.size()==2){
bigHeap.add(smallHeap.poll());
}
}
if(smallHeap.size()==bigHeap.size()){
return (smallHeap.peek()+bigHeap.peek()) / 2;
}else{
return smallHeap.size()>bigHeap.size()?smallHeap.peek():bigHeap.peek();
}
}
动态规划
从S走到E,走K步,一共有多少种走法
/**
* 计算从西部边界走到东部边界的方式数量。
*
* @param N 网格的行数
* @param S 网格的列数
* @param E 东部边界的位置
* @param K 步数限制
* @return 可以从西部边界走到东部边界的方式数量
*/
public static int walkWay(int N,int S,int E,int K){
return f1(N,E,K,S);
}
/**
* 递归方法,计算从西部边界走到东部边界的方式数量。
*
* @param N 网格的行数
* @param E 东部边界的位置
* @param res 还可以走的步数
* @param cur 当前所在位置
* @return 可以从西部边界走到东部边界的方式数量
*/
public static int f1(int N,int E,int res,int cur){
// 如果没有步数可用了
if(res==0){
return cur==E?1:0;
}
// 如果当前位置在第一列,只能向右走
if(cur==1){
return f1(N,E,res-1,cur+1);
}
// 如果当前位置在最后一列,只能向左走
if(cur==N){
return f1(N,E,res-1,cur-1);
}
// 递归计算从当前位置向左和向右走的方式数量之和
return f1(N,E,res-1,cur-1)+f1(N,E,res-1,cur+1);
}
/**
* 使用动态规划计算从西部边界走到东部边界的方式数量。
*
* @param N 网格的行数
* @param S 网格的列数
* @param E 东部边界的位置
* @param K 步数限制
* @return 可以从西部边界走到东部边界的方式数量
*/
public static int walkWay2(int N,int S,int E,int K){
int[][] dp = new int[K+1][N+1];
// 初始化动态规划数组
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
dp[i][j] = -1;
}
}
return f2(N,E,K,S,dp);
}
/**
* 动态规划递归方法,计算从西部边界走到东部边界的方式数量。
*
* @param n 网格的行数
* @param e 东部边界的位置
* @param res 还可以走的步数
* @param cur 当前所在位置
* @param dp 动态规划数组
* @return 可以从西部边界走到东部边界的方式数量
*/
private static int f2(int n, int e, int res, int cur, int[][] dp) {
// 如果已经计算过,则直接返回结果
if(dp[res][cur]!=-1){
return dp[res][cur];
}
// 如果没有步数可用了
if (res == 0) {
dp[res][cur]=cur==e?1:0;
return dp[res][cur];
}
// 如果当前位置在第一列,只能向右走
if(cur==1){
dp[res][cur]=f2(n,e,res-1,cur+1,dp);
}else if(cur==n){
dp[res][cur]=f2(n,e,res-1,cur-1,dp);
}else{
// 递归计算从当前位置向左和向右走的方式数量之和
dp[res][cur]=f2(n,e,res-1,cur+1,dp)+f2(n,e,res-1,cur-1,dp);
}
return dp[res][cur];
}
选择硬币,使总面值为target,选择硬币数量最少的数量
/**
* 使用动态规划解决硬币找零问题,返回最少的选择次数。
* @param arr 硬币面额数组
* @param target 目标金额
* @return 最少的选择次数
*/
public static int coin(int[] arr,int target){
// 初始化动态规划数组
int[][] dp=new int[arr.length+1][target+1];
for(int i=0;i<arr.length;i++){
for(int j=0;j<=target;j++){
dp[i][j]=-2;
}
}
// 采用动态规划求解
return f1(arr,0,target,dp);
}
/**
* 使用动态规划解决硬币找零问题,返回最少的选择次数的另一种实现方式。
* @param arr 硬币面额数组
* @param target 目标金额
* @return 最少的选择次数
*/
public static int coin2(int[] arr,int target){
// 初始化动态规划数组
int[][] dp=new int[arr.length+1][target+1];
// 初始化第一列
for(int i=arr.length;i>=0;i--){
dp[i][0]=0;
}
// 初始化最后一行
for(int i=1;i<=target;i++){
dp[arr.length][i]=-1;
}
// 动态规划求解
for(int i=arr.length-1;i>=0;i--){
for(int j=1;j<=target;j++){
int p1=dp[i+1][j];
int p2=-1;
if(j-arr[i]>=0){
p2=dp[i+1][j-arr[i]];
}
// 状态转移方程
if(p1==-1 && p2==-1){
dp[i][j]=-1;
}else if(p1==-1){
dp[i][j]=p2+1;
}else if(p2==-1){
dp[i][j]=p1;
}else{
dp[i][j]=Math.min(p1,p2+1);
}
}
}
// 返回最少的选择次数
return dp[0][target];
}
/**
* 递归解法,求解硬币找零问题的最少选择次数。
* @param arr 硬币面额数组
* @param index 当前考虑的硬币索引
* @param target 目标金额
* @return 最少的选择次数
*/
private static int f1(int[] arr, int index, int target) {
// 基本情况
if(target<0) return -1;
if(target==0) return 0;
// 递归求解
int p1=f1(arr,index+1,target);
int p2=f1(arr,index+1,target-arr[index]);
// 结果合并
if(p1==-1 && p2==-1){
return -1;
} else if (p1 == -1) {
return p2+1;
} else if (p2==-1) {
return p1;
}else {
return Math.min(p1,p2+1);
}
}
/**
* 动态规划解法,求解硬币找零问题的最少选择次数,带记忆化搜索。
* @param arr 硬币面额数组
* @param index 当前考虑的硬币索引
* @param target 目标金额
* @param dp 动态规划数组,用于记忆化搜索
* @return 最少的选择次数
*/
private static int f1(int[] arr, int index, int target,int[][] dp) {
// 如果已经计算过,则直接返回结果
if(dp[index][target]!=-2){
return dp[index][target];
}
// 基本情况
if(target==0) {
dp[index][target]=0;
}else if(index==arr.length){
dp[index][target]=-1;
}else{
// 递归求解并合并结果
int p1=f1(arr,index+1,target);
int p2=f1(arr,index+1,target-arr[index]);
if(p1==-1 && p2==-1){
dp[index][target]=-1;
} else if (p1 == -1) {
dp[index][target]=p2+1;
} else if (p2==-1) {
dp[index][target]=p1;
}else {
dp[index][target]=Math.min(p1,p2+1);
}
}
// 返回最少选择次数
return dp[index][target];
}
/**
* 递归解法,求解硬币找零问题的所有选择方法数量。
* @param arr 硬币面额数组
* @param target 目标金额
* @param cur 当前累计金额
* @param index 当前考虑的硬币索引
* @return 所有的选择方法数量
*/
private static int f1(int[] arr, int target, int cur,int index) {
// 基本情况
if(cur==target){
return 1;
}
if(cur>target){
return 0;
}
// 递归求解并合并结果
if(index>arr.length-1){
return 0;
}
return f1(arr,target,cur+arr[index],index+1)+f1(arr,target,cur,index+1);
}
链表
1.逆转单链表
public static LinkNode reverse(LinkNode head){
if(head==null || head.next==null){
return head;
}
LinkNode cur=head;
LinkNode tmp=null;
LinkNode next=cur.next;
while (next!=null){
cur.next=tmp;
tmp=cur;
cur=next;
next=next.next;
}
cur.next=tmp;
return cur;
}
2.两个有序链表的公共部分
public static List<Integer> commonPart(LinkNode head1, LinkNode head2){
List<Integer> list=new ArrayList<>();
LinkNode p1=head1;
LinkNode p2=head2;
while(p1!=null && p2!=null){
if(p1.getVal()==p2.getVal()){
list.add(p1.getVal());
p1=p1.next;
p2=p2.next;
}else if(p1.getVal()>p2.getVal()){
p2=p2.next;
}else if(p1.getVal()<p2.getVal()){
p1=p1.next;
}
}
return list;
}
3.将单链表按照某值划分成左边小于,中间等于,右边大于的形式
普通写法:先将链表中的所有节点储存到一个数组中,然后对这个数组进行快排中的partition,然后依次设置这个数组中节点的next值。时间复杂度:O(N),空间复杂度:O(N)
public static LinkNode partition(LinkNode head,int num){
if(head==null || head.next==null) return head;
LinkNode tmp=head;
List<LinkNode> list=new ArrayList<>();
while (tmp!=null){
list.add(tmp);
tmp=tmp.next;
}
LinkNode[] arr = list.toArray(LinkNode[]::new);
partition(arr,0,arr.length-1,num);
for (int i = 1; i < arr.length; i++) {
arr[i-1].next=arr[i];
}
arr[arr.length-1].next=null;
return arr[0];
}
public static void partition(LinkNode[] arr,int l,int r,int num){
int less=l-1;
int more=r+1;
while(l<more){
if(arr[l].getVal()<num){
swap(arr,++less,l++);
}else if(arr[l].getVal()==num){
l++;
}else{
swap(arr,--more,l);
}
}
}
优化算法:使用6个变量分别存放小于、等于、大于num值的头、尾节点,然后使小于、等于、大于这三个区域链表头尾相连。这样不仅使得时间复杂度为O(N),空间复杂度为 O(1),并且使得各个链表之间的稳定性不发生变化。
public static LinkNode partition2(LinkNode head,int num){
if(head==null || head.next==null) return head;
LinkNode SH=null;//小于num的头结点
LinkNode ST=null;//小于num的尾节点
LinkNode EH=null;//等于num的头结点
LinkNode ET=null;//等于num的尾节点
LinkNode MH=null;//大于num的头结点
LinkNode MT=null;//大于num的尾节点
LinkNode cur=head;
while (cur!=null){
head=cur.next;
//这里必须要使cur的next节点为空,要保证给下面的6个节点赋值的节点是一个干净节点。
cur.next=null;
if(cur.getVal()<num){
if(SH==null){
SH=cur;
ST=cur;
}else{
ST.next=cur;
ST=cur;
}
}else if(cur.getVal()==num){
if(EH==null){
EH=cur;
ET=cur;
}else{
ET.next=cur;
ET=cur;
}
}else{
if(MH==null){
MH=cur;
MT=cur;
}else{
MT.next=cur;
MT=cur;
}
}
cur=head;
}
//如果小于区的头链表不为空,就连接小于区和等于区
if(SH!=null){
//小于区的尾节点连接等于区的头结点
ST.next=EH;
//如果不存在值等于num的链表区域,那么大于等于区的最后一个节点还是原来的小于区的最后一个节点
ET=ET==null?ST:ET;
}
if(MH!=null){
ET.next=MH;
}
return SH!=null?SH:(EH!=null?EH:MH);
}
4.回文链表
普通实现:先将链表中的所有值push进一个stack中,然后stack弹出一个栈顶值依次与链表中的值进行对比。时间复杂度:O(N),空间复杂度:O(N)。
public static boolean palindromic(LinkNode head){
if(head==null || head.next==null) return true;
Stack<Integer> stack = new Stack<>();
boolean res=true;
LinkNode cur=head;
//先将链表中的数都出入stack中
while (cur!=null){
stack.push(cur.getVal());
cur=cur.next;
}
//然后stack中弹出一个与链表的值进行对比
cur=head;
while(cur!=null){
if(cur.getVal()!=stack.pop()){
res=false;
break;
}
cur=cur.next;
}
return res;
}
优化实现:使用快慢指针n1、n2。n1每次走一步,n2每次走两步,n1到达链表的中间时,n2到达链表的尾部,然后将n1后面的链表节点进行逆序,n1节点的next指针置为空,然后n1重新赋值为头结点所在位置,从头结点往后走,n3赋值为尾节点从所在位置,从尾节点往前走,如果n1、n3节点的值不同,就返回错误,一直到n1、n3都到达中间的节点位置,值任然是相等的,这就是正确的回文链表,因为中间节点的next指针我们设置为了null,所以此时就可以判断比较已经结束了,然后再一次逆转右边的链表节点,让链表恢复为刚开始的样子。时间复杂度为O(N),空间复杂度为O(1)。
public static boolean palindromic2(LinkNode head){
if(head==null || head.next==null){
return true;
}
//快慢指针
LinkNode n1=head;
LinkNode n2=head;
//n1一次移动一位,n2一次移动两位,结束的时候,n1在中间,n2在结尾
while(n2.next!=null && n2.next.next!=null){
n1=n1.next;
n2=n2.next.next;
}
//根据中间节点,将整个链表分为左右两个部分
//n2为右边部分的第一个节点
n2=n1.next;
//断开连接
n1.next=null;
LinkNode n3=null;
while(n2!=null){
n3=n2.next;
n2.next=n1;
n1=n2;
n2=n3;
}
//while循环结束时,n1位置为右边最后一个节点的位置
n3=n1;
n2=n1;
//然后将n1为左边部分第一个节点
n1=head;
boolean res=true;
while(n1!=null && n3!=null){
if(n1.getVal()!=n3.getVal()){
res=false;
break;
}
n1=n1.next;
n3=n3.next;
}
//将右边部分恢复,此时n2为右边最后一个节点的位置
n1=n2.next;
n2.next=null;
while(n1!=null){
n3=n1.next;
n1.next=n2;
n2=n1;
n1=n3;
}
return res;
}
5.复制含有随机指针的链表
普通实现:用一个map储存,key为旧节点,value为新节点,然后根据旧节点的next和random值设置新节点的next和random值。时间复杂度O(N),空间复杂度O(N)。
public static ListNode copyLinkWithRandom1(ListNode head){
if(head==null) return head;
Map<ListNode,ListNode> map=new HashMap<>();
ListNode cur=head;
while (cur!=null){
map.put(cur,new ListNode(cur.val,null,null));
cur=cur.next;
}
cur=head;
while(cur!=null){
map.get(cur).next=map.get(cur.next);
map.get(cur).random=map.get(cur.random);
cur=cur.next;
}
return map.get(head);
}
优化实现:第一个while循环用来创建一个与cur的val值相同的新节点,将这个新节点插入cur的后面;第二个while循环用来同步新节点的random值,新节点的random值就在旧节点random的后面一个位置上,同样是新创建的节点;第三个while循环用来分开旧链表和新链表。时间复杂度:O(N),空间复杂度:O(1)。
public static ListNode copyLinkWithRandom2(ListNode head){
if(head==null) return head;
ListNode cur=head;
//这个while循环用来创建一个与cur的val值相同的新节点,将这个新节点插入cur的后面。
while(cur!=null){
ListNode newNode=new ListNode(cur.val,cur.next,null);
cur.next=newNode;
cur=newNode.next;
}
cur=head;
ListNode next=null;
//这个while循环用来同步新节点的random值,新节点的random值就在旧节点random的后面一个位置上
while (cur!=null){
next=cur.next;
next.random=cur.random!=null?cur.random.next:null;
cur=next.next;
}
cur=head;
//记录新链表的头结点
next=cur.next;
ListNode tmp=null;
//这个while循环用来分开旧链表和新链表
while (cur!=null){
tmp=cur.next;
cur.next=tmp.next;
tmp.next=cur.next;
cur=cur.next;
}
//返回新链表的头结点
return next;
}
6.两个链表相交的问题
给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,返回相交的第一个节点。如果不相交,返回null。
要求:如果两个链表长度为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
代码:
public static LinkNode getIntersectNode(LinkNode head1,LinkNode head2){
if(head1==null || head2==null) return null;
//如果这个链表有环,获取到它的环节点
LinkNode loop1=getLoopNode(head1);
LinkNode loop2=getLoopNode(head2);
//根据loop1、loop2节点,存在两个链表可能存在相交节点的情况就两种:1.两个链表都没有环。2.两个链表都有环
if(loop1==null && loop2==null){
//如果没有环,进入没有环的获取相交节点的函数
return noLoop(head1,head2);
}else if(loop1!=null && loop2!=null){
//如果有环,进入有环的获取相交节点的函数
return hasLoop(head1,loop1,head2,loop2);
}
//其他情况不可能相交
return null;
}
private static LinkNode getLoopNode(LinkNode head1) {
if(head1.next==null || head1.next.next==null){
return null;
}
//快慢指针
LinkNode n1=head1.next;
LinkNode n2=head1.next.next;
//如果有环,快指针和慢指针会在环中相遇
while (n1!=n2){
//如果直到快指针要走到null时,还没有和慢指针相遇,说明不存在环
if(n2.next==null || n2.next.next==null){
return null;
}
n2=n2.next.next;
n1=n1.next;
}
//快慢指针相遇之后,令一个指针回到头结点,然后两个指针一起开始移动,每次都只走一步,他们再次相遇的地方就是进入环的第一个 //节点,这是一个数学上的问题。
n2=head1;
while(n1!=n2){
n1=n1.next;
n2=n2.next;
}
return n1;
}
//这种情况下,如果两个链表相交,相交节点之后的链表完全相同,考虑相交之前的节点
private static LinkNode noLoop(LinkNode head1, LinkNode head2) {
int n=0;
LinkNode p1=head1;
LinkNode p2=head2;
//计算两个链表的长度
while(p1.next!=null){
n++;
p1=p1.next;
}
while(p2.next!=null){
n--;
p2=p2.next;
}
//如果最后一个节点不同,说明没有相交
if(p1!=p2){
return null;
}
p1=n>0?head1:head2;//p1是长的一个链表的头结点
p2=p1==head1?head2:head1;//p2是短的一个链表的头结点
n=Math.abs(n);
//保证进行比较的两个节点的起始节点站在同一起跑线
while(n-->0){
p1=p1.next;
}
while(p1!=p2){
p1=p1.next;
p2=p2.next;
}
return p1;
}
private static LinkNode hasLoop(LinkNode head1, LinkNode loop1, LinkNode head2, LinkNode loop2) {
/**
* 有环的情况分三种:
* 1.两个链表的环节点相同,说明相交在环节点之前
* 2.两个链表的环节点不相同,但是是处在一个环内的,loop1节点不断next,会到达loop2节点
* 3.两个链表的环节点不相同,并且也不再一个环内,这种就不存在相交节点。
*/
if(loop1==loop2){
//情况1--根没有环的思路相同,只不过最后一个节点变成了loop节点
LinkNode n1=head1;
LinkNode n2=head2;
int n=0;
while(n1!=loop1){
n++;
n1=n1.next;
}
while(n2!=loop2){
n--;
n2=n2.next;
}
n1=n>0?head1:head2;
n2=n1==head1?head2:head1;
n=Math.abs(n);
while(n-->0){
n1=n1.next;
}
while(n1!=n2){
n1=n1.next;
n2=n2.next;
}
return n1;
}else{
LinkNode cur=loop1.next;
while (loop1!=cur){
if(cur==loop2){
//情况2
return loop1;
}
cur=cur.next;
}
//情况3
return null;
}
}
二叉树
二叉树的递归序
public static void preorder1(TreeNode head){ if(head==null) return; //第一次当前节点是head节点 preorder1(head.left); //第二次当前节点是head节点,head.left递归完成之后 preorder1(head.right); //第三次当前节点是head节点,head.right递归完成之后 }
二叉树的遍历
先序遍历:
/**
* 递归先序遍历
* @param head
*/
public static void preorder1(TreeNode head){
if(head==null) return;
System.out.print(head.val+" ");
preorder1(head.left);
preorder1(head.right);
}
/**
* 非递归先序遍历
* 先将头结点压入stack,然后poll进行处理,然后先右后左将子节点push进stack。
* @param head
*/
public static void preorder2(TreeNode head){
Stack<TreeNode> stack=new Stack<>();
stack.push(head);
while (!stack.isEmpty()){
head= stack.pop();
System.out.print(head.val+" ");
if(head.right!=null) stack.push(head.right);
if(head.left!=null) stack.push(head.left);
}
}
中序遍历(深度优先遍历):
/**
* 递归中序遍历
* @param head
*/
public static void inorder1(TreeNode head){
if(head==null) return;
inorder1(head.left);
System.out.print(head.val+" ");
inorder1(head.right);
}
/**
* 非递归中序遍历
* 先将一个节点的左节点全部压入栈,然后弹出最后一个左节点,对弹出节点进行处理,将当前节点变成它的右节点,然后对这个节点重复前 * 面的操作
* @param head
*/
public static void inorder2(TreeNode head){
Stack<TreeNode> stack=new Stack<>();
while (!stack.isEmpty() || head!=null){
if(head!=null){
stack.push(head);
head=head.left;
}else{
head=stack.pop();
System.out.print(head.val+" ");
head=head.right;
}
}
}
后序遍历:
/**
* 递归后序遍历
* @param head
*/
public static void postorder1(TreeNode head){
if(head==null) return;
postorder1(head.left);
postorder1(head.right);
System.out.print(head.val+" ");
}
/**
* 非递归后序遍历
* @param head
*/
public static void postorder2(TreeNode head){
Stack<TreeNode> s1=new Stack<>();
Stack<TreeNode> s2=new Stack<>();
s1.push(head);
while (!s1.isEmpty()){
head = s1.pop();
//进入s2中的元素次序:中右左,出来时:左右中
s2.push(head);
if(head.left!=null) s1.push(head.left);
if(head.right!=null) s1.push(head.right);
}
while (!s2.isEmpty()){
head=s2.pop();
System.out.print(head.val+" ");
}
}
层序遍历(宽度优先遍历):
/**
* 层序遍历
* @param head
*/
public static void sequence(TreeNode head){
if(head==null) return;
Queue<TreeNode> queue=new LinkedList<>();
queue.add(head);
TreeNode cur=null;
while (!queue.isEmpty()){
cur=queue.poll();
System.out.print(cur.val+" ");
if(cur.left!=null){
queue.add(cur.left);
}
if(cur.right!=null){
queue.add(cur.right);
}
}
}
求一个二叉树的最大宽度
方法1:
public static void level(TreeNode head){
int max=Integer.MIN_VALUE;
Queue<TreeNode> queue=new LinkedList<>();
//map用来记录每一个节点在第几层
Map<TreeNode,Integer> map=new HashMap<>();
queue.add(head);
//头结点在第一层
map.put(head,1);
//当前层的节点个数
Integer curLevelNodes=0;
//当前是第几层
Integer level=1;
TreeNode cur=null;
while (!queue.isEmpty()){
cur=queue.poll();
if(level.equals(map.get(cur))){
//当前层的节点数加一
curLevelNodes++;
}else{
//这是下一层的左边第一个节点,层级加1,上一层的节点已经记录完了,与max进行比较,取大的一个
level++;
max=Math.max(max,curLevelNodes);
//当前已经发现了这一层的一个节点
curLevelNodes=1;
}
//当前节点的子节点的层级 为当前节点+1
if(cur.left!=null){
queue.add(cur.left);
map.put(cur.left,map.get(cur)+1);
}
if(cur.right!=null){
queue.add(cur.right);
map.put(cur.right,map.get(cur)+1);
}
}
//最后还要来比较一次,因为最后一层循环不会走while循环中else中的代码
max=Math.max(max,curLevelNodes);
System.out.println(max);
}
方法2:
public static void level2(TreeNode head){
int max=Integer.MIN_VALUE;
//记录当前层的节点数量
int curlevelNode=0;
//记录当前层最后一个节点所在的位置
TreeNode curEndNode=head;
//记录下一层最后一个节点所在的位置
TreeNode curNextEndNode=null;
Queue<TreeNode> queue=new LinkedList<TreeNode>();
queue.add(head);
TreeNode cur=null;
while (!queue.isEmpty()){
cur=queue.poll();
curlevelNode++;
if(cur.left!=null){
queue.add(cur.left);
curNextEndNode=cur.left;
}
if(cur.right!=null){
queue.add(cur.right);
curNextEndNode=cur.right;
}
/**
* 如果当前节点是这一层的最后一个节点,那么此时curNextEndNode就是下一层的最后一个节点,更新此时的层级
* **/
if(cur==curEndNode){
//当前层记录完了,开始记录下一层
max=Math.max(max,curlevelNode);
curEndNode=curNextEndNode;
curlevelNode=0;
}
}
System.out.println(max);
}
判断一个树是否是二叉搜索树
二叉搜索树(Binary Search Tree,简称 BST)是一种特殊的二叉树,它具有以下性质:
-
每个节点最多有两个子节点,分别称为左子节点和右子节点。
-
对于任意节点,其左子树中的所有节点的值小于该节点的值。
-
对于任意节点,其右子树中的所有节点的值大于该节点的值。
-
左子树和右子树也分别是二叉搜索树。
public static int preValue=Integer.MIN_VALUE;
public static boolean isBST(TreeNode head){
//如果当前节点为空,这个节点也是二叉搜索树
if(head==null) return true;
//判断这个节点的左子树是否是二叉搜索树
boolean left=isBST(head.left);
if(!left){
return false;
}
//如果当前节点的值不大于它的前面的节点的最大值,就不是二叉搜索树
if(head.val<=preValue){
return false;
}else{
//记录此节点及之前的最大值
preValue=head.val;
}
//走到这里,说明当前节点的左子树是二叉搜索树,并且当前节点的值大于它的左子树中的最大值,所以直接返回
return isBST(head.right);
}
使用树形DP的代码
//表示以当前节点为根的二叉树中最小的值、最大的值、以及是否是二叉树
public static class SInfo{
int max;
int min;
boolean isSearch;
public SInfo(int max,int min,boolean b){
this.max=max;
this.min=min;
isSearch=b;
}
}
/**
* 判断一颗二叉树是否是搜索树
* @param head
* @return
*/
public static boolean isBst(TreeNode head){
if(head==null){
return true;
}
return process3(head).isSearch;
}
public static SInfo process3(TreeNode head){
//如果为空,我们无法确定min值和max值,索性返回null
if(head==null){
return null;
}
SInfo leftInfo=process3(head.left);
SInfo rightInfo=process3(head.right);
//比较当前节点与其子节点的值,得到这颗子树的最大值、最小值,如果是左子树,最大值有用;如果是右子树,最小值有用。
int min=head.val;
int max=head.val;
if(leftInfo!=null){
min=Math.min(min,leftInfo.min);
max=Math.max(max,leftInfo.max);
}
if(rightInfo!=null){
min=Math.min(min,rightInfo.min);
max=Math.max(max, rightInfo.max);
}
boolean isSearch=true;
//判断是否符合二叉搜索树的条件
if(leftInfo!=null && (leftInfo.max>=head.val || !leftInfo.isSearch)){
isSearch=false;
}
if(rightInfo!=null && (rightInfo.min<=head.val || !rightInfo.isSearch)){
isSearch=false;
}
return new SInfo(max,min,isSearch);
}
判断一个树是否是完全二叉树
完全二叉树是一种特殊的二叉树,具有以下特点:
-
所有的叶子节点都出现在最底层或倒数第二层,并且最底层的叶子节点都集中在左侧。
-
如果节点有右子节点,那么该节点必须有左子节点。
-
底层缺失的节点是从左向右连续缺失的,即不能出现在中间某个位置缺失节点的情况。
//使用层序便利的方式来判断一颗树是否是完全二叉树
public static boolean isCST(TreeNode head){
if(head==null){
//如果树为空,是完全二叉树
return true;
}
//第一次遇到叶子节点变为true,并且在此之后遇到的节点必须全都是没有孩子的叶子节点
boolean leaf=false;
Queue<TreeNode> queue=new LinkedList<TreeNode>();
queue.add(head);
while(!queue.isEmpty()){
head=queue.poll();
//如果在遇到了第一个叶子节点之后,出现了一个含有子节点的叶子节点,那么这颗树就不符合完全二叉树的概念,返回false。
//因为是层序遍历,所以这个判断条件也包含了二叉树概念的:“所有的叶子节点都出现在最底层或倒数第二层” 这个条件。
if(leaf && (head.left!=null || head.right!=null)){
return false;
}
//如果一个节点的左子树为空,右子树不为空,那么这个节点就不符合完全二叉树的要求,返回false。
if(head.left==null && head.right!=null){
return false;
}
//遇到第一个叶子节点
if(head.left==null){
leaf=true;
}
if(head.left!=null){
queue.add(head.left);
}
if(head.right!=null){
queue.add(head.right);
}
}
return true;
}
判断一个树是否是满二叉树
满二叉树是一种特殊的二叉树,具有以下特点:
-
每个节点要么是叶子节点,要么有两个子节点。
-
所有叶子节点都在同一层级上。
满二叉树的特点使得它的节点数达到最大化,即除了叶子节点外,每个节点都有两个子节点,整棵树的节点数为 2^n - 1(n为树的高度)
树形DP的代码:
public static class Info{
int height;
int nodes;
public Info(int height,int nodes){
this.height=height;
this.nodes=nodes;
}
}
public static boolean isFullTree(TreeNode head){
if(head==null) return true;
Info info = process1(head);
return info.nodes == (1 << info.height)-1;
}
//这个函数需要返回以head根节点的二叉树的高度和节点个数
public static Info process1(TreeNode head){
//如果当前节点为空,那么高度为0,节点个数为0
if(head==null){
return new Info(0,0);
}
//得到当前节点左子树的信息
Info leftInfo=process1(head.left);
//得到当前节点右子树的信息
Info rightInfo=process1(head.right);
//这个节点子树的最大高度
int height=Math.max(leftInfo.height, rightInfo.height);
//子树的节点总数
int nodes=leftInfo.nodes+rightInfo.nodes;
//高度加1就是以当前节点为根节点的树的高度,nodes+1,同理
return new Info(height+1,nodes+1);
}
判断一个树是否是平衡二叉树
平衡二叉树(Balanced Binary Tree)是一种特殊的二叉树,其具有以下特点:
-
对于任意节点,其左子树和右子树的高度差不超过 1。
-
所有子树都符合上述平衡条件。
平衡二叉树的设计旨在保持树的高度尽可能低,从而提高树的查询、插入和删除等操作的效率。通过保持树的平衡,可以确保树的高度接近最小值,使得树的性能得到优化。常见的平衡二叉树包括 AVL 树和红黑树。
树形DP的代码:
public static class BInfo{
int height;
boolean isBalance;
public BInfo(int h,boolean b){
height=h;
isBalance=b;
}
}
/**
* 判断一颗树是否是平衡二叉树
* @param head
* @return
*/
public static boolean isBBT(TreeNode head){
if(head==null){
return true;
}
BInfo info = process2(head);
return info.isBalance;
}
public static BInfo process2(TreeNode head){
if(head==null){
//空树也是平衡二叉树
return new BInfo(0,true);
}
BInfo leftInfo=process2(head.left);
BInfo rightInfo=process2(head.right);
int height=Math.max(leftInfo.height,rightInfo.height)+1;
boolean isBalance=leftInfo.isBalance && rightInfo.isBalance && Math.abs(leftInfo.height-rightInfo.height)<2;
return new BInfo(height,isBalance);
}
两个节点的最近公共祖先
方法1:使用HashMap存储节点的父节点
public static TreeNode lowestCommonAncestor(TreeNode head,TreeNode o1,TreeNode o2){
if(head==null || head==o1 || head==o2){
return head;
}
if(o1==o2) return o1;
Map<TreeNode,TreeNode> map=new HashMap<TreeNode, TreeNode>();
//记录所有节点的父节点
map.put(head,head);
findFatherNode(map,head);
//用来保存o1节点的引用链
Set<TreeNode> set1=new HashSet<TreeNode>();
TreeNode cur=o1;
//知道cur等于根节点时,跳出循环
while(cur != map.get(cur)){
set1.add(cur);
cur=map.get(cur);
}
//记得把根节点加入集合
set1.add(head);
cur=o2;
while(true){
//因为我们是从o2节点往上寻找父节点的,所以如果是第一次遇到o1的路径中包含了当前o2的路径,说明这个就是最近的公共祖先
if(set1.contains(cur)){
return cur;
}
cur=map.get(cur);
}
}
方法2:使用递归,有两种情况(1)o1为o2的父节点或者o2为o1的父节点。(2)o1、o2处于同一个节点的两端
public static TreeNode lowestCommonAncestor2(TreeNode head,TreeNode o1,TreeNode o2){
if(head==null || head==o1 || head==o2){
return head;
}
TreeNode leftNode=lowestCommonAncestor2(head.left,o1,o2);
TreeNode rightNode=lowestCommonAncestor2(head.right,o1,o2);
//情况2
if(leftNode!=null && rightNode!=null){
return head;
}
//情况1
return leftNode!=null?leftNode:rightNode;
}
在二叉树中找到一个节点的后继节点
现在有一种新的二叉树的节点类型如下:
public class Node{
int val;
Node left;
Node right;
Node parent;
}
只给一个在二叉树中的某个节点node,请实现返回node的后继节点的函数。
在二叉树的中序遍历中,node的下一个节点叫做node的后继节点。
public static Node successorNode(Node node){
if(node==null) return null;
//情况1:当前节点有右孩子节点,那么后继节点就是右子树的最左边的一个节点。
if(node.right!=null){
return getRightMoreLeftNode(node.right);
}else{
//情况2:当前节点没有右节点,就往上寻找一个祖先节点,这个祖先节点是它父节点的左孩子,那么这个祖先节点的父节点就是当前节点的后继节点
//如果是最后一个节点,它没有后继节点,就返回null
Node parent=node.parent;
while(parent!=null && parent.left!=node){
node=parent;
parent=node.parent;
}
return parent;
}
}
字符串的序列化和反序列化
/**
* 先序遍历序列化一颗树
* 如果节点为空,为 '#_'
* 如果不为空,为 'val_'
* @param head
* @return
*/
public static String serializeTreeByPreOrder(TreeNode head){
if(head==null){
return "#_";
}
String s=head.val+"_";
s+=serializeTreeByPreOrder(head.left);
s+=serializeTreeByPreOrder(head.right);
return s;
}
/**
* 根据一个字符串反序列化构建二叉树
* @param s
* @return
*/
public static TreeNode reconByPreString(String s){
if(s==null) return null;
String[] split = s.split("_");
Queue<String> queue=new LinkedList<>();
for (int i = 0; i < split.length; i++) {
queue.add(split[i]);
}
return reconPreOrder(queue);
}
private static TreeNode reconPreOrder(Queue<String> queue) {
String s = queue.poll();
if(s.equals("#")){
return null;
}
TreeNode node=new TreeNode(Integer.valueOf(s));
node.left=reconPreOrder(queue);
node.right=reconPreOrder(queue);
return node;
}
折纸问题
把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开,此时的折痕是凹下去的,即折痕突起的方向是纸条的背面。如果从纸条的下边向上方先序对折两次,压出折痕后展开,此时有三条折痕,从上倒下依次是下折痕、下着痕和上折痕。
给定一个输入参数N,代表所有纸条都从下边向上方连续对折N次。
例如:N=1时,打印 down;N=2时,打印 down down up
/**
* 折纸问题就是一个二叉树问题,这个二叉树满足:
* (1)每n层的节点都是第n次折叠所产生的新的折痕,头结点是第一次产生的折叠,是凹进去的,为down
* (2)每一个节点的左孩子节点是凹,每一个节点的右孩子节点是凸
* (3)构建好这颗二叉树之后,中序遍历的结果就是纸条折叠n次之后,从上到下的折痕。
* @param N 一共要折多少次
*/
public static void Origami(int N){
printProcess(1,N,true);
}
/**
* @param i 当前是第几层 也就是第几次折叠
* @param N 一共有几层 也就是一共要折叠几次
* @param down true为凹,false为出
*/
public static void printProcess(int i,int N,boolean down){
if(i>N){
return;
}
//
printProcess(i+1,N,true);
System.out.print(down?"down ":"up ");
printProcess(i+1,N,false);
}
如果对折痕和二叉树的关系有疑问,可以去拿一个长纸条折一下。
图
图的数据结构定义
熟悉了一个图的结构,以后遇到图问题就把题目给出的信息转化成我们熟悉的图结构
public class Graph {
//图的点集 key--节点的值 value--节点信息 这里也可以改成数组
public HashMap<Integer,Node> nodes;
//图的边集
public HashSet<Edge> edges;
public Graph(){
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
public class Node {
//节点的值
public int value;
//当前节点的入度 (其他节点指向当前节点)
public int in;
//当前节点的出度(当前节点指向其他节点)
public int out;
//右当前节点直接指向的的节点集合
public ArrayList<Node> nexts;
//以当前节点为出度的边,就是属于这个节点的边
public ArrayList<Edge> edges;
public Node(int value){
this.value=value;
in=0;
out=0;
nexts=new ArrayList<>();
edges=new ArrayList<>();
}
}
public class Edge {
//当前边的权重
public int weight;
//当前边由哪一个节点出发
public Node from;
//到哪一个节点
public Node to;
public Edge(int weight,Node from,Node to){
this.weight=weight;
this.from=from;
this.to=to;
}
}
图的宽度优先遍历
1.利用队列和set集合实现
2.从源节点开始依次按照宽度进队列,然后弹出
3.每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
4.直到队列为空
/**
* 图中一个节点的宽度优先遍历
* @param node
*/
public static void BFS(Node node){
Queue<Node> queue=new LinkedList<>();
//保证队列中不会存入重复的节点
HashSet<Node> set=new HashSet<>();
queue.add(node);
set.add(node);
while(!queue.isEmpty()){
node=queue.poll();
//处理宽度优先遍历的节点
System.out.println(node.value);
//将当前节点的每一个没有重复的相邻节点加入队列之中
for (Node next : node.nexts) {
if(!set.contains(next)){
queue.add(next);
set.add(next);
}
}
}
}
图的深度优先遍历
1.利用栈实现
2.从源节点开始把节点按照深度放入栈,然后弹出
3.每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
4.直到栈变空
/**
* 图中一个节点的深度优先遍历
* @param node
*/
public static void DFS(Node node){
Stack<Node> stack=new Stack<>();
//保证栈中不会存入重复的节点
HashSet<Node> set=new HashSet<>();
stack.push(node);
set.add(node);
//处理
System.out.println(node.value);
while(!stack.isEmpty()){
node=stack.pop();
//每次只将当前节点的一个相邻节点存入栈中
for (Node next : node.nexts) {
if(!set.contains(next)){
//因为我们没有将当前节点的每一个值都存入栈中,所以还需要将当前节点入栈,遍历完一条链路之后再来遍历其他的链路
stack.push(node);
stack.push(next);
set.add(next);
//处理
System.out.println(next.value);
break;
}
}
}
}
拓扑排序
拓扑排序是对有向无环图(DAG,Directed Acyclic Graph)进行的一种排序,使得图中的所有顶点按照一定的顺序排列,使得对于图中的任意一条有向边 (u, v)
,顶点 u
在排序中都出现在顶点 v
的前面。换句话说,如果存在一条从顶点 u
到顶点 v
的路径,那么在拓扑排序中,u
必定在 v
的前面。
拓扑排序常用于描述一个系统中各个任务之间的依赖关系,例如编译器中的源文件依赖关系、任务调度中的任务执行顺序等。
拓扑排序的算法可以用深度优先搜索(DFS)来实现。具体步骤如下:
-
从图中选择一个没有前驱(即入度为0)的顶点并输出。
-
从图中删除该顶点及其所有出边。
-
重复步骤1和步骤2,直到所有顶点都被输出。如果图中还有顶点未被输出但是没有前驱的顶点,则说明图中存在环,无法进行拓扑排序。
如果一个有向图可以被成功的拓扑排序,则该图为有向无环图(DAG);反之,如果一个有向图中存在环,则无法进行拓扑排序。
/**
* 对一个图中的节点进行拓扑排序
* @param graph
* @return
*/
public static List<Node> sortedTopology(Graph graph){
//用来记录每一个节点剩余的入度
Map<Node,Integer> inMap=new HashMap<>();
//用来记录入度为0的节点
Queue<Node> zeroInNode=new LinkedList<>();
for (Node node : graph.nodes.values()) {
inMap.put(node,node.in);
if(node.in==0){
zeroInNode.add(node);
}
}
//用来存放拓扑排序的结果
List<Node> res=new ArrayList<>();
while(!zeroInNode.isEmpty()){
Node node = zeroInNode.poll();
res.add(node);
for (Node next : node.nexts) {
//从当前节点连接的其他节点中移除当前节点的影响,即入度减1
inMap.put(next, inMap.get(next)-1);
if(inMap.get(next)==0){
zeroInNode.add(next);
}
}
}
return res;
}
生成最小生成树
Kruskal算法
Kruskal算法是一种贪心算法,它通过不断选取权重最小的边,并保证不形成环来构建最小生成树。具体步骤如下:
-
将图中的所有边按照权重从小到大排序。
-
依次选择排序后的边,如果该边连接的两个顶点不在同一个连通分量中(即不会形成环),则将该边加入最小生成树,并将两个顶点合并为一个连通分量。
-
重复上述步骤,直到所有顶点都在同一个连通分量中,即最小生成树构建完成。
/**
* MySet用来简单实现一个并查集的功能,性能没有并查集好
*/
public static class MySet{
Map<Node,List<Node>> setMap;
/**
* 初始化Map,key为节点,value为节点所在的集合
* @param nodes
*/
public MySet(List<Node> nodes){
setMap=new HashMap<>();
for (Node node : nodes) {
List<Node> list=new ArrayList<>();
list.add(node);
setMap.put(node,list);
}
}
/**
* 判断两个节点所处的集合是否是同一个
* 如果是同一个,就会产生环,当前edge就不可用
* 如果不是同一个,就融合这个节点的集合
* @param
*/
public boolean isSameSet(Node from,Node to){
List<Node> fromNode = setMap.get(from);
List<Node> toMap = setMap.get(to);
return fromNode==toMap;
}
/**
* 融合这两个节点所在的集合,并且在map中修改对应的集合地址
* @param from
* @param to
* @return
*/
public void union(Node from,Node to){
List<Node> fromNode = setMap.get(from);
List<Node> toNode = setMap.get(to);
for (Node node : toNode) {
fromNode.add(node);
setMap.put(node,fromNode);
}
}
}
/**
* 使用Kruskal算法生成最小生成树
* @param graph
* @return
*/
public static Set<Edge> Kruskal(Graph graph){
//将图中的所有节点加入MySet中,每一个节点的初始集合只有它自己
MySet unionFind=new MySet(graph.nodes.values().stream().toList());
//使用一个优先队列,让队列中的edge以weight进行排序
PriorityQueue<Edge> queue=new PriorityQueue<>(Comparator.comparingInt(o -> o.weight));
for (Edge edge : graph.edges) {
queue.add(edge);
}
//返回一个set集合
Set<Edge> res=new HashSet<>();
while (!queue.isEmpty()){
Edge edge = queue.poll();
//如果一条边的两个节点不是处于同一个集合中,说明加入这条边,生成树不会产生环
if(!unionFind.isSameSet(edge.from,edge.to)){
//在返回集合中添加这个节点
res.add(edge);
//合并这两个节点所在的集合
unionFind.union(edge.from,edge.to);
}
}
return res;
}
Prim算法
Prim算法是一种贪心算法,从一个顶点开始,逐步选取与当前生成树相连的权重最小的边,直到所有顶点都被包含在生成树中。具体步骤如下:
-
选择一个起始顶点,将其加入生成树。
-
从与生成树相连的顶点中选择一条权重最小的边,并将连接的顶点加入生成树。
-
重复上述步骤,直到所有顶点都被包含在生成树中。
/**
* 使用Prime算法生成最小生成树
* @param graph
* @return
*/
public static Set<Edge> Prime(Graph graph){
//这个set集合用来保证没有重复的点进入,避免形成环
HashSet<Node> set=new HashSet<>();
//用来存放解锁的边,按权重排序--set集合每加入一个节点,这个节点的所有边就解锁了
PriorityQueue<Edge> queue=new PriorityQueue<>(Comparator.comparingInt(o -> o.weight));
//用来存放生成树的边
Set<Edge> res=new HashSet<>();
for (Node node : graph.nodes.values()) {//随便选择一个节点
//一次遍历完一个连通图
if(!set.contains(node)){
set.add(node);
for (Edge edge : node.edges) {
queue.add(edge);
}
while (!queue.isEmpty()){
Edge edge = queue.poll();
if(!set.contains(edge.to)){
res.add(edge);
for (Node next : edge.to.nexts) {
if(!set.contains(next)){
set.add(next);
for (Edge toEdge : next.edges) {
//queue队列中可能会有重复的edge,但是没有关系,因为重复的edge不能通过set集合的判断,对结果造成不了影响
queue.add(toEdge);
}
}
}
}
}
}
//如果图是一整个连通图的话,这里就可以直接break了
// break;
}
return res;
}
Dijkstra算法
Dijkstra算法是用于解决单源最短路径问题的一种算法,可以用来计算图中从一个固定顶点到其他所有顶点的最短路径。该算法的基本思想是通过逐步扩展最短路径集合来逐步找到从源点到其他各顶点的最短路径。注意:需要这个图中没有权值整体累加和为负数的环。
Dijkstra算法的步骤如下:
-
初始化:将源点到自身的距离设为0,将源点到其他所有顶点的距离设为无穷大(或一个很大的数),将所有顶点标记为未访问。
-
重复以下步骤,直到所有顶点都被访问:
-
从未访问的顶点中选择一个距离最小的顶点(设为当前顶点)。
-
对当前顶点的所有邻接顶点,如果通过当前顶点到达该邻接顶点的距离小于该邻接顶点的当前最短距离,则更新该邻接顶点的最短距离为通过当前顶点到达该邻接顶点的距离,并将当前顶点设为该邻接顶点的前驱顶点。
-
将当前顶点标记为已访问。
-
-
完成后,每个顶点的最短路径和对应的前驱顶点就被确定了。
/**
* 获取head节点到它所处连通图中每一个节点的最短距离
* @param head
* @return
*/
public static HashMap<Node,Integer> Dijkstra(Node head){
/**
* key-Node:从head节点出发到key
* value-Integer:从head节点出发到key的最小距离
*/
HashMap<Node,Integer> distanceMap=new HashMap<>();
distanceMap.put(head,0);
//已经选取过的节点,不能再选取
HashSet<Node> selectedNodes=new HashSet<>();
Node minNode=getMinUnSelectedNode(selectedNodes,distanceMap);
while(minNode!=null){
for (Edge edge : minNode.edges) {
Node node = edge.to;
if(!distanceMap.containsKey(node)){
//如果distanceMap还不包含当前node,说明head节点距离当前节点的距离是无穷大,
// 就将head节点到node节点的距离设置为head节点到minNode节点的距离+minNode节点与node节点之间的距离
distanceMap.put(node,edge.weight+distanceMap.get(minNode));
}else{
//比较原来head节点距离node节点的权值与head节点通过当前minNode节点到达node节点的权值的大小,取小的一个
distanceMap.put(node,Math.min(distanceMap.get(node),edge.weight+distanceMap.get(minNode)));
}
}
//当前minNode节点已经选取过了,不能再选取
selectedNodes.add(minNode);
//重新寻找未被选取过的距离最小的节点
minNode=getMinUnSelectedNode(selectedNodes,distanceMap);
}
return distanceMap;
}
/**
* 获取还没有被选择过的节点中,head到这个节点的距离最小的一个
* @param selectedNodes
* @param res
* @return
*/
private static Node getMinUnSelectedNode(HashSet<Node> selectedNodes, HashMap<Node, Integer> res) {
int min=Integer.MAX_VALUE;
Node minNode=null;
for (Node node : res.keySet()) {
if(!selectedNodes.contains(node) && res.get(node)<min){
minNode=node;
min=res.get(node);
}
}
return minNode;
}
前缀树
前缀树(Trie树)是一种树形数据结构,用于高效地存储和检索字符串集合。它的特点是能够快速查找具有相同前缀的字符串,并且可以有效地支持诸如前缀匹配、自动完成等功能。
前缀树的基本性质如下:
-
每个节点包含若干个子节点,每个子节点代表一个字符。
-
从根节点到任意一个节点的路径表示一个字符串。
-
每个节点的所有子节点包含的字符互不相同。
前缀树的构建过程通常是从空根节点开始,逐渐插入字符串中的字符,直至所有字符串都被插入完成。在插入过程中,如果某个字符的子节点已存在,则直接移动到该子节点;如果不存在,则创建一个新的子节点。
前缀树的应用包括字符串搜索、自动完成、拼写检查等。由于其能够高效地支持按前缀搜索,因此在搜索引擎、字典、单词游戏等领域有着广泛的应用
public static class TrieNode{
//当前节点被经过了几次
public int pass;
//当前是否是一个字符串的结束
public int end;
public TrieNode[] nexts;
//如果字符特别多,那么就可以使用hashmap来表示,key表示路径,value表示通过这个路径到达的下一个位置
// public HashMap<Character,TrieNode> nexts;
public TrieNode(){
pass=0;
end=0;
//小写字母:a-z---> 0-25
nexts=new TrieNode[26];
}
}
public static class Trie{
public TrieNode root;
public Trie(){
root = new TrieNode();
}
/**
* 往前缀树中添加一个字符串
* @param str
*/
public void insert(String str){
char[] chs = str.toCharArray();
TrieNode cur=root;
//头结点每一次都要加1
cur.pass++;
//记录字符的位置
int index=0;
for (int i = 0; i < chs.length; i++) {
index=chs[i]-'a';
//如果前缀树中还没有包含当前字符的路径,就新建一个。
if(cur.nexts[index]==null){
cur.nexts[index]=new TrieNode();
}
cur=cur.nexts[index];
cur.pass++;
}
cur.end++;
}
/**
* 删除一个字符串
* @param word
*/
public void delete(String word){
char[] chs = word.toCharArray();
int index=0;
TrieNode node=root;
node.pass--;
for (char ch : chs) {
index=ch-'a';
if(--node.nexts[index].pass==0){
node.nexts[index]=null;
return;
}
node=node.nexts[index];
}
node.end--;
}
//查找一个字符串
public int search(String word){
char[] chs = word.toCharArray();
int index=0;
TrieNode node=root;
for (char ch : chs) {
index=ch-'a';
if(node.nexts[index]==null){
return 0;
}
node=node.nexts[index];
}
return node.end;
}
//查找以prefix为前缀的字符串数量
public int prefixStr(String prefix){
char[] chs = prefix.toCharArray();
int index=0;
TrieNode node=root;
for (char ch : chs) {
index=ch-'a';
if(node.nexts[ch]==null){
return 0;
}
node=node.nexts[index];
}
return node.pass;
}
}
贪心
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望最终能够达到全局最优解的算法设计方法。贪心算法通常适用于一些具有最优子结构性质的问题,即问题的最优解可以通过一系列局部最优解的组合得到。
贪心算法的基本思想是通过局部最优选择来构建全局最优解,每次选择都是当前情况下的最佳选择,而不考虑当前选择对未来的影响。由于贪心算法每一步都选择最优解,因此它具有简单、高效的特点,适用于一些求解最优化问题的场景。
然而,贪心算法并不保证能够得到全局最优解,因为它缺乏回溯的能力,可能会陷入局部最优而无法达到全局最优。因此,在应用贪心算法时,需要注意问题是否具有贪心选择性质,以及是否能够通过贪心选择逐步构建最优解。
贪心算法的解题套路:
1.实现一个不依靠贪心算法的解法X,可以使用最暴力的尝试。
2.脑补出贪心策略A、贪心策略B、贪心策略C......
3.用解法X和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
4.不要去纠结贪心策略的证明
题目1
public static int lessMoney(int[] arr){
PriorityQueue<Integer> queue=new PriorityQueue<>();
for (int i : arr) {
queue.add(i);
}
int res=0;
int cur=0;
while (queue.size()!=1){
cur=queue.poll()+queue.poll();
res+=cur;
queue.add(cur);
}
return res;
}
题目2
public static int findMaximizedCapital(int k,int m,int[] profits,int[] costs){
//根据花费排序-从小到大
PriorityQueue<Node> queue1=new PriorityQueue<>(Comparator.comparingInt(o -> o.c));
//根据收益排序-从大到小
PriorityQueue<Node> queue2=new PriorityQueue<>((o1, o2) -> o2.p-o1.p);
for(int i=0;i<profits.length;i++){
queue1.add(new Node(profits[i],costs[i]));
}
for (int i = 0; i < k; i++) {
//如果还存在可以做的项目,就解锁所有可以做的项目(所有花费小于剩余资金的项目都加入queue2)
while (!queue1.isEmpty() && queue1.peek().c<=m){
queue2.add(queue1.poll());
}
//如果queue2中还存在可以获得收益的项目,即queue2不为空
if(!queue2.isEmpty()){
return m;
}
//将当前项目的收益加入剩余资金中
m+=queue2.poll().p;
}
return m;
}
并查集
package Hash;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
public class UnionFind<V> {
public static class Element<V>{
public V value;
public Element(V v){
value=v;
}
}
//存放元素的集合
public HashMap<V,Element<V>> elementMap;
//每个元素的父亲结点
public HashMap<Element<V>,Element<V>> fatherMap;
//每一个头结点所在集合的大小
public HashMap<Element<V>,Integer> sizeMap;
public UnionFind(List<V>list){
elementMap=new HashMap<>();
fatherMap=new HashMap<>();
sizeMap=new HashMap<>();
for (V v : list) {
Element<V> element = new Element<>(v);
elementMap.put(v,element);
fatherMap.put(element,element);
sizeMap.put(element,1);
}
}
public boolean isSameSet(V a,V b){
if(elementMap.containsKey(a) && elementMap.containsKey(b)){
Element<V> aHead=findHead(elementMap.get(a));
Element<V> bHead=findHead(elementMap.get(b));
return aHead==bHead;
}
return false;
}
public void union(V a,V b){
if(elementMap.containsKey(a) && elementMap.containsKey(b)){
Element<V> aHead=findHead(elementMap.get(a));
Element<V> bHead=findHead(elementMap.get(b));
if(aHead!=bHead){
Element<V> big=sizeMap.get(aHead)>sizeMap.get(bHead)?aHead:bHead;
Element<V> small=big==aHead?bHead:aHead;
fatherMap.put(small,big);
sizeMap.put(big,sizeMap.get(big)+sizeMap.get(small));
sizeMap.remove(small);
}
}
}
private Element<V> findHead(Element<V> element) {
Stack<Element<V>> stack=new Stack<>();
while (element!=fatherMap.get(element)){
stack.push(element);
element=fatherMap.get(element);
}
//扁平化处理,让一个集合中的所有非父节点都指向父节点。
while (!stack.isEmpty()){
Element<V> pop = stack.pop();
fatherMap.put(pop,element);
}
return element;
}
}
KMP
KMP(Knuth-Morris-Pratt)算法是一种用于字符串匹配的高效算法,用于在一个文本串S内查找一个模式串P的出现位置。KMP算法的主要优势在于当出现不匹配时,它利用已经匹配过的信息,避免重新匹配文本串中已经比对过的部分,从而减少了比对的次数,提高了匹配的效率。
KMP算法的基本思想是在匹配过程中,当发现不匹配时,不是简单地回到文本串中的下一个位置重新开始匹配,而是利用已经比对过的信息,将模式串向右移动一定的距离,再继续匹配,以避免重复比对。这样可以有效地减少匹配时的回溯次数,提高匹配效率。
KMP算法的关键是计算模式串的"最长前缀后缀匹配表"(Partial Match Table),即一个部分匹配值数组,用于记录模式串中每个位置对应的最长公共前缀和后缀的长度。根据这个表,当发生不匹配时,可以根据已经匹配的部分快速移动模式串,而不需要重新比对已经匹配的部分。
总的来说,KMP算法通过预处理模式串,构建最长前缀后缀匹配表,利用已匹配的信息避免重复比对,从而提高了字符串匹配的效率。
/**
* 获取字符串str2在字符串str1中第一次出现的位置
* @param str1
* @param str2
* @return
*/
public static int getIndexOf(String str1,String str2){
if(str1==null || str2==null || str1.length()==1 || str1.length()<str2.length()){
return -1;
}
char[] ch1 = str1.toCharArray();
char[] ch2 = str2.toCharArray();
//x记录当前遍历的ch1的位置
int x=0;
//y记录当前遍历的ch2的位置
int y=0;
/*next[i]数组表示前i-1个字符串中,前缀和后缀相同的前缀的下一个位置
例如: abcabd next[0]=-1;next[1]=0;next[2]=0;next[3]=0;next[4]=1;next[5]=2;
012345
*/
int[] next=getNextArray(ch2);
while (x<ch1.length && y<ch2.length){
if(ch1[x]==ch2[y]){
x++;
y++;
}else if(y==0){
x++;
}else{
y=next[y];
}
}
//如果是y越界,表示在str1中找到了str2,返回第一次出现的位置,其他情况就返回-1
return y==ch2.length?x-y:-1;
}
private static int[] getNextArray(char[] ch) {
if(ch.length==1){
return new int[]{-1};
}
int[] res=new int[ch.length];
res[0]=-1;
res[1]=0;
int i=2;
int cn=0;
while (i<ch.length){
if(ch[i-1]==ch[cn]){
res[i++]=++cn;
}else if(cn>0){
cn=res[cn];
}else{
res[i++]=0;
//这里注意cn要重新赋值为0
cn=0;
}
}
return res;
}
Manacher算法
Manacher算法是用来查找字符串中最长回文子串的算法,其核心思想是利用回文串的对称性来快速计算回文半径。
算法步骤如下:
-
预处理字符串,在每个字符和字符串两端插入特殊字符(通常为'#'),使得字符串长度为奇数,这样可以统一处理回文串长度为奇数和偶数的情况。
-
维护一个辅助数组P,P[i]表示以字符s[i]为中心的最长回文子串的半径(即回文串长度为P[i]*2+1)。
-
初始化两个变量,mx表示当前已知的最长回文子串的右边界,id表示最长回文子串的中心位置。
-
遍历字符串,对于每个位置i:
-
如果i在mx的左边,那么P[i]至少等于P[2id-i],因为i关于id的对称点2id-i的回文半径是已知的,如果i+P[2id-i]<=mx,则P[i]=P[2id-i],否则P[i]>=mx-i。
-
如果i在mx的右边或者等于mx,则从当前位置向两边扩展计算P[i]的值。
-
更新mx和id。
-
-
遍历完成后,找到最大的P[i],该位置及其对应的回文子串即为最长回文子串。
Manacher算法的时间复杂度为O(n),空间复杂度为O(n)。
public static int maxLcpsLength(String s){
if(s==null || s.length()==0){
return 0;
}
//修改之后的字符串数组 121->#1#2#1#
char[] str=manacherString(s);
//回文半径数组,存放下标对应的字符的回文半径的大小
int[] pAddr=new int[str.length];
//回文右边界
int r=0;
//回文右边界对应的中心
int c=0;
//最长回文串的长度
int max=Integer.MIN_VALUE;
for(int i=0;i<str.length;i++){
int mirror=2*c-i;
if(i<r){
/*
此时i一定在c的右侧,mirror位置就是i关于c对称的一个对称点
这时根据mirror的回文区域可以分为以下几种情况:
(1)mirror的回文区域在l-r之间(l为最大回文区域的左边界,r为右边界),此时i和mirror的回文区域一样,不会继续扩
(2)mirror的回文区域在l-r之外,此时i的回文区域是r-i这一部分,不会继续扩
(3)mirror的回文区域的左边界与l相等,此时i至少有的回文区域也是r-i这一部分,可能还会继续扩
*/
pAddr[i]=Math.min(pAddr[mirror],r-i);
/*
* 情况(1)(r - i) > p[mirror] p[i]=p[mirror]
* 情况(2)(r - i) < p[mirror] p[i]=r-i;
* 情况(3)(r - i) = p[mirror] p[i]=r-i;
*/
}
int a=i+(pAddr[i]+1);//i的回文右边界之外的第一个元素
int b=i-(pAddr[i]+1);//i的回文左边界之外的第一个元素
while(a<str.length && b>=0 && str[a]==str[b]){
/*
到这里,情况就是
一、i<r的第(3)种情况还会继续往外扩,上面的if分支是做了优化
二、i>r,需要自己暴力扩
*/
pAddr[i]++;
a++;
b--;
}
//更新最大右边界和对应的中心点
if(i+pAddr[i]>r){
c=i;
r=i+pAddr[i];
}
//记录最长的回文串长度
max=Math.max(pAddr[i],max);
}
return max;
}
private static char[] manacherString(String str) {
char[] charArr=str.toCharArray();
char[] res=new char[2*str.length()+1];
int index=0;
for(int i=0;i<res.length;i++){
res[i]=(i & 1)==0?'#':charArr[index++];
}
return res;
}
中级提升班
滑动窗口
绳子最多覆盖的点
方法1:每一次固定绳子右端,使用二分,时间复杂度:O(N*log(N))
public static int MaxCover(int[] arr,int L){
//方法1:使用二分法,每一次固定绳子右端,找到能覆盖的点数,
int res=Integer.MIN_VALUE;
for(int i=0;i<arr.length;i++){
//寻找数组i位置左侧位置中长度大于L-arr[i]的最左边的位置
res=Math.max(res,i-BinarySearch(arr,arr[i]- L,0,i)+1);
}
return res;
}
public static int BinarySearch(int[] arr,int target,int l,int r){
if(l>r) return -1;
int t=0;
while (l<=r){
int mid=l+((r-l)>>>1);
if(arr[mid]>=target){
//寻找大于等于target最左边位置
t=mid;
r=mid-1;
}else{
l=mid+1;
}
}
return t;
}
方法2:每一次固定左端,使用滑动窗口,时间复杂度:O(N)
public static int MaxCover2(int[] arr,int L){
//方法2:使用滑动窗口
int l=0,r=0;
int res=Integer.MIN_VALUE;
while (l<arr.length){
//如果r+1位置的点与l位置的点距离小于等于5,则r右移
while (r<arr.length-1 && arr[r+1]-arr[l]<=5){
r++;
}
//到这里,r+1位置的点与l位置的点距离大于5,所以l~r的就是包含的点数,更新res
res=Math.max(res,r-l+1);
l++;
}
return res;
}
打表法
买苹果所用的最少袋子
先把这个题暴力跑出来
public static int BuyAppleMinBag(int n){
if(n<0){
return -1;
}
if((n & 1)==1){
return -1;
}
int bag6=-1;
int bag8=n/8;
int rest=n-8*bag8;
while (bag8>=0){
int resUse6=rest % 6==0?(rest/6):-1;
if(resUse6!=-1){
bag6=resUse6;
break;
}
bag8--;
rest=n-8*bag8;
}
return bag6==-1?-1:bag6+bag8;
}
然后枚举苹果数从0~100,需要最少袋子的数量
for(int i=0;i<=100;i++){
System.out.println(i+"=>"+BuyAppleMinBag(i));
}
自己去idea上跑一下这段代码,观察得到的结果,发现从18开始,凡是偶数都不为-1,并且每一个袋子数量发生变化的个数相差8个
根据这个规律,我们可以将代码优化为如下代码
public static int BuyAppleMinBag2(int n){
if((n&1)==1){//如果是奇数,就一定返回-1
return -1;
}
if(n<18){//如果小于18,就直接枚举就行
if(n==6 || n==8) return 1;
else if(n==12 || n==14 || n==16 ) return 2;
else return -1;
}
//这里加3是因为如果n=18,需要3个袋子,其余的就看看比n大的数有几个8,有几个8就在3的基础上加上几。
return (n-18)/8+3;
}
牛吃草
草一共有n的重量,两只牛轮流吃草,A牛先吃,B牛后吃 每只牛在自己的回合,吃草的重量必须是4的幂,1、4、16、64.... 谁在自己的回合正好把草吃完谁就赢,根据输入的n,返回谁赢
这道题跟以前的那道递归中的选择纸牌问题有点类似。
递归代码
/**
* A 先吃
* B 后吃
* @param n
* @return 返回谁吃到了最后一口
*/
public static String eatGrass(int n){
int i=0;
if(isPower4(n)){
return "A";
}
//想方设法的让当前先吃的赢
while (Math.pow(4,i)<n){
if("B".equals(eatGrass((int) (n-Math.pow(4,i))))){
return "A";
}
i++;
}
//实在没有办法了再让后吃的赢
return "B";
}
//判断一个数是否是4的幂
private static boolean isPower4(int n) {
return (n & (n-1))==0 && (n & 0x55555555)!=0;
}
得到的结果
从0开始递增,结果以BABAA的顺序循环
所以就可以得到如下代码
public static String eatGrass2(int n){
if(n % 5 ==0 || n % 5 == 2){
return "B";
}else {
return "A";
}
}
预处理数组
最少染色个数
public static int minPainting(String s){//O(n)
char[] chs = s.toCharArray();
//统计0~i范围上有几个G
int[] G=new int[chs.length];
G[0]=chs[0]=='G'?1:0;
//统计i~n-1范围上有几个R
int[] R=new int[chs.length];
R[0]=chs[chs.length-1]=='R'?1:0;
//遍历所有位置,得到G[i]和R[i]
for(int i=1;i<chs.length;i++){
G[i]=chs[i]=='G'?G[i-1]+1:G[i-1];
R[chs.length-i-1]=chs[chs.length-1-i]=='R'?R[chs.length-i]+1:R[chs.length-i];
}
//遍历所有位置,求出最小值
int min=Integer.MAX_VALUE;
for(int i=0;i<chs.length;i++){
min=Math.min(min,G[i]+R[i]);
}
return min;
}
最大正方行的边长
/**
* 计算二维数组中最大边长为正方形的完全由1组成的子矩阵的边长。
*
* @param arr 二维数组,仅包含0和1。
* @return 最大边长为正方形的子矩阵的边长。
*/
public static int maxSquareLength(int[][] arr){
int N=arr.length; // 数组的行数
int M=arr[0].length; // 数组的列数
int[][] left=new int[N][M]; // 存储每个位置左边连续1的数量
int[][] down=new int[N][M]; // 存储每个位置下面连续1的数量
// 计算每个位置左边和下面连续1的数量
for(int i=N-1;i>=0;i--){
for(int j=M-1;j>=0;j--){
left[i][j]=arr[i][j]==0?0:(j==M-1?1:left[i][j+1]+1);
down[i][j]=arr[i][j]==0?0:(i==N-1?1:down[i+1][j]+1);
}
}
int max=Integer.MIN_VALUE; // 最大正方形的边长,默认为最小整数
// 遍历每个位置,计算以该位置为右上角的最大正方形边长
for(int i=0;i<N;i++){
for (int j=0;j<M;j++){
for(int t=1;t<=Math.min(N-i,M-j);t++){
// 当前位置及扩展的区域满足要求时,更新最大边长
if(left[i][j]>=t && down[i][j]>=t && left[i+t-1][j]>=t && down[i][j+t-1]>=t){
max=Math.max(max,t);
}
}
}
}
return max;
}
已知一个概率函数,生成另外一个概率函数
(1)代码如下
/**
* 生成一个1到5之间的随机整数。
* @return 1到5之间的随机整数。
*/
public static int f(){
return (int) (Math.random()*5)+1;
}
/**
* 生成一个随机的0或1,但是当随机数为1或2时返回0,为3或4时返回1,直到不为5才返回结果。
* @return 随机的0或1。
*/
public static int rand0or1(){
int n=0;
do {
n=f();
if(n==1 || n==2){
return 0;
}
if(n==3 || n==4){
return 1;
}
}while (n==5);
return n;
}
/**
* 生成一个1到7之间的随机整数,
* @return 1到7之间的随机整数。
*/
public static int g(){
int res=0;
//这个do~while循环是为了生成0~6之间的一个随机整数
do {
res=0;
//生成000~111 的整数,每一次循环生成一位
for (int i = 0; i < 3; i++) {
res=res<<1 | rand0or1();
}
//如果生成的是111->7,就重新来一遍
}while (res==7);
//返回一个1~7之间的随机整数
return res+1;
}
(2)问与第(1)问方法相同,同样是先生成0~1生成器,然后借助0~1生成器随机生成0~(d-c)的数,然后加上c返回即可。
第(3)问
public static int g2(){
//f函数生成0的概率为p,生成1的概率为1-p
int r1=f();
int r2=f();
//如果r1==r2,表示两次的概率为p*p或(1-p)*(1-p)
while (r1==r2){
r1=f();
r2=f();
}
//到这里,r1一定不等于r2,所以概率都是p*(1-p)
return (r1==0 && r2==1)?1:0;
}
给定一个n,返回能形成多少种不同的二叉树结构
/**
* 通过递归计算二叉树的数量。
* @param n 树中节点的数量。
* @return 返回以给定节点数量构建的二叉树的数量。
*/
public static int TreeNumsByN(int n){
// 当节点数量小于0时,不可能构建出二叉树,返回0
if(n<0){
return 0;
}
// 节点数量为0或1时,只有一种构建方式,即空树或单节点树
if(n==0 || n==1){
return 1;
}
// 节点数量为2时,有两种构建方式,即左右子树都为空或各有一个节点
if(n==2){
return 2;
}
int res=0;
// 遍历所有可能的分割点,以i为分割点时,左子树有i个节点,右子树有n-i-1个节点
for(int i=0;i<=n-1;i++){
int leftNum=TreeNumsByN(i); // 计算左子树的构建方式数量
int rightNum=TreeNumsByN(n-i-1); // 计算右子树的构建方式数量
res+=leftNum*rightNum; // 累加左右子树构建方式数量的乘积
}
return res;
}
/**
* 通过动态规划计算二叉树的数量。
* @param n 树中节点的数量。
* @return 返回以给定节点数量构建的二叉树的数量。
*/
public static int TreeNumsByN2(int n){
int[] dp=new int[n+1]; // dp[i]表示构建i个节点的二叉树的数量
dp[0]=1; // 节点数量为0时,只有一种构建方式
// 遍历从1到n的所有节点数量,计算对应的二叉树数量
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){ // 遍历所有可能的左子树节点数量
dp[i]+=dp[j]*dp[i-j-1]; // 累加左子树数量和右子树数量的乘积
}
}
return dp[n]; // 返回构建n个节点的二叉树的数量
}
完整括号字符串
/**
* 将字符串s转换为一个完整的括号字符串,计算至少需要添加多少个括号
* @param s 待处理的字符串,包含括号字符
* @return 返回需要添加的最小括号数量,使得字符串成为有效的括号序列
*/
public static int CountBrackets(String s){
int count=0; // 记录当前未匹配的左括号数量
int res=0; // 记录需要添加的括号数量
for(int i=0;i<s.length();i++){
if(s.charAt(i)=='('){
count++; // 遇到左括号,未匹配的左括号数量增加
}else{
if(count==0){
res++; // 遇到无法匹配的右括号,需要添加一个左括号来匹配
}else{
count--; // 遇到右括号,且有匹配的左括号,未匹配的左括号数量减少
}
}
}
// 未匹配的左括号需要添加对应的右括号来使其有效,未匹配的右括号也需要添加左括号来匹配
return res+count;
}
数组中,求差值为K的去重数字对
/**
* 给定一个数组,寻找其中差值为k的去重数字对
* @param arr 输入的整数数组
* @param k 数字对的差值
* @return 返回一个包含所有符合条件的数字对的列表,每个数字对都是唯一的
*/
public static List<List<Integer>> diffValueByK(int[] arr, int k){
// 使用HashSet存储数组中的唯一元素
HashSet<Integer> set=new HashSet<>();
List<List<Integer>> res=new ArrayList<>(); // 结果列表,用于存储找到的差值为k的数字对
// 将数组中的元素添加到HashSet中
for (int i : arr) {
set.add(i);
}
// 遍历HashSet中的每个元素,查找差值为k的数字对
for (Integer i : set) {
// 如果HashSet中包含当前元素加上k的结果,则找到一个符合条件的数字对
if(set.contains(i+k)){
List<Integer> list = new ArrayList<Integer>();
list.add(i); // 添加较小的数字
list.add(i+k); // 添加较大的数字
res.add(list); // 将找到的数字对添加到结果列表中
}
}
return res;
}
最多可以进行多少次magic操作
/**
* 计算最多可以进行多少次magic操作。magic操作定义为将一个数组中的一个元素移动到另一个数组中,使得两个数组的平均值更加接近。
* @param a 第一个整数数组
* @param b 第二个整数数组
* @return 可以进行的magic操作的最大次数
*/
public static int maxCountMagic(int[] a,int[] b){
// 计算数组a和b的总和
double sumA=0;
for(int i=0;i<a.length;i++){
sumA+=(double) a[i];
}
double sumB=0;
for(int i=0;i<b.length;i++){
sumB+=(double) b[i];
}
// 判断哪个数组的平均值更大,将较大的数组标记为arrMore,较小的数组标记为arrLess
double sumMore=0;
double sumLess=0;
int[] arrMore=null;
int[] arrLess=null;
if(avg(sumA,a.length)==avg(sumB,b.length)){
return 0; // 如果两个数组的平均值已经相等,则不需要进行任何操作
}else if(avg(sumA,a.length)>avg(sumB,b.length)){
arrMore=a;
arrLess=b;
sumMore=sumA;
sumLess=sumB;
}else{
arrMore=b;
arrLess=a;
sumMore=sumB;
sumLess=sumA;
}
// 使用HashSet存储arrLess中的元素,以便快速查找
HashSet<Integer> set=new HashSet<>();
for (int less : arrLess) {
set.add(less);
}
// 对arrMore进行排序,以便按顺序检查每个元素
Arrays.sort(arrMore);
int moreSize=arrMore.length;
int lessSize=arrLess.length;
int ops=0; // 记录操作次数
// 遍历arrMore,对每个元素检查是否可以进行magic操作
for(int i=0;i<arrMore.length;i++){
double cur = (double) arrMore[i];
// 如果当前元素大于arrLess的平均值且小于arrMore的平均值,并且不在arrLess中,则进行magic操作
if(cur>avg(sumLess,lessSize) && cur<avg(sumMore,moreSize) && !set.contains(arrMore[i])){
ops++;
moreSize--;
lessSize++;
sumMore-=cur;
sumLess+=cur;
set.add(arrMore[i]); // 将元素添加到set,标记为已移动
}
}
return ops; // 返回magic操作的最大次数
}
// 计算数组的平均值
private static double avg(double sum, int count){
return sum / (double)count;
}
给定一个合法括号串,得到最大深度
/**
* 计算给定合法括号字符串中的最大深度
* @param s 一个由'('和')'组成的合法括号字符串
* @return 返回字符串中最大的深度,即最大嵌套层数
*/
public static int maxLength2(String s){
// 初始化计数器和结果变量
int count=0;
int res=Integer.MIN_VALUE; // 使用Integer.MIN_VALUE作为初始最大深度,以确保能正确更新最大值
// 遍历字符串中的每个字符
for(int i=0;i<s.length();i++){
// 遇到左括号,计数器增加
if(s.charAt(i)=='('){
count++;
}else{ // 遇到右括号,计数器减少
count--;
}
// 更新最大深度
res=Math.max(res,count);
}
return res;
}
给定一个括号串,得到其中最长合法括号子串的长度
/**
* 计算给定字符串中最长括号匹配子串的长度。
* @param s 给定的字符串,只包含'('和')'字符。
* @return 返回最长括号匹配子串的长度。
*/
public static int maxLength(String s){
// 初始化动态规划数组,dp[i]表示以第i个字符结尾的最长匹配子串长度
int[] dp=new int[s.length()];
// 初始化结果为最小整数,用于动态更新最长匹配子串长度
int res=Integer.MIN_VALUE;
// pre用于记录当前右括号之前最近的左括号的位置
int pre=0;
for(int i=1;i<s.length();i++){
// 遇到右括号时,计算当前左右括号匹配的长度
if(s.charAt(i)==')'){
pre=i-dp[i-1]-1;
// 如果pre位置为左括号,更新dp[i]值
if(pre>=0 && s.charAt(pre)=='('){
dp[i]=2+dp[i-1]+(pre>0?dp[pre-1]:0);
}
}
// 更新最长匹配子串长度
res=Math.max(res,dp[i]);
}
return res;
}
借助一个栈让另一个栈中的数据有序
/**
* 将给定的栈重新排序,使得栈中的元素按照非递减顺序排列。只能借助一个额外的栈。
* @param stack 需要重新排序的栈,类型为Stack<Integer>。
*/
public static void stackOrderly(Stack<Integer> stack){
// 创建一个临时栈来辅助排序
Stack<Integer> tmp=new Stack<>();
// 先将原栈顶元素出栈并放入临时栈中,此元素将成为新栈的底元素
tmp.push(stack.pop());
// 循环处理原栈中剩余的元素,直到原栈为空---将临时栈中的数据按降序排序
while (!stack.isEmpty()){
// 将原栈顶元素出栈
Integer cur = stack.pop();
// 如果当前元素不大于临时栈顶元素(若临时栈为空,则将Integer.MAX_VALUE视为临时栈的元素),则将当前元素入临时栈
if(cur<=(tmp.isEmpty()?Integer.MAX_VALUE:tmp.peek())){
tmp.push(cur);
}else{
// 如果当前元素大于临时栈顶元素,则将临时栈顶元素返回原栈,然后将当前元素入原栈,这样就实现了插入排序的效果
stack.push(tmp.pop());
stack.push(cur);
}
}
// 将临时栈中的元素依次返回原栈,此时临时栈的元素已按降序顺序排列
while (!tmp.isEmpty()){
stack.push(tmp.pop());
};
}
数字转换为不同字符串的个数
/**
* 给定一个数字,计算可以得到多少个不同的字符串
* 数字的每一位可以转换为字母(A-Z),其中1代表A,2代表B,以此类推,直到Z。
* 但是,如果数字的前一位是2,后一位的数值必须在0到6之间,以确保字母不会超出Z的范围。
* @param num 待计算的数字
* @return 可以生成的不同字符串的数量
*/
public static int strCombination(int num){
// return strWay(String.valueOf(num),0);
return strWay2(String.valueOf(num));
}
/**
* 递归方法,计算基于给定字符串s和当前索引i能够生成的不同字符串数量。
* @param s 转换为字符串的数字
* @param i 当前处理的字符索引
* @return 在当前位置i能够生成的不同字符串数量
*/
public static int strWay(String s,int i){
// 当处理到字符串末尾时,返回1,表示找到一个有效的字符串组合
if(i==s.length()){
return 1;
}
// 如果当前字符为0,无法继续生成有效字符串,返回0
if(s.charAt(i)=='0'){
return 0;
}
// 当前字符为1时,递归考虑下一个字符,以及跳过下一个字符的情况
if(s.charAt(i)=='1'){
int res=strWay(s,i+1);
if(i+1<s.length()){
res+=strWay(s,i+2);
}
return res;
}
// 当前字符为2时,递归考虑下一个字符,但如果下一个字符在0到6之间,则还可以考虑跳过下一个字符的情况
if(s.charAt(i)=='2'){
int res=strWay(s,i+1);
if((i+1)<s.length() && s.charAt(i+1)>='0' && s.charAt(i+1)<='6'){
res+=strWay(s,i+2);
}
return res;
}
// 对于大于2的数字,直接递归考虑下一个字符
return strWay(s,i+1);
}
/**
* 动态规划方法,计算基于给定字符串s能够生成的不同字符串数量。
* @param s 转换为字符串的数字
* @return 可以生成的不同字符串的数量
*/
public static int strWay2(String s){
int[] dp=new int[s.length()+1]; // 动态规划数组,dp[i]表示处理到第i个字符时的不同字符串数量
dp[s.length()]=1; // 初始化,处理到末尾字符时有一个有效字符串
dp[s.length()-1]=s.charAt(s.length()-1)=='0'?0:1; // 处理倒数第二个字符,如果为0则没有有效字符串,否则有1个
// 从倒数第三个字符开始向前处理
for(int i=s.length()-2;i>=0;i--){
if(s.charAt(i)=='0'){
dp[i]=0; // 如果当前字符为0,无法生成有效字符串
}else{
// 如果当前字符不为0,考虑两种情况:直接递归到下一个字符,或者跳过下一个字符(取决于下一个字符的值是否合适)
dp[i]=dp[i+1]+(((s.charAt(i)-'0')*10 + (s.charAt(i+1)-'0')<27)?dp[i+2]:0);
}
}
return dp[0]; // 返回处理到第一个字符时的不同字符串数量
}
二叉树的根节点到叶子节点中权值最大的值为多少
/**
* 计算以给定节点为根的二叉树中,从根到叶的最大路径和
* @param node 树的根节点
* @return 从根到叶的最大路径和
*/
public static int maxPath(TreeNode node){
// 当节点为叶子节点时,返回该节点的值
if(node.left==null && node.right==null){
return node.val;
}
int next=Integer.MIN_VALUE; // 初始化next为最小整数值,用于比较和存储左子树或右子树的最大路径和
// 如果存在左子树,则计算左子树的最大路径和
if(node.left!=null){
next=maxPath(node.left);
}
// 如果存在右子树,则计算右子树的最大路径和,并与左子树的最大路径和取较大值
if(node.right!=null){
next=Math.max(next,maxPath(node.right));
}
// 返回当前节点值加上左子树或右子树中较大的路径和
return node.val+next;
}
判断一个数是否在一个二维数组中
/**
* 判断目标值aim是否在二维数组arr中
* @param arr 二维数组,待搜索的目标数组
* @param aim 需要搜索的目标值
* @return boolean 返回true如果目标值在数组中,否则返回false
*/
public static boolean aimInArr(int[][] arr,int aim){
// 获取二维数组的行数和列数
int n=arr.length;
int m=arr[0].length;
int i=0;
int j=m-1;
// 从数组的右上角开始搜索
while (i>=0 && i<n && j<m && j>=0){
// 如果找到目标值,返回true
if(arr[i][j]==aim){
return true;
}
// 如果当前元素大于目标值,向左移动一列
if(arr[i][j]>aim){
j--;
}else{
// 如果当前元素小于目标值,向下移动一行
i++;
}
}
// 如果循环结束还未返回true,则目标值不在数组中,返回false
return false;
}
超级洗衣机
假设有 n
台超级洗衣机放在同一排上。开始的时候,每台洗衣机内可能有一定量的衣服,也可能是空的。
在每一步操作中,你可以选择任意 m
(1 <= m <= n
) 台洗衣机,与此同时将每台洗衣机的一件衣服送到相邻的一台洗衣机。
给定一个整数数组 machines
代表从左至右每台洗衣机中的衣物数量,请给出能让所有洗衣机中剩下的衣物的数量相等的 最少的操作步数 。如果不能使每台洗衣机中衣物的数量相等,则返回 -1
。
/**
* 查找使得所有机器能量平衡的最小移动次数。
* 给定一个表示每台机器当前能量的整数数组,每次可以将任意一台机器的能量加1或减1。
* 返回使所有机器能量相等所需的最小移动次数,若无法平衡则返回-1。
*
* @param machines 整数数组,表示每台机器的能量值。
* @return 最小移动次数,若无法平衡则返回-1。
*/
public static int findMinMoves(int[] machines) {
// 检查输入数组是否为空或机器数量小于2,若是则直接返回-1
if(machines==null || machines.length<2){
return -1;
}
int m=machines.length;
int sum=0;
// 计算所有机器能量的总和
for (int i = 0; i < machines.length; i++) {
sum+=machines[i];
}
// 如果总能量不能被机器数量整除,表示无法平均分配能量,返回-1
if(sum%m!=0){
return -1;
}
// 计算平均能量值
int avg=sum/m;
int leftSum=0;
int res=Integer.MIN_VALUE;
// 遍历每台机器,计算将机器分为左右两部分时,左右两部分需要移动的能量之和
for (int i = 0; i < m; i++) {
int leftRest=leftSum-i*avg; // 左侧部分剩余能量
int rightRest=(sum-leftSum-machines[i])-(m-i-1)*avg; // 右侧部分剩余能量
// 如果左右两侧剩余能量都为负数,表示该划分方式无法使得机器能量平衡
if(leftRest<0 && rightRest<0){
res=Math.max(res,Math.abs(leftRest)+Math.abs(rightRest));
}else{
// 取左右两侧剩余能量的较大绝对值作为移动次数,并更新最小移动次数
res=Math.max(res,Math.max(Math.abs(leftRest),Math.abs(rightRest)));
}
leftSum+=machines[i]; // 更新左侧部分的总能量
}
return res;
}
螺旋打印矩阵
/**
* 螺旋打印矩阵
* @param matrix 输入的二维矩阵
* 该方法采用螺旋遍历的方式,从矩阵的外层边界逐渐向内收缩,打印出矩阵的所有元素。
*/
public static void spiralOrderPrint(int[][] matrix)
{
// 检查矩阵是否为空
if(matrix==null){
return;
}
// 初始化边界
int a=0,b=0;
int c=matrix.length-1,d=matrix[0].length-1;
// 通过循环控制遍历过程,直到遍历完矩阵的所有元素
while(a<=c && b<=d){
// 打印当前边界上的元素
printEdge(matrix,a,b,c,d);
// 更新边界,逐渐向内收缩
a+=1;b+=1;
c-=1;d-=1;
}
}
/**
* 打印边界上的元素
* @param arr 二维数组
* @param a 左上角行坐标
* @param b 左上角列坐标
* @param c 右下角行坐标
* @param d 右下角列坐标
* 该方法根据边界的位置,分别按行、按列或按对角线打印数组元素,实现螺旋打印的效果。
*/
public static void printEdge(int[][] arr,int a,int b,int c,int d){
// 判断边界位置,分别处理
if(a==c){
// 处于同一列,按行打印
for(int i=b;i<=d;i++){
System.out.print(arr[a][i]+" ");
}
}else if(b==d){
// 处于同一行,按列打印
for(int i=a;i<=c;i++){
System.out.print(arr[i][b]+" ");
}
}else{
// 处于不同行不同列,按对角线打印
int curR=a;
int curC=b;
// 从左上到右下打印
while(curC!=d){
System.out.print(arr[curR][curC++]+" ");
}
// 从上到下打印
while (curR!=c){
System.out.print(arr[curR++][curC]+" ");
}
// 从右上到左下打印
while(curC!=a){
System.out.print(arr[curR][curC--]+" ");
}
// 从下到上打印
while (curR!=b){
System.out.print(arr[curR--][curC]+" ");
}
}
}
将矩阵顺时针旋转90度
/**
* 顺时针90度旋转矩阵
* @param arr 二维整数数组,代表要旋转的矩阵
*/
public static void rotate(int[][] arr){
// 如果数组为空或行数小于2,直接返回,无需旋转
if(arr==null || arr.length<2){
return;
}
// 初始化四个边界的索引
int a=0,b=0;
int c=arr[0].length-1;
int d=arr.length-1;
// 旋转矩阵的核心算法,遍历矩阵的四个边界,逐步缩小旋转范围
while (a<=c && b<=d){
rotateEdge(arr,a++,b++,c--,d--);
}
}
/**
* 顺时针90度旋转矩阵的边缘部分
* 对角线上的四个点分别记为(a,b), (a,c), (d,c), (d,b),每次旋转操作涉及这四个点的元素交换
* @param arr 二维整数数组,代表要旋转的矩阵
* @param a 矩阵的上边界索引
* @param b 矩阵的左边界索引
* @param c 矩阵的右边界索引
* @param d 矩阵的下边界索引
*/
private static void rotateEdge(int[][] arr, int a, int b, int c, int d) {
// 遍历边界上的元素,进行旋转操作
for(int i=0;i<c-b;i++){
// 依次存储要交换的四个元素
int tmp=arr[a][b+i];
arr[a][b+i]=arr[d-i][b];
arr[d-i][b]=arr[d][c-i];
arr[d][c-i]=arr[a+i][c];
arr[a+i][c]=tmp;
}
}
锯齿形打印二维数组
/**
* 打印二维矩阵的锯齿形序列。
* @param arr 表示二维矩阵的整型数组,如果为空或长度为0,则不进行打印。
*/
public static void printMatrixZigZag(int[][] arr){
// 检查输入矩阵是否为空或无元素
if(arr==null || arr.length<1){
return;
}
int ar=0,ac=0,br=0,bc=0;
int endR=arr.length-1; // 矩阵最后一行的索引
int endC=arr[0].length-1; // 矩阵最后一列的索引
boolean flag=false; // 标记当前行是否从左到右打印
while(ar<=endR){
// 打印每一层的元素
printLevel(arr,ar,ac,br,bc,flag);
// 根据当前行是否从左到右打印,更新下一层的起始位置
ar=ac==endC?ar+1:ar;
ac=ac==endC?ac:ac+1;
// 更新下一层的结束位置
bc=br==endR?bc+1:bc;
br=br==endR?br:br+1;
// 切换下一层的打印方向
flag=!flag;
}
}
/**
* 打印矩阵的一层元素。
* @param arr 表示二维矩阵的整型数组。
* @param ar 表示当前层的起始行索引。
* @param ac 表示当前层的起始列索引。
* @param br 表示当前层的结束行索引。
* @param bc 表示当前层的结束列索引。
* @param flag 标记当前层是否从左到右打印。
*/
private static void printLevel(int[][] arr, int ar, int ac, int br, int bc,boolean flag) {
if(flag){
// 从左到右打印当前层
while (ar<=br){
System.out.print(arr[ar++][ac--]+" ");
}
}else{
// 从右到左打印当前层
while (ar<=br){
System.out.print(arr[br--][bc++]+" ");
}
}
}
将s拼接到长度为n的最小操作步骤数
public static int minOps(int n){
if(n<2){
return 0;
}
if(isPrime(n)){
return n-1;
}
int[] divSumAndCount=divSumAndCount(n);
return divSumAndCount[0]-divSumAndCount[1];
}
/**
* @param n 不是质数
* @return (1)所有因子的和,但是不包括1 (2)所有因子的个数,但是因子不包括1
*/
private static int[] divSumAndCount(int n) {
int sum=0;
int count=0;
for(int i=2;i<=n;i++){
while (n%i==0){
sum+=i;
count++;
n/=i;
}
}
return new int[]{sum,count};
}
public static boolean isPrime(int n) {
if (n <= 1) {
return false; // 质数定义排除了 1 和负数
}
if (n == 2) {
return true; // 2 是唯一的偶数质数
}
if (n % 2 == 0) {
return false; // 奇数才可能为质数,排除偶数(除了 2)
}
// 只需检查从 3 到 sqrt(n) 之间的奇数是否能整除 n
for (int i = 3; i <= Math.sqrt(n); i += 2) {
if (n % i == 0) {
return false; // 若找到任意一个因子,说明 n 不是质数
}
}
return true; // 如果没有找到任何因子,n 是质数
}
求数组中出现次数最多的前k个
public static List<String> getKStr(String[] strs,int k){
if(k>strs.length){
return null;
}
HashMap<String,Integer> map=new HashMap<>();
for (String str : strs) {
if(map.containsKey(str)){
map.put(str,map.get(str)+1);
}else{
map.put(str,1);
}
}
//利用大根堆,将不重复的字符串放入大根堆,然后弹出k个就行
PriorityQueue<String> queue = new PriorityQueue<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return map.get(o2) - map.get(o1);
}
});
queue.addAll(map.keySet());
List<String> res=new ArrayList<>();
while (k-->0){
res.add(queue.poll());
}
return res;
}
public static List<String> getKStr2(String[] strs,int k){
if(k>strs.length){
return null;
}
HashMap<String,Integer> map=new HashMap<>();
for (String str : strs) {
if(map.containsKey(str)){
map.put(str,map.get(str)+1);
}else{
map.put(str,1);
}
}
//利用小根堆,让小根堆中的元素只有k个
PriorityQueue<String> queue = new PriorityQueue<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return map.get(o1) - map.get(o2);
}
});
for (String s : map.keySet()) {
if(queue.size()<k){
queue.add(s);
}else{
//如果当前s的词频比堆中最小的那个要大,那么s就代替堆中最小的那个
if(map.get(s)>map.get(queue.peek())){
queue.poll();
queue.add(s);
}
}
}
List<String> res=new ArrayList<>();
while (k-->0){
res.add(queue.poll());
}
return res;
}
改进:我要一边输入,一遍统计出现次数最多的前k的字符
方法:使用自己修改的最小堆
public static class Node{
String str;//当前节点表示的字符串值
int times;//表示当前字符串出现的次数
public Node(String str,int times){
this.str=str;
this.times=times;
}
}
public static class TopHeap{
int heapSize;//堆的大小
HashMap<String,Node> strAndNodeMap;//字符串和节点的映射
HashMap<Node,Integer> indexMap;//节点和索引的映射
Node[] heap;
public TopHeap(int k){
heapSize=0;
heap=new Node[k];
strAndNodeMap=new HashMap<>();
indexMap=new HashMap<>();
}
public void add(String s){
int preIndex=-1;
Node node;
if(!strAndNodeMap.containsKey(s)){
node=new Node(s,1);
strAndNodeMap.put(s,node);
indexMap.put(node,-1);
}else{
node = strAndNodeMap.get(s);
node.times++;
preIndex=indexMap.get(node);
}
if(preIndex==-1){
if(heapSize==heap.length){
if(node.times>heap[0].times){
indexMap.put(heap[0],-1);
indexMap.put(node,0);
heap[0]=node;
Heapify(0);
}
}else{
heap[heapSize]=node;
indexMap.put(node,heapSize);
heapInsert(heapSize++);
}
}else{
Heapify(preIndex);
}
}
public List<String> getTopK(){
List<String> res=new ArrayList<>();
for (int i = 0; i < heapSize; i++) {
res.add(heap[i].str);
}
return res;
}
public void heapInsert(int index){
int parent=(index-1)/2;
while (index!=parent){
if(heap[index].times<heap[parent].times){
swap(index,parent);
index=parent;
parent=(index-1)/2;
}else{
break;
}
}
}
public void Heapify(int index){
int left=2*index+1;
int right=left+1;
while (left<heapSize){
int smallest=right<heapSize && heap[left].times<heap[right].times?left:right;
if(heap[smallest]==null){
break;
}
smallest=heap[smallest].times<heap[index].times?smallest:index;
if(smallest==index){
break;
}
swap(index,smallest);
index=smallest;
left=2*index+1;
right=left+1;
}
}
private void swap(int index, int parent) {
indexMap.put(heap[index],parent);
indexMap.put(heap[parent],index);
Node tmp=heap[index];
heap[index]=heap[parent];
heap[parent]= tmp;
}
}
public static List<String> getKStr3(String[] strs,int k){
if(k>strs.length){
return null;
}
TopHeap heap=new TopHeap(k);
for (String str : strs) {
heap.add(str);
}
return heap.getTopK();
}
自定义一个可以随时返回最小值的栈
使用两个栈,一个普通栈,一个维护最小值的栈,每一次普通栈入栈,将这个值和最小栈中的栈顶值进行比较,如果大于栈顶值,那么还是将栈顶值压入最小栈,否则将新值加入最小栈,在弹出的时候普通栈弹出一个,最小栈也要跟着弹出。
/**
* 自定义栈类,支持普通栈操作以及获取栈中最小元素的操作。
*/
public static class MyStack {
// 用于存储添加进来的元素
public Stack<Integer> pushStack;
// 用于存储最小元素
public Stack<Integer> minStack;
/**
* 构造函数,初始化两个栈。
*/
public MyStack() {
pushStack = new Stack<>();
minStack = new Stack<>();
}
/**
* 弹出栈顶元素,并保持最小元素栈的正确性。
*
* @return 栈顶元素
*/
public Integer pop(){
minStack.pop(); // 移除最小元素栈的栈顶元素,以保持最小元素栈的同步
return pushStack.pop(); // 返回普通栈的栈顶元素
}
/**
* 将元素压入栈中,并更新最小元素栈。
*
* @param data 压入栈的元素
*/
public void push(Integer data){
pushStack.push(data); // 先将元素压入普通栈
if(minStack.isEmpty()){ // 如果最小元素栈为空,则直接将当前元素压入最小元素栈
minStack.push(data);
}else{
if(data<=minStack.peek()){ // 如果当前元素小于等于最小元素栈的栈顶元素,则将其压入最小元素栈
minStack.push(data);
}else{
// 如果当前元素大于最小元素栈的栈顶元素,则只将最小元素栈的栈顶元素复制一份压入最小元素栈,保持最小元素栈递增有序
minStack.push(minStack.peek());
}
}
}
/**
* 获取栈中的最小元素。
*
* @return 最小元素
*/
public Integer getMin(){
return minStack.peek(); // 返回最小元素栈的栈顶元素,即最小元素
}
}
使用队列实现栈的功能
队列实现栈:使用两个队列,q1、q2,第一次按给q1中加入数据,然后再将数据出队列的时候先将q1中的前n-1个数加入q2队列中,将q1队列中剩下的数弹出,然后将q2中的数据放回q1。
/**
* 使用两个队列实现一个栈的功能。
*/
public static class QueueToStack{
public Queue<Integer> queue1; // 主队列,用于存储栈元素
public Queue<Integer> queue2; // 辅助队列,用于实现栈的出栈操作
/**
* 构造函数,初始化两个队列。
*/
public QueueToStack(){
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
/**
* 将元素压入栈中(即入队列)。
* @param data 需要压入栈的元素
*/
public void push(Integer data){
queue1.add(data);
}
/**
* 从栈中弹出元素(即出队列)。
* @return 弹出的元素值
*/
public Integer pop(){
// 将queue1中除最后一个元素外的所有元素转移到queue2中
while (queue1.size()>1){
queue2.add(queue1.poll());
}
// 获取并移除queue1中的最后一个元素
Integer res = queue1.poll();
// 将queue2中的元素重新转移到queue1中,恢复队列顺序
while (!queue2.isEmpty()){
queue1.add(queue2.poll());
}
return res;
}
}
使用栈实现队列的功能
栈实现队列:使用两个栈,一个push栈、一个pop栈,每日一次push,先判断如果pop栈为空,就把push栈中的数据倒入pop栈,每一次pop,一样执行前面的判断逻辑,再pop
/**
* 使用两个栈实现队列的基本功能。
* 这个类提供了add和poll方法,分别模拟队列的入队和出队操作。
*/
public static class StackToQueue{
// 用于入队操作的栈
public Stack<Integer> pushStack;
// 用于出队操作的栈
public Stack<Integer> popStack;
/**
* 类的构造函数,初始化两个栈。
*/
public StackToQueue(){
pushStack = new Stack<>();
popStack = new Stack<>();
}
/**
* 将元素入队。
* @param data 要入队的元素
*/
public void add(Integer data){
pushStack.push(data); // 将元素放入入队栈
dao(); // 调整栈,确保popStack非空
}
/**
* 出队一个元素。
* @return 出队的元素
*/
public Integer poll(){
dao(); // 调整栈,确保popStack顶部是下一个要出队的元素
return popStack.pop(); // 从popStack中取出并返回顶部元素
}
/**
* 调整栈的结构,将pushStack中的元素全部移到popStack中,以便于poll操作。
*/
private void dao() {
if(popStack.isEmpty()){ // 当popStack为空时,执行调整
while (!pushStack.isEmpty()){ // 将pushStack中的元素依次移到popStack中
popStack.push(pushStack.pop());
}
}
}
}
动态规划的空间压缩技巧
/**
* 求一个数组中从左上角到右下角的最小路径和
* @param matrix 二维数组,表示需要遍历的矩阵
* @return 返回从左上角到右下角的最小路径和
*/
public static int minPath(int[][] matrix){
// 检查输入是否合法
if(matrix==null || matrix.length<1){
return -1;
}
// 初始化dp数组,用于记录到达每个位置的最小路径和
int[] dp=new int[matrix[0].length];
dp[0]=matrix[0][0]; // 初始值,即左上角的值
// 计算第一行到每个位置的最小路径和
for(int i=1;i<matrix[0].length;i++){
dp[i]=dp[i-1]+matrix[0][i];
}
// 动态规划计算从第二行开始的每个位置的最小路径和
for(int i=1;i<matrix.length;i++){
for(int j=0;j<matrix[0].length;j++){
// 选择从左边或上边到达当前位置的最小路径和,并加上当前位置的值
dp[j]=Math.min((j-1)>=0?dp[j-1]:Integer.MAX_VALUE,dp[j])+matrix[i][j];
}
}
// 返回右下角位置的最小路径和
return dp[dp.length-1];
}
最多能接多少水
类似于洗衣机问题,每到数组中的一个位置,比较它左边的最大长度和右边的最大长度的最小值,然后减去当前的长度,如果大于0,就是这个位置可以接多少雨水
/**
* 计算容器中可以接最多的水的面积
* @param height 容器边缘的高度数组
* @return 最大接水面积
*/
public static int maxWater(int[] height){
// 当高度数组为空或长度小于2时,无法接水,直接返回0
if(height==null || height.length<2){
return 0;
}
// 初始化左侧最大高度数组和右侧最大高度数组
int[] maxLeft=new int[height.length];
int[] maxRight=new int[height.length];
// 计算每个位置左侧的最大高度
for (int i = 1; i < height.length; i++) {
maxLeft[i]=Math.max(maxLeft[i-1],height[i-1]);
}
// 计算每个位置右侧的最大高度
for(int i=height.length-2;i>=0;i--){
maxRight[i]=Math.max(maxRight[i+1],height[i+1]);
}
// 遍历每个位置,计算可以接的水量,并累加
int res=0;
for (int i = 0; i < height.length; i++) {
// 计算当前位置可以接水的最大高度
int min=Math.min(maxLeft[i],maxRight[i]);
// 如果当前位置的高度小于可接水的最大高度,则计算接水量并累加到结果中
res+=min>height[i]?min-height[i]:0;
}
return res;
}
判断两个字符串是否互为旋转词
KMP算法解决
/**
* 判断两个字符串是否是旋转
* @param str1 第一个字符串
* @param str2 第二个字符串
* @return 如果两个字符串是旋转关系,则返回true;否则返回false。
*/
public static boolean revolvingWord(String str1,String str2){
// 首先检查两个字符串长度是否相等,不相等则不可能是旋转关系
if(str1.length()!=str2.length()){
return false;
}
// 将两个字符串各自拼接一次,以处理旋转字符串的情况
String s1=str1+str1;
String s2=str2+str2;
// 使用KMP算法分别检查两个字符串是否包含对方
return KMP(s1,str2) && KMP(s2,str1);
}
/**
* 使用KMP算法匹配一个字符串是否包含另一个字符串
* @param str1 基础字符串
* @param str2 目标字符串
* @return 如果目标字符串str2包含在基础字符串str1中,则返回true;否则返回false。
*/
public static boolean KMP(String str1,String str2){
// 计算目标字符串str2的next数组
int[] next=getNextArray(str2);
int l1=0,l2=0;
// 遍历两个字符串,进行匹配
while (l1<str1.length() && l2<str2.length()){
if(str1.charAt(l1)==str2.charAt(l2)){
l1++;
l2++;
}else if (l2==0){
l1++;
}else{
// 当前字符不匹配时,根据next数组回溯
l2=next[l2];
}
}
// 如果l2等于str2的长度,则表示匹配成功
return l2==str2.length();
}
/**
* 计算给定字符串的next数组
* @param str 目标字符串
* @return 返回目标字符串的next数组
*/
private static int[] getNextArray(String str2) {
int[] chs=new int[str2.length()];
// 初始化next数组
chs[0]=-1;
chs[1]=0;
int cn=0;
// 计算next数组
for(int i=2;i<str2.length();i++){
if(str2.charAt(i)==str2.charAt(cn)){
// 当前字符与模式串的字符匹配,next值加一
chs[i] = ++cn;
}else if(cn>0){
// 当前字符与模式串的字符不匹配,根据next数组回溯
cn=chs[cn];
}else{
// 当前字符与模式串的字符不匹配,且模式串中没有字符时,next值为0
chs[i]=0;
cn=0;
}
}
return chs;
}
零食放法
/**
* 计算可以放入背包的零食组合数量。
* @param n 零食的袋数
* @param w 背包的容量
* @param v 每袋零食的价值数组
* @return 可以放入背包的零食组合的总数
*/
public static int foodNumber(int n,int w,int[] v){
int res=0; // 初始化结果为0
int[][] dp=new int[n][w+1]; // 创建动态规划数组
// 初始化第一袋零食的情况
if(v[0]<=w){
dp[0][v[0]]=1;
}
dp[0][0]=1; // 背包容量为0时,有一种放法(不放)
// 动态规划遍历
for(int i=1;i<n;i++){
for(int j=0;j<=w;j++){
// 当前零食不放入背包
if(j==0){
dp[i][j]=1;
}else{
// 当前零食放入背包或不放入背包两种情况
dp[i][j]=dp[i-1][j]+((j-v[i])>=0?dp[i-1][j-v[i]]:0);
}
}
}
// 统计所有可能的零食组合数量
for(int i=0;i<=w;i++){
res+=dp[n-1][i];
}
return res; // 返回结果
}
找满意的工作
/**
*
* @param jobs 工作集合
* @param ability 能力
* @return
*/
public static int[] getMoneys(Job[] jobs,int[] ability){
//将工作按照难度升序排序,难度相同的按照报酬降序排序
Arrays.sort(jobs, (o1, o2) -> o1.hard - o2.hard != 0 ?
o1.hard - o2.hard :
o2.money - o1.money);
int[] res=new int[ability.length];
//有序表
TreeMap<Integer,Integer> map=new TreeMap<>();
map.put(jobs[0].hard,jobs[0].money);
Job pre=jobs[0];
for(int i=1;i<jobs.length;i++){
//将难度比前一个大,报酬也比前一个大的工作加入有序表中
//如果难度比前一个大,但是报酬却比前一个少,则无论如何都选不到这个工作。
if(jobs[i].hard != pre.hard && jobs[i].money>pre.money){
pre=jobs[i];
map.put(pre.hard,pre.money);
}
}
for(int i=0;i<ability.length;i++){
Integer key = map.floorKey(ability[i]);//返回小于等于给定键的最大键,如果不存在这样的键,则返回 null。
res[i]=key!=null?map.get(key):0;
}
return res;
}
将给定的字符串转化为目录
使用前缀树的思想实现
public static class Node{
// 节点名称
public String name;
// 存储指向下一个节点的映射关系
public TreeMap<String,Node> nextMap;
/**
* 构造函数:初始化一个节点
* @param name 节点名称
*/
public Node(String name){
this.name=name;
nextMap=new TreeMap<>();
}
}
/**
* 根据给定的字符串数组打印对应的目录结构
* @param folderPaths 目录路径数组,每个元素为一个目录路径字符串
*/
public static void print(String[] folderPaths){
// 如果输入为空,则直接返回
if(folderPaths==null || folderPaths.length==0){
return;
}
// 生成目录树
Node head=generateFolderTree(folderPaths);
// 打印目录树
printProcess(head,0);
}
/**
* 根据目录路径数组生成目录树
* @param folderPaths 目录路径数组
* @return 根节点
*/
private static Node generateFolderTree(String[] folderPaths) {
// 创建根节点
Node head=new Node("");
// 遍历每个目录路径
for (String folderPath : folderPaths) {
// 按照路径分隔符分割目录路径
String[] paths = folderPath.split("\\\\");//因为split里面既有正则也有转义,所以要以'\\'切割,就需要4个斜杠
Node cur=head; // 当前节点指针
// 遍历每个路径元素,生成目录树
for (int i = 0; i < paths.length; i++) {
// 如果当前节点不存在该路径元素,则添加新节点
if(!cur.nextMap.containsKey(paths[i])){
cur.nextMap.put(paths[i],new Node(paths[i]));
}
// 指向下一个节点
cur=cur.nextMap.get(paths[i]);
}
}
return head;
}
/**
* 递归打印目录树
* @param head 当前节点
* @param level 当前层级
*/
private static void printProcess(Node head, int level) {
// 非根节点则打印节点名称
if(level!=0){
System.out.println(get2nSpace(level)+head.name);
}
// 递归打印所有子节点
for (Node node : head.nextMap.values()) {
printProcess(node,level+1);
}
}
/**
* 生成指定数量的空格字符串
* @param n 空格数量
* @return 空格字符串
*/
private static String get2nSpace(int n) {
StringBuilder res= new StringBuilder();
// 循环生成空格
for (int i = 0; i < n; i++) {
res.append(" ");
}
return res.toString();
}
将二叉搜索树转化为双向链表
使用二叉树的递归套路,对于一个根节点,得到它左右节点的start和end
public static class Info{
// 起始节点和结束节点,用于保存中序遍历二叉搜索树后的链表头尾节点
public TreeNode start;
public TreeNode end;
// 构造函数,初始化起始和结束节点
public Info(TreeNode start, TreeNode end){
this.start = start;
this.end = end;
}
}
/**
* 将给定的二叉搜索树转换为双向链表
* @param head 二叉搜索树的头结点
* @return 双向链表的头结点
*/
public static TreeNode getDoubleLinkedList(TreeNode head){
// 调用process方法处理二叉树,并返回链表的头结点
return process(head).start;
}
/**
* 中序遍历二叉搜索树,将树转换为双向链表
* @param head 当前节点
* @return 包含链表起始和结束节点的Info对象
*/
public static Info process(TreeNode head){
// 递归终止条件,当节点为空时,返回空的Info对象
if(head==null){
return new Info(null,null);
}
// 递归处理左子树
Info leftInfo=process(head.left);
// 递归处理右子树
Info rightInfo=process(head.right);
// 将左子树链表的尾节点连接到当前节点
if(leftInfo.end!=null){
leftInfo.end.right=head;
}
// 将当前节点连接到左子树链表的尾节点
head.left=leftInfo.end;
// 将右子树链表的头节点连接到当前节点
head.right=rightInfo.start;
// 将当前节点连接到右子树链表的头节点
if(rightInfo.start!=null){
rightInfo.start.left=head;
}
// 返回包含新链表起始和结束节点的Info对象
return new Info(leftInfo.start==null?head:leftInfo.start,rightInfo.end==null?head:rightInfo.end);
}
给定一个二叉树的头结点,返回其中包含的最大二叉搜索树的子树的节点个数
/**
* Info2类用于存储关于最大二叉搜索树的信息
*/
public static class Info2{
public int maxBSTSize;// 当前包含的最大二叉搜索树的节点个数
public boolean isBST;// 包含当前节点的是否是二叉搜索树
public int max;// 最大值
public int min;// 最小值
public TreeNode BSTHead;// 包含当前节点的最大二叉搜索树的头结点
/**
* 构造函数,初始化Info2对象
* @param maxBSTSize 最大二叉搜索树的节点个数
* @param isBST 是否是二叉搜索树
* @param max 最大值
* @param min 最小值
* @param BSTHead 二叉搜索树的头结点
*/
public Info2(int maxBSTSize, boolean isBST, int max, int min, TreeNode BSTHead){
this.maxBSTSize = maxBSTSize;
this.isBST = isBST;
this.max = max;
this.min = min;
this.BSTHead = BSTHead;
}
}
/**
* 获取二叉树中最大二叉搜索树的节点个数
* @param head 二叉树的头结点
* @return 最大二叉搜索树的节点个数
*/
public static int getMaxBSTSize(TreeNode head){
if(head==null){
return 0;
}
return f1(head).maxBSTSize;
}
/**
* 递归函数,计算二叉树中包含的最大二叉搜索树的信息
* @param head 当前节点
* @return 包含最大二叉搜索树信息的Info2对象
*/
public static Info2 f1(TreeNode head){
if (head==null){
return null;
}
// 递归计算左子树和右子树的最大二叉搜索树信息
Info2 leftInfo=f1(head.left);
Info2 rightInfo=f1(head.right);
int maxBSTSize=0;
boolean isBST=false;
TreeNode BSTHead=head;
int max=head.val;
int min=head.val;
// 处理左子树存在的情况,更新最大二叉搜索树信息
if(leftInfo!=null){
max=Math.max(max,leftInfo.max);
min=Math.min(min,leftInfo.min);
BSTHead=leftInfo.BSTHead;
maxBSTSize=leftInfo.maxBSTSize;
}
// 处理右子树存在的情况,更新最大二叉搜索树信息
if(rightInfo!=null && rightInfo.maxBSTSize>maxBSTSize){
max=Math.max(max,rightInfo.max);
min=Math.min(min,rightInfo.min);
maxBSTSize=rightInfo.maxBSTSize;
BSTHead=rightInfo.BSTHead;
}
// 检查以当前节点为根的子树是否构成二叉搜索树,是则更新最大二叉搜索树信息
if(
(leftInfo==null || leftInfo.isBST)
&&
(rightInfo==null || rightInfo.isBST)
){
if(
(leftInfo==null || head.val>leftInfo.max)
&&
(rightInfo==null || head.val<rightInfo.min)
){
maxBSTSize= (leftInfo==null?0:leftInfo.maxBSTSize)
+(rightInfo==null?0:rightInfo.maxBSTSize)
+1;
BSTHead=head;
isBST=true;
}
}
// 返回当前节点及其子树的最大二叉搜索树信息
return new Info2(maxBSTSize,isBST,max,min,BSTHead);
}
帖子的最高分数
/**
* 计算数组中连续子数组的最大和
* @param arr 输入的整数数组
* @return 返回数组中连续子数组的最大和。如果输入为null或空数组,则返回0。
*/
public static int getMaxSum(int[] arr){
// 检查数组是否为空,如果为空则直接返回0
if(arr==null || arr.length<1){
return 0;
}
int max=Integer.MIN_VALUE; // 初始化最大和为最小整数值
int cur=0; // 当前子数组的和
for (int j : arr) {
cur += j; // 累加当前元素到当前子数组的和
max = Math.max(max, cur); // 更新最大和
cur = Math.max(cur, 0); // 如果当前和为负数,则重置为0,因为不可能有负数对当前和产生正贡献
}
return max;
}
矩阵中最大子矩阵的累加和
/**
* 返回矩阵中最大子矩阵和
* @param arr 二维整数数组,代表待计算的矩阵
* @return 矩阵中最大子矩阵和
*/
public static int getMaxSum2(int[][] arr){
// 检查输入矩阵是否为空或无元素,若是则直接返回0
if(arr==null || arr.length<1){
return 0;
}
int max=Integer.MIN_VALUE; // 初始化最大和为最小整数值
// 遍历所有可能的子矩阵起始行
for(int i=0;i<arr.length;i++){
int[] t=new int[arr[0].length]; // 用于存储当前列的累加和
// 遍历从当前行开始的所有行
for(int j=i;j<arr.length;j++){
int cur=0; // 当前子矩阵的和
// 遍历当前子矩阵的所有列
for(int k=0;k<arr[0].length;k++){
t[k]+=arr[j][k]; // 更新当前列的累加和---压缩数组的方式
cur+=t[k]; // 计算当前子矩阵的和
max=Math.max(max,cur); // 更新最大和
cur=Math.max(cur,0); // 保证cur始终为正或0,应用动态规划思想
}
}
}
return max; // 返回最大和
}
至少需要多少路灯
/**
* 计算至少需要多少路灯才能照亮字符串s中的所有点
* 字符串s中只有'.'和'X'两种字符,其中'X'代表需要照亮的位置
* 路灯可以影响其左侧、中间和右侧三个位置
* @param s 需要照亮的字符串
* @return 至少需要的路灯数量
*/
public static int minLight(String s){
// 如果字符串为空或长度为0,则不需要路灯
if(s==null || s.length()<1){
return 0;
}
int res=0; // 需要的路灯数量
int i=0; // 字符串遍历索引
// 遍历字符串s
while (i<s.length()){
// 如果当前字符为'.',需要照亮
if(s.charAt(i)=='.'){
// 检查当前点右侧是否为'X',如果是则只需要一个路灯即可照亮当前点和右侧的'X'
if(i+1<s.length() && s.charAt(i+1)=='X'){
res++;
i+=2;
// 检查当前点右侧是否为'.',如果是则需要一个路灯照亮当前点和右侧的点,同时移动索引到下一个非'.'位置
}else if(i+1<s.length() && s.charAt(i+1)=='.'){
res++;
i+=2;
// 如果连续三个点都需要照亮,只需要一个路灯即可,因此继续移动索引到下一个非'.'位置
if(i<s.length() && s.charAt(i)=='.'){
i++;
}
// 如果当前点是字符串末尾的点,只需要一个路灯即可照亮
}else if(i+1>=s.length()){
res++;
i++;
}
}else{
// 如果当前字符已经是'X',不需要照亮,直接移动索引到下一个位置
i++;
}
}
return res; // 返回至少需要的路灯数量
}
给定二叉树的先序遍历和后序遍历,返回后序遍历
public static int[] getPostArray(int[] pre,int[] in){
if(pre==null || in==null || pre.length!=in.length){
return null;
}
int[] post=new int[pre.length];
int n=pre.length;
process(pre,in,post,0,n-1,0,n-1,0,n-1);
return post;
}
private static void process(int[] pre, int[] in, int[] post, int prei, int prej, int ini, int inj, int posti, int postj) {
if(prei>prej){
return;
}
if(prei==prej){
post[postj]=pre[prei];
return;
}
int a=pre[prei];
post[postj]=a;
int find=0;
//这里可以优化,加一个辅助数组,提前记录in数组中每个元素的位置
for(int i=ini;i<=inj;i++){
if(in[i]==a){
find=i;
break;
}
}
process(pre,in,post,prei+1,prei+find-ini,ini,find-1,posti,posti+find-ini-1);
process(pre,in,post,prei+find-ini+1,prej,find+1,inj,posti+find-ini,postj-1);
}
最长递增子序列
普通方法:使用递归,时间复杂度:O(N^2)
public static int increasingSubsequence(int[] arr){
if(arr==null || arr.length<1){
return 0;
}
//从0~i-1的子序列的最大长度
int[] dp=new int[arr.length];
dp[0]=1;
for(int i=1;i<arr.length;i++){
for(int j=0;j<i;j++){
if(arr[i]>arr[j]){
dp[i]=Math.max(dp[i],dp[j]);
}
}
dp[i]=dp[i]+1;
}
return dp[arr.length-1];
}
优化方法:O(nlog(n))
/**
* 计算给定数组中的最长递增子序列的长度。
*
* @param arr 给定的整数数组
* @return 最长递增子序列的长度
*/
public static int increasingSubsequence2(int[] arr){
// 处理空数组或长度为0的情况
if(arr==null || arr.length<1){
return 0;
}
int[] dp=new int[arr.length]; // dp数组用于存储以当前位置结尾的最长递增子序列长度
dp[0]=1; // 初始化第一个元素的最长递增子序列长度为1
int[] ends=new int[arr.length]; // ends数组用于存储当前最长递增子序列的末尾元素
ends[0]=arr[0]; // 将数组的第一个元素作为初始的最长递增子序列的末尾元素
int index=1; // index用于记录ends数组中实际存储的元素个数
// 遍历数组,计算每个位置上的最长递增子序列长度
for(int i=1;i<arr.length;i++){
// 二分搜索在ends数组中比arr[i]大的数最左边的位置
int t=binarySearch(ends,arr[i],0,index-1);
if(t<index && t>=0){
// 如果找到,则替换该位置的元素,并更新dp[i]
ends[t]=arr[i];
dp[i]=t+1;
}else{
// 如果未找到,将arr[i]添加到ends数组中,并更新dp[i]和index
ends[index++]=arr[i];
dp[i]=index;
}
}
// 返回最后一个位置上的最长递增子序列长度
return dp[arr.length-1];
}
/**
* 二分搜索在ends数组中第一个大于等于target的元素的位置。
*
* @param ends 给定的有序整数数组
* @param i 要搜索的目标值
* @param l 搜索范围的左边界
* @param r 搜索范围的右边界
* @return 第一个大于i的元素的位置,如果不存在则返回-1
*/
private static int binarySearch(int[] ends, int i,int l,int r) {
if(l>r){
return -1;
}
int t=-1;
// 二分搜索
while (l<=r){
int mid=l+((r-l)>>1);
if(ends[mid]>i){
// 如果找到大于target的元素,记录位置并继续向左搜索
t=mid;
r=mid-1;
}else{
// 否则,向右搜索
l=mid+1;
}
}
return t;
}
找到[1,n]中所有未出现的整数
/**
* 查找在数组中没有出现的数字
*
* @param arr 给定的整数数组
* @param n 数组的长度
* @return 返回一个列表,包含在给定数组中没有出现的数字
*/
public static List<Integer> findNotAppear(int[] arr, int n){
// 遍历数组,通过修改数组的方式标记出现的数字
for (int num : arr) {
modify(arr,num);
}
List<Integer> list = new ArrayList<>();
// 检查数组中未被标记的数字,将其添加到结果列表中
for (int i = 0; i < n; i++) {
if (arr[i]!=i+1){
list.add(i+1);
}
}
return list;
}
/**
* 将数组中指定的数字移动到其值所指示的位置
*
* @param arr 给定的整数数组
* @param num 需要进行移动操作的数字
*/
private static void modify(int[] arr, int num) {
// 循环直到数字被放置到其正确的位置上
while (num!=arr[num-1]){
int tmp=arr[num-1];
arr[num-1]=num;
num=tmp;
}
}
让人气刚好达到目标值所花费的C币数
/**
* 计算达到指定人气值所需的最小硬币数。
*
* @param add 每个增加人气值所需的硬币数。
* @param times 每个乘以人气值所需的硬币数。
* @param del 每个减少人气值所需的硬币数。
* @param start 起始人气值。
* @param end 目标人气值。
* @return 达到目标人气值所需的最小硬币数,如果无法达到则返回-1。
*/
public static int minCoins(int add,int times,int del,int start,int end){
// 如果起始人气值大于目标人气值,直接返回-1
if(start>end){
return -1;
}
// 调用process函数计算最小硬币数
return process(0,end,add,times,del,start,end*2,((end-start)/2)*add);
}
/**
* 递归计算达到指定人气值所需的最小硬币数。
*
* @param preMoney 当前已投入的硬币数。
* @param aim 目标人气值。
* @param add 每个增加人气值所需的硬币数。
* @param times 每个乘以人气值所需的硬币数。
* @param del 每个减少人气值所需的硬币数。
* @param cur 当前人气值。
* @param limitAim 允许的最大目标人气值。
* @param limitCoin 允许投入的最大硬币数。
* @return 达到目标人气值所需的最小硬币数,如果无法达到则返回Integer.MAX_VALUE。
*/
private static int process(int preMoney, int aim, int add, int times, int del, int cur, int limitAim, int limitCoin) {
//注意添加basecase
// 如果当前已投入的硬币数超过允许的最大目标人气值,返回Integer.MAX_VALUE表示无法达到
if(preMoney>limitAim){
return Integer.MAX_VALUE;
}
// 如果当前人气值小于0,返回Integer.MAX_VALUE表示无法达到
if(cur<0){
return Integer.MAX_VALUE;
}
// 如果当前人气值超过允许的最大目标人气值,返回Integer.MAX_VALUE表示无法达到
if(cur>limitAim){
return Integer.MAX_VALUE;
}
// 如果当前人气值已经等于目标人气值,返回当前已投入的硬币数
if(cur==aim){
return preMoney;
}
int min=Integer.MIN_VALUE;
// 尝试通过增加人气值的方式计算最小硬币数
int p1=process(preMoney+add,aim,add,times,del,cur+2,limitAim,limitCoin);
if(p1!=Integer.MAX_VALUE){
min=Math.min(min,p1);
}
// 尝试通过减少人气值的方式计算最小硬币数
int p2=process(preMoney+del,aim,add,times,del,cur-2,limitAim,limitCoin);
if(p2!=Integer.MAX_VALUE){
min=Math.min(min,p2);
}
// 尝试通过乘以人气值的方式计算最小硬币数
int p3=process(preMoney+times,aim,add,times,del,cur*2,limitAim,limitCoin);
if(p3!=Integer.MAX_VALUE){
min=Math.min(min,p3);
}
// 返回三种方式中的最小硬币数
return min;
}
可以达到desired结果的组合方式
/**
* 计算给定表达式中,满足期望结果desired的组合数。
* @param express 仅包含0, 1, ^, |, &的表达式字符串
* @param desired 期望的计算结果,true或false
* @return 满足期望结果的表达式组合数
*/
public static int num(String express,boolean desired){
// 空表达式或无效表达式返回0
if(express==null || express.equals("")){
return 0;
}
char[] chars = express.toCharArray();
// 验证表达式有效性
if(!isValid(chars)){
return 0;
}
// 递归处理表达式求解
return process(chars,0,chars.length-1,desired);
}
/**
* 验证表达式字符数组的有效性。
* @param exp 表达式字符数组
* @return 如果表达式有效返回true,否则返回false
*/
public static boolean isValid(char[] exp){
// 表达式长度为偶数,表明缺少操作数或操作符,无效
if((exp.length & 1)==0) return false;
// 遍历操作数,确保只有0和1
for (int i = 0; i < exp.length; i+=2) {
if(!(exp[i]=='0' || exp[i]=='1')){
return false;
}
}
// 遍历操作符,确保只有^, |, &
for (int i = 1; i < exp.length; i+=2) {
if(!(exp[i]=='^' || exp[i]=='&' || exp[i]=='|')){
return false;
}
}
return true;
}
/**
* 递归处理表达式,计算满足desired结果的组合数。
* @param chs 表达式的字符数组
* @param L 当前处理的左边界
* @param R 当前处理的右边界
* @param desired 期望计算结果
* @return 满足期望结果的组合数
*/
private static int process(char[] chs, int L, int R, boolean desired) {
// 基本情况,单个操作数与期望结果比较
if(L==R){
if(chs[L]=='1'){
return desired?1:0;
}else{
return desired?0:1;
}
}
int res=0;
// 遍历所有操作符,计算所有可能情况
for(int i=L+1;i<=R;i+=2){
// 根据当前操作符,计算对应组合数
if(desired){
switch (chs[i]){
case '&':
res+=process(chs,L,i-1,true) * process(chs,i+1,R,true);
break;
case '|':
res+=process(chs,L,i-1,true) * process(chs,i+1,R,true);
res+=process(chs,L,i-1,true) * process(chs,i+1,R,false);
res+=process(chs,L,i-1,false) * process(chs,i+1,R,true);
break;
case '^':
res+=process(chs,L,i-1,true) * process(chs,i+1,R,false);
res+=process(chs,L,i-1,false) * process(chs,i+1,R,true);
break;
}
}else{
switch (chs[i]){
case '&':
res+=process(chs,L,i-1,false) * process(chs,i+1,R,false);
res+=process(chs,L,i-1,true) * process(chs,i+1,R,false);
res+=process(chs,L,i-1,false) * process(chs,i+1,R,true);
break;
case '|':
res+=process(chs,L,i-1,false) * process(chs,i+1,R,false);
break;
case '^':
res+=process(chs,L,i-1,true) * process(chs,i+1,R,true);
res+=process(chs,L,i-1,false) * process(chs,i+1,R,false);
break;
}
}
}
return res;
}
一个字符串中找到没有重复子串的最长长度
/**
* 计算字符串中最大的不重复子串的长度。
*
* @param str 输入的字符串
* @return 最大的不重复子串的长度
*/
public static int maxUnique(String str){
// 如果字符串为空或长度为0,直接返回0
if(str==null || str.length()<1){
return 0;
}
// 字符串长度为1时,返回1
if(str.length()==1) return 1;
// 使用HashMap来记录每个字符最后出现的位置
HashMap<Character,Integer> map=new HashMap<>();
char[] chars = str.toCharArray();
for (char c : chars) {
map.put(c,-1); // 初始化每个字符的位置为-1
}
int pre=0; // 记录前面出现的最长不重复子串的末尾位置
int len=0; // 记录当前最长不重复子串的长度
for (int i = 0; i < chars.length; i++) {
pre=Math.max(pre,map.get(chars[i])); // 更新pre为当前字符之前出现的最长不重复子串的末尾位置
len=Math.max(len,i-pre); // 更新len为当前最长不重复子串的长度
map.put(chars[i],i); // 更新字符的位置为当前位置
}
return len;
}
将str1编辑成str2的最小代价
public static int func(String str1,String str2,int ic,int dc,int rc){
int[][] dp=new int[str1.length()+1][str2.length()+1];
for(int i=0;i<dp.length;i++){
for (int j=0;j<dp[0].length;j++){
dp[i][j]=Integer.MAX_VALUE;
}
}
dp[0][0]=0;
for(int i=1;i<=str2.length();i++){
dp[0][i]=i*ic;
}
for(int i=1;i<=str1.length();i++){
dp[i][0]=i*dc;
}
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(str1.charAt(i-1)==str2.charAt(j-1)){
//情况4:str1[i-1]==str2[j-1],考虑将str1的前i-1个字符串变成str2的前j-1个字符串的代价
dp[i][j]=Math.min(dp[i][j],dp[i-1][j-1]);
}else{
//情况3:str1[i-1]!=str2[j-1],先将str1的前i-1个字符串变成str2的前j-1个字符串,然后将str1的第i个字符串替换为str2的第j个字符串,计算总代价
dp[i][j]=Math.min(dp[i][j],dp[i-1][j-1]+rc);
}
//情况2:将str1的前i-1个字符串变成str2的前j个字符串,然后删除str1的第i个字符,计算总代价
dp[i][j]=Math.min(dp[i][j],dp[i-1][j]+dc);
//情况1:将str1的前i个字符串变成str2的前j-1个字符串,然后插入str2的第j个字符,计算总代价
dp[i][j]=Math.min(dp[i][j],dp[i][j-1]+ic);
}
}
return dp[str1.length()][str2.length()];
}
删除多余字符串,使最终字符串的字典序最小
/**
* 在str中,每种字符都要保留一个,让最后的结果字典序最小,并返回
* @param str
* @return
*/
public static String remove(String str){
if(str==null || str.equals("")) return "";
int[] map=new int[256];
for (int i = 0; i < str.length(); i++) {
map[str.charAt(i)]++;
}
int minCharIndex=0;//记录str中ASCII码最小的字符的索引
for(int i=0;i<str.length();i++){
//记录在下面一个if语句break前,最小的字符的索引位置
minCharIndex=str.charAt(i)<str.charAt(minCharIndex)?i:minCharIndex;
if(--map[str.charAt(i)]==0){//出现了字符个数为0的情况,就说明i位置右边以及没有这个字符了,就需要在i的左边以ASCII码的大小进行排查。
break;
}
}
//前i个中最小的ASCII码的字符+去掉这个字符后的结果
return String.valueOf(str.charAt(minCharIndex))
+
remove(
str
.replaceAll(String.valueOf(str.charAt(minCharIndex)),"")
);
}
高级进阶班
在高级班里面很多题目都没太搞懂,所以只记录了相对简单的一些题
查找数组中相邻最大差值
/**
* 查找数组中相邻最大差值
*
* @param nums 整型数组,包含可能为负数或零的整数
* @return 返回数组中最大非空间隔,如果数组长度小于2或为空,则返回0
*/
public static int maxGap(int[] nums){
// 检查数组是否为空或长度小于2
if(nums == null || nums.length < 2){
return 0;
}
int len=nums.length;
boolean[] hasGap=new boolean[len+1]; // 标记每个桶是否包含数字
int[] mins=new int[len+1]; // 存储每个桶的最小值
int[] maxs=new int[len+1]; // 存储每个桶的最大值
int min=Integer.MAX_VALUE; // 找到数组中的最小值
int max=Integer.MIN_VALUE; // 找到数组中的最大值
// 遍历数组,找到最小值和最大值
for (int i = 0; i < len; i++) {
min=Math.min(min,nums[i]);
max=Math.max(max,nums[i]);
}
// 分配每个数字到对应的桶中,并记录每个桶的最小值和最大值
for (int i = 0; i < len; i++) {
int j=bukcet(nums[i],len,min,max);
mins[j]=hasGap[j]?Math.min(mins[j],nums[i]):nums[i];
maxs[j]=hasGap[j]?Math.max(maxs[j],nums[i]):nums[i];
hasGap[j]=true;
}
int preMax=maxs[0]; // 记录当前遍历过的桶的最大值
int res=0; // 记录最大间隔
// 遍历每个桶,计算最大间隔
for(int i=1;i<=len;i++){
if(hasGap[i]){
res=Math.max(res,mins[i]-preMax);
preMax=maxs[i];
}
}
return res;
}
/**
* 根据数字大小分配到对应的桶中
*
* @param num 需要分配的数字
* @param len 桶的数量
* @param min 数组中的最小值
* @param max 数组中的最大值
* @return 返回数字分配的桶的索引
*/
private static int bukcet(int num, int len, int min, int max) {
return (int)(((num-min)*len)/(max-min));
}
最多有多少不重叠的非空区间
首先定义变量xor,用于记录当前子数组的异或值。 创建一个长度与输入数组相同的数组dp,用于记录以当前元素结尾的子数组中,满足异或和为0的子数组的最大数量。 使用HashMap记录每个异或值第一次出现的位置,便于后续计算。 遍历输入数组,计算当前位置的异或值,并判断该异或值是否在HashMap中已经存在。 如果存在,说明存在一段子数组的异或和为0,更新dp数组中当前位置的值为之前该异或值出现位置的dp值加1,如果是第一次出现0值,就手动设置为1。 如果不存在,说明当前位置是该异或值第一次出现,将该异或值添加到HashMap中,并将dp数组中当前位置的前一个位置上的值。 在遍历过程中,使用Math.max函数保证dp数组中每个位置的值都是以该位置结尾的子数组中满足异或和为0的最大数量。 返回dp数组最后一个元素的值,即整个数组中满足异或和为0的最大子数组数量。
/**
* 计算最多有多少个子数组的异或和为0
* @param arr
* @return
*/
public static int mostEOR(int[] arr){
int xor=0;//记录当前异或值
int[] dp=new int[arr.length];
/*
key->遍历过程中出现的异或值
value-> key所对应的索引位置
*/
HashMap<Integer,Integer> map=new HashMap<>();
map.put(0,-1);
for (int i = 0; i < arr.length; i++) {
xor ^= arr[i];
if(map.containsKey(xor)){
//进入了这里就表示出现了异或和为0的情况
//得到前面出现了异或和等于当前异或和的位置,然后在这个位置的dp值加1
int pre=map.get(xor);
/*
* pre=-1表示当前异或值为0,并且是第一次出现异或值为0的子数组,map中最开始设置了异或和为0的地址为-1,所以要手动将dp值设置为1,
* 并且将map中出现异或和为0的索引位置更新为当前位置,所以之后出现异或值为0的子数组,dp值直接在前一个出现0的位置加1
* 对于其他重复出现的值也是一样的
*/
dp[i]=pre==-1?1:(dp[pre]+1);
}
if(i>0){
//比较当前dp值和前一个dp值,取较大值
dp[i]=Math.max(dp[i-1],dp[i]);
}
//有则更新,无者添加
map.put(xor,i);
}
return dp[dp.length-1];
}
用多少种方法拼出m的面值
/**
* 使用arr1和arr2中的值,得到target值的方法数量,其中arr1中的值可以使用多次,arr2中的值只能使用一次
* @param arr1 arr1中的数可以使用多次
* @param arr2 arr2中的数只能使用1次
* @param target 目标值
* @return 用多少种方法可以得到target值
*/
public static int maxCount(int[] arr1,int[] arr2,int target){
//记录可以使用多次的数组的辅助数组
int[][] dp1=new int[arr1.length+1][target+1];
//记录可以使用1次的数组的辅助数组
int[][] dp2=new int[arr2.length+1][target+1];
//设置初始值,target==0,有一种方法:就是一个都不选
for(int i=0;i<=target;i++){
if(i % arr1[0] == 0 || i==0){
dp1[0][i]=1;
}
if(i== arr2[0] || i==0){
dp2[0][i]=1;
}
}
for(int i=0;i<arr1.length;i++){
dp1[i][0]=1;
}
for(int i=0;i<arr2.length;i++){
dp2[i][0]=1;
}
for(int i=1;i<arr1.length;i++){
for(int j=1;j<=target;j++){
dp1[i][j]=dp1[i-1][j]+((j-arr1[i]>=0)?dp1[i][j-arr1[i]]:0);
}
}
for(int i=1;i<arr2.length;i++){
for(int j=1;j<=target;j++){
dp2[i][j]=dp2[i-1][j]+((j-arr2[i]>=0)?dp2[i-1][j-arr2[i]]:0);
}
}
int res=0;
for(int i=0;i<=target;i++){
res+=dp1[arr1.length-1][i]*dp2[arr2.length-1][target-i];
}
return res;
}
求子数组中累加和小于等于K的最长子数组长度
//求子数组中累加和小于等于K的最长子数组长度
public static int maxLengthAwesome(int[] arr,int k){
if(arr==null || arr.length==0){
return 0;
}
//记录从i开始,最小累加和
int[] minSums=new int[arr.length];
//记录从i开始,最小累加和的右边界
int[] minSumEnds=new int[arr.length];
minSums[arr.length-1]=arr[arr.length-1];
minSumEnds[arr.length-1]=arr.length-1;
//从右往左遍历
for(int i=arr.length-2;i>=0;i--){
if(minSums[i+1]<0){
minSums[i]=arr[i]+minSums[i+1];
minSumEnds[i]=minSumEnds[i+1];
}else{
minSums[i]=arr[i];
minSumEnds[i]=i;
}
}
int end=0;
int sum=0;
int res=0;
//i是窗口最左的位置,end是窗口最右位置的下一个位置(终止位置)
for(int i=0;i<arr.length;i++){
//while循环结束之后:
//1)如果以i开头的情况下,累加和<=k的最长子数组是arr[i...end-1],看看这个数组能不能更新res;
//2)如果以i开头的情况下,累加和<=k的最长子数组比arr[i...end-1]短,更新还是不更新res都不会影响最终结果
while (end < arr.length && sum+minSums[end]<=k){
sum+=minSums[end];
end=minSumEnds[end]+1;
}
res=Math.max(res,end-i);
if(end>i){//窗口内还有数
sum-=arr[i];
}else{//窗口内已经没有数了,说明从i开头的所有子数组累加和都不可能<=k
end=i+1;
}
}
return res;
}
字符串与整数根据不同规则相互转换,k伪进制
K伪进制值的是:对于一个10进制的数以K进制表示,这个进制的每一位上至少为1,这样得到一共有多少位,然后再从高位到地位计算每一位上的数。
对于第一种字符串与整数的对应规则,我们应该使用伪26进制完成
对于第二种规则,我们使用伪3进制完成
public static String intToStr(int num,int digit){//数字转字符串,digit表示是多少进制的
int len=0;
int tmp=num;
while (true){//计算一共需要多少伪digit进制的位数,每一次要‘-’
if(tmp>=Math.pow(digit,len)){
tmp-=Math.pow(digit,len);
len++;
}else{
break;
}
}
tmp=num;
//arr[0..len-1],len-1是高位,0是低位
int[] arr=new int[len];
for(int i=0;i<len;i++){
arr[i]=1;//伪k进制的每一位至少都要是1
tmp-=Math.pow(digit,i);
}
while (tmp>0){
int l=tmp/(int) Math.pow(digit,len-1);
arr[len-1]+=l;
tmp%=(int) Math.pow(digit,len-1);
len--;
}
StringBuffer sb = new StringBuffer();
for(int i=arr.length-1;i>=0;i--){
sb.append((char)(arr[i]+'A'-1));
}
return sb.toString();
}
public static int strToInt(String str,int digit){//字符串转数组
int len=str.length();
int[] arr=new int[len];
for (int i = 0; i < len; i++) {
arr[len-i-1]=str.charAt(i)-'A'+1;
}
int res=0;
for(int i=0;i<len;i++){
res+=Math.pow(digit,i)*arr[i];
}
return res;
}
蛇的最长长度
public static int snake(int[][] matrix){
int res=Integer.MIN_VALUE;
Info[][] dp=new Info[matrix.length][matrix[0].length];
for(int i=0;i<matrix.length;i++){
for(int j=0;j<matrix[0].length;j++){
Info info = process(matrix, i, j,dp);
res=Math.max(res,Math.max(info.yes,info.no));
}
}
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
System.out.print(dp[i][j].toString()+" ");
}
System.out.println();
}
return res;
}
/**
* 返回从最左侧到(row,col)位置的不用能力和用能力最大值,负数表示无效
* @param matrix
* @param row
* @param col
* @return
*/
private static Info process(int[][] matrix, int row, int col) {
if(col==0){//如果在第一列,就直接返回当前的值
return new Info(-matrix[row][col],matrix[row][col]);
}
int preYes=-1;
int preNo=-1;
//情况1:当前位置的左侧
Info left=process(matrix,row,col-1);
preNo=left.no>=0?left.no:preNo;
preYes=left.yes>=0?left.yes:preYes;
//情况2:当前位置的左上角
if(row>0){
Info leftUp=process(matrix,row-1,col-1);
if(leftUp.no>=0){
preNo=Math.max(preNo,leftUp.no);
}
if(leftUp.yes>=0){
preYes=Math.max(preYes,leftUp.yes);
}
}
//情况3:当前位置的左下角
if(row<matrix.length-1){
Info leftDown=process(matrix,row+1,col-1);
if(leftDown.no>=0){
preNo=Math.max(preNo,leftDown.no);
}
if(leftDown.yes>=0){
preYes=Math.max(preYes,leftDown.yes);
}
}
int yes=-1;
int no=-1;
if(preNo>=0){//只有前面的结果大于0,才能到达当前位置
//没有使用过能力的只有这一种情况:前面没有使用过,当前位置也不适用
no = preNo+matrix[row][col];
//使用能力的情况1:前面没有使用过,当前使用
yes=preNo+(-matrix[row][col]);
}
if(preYes>=0){//只有前面的结果大于0,才能到达当前位置
//使用能力的情况2:前面使用过,当前不使用
yes=Math.max(yes,preYes+matrix[row][col]);
}
return new Info(yes,no);
}
改为记忆化DP
private static Info process(int[][] matrix, int row, int col,Info[][] dp) {
if(dp[row][col]!=null){
return dp[row][col];
}
if(col==0){//如果在第一列,就直接返回当前的值
dp[row][col]=new Info(-matrix[row][col],matrix[row][col]);
return dp[row][col];
}
int preYes=-1;
int preNo=-1;
//情况1:当前位置的左侧
Info left=process(matrix,row,col-1,dp);
preNo=left.no>=0?left.no:preNo;
preYes=left.yes>=0?left.yes:preYes;
//情况2:当前位置的左上角
if(row>0){
Info leftUp=process(matrix,row-1,col-1,dp);
if(leftUp.no>=0){
preNo=Math.max(preNo,leftUp.no);
}
if(leftUp.yes>=0){
preYes=Math.max(preYes,leftUp.yes);
}
}
//情况3:当前位置的左下角
if(row<matrix.length-1){
Info leftDown=process(matrix,row+1,col-1,dp);
if(leftDown.no>=0){
preNo=Math.max(preNo,leftDown.no);
}
if(leftDown.yes>=0){
preYes=Math.max(preYes,leftDown.yes);
}
}
int yes=-1;
int no=-1;
if(preNo>=0){//只有前面的结果大于0,才能到达当前位置
//没有使用过能力的只有这一种情况:前面没有使用过,当前位置也不适用
no = preNo+matrix[row][col];
//使用能力的情况1:前面没有使用过,当前使用
yes=preNo+(-matrix[row][col]);
}
if(preYes>=0){//只有前面的结果大于0,才能到达当前位置
//使用能力的情况2:前面使用过,当前不使用
yes=Math.max(yes,preYes+matrix[row][col]);
}
dp[row][col]=new Info(yes,no);
return dp[row][col];
}
计算字符串公式的结果(套路:所有括号、优先级的题都可以使用)
public static int getValue(String str){
return value(str.toCharArray(),0)[0];
}
//int[0]->当前括号中计算得到的值,int[1]->当前括号结束的位置(即')'的位置)
public static int[] value(char[] str,int i){
Stack<String> stack=new Stack<>();
int num=0;
int[] res=null;//res[0]:当前括号中计算得到的值 res[1]:当前括号结束的位置(即')'的位置)
while (i<str.length && str[i]!=')'){
if(str[i]>='0' && str[i]<='9'){
num=num*10+str[i++]-'0';
}else if(str[i]=='('){
res=value(str,i+1);
num=res[0];
i=res[1]+1;
}else{//遇到运算符号
addNum(stack,num);
stack.push(String.valueOf(str[i++]));
num=0;
}
}
addNum(stack,num);
return new int[]{getNum(stack),i};
}
//计算栈中的值,这个时候,保证栈中的运算符号只有+和-
private static int getNum(Stack<String> stack) {
int res=Integer.parseInt(stack.pop());
String fuhao=null;
int cur=0;
while (!stack.isEmpty()){
fuhao=stack.pop();
cur=Integer.parseInt(stack.pop());
if(fuhao.equals("+")){
res+=cur;
}else{
res=cur-res;
}
}
return res;
}
//将当前数字加入stack中,需要判断栈顶位置的运算符号
private static void addNum(Stack<String> stack, int num) {
if (!stack.isEmpty()){
int cur=0;
String pop = stack.pop();
if(pop.equals("+") || pop.equals("-")){
stack.push(pop);
}else{
cur=Integer.parseInt(stack.pop());
num=pop.equals("*")?num*cur:num/cur;
}
}
stack.push(String.valueOf(num));
}
两个字符串的最长公共子串(DP)
public static int maxSubstring(String str1,String str2){
//dp[i][j]:字符串以str1中i结尾,以str2中j结尾的最大公共子串长度
int[][] dp=new int[str1.length()][str2.length()];
for(int i=0;i<str2.length();i++){
if(str1.charAt(0)==str2.charAt(i)){
dp[0][i]=1;
}
}
for(int i=0;i<str1.length();i++){
if(str1.charAt(i)==str2.charAt(0)){
dp[i][0]=1;
}
}
for(int i=1;i<str1.length();i++){
for(int j=1;j<str2.length();j++){
dp[i][j]=str1.charAt(i)==str2.charAt(j)?dp[i-1][j-1]+1:0;
}
}
int max=Integer.MIN_VALUE;
for(int i=0;i<str1.length();i++){
for(int j=0;j<str2.length();j++){
max=Math.max(max,dp[i][j]);
}
}
return max;
}
空间优化
public static int maxSubstring2(String str1,String str2){
int[] dp=new int[str2.length()];
int max=0;
for(int i=0;i<str2.length();i++){
if(str1.charAt(0)==str2.charAt(i)){
dp[i]=1;
}
max=Math.max(max,dp[i]);
}
for(int i=1;i<str1.length();i++){
for(int j=str2.length()-1;j>=0;j--){
if(j==0){
dp[j]=str1.charAt(i)==str2.charAt(j)?1:0;
}else{
dp[j]=str1.charAt(i)==str2.charAt(j)?dp[j-1]+1:0;
}
max=Math.max(max,dp[j]);
}
}
return max;
}
两个字符串的最长公共子序列(DP)
public static int maxSubstring3(String str1,String str2){
int[][] dp=new int[str1.length()][str2.length()];
for(int i=0;i<str2.length();i++){
if(str1.charAt(0)==str2.charAt(i)){
dp[0][i]=1;
}
}
for(int i=0;i<str1.length();i++){
if(str1.charAt(i)==str2.charAt(0)){
dp[i][0]=1;
}
}
for(int i=1;i<str1.length();i++){
/**
* 情况1:不以i、j结尾:dp[i][j]=dp[i-1][j-1]
* 情况2:以i、以j结尾:dp[i][j]=dp[i-1][j-1]+1
* 情况3:以i结尾、不以j结尾:dp[i][j]=dp[i][j-1]
* 情况4:不以i结尾、以j结尾:dp[i][j]=dp[i-1][j]
*/
for(int j=1;j<str2.length();j++){
if(str1.charAt(i)==str2.charAt(j)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=dp[i-1][j-1];
}
dp[i][j]=Math.max(dp[i][j],Math.max(dp[i-1][j],dp[i][j-1]));
}
}
return dp[str1.length()-1][str2.length()-1];
}
同时让N个人过河最少需要几条船
先找到<=(limit)/2最右边的位置,记为L,然后定义R为L+1,然后比较L和R上的和与limit进行比较,如果大于等于,L左移;如果小于,R右移,L左移
//arr有序
public static int minShip(int[] arr,int limit){
//如果出现了比limit大的数,这个数无论如何也过不了河
for(int i=0;i<arr.length;i++){
if(arr[i]>limit) return 0;
}
//如果第一个值大于limit/2,那么后面的每一个值都对应一条船
if(arr[0]>limit/2) return arr.length;
//如果最后一个值小于等于limit/2,那么前面所有的值都是两个值匹配一个船,向上取整
if(arr[arr.length-1]<=limit/2) return (arr.length+1)/2;
int m=findLessOrEqual(arr,limit/2);//m是小于等于limit/2最右边的位置
int l=m,r=m+1;
int unused=0;//没有匹配成功的数量
while (l>=0){
int solved=0;//当前已经匹配的数量
while (r<arr.length && arr[l]+arr[r]<=limit){
r++;//如果匹配成功,r右移
solved++;
}
if(solved==0){//如果在上面的while循环中没有一个匹配成功,说明当前l上的值不能与r及其之后的值匹配,l--
unused++;
l--;
}else{
l=l-solved;//如果l往左移solved个单位
}
}
int res1=(unused+1)/2;//左边没有匹配的值除以2,向上取整,就是需要的船的数量
int res2=(m+1-unused);//左边已经和右边匹配的船的数量
int res3=arr.length-m-1-res2;//右边没有匹配的值,每一个船只能对应一个值
return res1+res2+res3;
}
private static int findLessOrEqual(int[] arr, int target) {
int l=0,r=arr.length-1;
int t=0;
while (l<=r){
int mid=l+((r-l)>>1);
if(arr[mid]>target){
r=mid-1;
}else if(arr[mid]<=target){
l=mid+1;
t=mid;
}
}
return t;
}
给定一个字符串,求最长的回文子序列(DP-范围查询)
/**
* 给定一个字符串,求最长的回文子序列
* @param str 给定的字符串
* @return 最长回文子序列的长度
*/
public static int func1(String str){
char[] chs = str.toCharArray();
int len=chs.length;
//dp数组,dp[i][j]表示以i为起点,j为终点字符串的最长回文子序列的长度
int[][] dp=new int[len][len];
//因为i<=j,所以dp数组只需要对角线及以上的部分
for(int i=0;i<len;i++){
//处于对角线上的值,i==j,最长的回文子序列长度为1
dp[i][i]=1;
//处于对角线上面一个位置的值,j==i+1,这是字符串的长度为2,如果两个字符相等,最长的回文子序列长度为2,,否则为1
if(i<len-1){
dp[i][i+1]=chs[i]==chs[i+1]?2:1;
}
}
//从倒数第三行开始
for(int i=len-3;i>=0;i--){
//因为每一行的前两列我们在上面给出了,所以这里从i+2列开始
for(int j=i+2;j<len;j++){
//如果i和j位置的字符相等,就不考虑i和j位置,找i和j中间的最长回文子序列然后+2
if(chs[i]==chs[j]){
dp[i][j]=dp[i+1][j-1]+2;
}
//如果i和j位置的字符不相等,考虑i+1~j、i~j-1、i+1~j-1这三种情况,取最大值
dp[i][j]=Math.max(dp[i][j],Math.max(dp[i+1][j],Math.max(dp[i][j-1],dp[i+1][j-1])));
}
}
//返回以0为起点,len为重点的字符串的最长回文子序列的长度
return dp[0][len-1];
}
添加最少字符的情况下,让字符串整体都是回文子串(DP-范围查询)
/**
* 添加最少字符的情况下,让字符串整体都是回文子串,返回修改后的回文子串
* @param str
* @return
*/
public static String func2(String str){
char[] chs = str.toCharArray();
int len = chs.length;
//dp[i][j]表示以i为起点,j为终点的字符串最少需要添加几个字符才能变成回文串
int[][] dp=new int[len][len];
for(int i=0;i<len;i++){
//对于i==j的情况,长度为1的字符串本身就是回文串,不需要添加,dp[i][j]为0,
dp[i][i]=0;
if(i<len-1){
//对于长度为2的字符串,如果两个字符相同,不需要添加,如果不同,只需要添加一个,这里列出了与对角线上面的一条斜线
dp[i][i+1]=chs[i]==chs[i+1]?0:1;
}
}
//从倒数第三行开始
for(int i=len-3;i>=0;i--){
//因为每一行的前两列我们在上面给出了,所以这里从i+2列开始
for(int j=i+2;j<len;j++){
//如果i和j位置的字符相等,就不考虑i和j位置,找i和j中间需要添加的最少字符即可
if(chs[i]==chs[j]){
dp[i][j]=dp[i+1][j-1];
}else{
//如果i和j位置的字符不相等,考虑i+1~j、i~j-1中最少需要添加的字符取最小值加1
dp[i][j]=Math.min(dp[i+1][j],dp[i][j-1])+1;
}
}
}
//len2表示的是添加最少字符的情况下,让字符串整体都是回文子串的长度
int len2=len+dp[0][len-1];
char[] newStr=new char[len2];
//l表示原字符串的起始位置,r表示原字符串的结束位置
int l=0,r=len-1;
//newL表示新字符串的起始位置,newR表示新字符串的结束位置
int newL=0,newR=len2-1;
while (l<=r){
newStr[newL++]=chs[l];
newStr[newR--]=chs[l];
/*
* 例如:原字符串为:1a2b3ca 长度为7,索引0~6,dp[0][6]=4,newStr长度为9,索引0~8
* 先比较[0]、[6]位置的值不同,所以newStr[0]=1,newStr[8]=1,比较[1]~[6]
* [1]、[6]相同,newStr[1]=a,newStr[7]=a,比较[2]~[5]
* [2]、[5]不同,newStr[2]=2,newStr[6]=2,比较[3]~[5]
* ..............................................
*
*/
//如果l和r位置的字符相等,l右移,r左移
if(chs[l]==chs[r]){
l++;
r--;
}else{
//如果l和r位置的字符不相等,r左移
l++;
}
}
return new String(newStr);
}
将字符串全部切成回文子串的最小分割数(DP-从右往左+范围查询)
/*
* 将字符串全部切成回文子串的最小分割数
*
*/
public static int func3(String str){
char[] chs = str.toCharArray();
int len = chs.length;
//dp[i]表示以i为终点的字符串的最小分割数
int[] dp=new int[len+1];
Arrays.fill(dp,Integer.MAX_VALUE);
boolean[][] valid=new boolean[len][len];
for(int i=0;i<len;i++){
valid[i][i]=true;
}
for(int i=len-2;i>=0;i--){
for(int j=i+1;j<len;j++){
if(chs[i]==chs[j]){
valid[i][j]=valid[i+1][j-1];
}else{
valid[i][j]=false;
}
}
}
dp[len]=0;
dp[len-1]=1;
for(int i=len-2;i>=0;i--){
for(int j=i;j<len;j++){
if(valid[i][j]){
dp[i]=Math.min(dp[i],dp[j+1]+1);
}
}
}
return dp[0]-1;
}
子数组中最大异或和
数组中的值有正、有负、有0
//两层for循环,时间复杂度为O(N^2)
public static int maxEOR(int[] arr){
if(arr==null || arr.length==0){
return 0;
}
int res=0;
int[] preSum=new int[arr.length];
preSum[0]=arr[0];
for(int i=1;i<arr.length;i++){
preSum[i]=arr[i]^preSum[i-1];
}
for(int i=0;i<arr.length;i++){
for(int start=0;start<=i;start++){
//[start...i]的异或和=[0..i]^[0...start-1]
int sum=preSum[i] ^ (start==0?0:preSum[start-1]);
res=Math.max(res,sum);
}
}
return res;
}
优化:使用前缀树+贪心
public static class Node{
public Node[] nexts=new Node[2];
}
public static class NumTrie{
public Node head=new Node();
public void add(int num){
Node cur=head;
for(int i=31;i>=0;i--){
int path=((num>>i)&1);
if(cur.nexts[path]==null){
cur.nexts[path]=new Node();
}
cur=cur.nexts[path];
}
}
//在前缀树中找到与num异或最大的数,将异或之后的结果返回
public int maxXOR(int num){
Node cur=head;
int res=0;
for(int i=31;i>=0;i--){
//取出num的当前位
int path=((num>>i)&1);
//如果当前是符号位,0期待的是0,1期待的也是1,因为这样异或才为正数
//如果当前不是符号为,0期待的是1,1期待的是0,这样异或的结果才会更大
int best=i==31?path:(path^1);
//如果期待的路径不存在,就取反
best=cur.nexts[best]==null?(path^1):best;
//当前位的异或结果
res|=(path^best)<<i;
//移动到下一位
cur=cur.nexts[best];
}
return res;
}
}
public static int maxEor2(int[] arr){
if(arr==null || arr.length==0){
return 0;
}
NumTrie numTrie=new NumTrie();
int res=0;
int sum=0;
numTrie.add(0);
for(int i=0;i<arr.length;i++){
//0~i的异或和
sum^=arr[i];
//numTire中装着所有:一个数也没有、0~0,0~1,0~2,0~3...0~i-1的异或和
//这样可以得到0~i中的最大异或和
res=Math.max(res,numTrie.maxXOR(sum));
numTrie.add(sum);
}
return res;
}