5 KMP算法
问题:字符串str1和str2,str1是否包含str2,如果包含返回str2在str1中开始的位置。如何做到时间复杂度O(N)完成?
str1=”abc123ssss4abc” str2=”1234” str2为子串
① 先得到一个next数组,里面记录了字串的当前位置之前的最大相同前缀和后缀
eg: str=”1234” i=0,next[0]=-1;i=1,next[1]=0;i=2,next[2]=0
eg: str=”abc1222abc” next={-1,0,0,0,0,0,0,1,2,3}
② 再根据字符比较,根据已有的next数组,可以加速字符串的匹配进程。
暴力解法 ,时间复杂度O(N^2)
//str2 子字符串 暴力解法
public static int getSubStringIndex(String str1,String str2){
if(str1 == null || str2 == null || str2.length() < 1 || str2.length() > str1.length()){
return -1;
}
int startIndex = 0;
boolean flag = false;
for(int i = 0;i < str1.length();i++){
if(str1.charAt(i) == str2.charAt(0)) {
startIndex = i;
flag = false;
for (int j = 1; j < str2.length();j++) {
if(str2.charAt(j) != str1.charAt(++startIndex)){
flag = true;
break;
}
}
if(!flag){
return startIndex - str2.length() + 1;
}
}
}
return -1;
}
KMP算法 时间复杂度O(N)
//KMP算法加速
public int getSubStringIndex_KMP(String str1,String str2){
if(str1 == null || str2 == null || str2.length() < 1 || str1.length() < str2.length()){
return -1;
}
char[] char1 = str1.toCharArray();
char[] char2 = str2.toCharArray();
int i1 = 0;
int i2 = 0;
int[] next = getKMPNexts(char2);
while(i1 < char1.length && i2 < char2.length){
if(char1[i1] == char2[i2]){
i1++;
i2++;
}else if(i2 == 0){ //next[i2] == -1; //判断子字符串的判断是否回到了第一个元素,无法再往前跳了
//当子字符串回到第一个元素,说明当前str1中判断的元素已经不满足,要后移再继续判断
i1++;
}else {
i2 = next[i2];
}
}
return i2 == str2.length() ? i1 - i2 : -1;
}
//获得子串中当前元素的位置之前具有最大相同前缀和最大相同后缀的数组
public static int[] getKMPNexts(char[] chars){
if(chars.length == 1){
return new int[]{-1};
}
int[] next = new int[chars.length];
//对于数组前两个元素,是直接人为规定的(相同的最大前缀和后缀不能为本身)
next[0] = -1;
next[1] = 0;
int i = 2;
int j = next[i-1];
for(;i < chars.length;i++) {
//如果当前位置的前一个字符能和其最大前缀的后一个字符相同
if (chars[i - 1] == chars[j]) { //j位置 0~j-1 为j个
next[i] = ++j; //next[i] = next[i-1] + 1
} else if (j > 0){
j = next[j];
} else {
next[i] = 0;
}
}
return next;
}
6 Manacher算法
问题:字符串str中,最长回文子串的长度如何求解?如何做到时间复杂度O(N)
回文半径:以一个中心向左右两边扩,扩出来的整个区域大小为回文直径,一半的长度为回文半径。
public class Manacher {
public static int getMaxPalindromeSize(String str){
if(str == null || str.length() == 0){
return 0;
}
//先对str进行字符处理,加入虚拟字符
char[] chars = manacher(str);
//新建一个回文半径的数组,保存每一个字符的回文半径
int[] pArr = new int[chars.length];
//如果有以C为中心,R为回文半径的回文字符串,遍历到i位置(一般在C后面),i如果在C的回文半径里面,那么肯定有以C为对称轴的i'
//只需要判断i‘的回文半径,在根据其回文半径大小分小点讨论即可。
int C = -1; //定义回文字符的中心
int R = -1; //定义回文右边界再往右一个位置, 最右的有效区是 R-1 位置
int maxSize = Integer.MIN_VALUE; //所有回文字串的最大长度
for(int i = 0;i < chars.length;i++){ //每个位置都要求回文半径
//i 至少的回文区域 2*c-i表示的是i’ 即i关于C的对称点
pArr[i] = R > i ? Math.min(pArr[2 * C - i],R - i) : 1;
while (i + pArr[i] < chars.length && i - pArr[i] > -1){
if(chars[i + pArr[i]] == chars[i - pArr[i]]){ //在至少的回文区域往外扩,有相同的就加一
pArr[i]++;
}else {
break;
}
}
if(i + pArr[i] > R){ //有边界扩大,中心点变化
R = i + pArr[i];
C = i;
}
maxSize = Math.max(maxSize,pArr[i]);
}
//处理串的回文半径代表的 是 原始串的回文半径+1
return maxSize - 1;
}
//给一个字符串,进行字符处理,在每个字符两端加上虚拟字符
public static char[] manacher(String str){
char[] charArr = str.toCharArray();
char[] res = new char[str.length() * 2 + 1];
int index = 0;
for(int i = 0;i != res.length;i++){
res[i] = (i & 1) == 0 ? '#' : charArr[index++];
}
return res;
}
}
7 滑动窗口(采用双向队列)
问题:有一个整型数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。
eg:数组为[4,3,5,4,3,3,6,7],窗口大小为3时:
[4,3,5],4,3,3,6,7 窗口中最大值为5
4,[3,5,4],3,3,6,7 窗口中最大值为5
4,3,[5,4,3],3,6,7 窗口中最大值为5
4,3,5,[4,3,3],6,7 窗口中最大值为4
4,3,5,4,[3,3,6],7 窗口中最大值为6
4,3,5,4,3,[3,6,7] 窗口中最大值为7
如果数组长度为n,窗口大小为w,则一共产生n-w+1个窗口的最大值。
请实现一个函数。输入:整型数组arr,窗口大小为w。
输出:一个长度为n-w+1的数组res,res[i]表示每一种窗口状态下的以本题为例,结果应该返回[5,5,5,4,6,7]
//w:窗口大小
//使用双向队列 队列中存放数组的下标,担保证队列中对应数组元素从大到小
public static int[] getMaxWindowNum(int[] arr,int w){
if(arr == null || arr.length < w){
return null;
}
//创建一个双向队列,保存窗口滑动时经过的数据
Deque<Integer> arrDeque = new ArrayDeque<>();
int[] res = new int[arr.length - w + 1];//窗口滑动过程产生的结果集
int i = 0;
for(;i < arr.length;i++){
//当前比对元素如果比队列尾元素小,直接添加,否则依次移除最后一个元素,直到队尾元素大于比对元素
while(!arrDeque.isEmpty() && arr[arrDeque.getLast()] <= arr[i]){
arrDeque.removeLast();
}
arrDeque.addLast(i);
if(i - w == arrDeque.getFirst()){//左移时,原左边界是否时最大的数
arrDeque.removeFirst();
}
if(i - w + 1 >= 0) { //放入窗口的的最大值
res[i - w + 1] = arr[arrDeque.getFirst()];
}
}
return res;
}
8 单调栈
在数组中想找到一个数,左边和右边比这个数小,且离这个数最近的位置。
如果对每一个数都想求这样的信息,能不能整体代价达到O(N)?需要使用到单调栈结构
找左边和右边比这个数小 --> 栈从低到顶部应该是从小到大
找左边和右边比这个数大 --> 栈从低到顶部应该是从大到小
对于数组中有重复的数,需要压入链表在进栈,保证遍历时,重复的数被放在一个栈层。
//找左边和右边比这个数小 --> 栈从低到顶部应该是从小到大
//找左边和右边比这个数大 --> 栈从低到顶部应该是从大到小
public static Integer[][] getMaxLeftAndRight(int[] arr){ //arr中没有重复的数
if(arr == null){
return null;
}
//使用单调栈,栈中存放元素下标,使数组元素依次进栈,但在栈中按照从大到小的排序方式
Stack<Integer> stack = new Stack<>();
Integer[][] res = new Integer[arr.length][2]; //res[i][0] = arr[i]左边比这个数大 res[i][1] = arr[i]右边比这个数大
int index = 0; //临时变量
for(int i = 0;i < arr.length;i++){
//当前元素比栈顶元素大时,依次弹出栈顶元素并打印信息(右边大的第一个就是当前比对元素,左边大的第一个元素就是栈顶下一个元素)
//一直弹出和打印信息,知道栈顶元素大于比对元素结束
while(!stack.isEmpty() && arr[i] > arr[stack.peek()]){
index = stack.pop();
res[index][1] = arr[i]; //右边第一个大的
res[index][0] = stack.isEmpty() ? null : arr[stack.peek()]; //左边第一个小的
}
stack.push(i);
}
while (!stack.isEmpty()){//元素以及遍历到最后,但栈中依然还有元素
index = stack.pop();
res[index][1] = null;
res[index][0] = stack.isEmpty() ? null : arr[stack.peek()];
}
return res;
}
arr中有重复的或者没有重复的均可以使用
public static Integer[][] getMaxLeftAndRight2(int[] arr){ //arr中没有重复的数
if(arr == null){
return null;
}
//使用单调栈,栈中存放元素下标,使数组元素依次进栈,但在栈中按照从大到小的排序方式
Stack<LinkedList<Integer>> stack = new Stack<>();
Integer[][] res = new Integer[arr.length][2];
LinkedList<Integer> temp = null; //临时变量
for(int i = 0;i < arr.length;i++){
while(!stack.isEmpty() && arr[i] > arr[stack.peek().peekFirst()]){
temp = stack.pop();
while(!temp.isEmpty()) {
int index = temp.pop();
res[index][1] = arr[i]; //右边第一个大的
res[index][0] = stack.isEmpty() ? null : arr[stack.peek().peekFirst()]; //左边第一个小的
}
}
if (!stack.isEmpty() && arr[i] == arr[stack.peek().peekFirst()]){ //如果有重复的,直接在链表对应后面加
stack.peek().addLast(i);
}else {
temp = new LinkedList<>();
temp.addFirst(i);
stack.push(temp);
}
}
while (!stack.isEmpty()){//元素以及遍历到最后,但栈中依然还有元素
temp = stack.pop();
while(!temp.isEmpty()) {
int index = temp.pop();
res[index][1] = null; //右边第一个大的
res[index][0] = stack.isEmpty() ? null : arr[stack.peek().peekFirst()]; //左边第一个小的
}
}
return res;
}
问题:正数数组中累积和与最小值的乘积,假设叫做指标A
给定一个数组,请返回子数组中,指标A最大的值。
寻找以当前元素为最小值的子数组,找到左边和右边靠近该元素但小于的值下标(左右边界取不到),这样,该子数组中的最大A指标=该元素 * 子数组的累加和
通过单调栈的方式找到两边靠近且小于其元素的下标
//以当前元素为最小,扩充一个数组,知道左右两边都找到比它小的停止,左右两边都取不到 ---> 通过单调栈找左右两边最靠近的小于的元素下标
//求出当前数组的累加和 当前数组的最小值就是当前元素 乘积为A
public static int getIndex_A(int[] arr){
if(arr == null){
return -1;
}
int maxIndexA = Integer.MIN_VALUE;
Integer[][] minArr = getMinLeftAndRight(arr);
for(int i = 0;i < minArr.length;i++){
int L = minArr[i][0] == null ? -1 : minArr[i][0]; //返回null,表示左边没有比其小的,因此左边界在0前面
int R = minArr[i][1] == null ? arr.length : minArr[i][1]; // 返回null,表示右边没有比其小的,因此有边界在最后一个下标后面
int sum = 0;
for(int j = L + 1;j < R;j++){ //以当前元素为最小值的数组的累加和
sum += arr[j];
}
int indexA = sum * arr[i]; //指标A
System.out.println("i = " + i + " ,indexA = " + indexA);
maxIndexA = maxIndexA > indexA ? maxIndexA : indexA;
}
return maxIndexA;
}
//通过一个单调栈 (小到大) 找到以每个元素为最小,左边最靠近和右边最靠近比其的小的值的下标
public static Integer[][] getMinLeftAndRight(int[] arr){ //arr中没有重复的数
if(arr == null){
return null;
}
//使用单调栈,栈中存放元素下标,使数组元素依次进栈,但在栈中按照从小到大的排序方式
Stack<LinkedList<Integer>> stack = new Stack<>();
Integer[][] res = new Integer[arr.length][2];
LinkedList<Integer> temp = null; //临时变量
for(int i = 0;i < arr.length;i++){
while(!stack.isEmpty() && arr[i] < arr[stack.peek().peekFirst()]){
temp = stack.pop();
while(!temp.isEmpty()) {
int index = temp.pop();
res[index][1] = i; //右边第一个小的下标
res[index][0] = stack.isEmpty() ? null : stack.peek().peekFirst(); //左边第一个小的下标
}
}
if (!stack.isEmpty() && arr[i] == arr[stack.peek().peekFirst()]){ //如果有重复的,直接在链表对应后面加
stack.peek().addLast(i);
}else {
temp = new LinkedList<>();
temp.addFirst(i);
stack.push(temp);
}
}
while (!stack.isEmpty()){//元素以及遍历到最后,但栈中依然还有元素
temp = stack.pop();
while(!temp.isEmpty()) {
int index = temp.pop();
res[index][1] = null; //右边第一个大的
res[index][0] = stack.isEmpty() ? null : stack.peek().peekFirst(); //左边第一个小的
}
}
return res;
}