算法题库-数组
- (一)⭐️⭐️⭐️数组
- 【1】双指针
- (3)⭐️《剑指offer》13-调整数组顺序使奇数位于偶数前面【数组、排序】
- (4)《剑指offer》19-顺时针打印矩阵【多维数组、遍历】
- (5)⭐️《剑指offer》28-数组中出现次数超过一半的数字
- (10)⭐️《剑指offer》40 如何找到数组中只出现过一次的数字【排序+指针】
- (13)⭐️《剑指offer》51 求数组中和为S的两个数字【双指针碰撞】
- (14)《剑指offer》按顺序打印出从 1 到最大的 n 位十进制数【数组遍历赋值】
- (15)《剑指offer》0~n-1中缺失的数字
- (18)⭐️两个有序数组,分别找出两个数组中不同的值【双指针】
- (19)⭐️合并两个有序递增的数组,成一个新的有序递增的数组【双指针】
- (22)⭐️三数之和
- (23)⭐️缺失的第一个正整数
- 【2】二分查找
- 【3】动态规划、贪心算法
- 【4】HashSet、HashMap、Stack等
- (二)⭐️⭐️字符串
- 字符串的基本知识
- (1)《剑指offer》2-替换空格【StringBuilder等、函数方法】
- (2)《剑指offer》27-字符串的排列
- (3)《剑指offer》34 如何找到第一个只出现一次的字符【借助哈希表、数组、Set等】
- (4)《剑指offer》43 如何左旋转字符串【reverse函数、三次反转】
- (5)《剑指offer》44 如何反转单词序列【两次反转、栈】
- (6)《剑指offer》49 如何把字符串转化为整数【看视频补充】
- (7)《剑指offer》52 正则化表达式匹配【动态规划】
- (8)《剑指offer》53 表示数值的字符串
- (9)《剑指offer》54 第一个只出现一次的字符【借助哈希表、数组、Set等】
- (10)《剑指offer》最长不含重复字符的子字符串【动态规划+哈希表】
- (11)《剑指offer》把数字翻译成字符串【动态规划】
- (12)⭐️字符串变形
- (13)⭐️最长公共前缀
- (14)⭐️有效括号序列
- (15)⭐️反转字符串
- (16)⭐️判断是否为回文字符串
- (17)最长回文子串
- (18)🌶最长的括号子串
- (19)🌶最小覆盖子串
- (四)二叉树
- (五)斐波那契数列
- (六)链表
- (七)深度优先算法
(一)⭐️⭐️⭐️数组
数组的基本知识
基础数组知识文章:http://t.csdn.cn/TwFEk
(1)要先关注一点,数组是否是有序的,这个很关键
(2)熟练使用指针
(3)数组的初始化
int[] arr = new int[]{1,2,3,4,5};
int[] arr = {1,2,3,4,5};
int array3[][] = { { 1, 1, 1 }, { 2, 2, 2 } };
int array4[][] = new int[][] { { 1, 1, 1 }, { 2, 2, 2 } };
(4)获取数组的长度
// 获取二维数组行数(有多少行)
int length1 = array.length;
// 获取二维数组列数(有多少列)
int length2 = array[0].length;
(5)数组的遍历
int[] arr = { 1, 2, 3, 4, 5 };
for (int i : arr) {
System.out.println(i);
}
(6)数组的冒泡排序要会
int temp = 0;
for (int j = 1; j < numbers.length - 1; j++) {
for (int i = 0; i < numbers.length - j; i++) {
if (numbers[i] > numbers[i + 1]) {
temp = numbers[i + 1];
numbers[i + 1] = numbers[i];
numbers[i] = temp;
}
}
}
(7)【排序+指针】可以解决很多问题
(8)要会借助HashSet的add方法添加是否成功,找重复的数据
(9)要会借助HashMap的判断和remove判断数据是否重复,找不重复的数据
【1】双指针
(3)⭐️《剑指offer》13-调整数组顺序使奇数位于偶数前面【数组、排序】
(1)题目描述:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
(2)解题思路
使用双指针来找数组里的奇数和偶数,然后交换奇数和偶数的位置,把前面的偶数和后面的奇数互换位置
(3)第一遍解题
i和j两个指针,先遍历i,用i指针找偶数,找到偶数后再用j遍历剩下的去找奇数,找到奇数后就交换数据位置,并且breake这个j的遍历
public static int[] method01(int[] array01,int len) {
// 两个指针,i找前面的偶数,j找后面的奇数
int i=0;
int j;
while (i<len) {
// 如果i值是奇数,i就往后走
if(array01[i]%2!=0){
i++;
continue;
}
// 如果i值是偶数,就用j去找奇数
if(array01[i]%2==0){
for (j=i+1; j < len; j++) {
if(array01[j]%2!=0){
// i为偶数,j为奇数,两者位置互换
int temp = array01[i];
array01[i] = array01[j];
array01[j] = temp;
break;
}
}
}
i++;
}
return array01;
}
对结构进行简单的优化和调整
public static int[] method01(int[] array01,int len) {
// 两个指针,i找前面的偶数,j找后面的奇数
int i=0;
int j;
while (i<len) {
// 如果i值是偶数,就用j去找奇数
if(array01[i]%2==0){
for (j=i+1; j < len; j++) {
if(array01[j]%2!=0){
// i为偶数,j为奇数,两者位置互换
int temp = array01[i];
array01[i] = array01[j];
array01[j] = temp;
break;
}
}
}
i++;
}
return array01;
}
(4)第二遍解题
代码问题:奇数可以保证还是按原来的顺序排列,但是后面的偶数不能保证顺序排列
原因分析:交换位置的时候[1,3,2,4,5,6]使用2和5直接进行交换的,应该让5依次和前面的值进行交换
解决思路:计算出i和j之间差了几个位置,然后遍历几次,让j位置的数据依次和前面的数据交换位置,最后交换到i的位置
public static int[] method01(int[] array01,int len) {
// 两个指针,i找前面的偶数,j找后面的奇数
int i=0;
int j;
while (i<len) {
// 如果i值是偶数,就用j去找奇数
if(array01[i]%2==0){
for (j=i+1; j < len; j++) {
if(array01[j]%2!=0){
// i为偶数,j为奇数,两者位置互换
int count=j-i;
while (count>0) {
int temp = array01[i+count-1];
array01[i+count-1] = array01[i+count];
array01[i+count] = temp;
count--;
}
break;
}
}
}
i++;
}
return array01;
}
(5)第三遍解题
优化分析:j位置的数据往前移动的时候,不是每次都要交换数据的,只需要i后面的值依次往后移动一位,然后把原来i的值放在j的值后面就可以了
public static int[] method01(int[] array01,int len) {
// 两个指针,i找前面的偶数,j找后面的奇数
int i=0;
int j;
while (i<len) {
// 如果i值是偶数,就用j去找奇数
if(array01[i]%2==0){
for (j=i+1; j < len; j++) {
if(array01[j]%2!=0){
// i为偶数,j为奇数,两者位置互换
int count=j-i;
int temp = array01[i];
array01[i] = array01[j];
while (count>1) {
array01[i+count] = array01[i+count-1];
count--;
}
array01[i+1]=temp;
break;
}
}
}
i++;
}
return array01;
}
(4)《剑指offer》19-顺时针打印矩阵【多维数组、遍历】
(1)题目描述:
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵:
[[1,2,3,4],
[5,6,7,8],
[9,10,11,12],
[13,14,15,16]]
则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> list = new ArrayList<>();
if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
return list;
}
int up = 0;
int down = matrix.length-1;
int left = 0;
int right = matrix[0].length-1;
while(true){
// 最上面一行
for(int col=left;col<=right;col++){
list.add(matrix[up][col]);
}
// 向下逼近
up++;
// 判断是否越界
if(up > down){
break;
}
// 最右边一行
for(int row=up;row<=down;row++){
list.add(matrix[row][right]);
}
// 向左逼近
right--;
// 判断是否越界
if(left > right){
break;
}
// 最下面一行
for(int col=right;col>=left;col--){
list.add(matrix[down][col]);
}
// 向上逼近
down--;
// 判断是否越界
if(up > down){
break;
}
// 最左边一行
for(int row=down;row>=up;row--){
list.add(matrix[row][left]);
}
// 向右逼近
left++;
// 判断是否越界
if(left > right){
break;
}
}
return list;
}
}
(5)⭐️《剑指offer》28-数组中出现次数超过一半的数字
(1)题目描述:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
解法一:候选法
根据题意,目标数字在数字中占比超过一半,那么发现一个众数就+1,发现一个非众数就-1,那么最终的结果一定是大于0的。
所以准备一个指针,从数组第一位数开始往后判断,有相同数就+1,不相同数就-1,如果值为0了就把i前进一位
public static int method02(int[] array01,int len) {
int i = 1;
int goal = array01[0];
int goal_count = 1;
while (i<len) {
if(array01[i]==goal){
goal_count++;
} else {
goal_count--;
}
if(goal_count==0){
goal=array01[i];
goal_count=1;
}
i++;
}
return goal;
}
(10)⭐️《剑指offer》40 如何找到数组中只出现过一次的数字【排序+指针】
(1)题目描述:
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次****(成对出现,则两个相同的数异或)。请写程序找出这两个只出现一次的数字。要求时间复杂度为O(n),空间复杂度为O(1)。
(2)解法一:排序+指针
1.总体思路:先对数组从小到大排序,这两相同的两个数就相邻了
2.采用类似双指针的思想,当下一个数和自身相同时,移动两步;当下个数与当前数不同时,则将当前数加入答案,移动一步
时间复杂度:O(nlogn)
空间复杂度:O(1)
public class array_05 {
public static void main(String[] args) {
int array0[]={1,2,3,3,4,3,7,9,7};
ArrayList<Integer> arrayList = new ArrayList<>();
int len=array0.length;
int temp = 0;
for (int j = 1; j < array0.length - 1; j++) {
for (int i = 0; i < array0.length - j; i++) {
if (array0[i] > array0[i + 1]) {
temp = array0[i + 1];
array0[i + 1] = array0[i];
array0[i] = temp;
}
}
}
int m=0;
while (m<len) {
// 如果m刷新后的位置是最后一个数,那么直接确定是唯一的值
if (m==len-1) {
arrayList.add(array0[m]);
break;
}
// m位置和下一个位置的值比较,如果不等,就是唯一值
if(array0[m]!=array0[m+1]){
arrayList.add(array0[m]);
m++;
} else {
// 如果m位置和下一个位置的相等,那就把所有相等的值全部遍历出来,直到找到不等的值,然后刷新m到这个不等的值的位置
for (int n=m+1;n<len;n++) {
if(array0[m]==array0[n]){
continue;
} else {
m=n;
break;
}
}
}
}
System.out.println(arrayList.toString());
}
}
(3)解法二:使用HashSet辅助
遍历数组,挨个放进Hash
(13)⭐️《剑指offer》51 求数组中和为S的两个数字【双指针碰撞】
(1)题目描述
输入一个升序数组 array 和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,返回任意一组即可,如果无法找出这样的数字,返回一个空数组即可。
(2)解法一
已经是排好序的数组了,使用双指针碰撞来遍历
public class array_11 {
public static void main(String[] args) {
int array[] = {1,2,4,7,11,15};
int target = 15;
int len = array.length;
int left = 0;
int right = len-1;
ArrayList<Integer> res = new ArrayList<Integer>();
while (left<right) {
if(array[left]+array[right]==target){
res.add(array[left]);
res.add(array[right]);
break;
} else if(array[left]+array[right]>target){
right--;
} else if(array[left]+array[right]<target){
left++;
}
}
System.out.println(res.toString());
}
}
(2)解法二:双指针(头尾指针值相加和,和大了尾指针左移,和小了头指针右移。因为数组是递增的,这样可以保证和是逐渐靠近目标值的)
这道题目还有一个条件是数组是升序序列,在方法一中没有用到。这个条件有什么用?既然数组是有序的,那我们肯定知道和找到一定程度就不找了,我们为什么要从最小的两个数开始相加呢?我们可以用二分法的思路,从中间开始找。
使用双指针指向数组第一个元素和最后一个元素,然后双指针对撞移动,如果两个指针下的和正好等于目标值sum,那我们肯定找到了,如果和小于sum,说明我们需要找到更大的,那只能增加左边的元素,如果和大于sum,说明我们需要找更小的,只能减小右边的元素。
step 1:准备左右双指针分别指向数组首尾元素。
step 2:如果两个指针下的和正好等于目标值sum,则找到了所求的两个元素。
step 3:如果两个指针下的和大于目标值sum,右指针左移;如果两个指针下的和小于目标值sum,左指针右移。
step 4:当两指针对撞时,还没有找到,就是数组没有。
1-第一遍解题
public class array_04 {
public static void main(String[] args) {
int array01[]={1,2,4,7,11,15};
int sum=15;
int len=array01.length;
int i=0;
ArrayList<Integer> res = new ArrayList<Integer>();
boolean flag=true;
while (i<len) {
int i_value = array01[i];
for (int j=len-1;j>i;j--) {
int j_value = array01[j];
if(i_value+j_value>sum){
continue;
} else if(i_value+j_value==sum){
res.add(i_value);
res.add(j_value);
flag=false;
} else if(i_value+j_value<sum){
break;
}
}
if(!flag){
break;
}
i++;
}
System.out.println(res.toString());
}
}
2-第二遍优化:去掉一层遍历
public static ArrayList<Integer> method02(int[] array,int sum) {
int len=array.length;
int i=0;
int j=len-1;
ArrayList<Integer> res = new ArrayList<Integer>();
while (i<j) {
int i_value = array[i];
int j_value = array[j];
if(i_value+j_value==sum){
res.add(i_value);
res.add(j_value);
break;
} else if(i_value+j_value<sum){
i++;
} else {
j--;
}
}
return res;
}
(3)解法二:哈希表
step 1:构建一个哈希表,其中key值为遍历数组过程中出现过的值,value值为其相应的下标,因为我们最终要返回的是下标。
step 2:遍历数组每个元素,如果目标值减去该元素的结果在哈希表中存在,说明我们先前遍历的时候它出现过,根据记录的下标,就可以得到结果。
step 3:如果相减后的结果没有在哈希表中,说明先前遍历的元素中没有它对应的另一个值,那我们将它加入哈希表,等待后续它匹配的那个值出现即可。
import java.util.*;
public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
ArrayList<Integer> res = new ArrayList<Integer>();
//创建哈希表,两元组分别表示值、下标
HashMap<Integer, Integer> mp = new HashMap<Integer, Integer>();
//在哈希表中查找target-numbers[i]
for(int i = 0; i < array.length; i++){
int temp = sum - array[i];
//若是没找到,将此信息计入哈希表
if(!mp.containsKey(temp)){
mp.put(array[i], i);
}
else{
//取出数字添加
res.add(temp);
res.add(array[i]);
break;
}
}
return res;
}
}
(4)解法三:二分法+指针(双循环,遍历确定一个值a,二分法去查有没有另外一个值b)
1-两个指针指向头和尾
2-目标值减去数组的第一个值为value
3-拿value和mid指针指向的值比较
4-如果value>mid,那么说明目标值应该在mid指针的右边,所以缩小范围,头指针改为mid+1,头尾指针全部到mid右边范围来找
5-如果value<mid,那么说明目标值应该在mid指针的左边,尾指针改为mid-1
总结:就是使用循环先确定一个值a,然后拿target-a得出另外一个值b,然后使用双指针二分法去查这个值b。如果查不到就换一个a值,然后再遍历查。比暴力法好点的就是查b的时候用了二分法
public int[] twoSum(int[] nums, int target) {
if(nums.length == 0 || nums.length == 1) return new int[0];
int[] res = new int[2];
for(int i=0;i<nums.length;i++) {
int a = nums[i];
int low = i;
int high = nums.length-1;
while(low<=high) {
int mid = (low+high)/2 ;
if(nums[mid] == target-a) {
res[0] = a;
res[1] = nums[mid];
return res;
}else if(nums[mid] > target-a){
high = mid -1;
}else {
low = mid+1;
}
}
}
return res;
}
(14)《剑指offer》按顺序打印出从 1 到最大的 n 位十进制数【数组遍历赋值】
(1)题目描述:
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
输入3,输出1-999的数组
输入2,输出1-99的数组
输入1,输出[1,2,3,4,5,6,7,8,9]
(2)代码实例
public int[] printNumbers(int n) {
int length = (int)Math.pow(10,n)-1;
int[] res = new int[length];
for(int i=0;i<length;i++) {
res[i] = i+1;
}
return res;
}
(15)《剑指offer》0~n-1中缺失的数字
public int missingNumber(int[] nums) {
int[] array = new int[nums.length];
for(int i=0;i<nums.length;i++) {
array[i] = i;
}
for(int i=0;i<nums.length;i++) {
if(array[i]!=nums[i])return array[i];
}
return nums.length;
}
(18)⭐️两个有序数组,分别找出两个数组中不同的值【双指针】
例如两个数组:
{1,2,3,4,7,9}
{2,3,5,6,7,8,10}
输出结果:
{1,4,9}
{5,6,8,10}
public class array_03 {
public static void main(String[] args) {
int array01[]={1,2,3,4,7,9};
int array02[]={2,3,5,6,7,8,10};
int len01=array01.length;
int len02=array02.length;
int i=0;
int j=0;
ArrayList arrayListI = new ArrayList();
ArrayList arrayListJ = new ArrayList();
while (i<len01 || j<len02) {
if(i==len01){
arrayListJ.add(array02[j]);
j++;
} else if(j==len02){
arrayListI.add(array01[i]);
i++;
} else if(array02[j]==array01[i]){
i++;
j++;
} else if(array02[j]>array01[i]){
arrayListI.add(array01[i]);
i++;
} else if(array02[j]<array01[i]){
arrayListJ.add(array02[j]);
j++;
}
}
System.out.println(arrayListI.toString());
System.out.println(arrayListJ.toString());
}
}
(19)⭐️合并两个有序递增的数组,成一个新的有序递增的数组【双指针】
(1)题目描述
给出一个有序的整数数组 A 和有序的整数数组 B ,请将数组 B 和数组 A 中合并,变成一个有序的升序数组
输入:[4,5,6],[1,2,3]
返回值:[1,2,3,4,5,6]
(2)解法一:双指针遍历
用一个while循环来解决,同时要判断如何判断数组长度边界的问题
public class array_06 {
public static void main(String[] args) {
int array01[]={4,5,6};
int array02[]={1,2,3};
ArrayList<Integer> arrayList = new ArrayList<>();
int len01=array01.length;
int len02=array02.length;
int i=0;
int j=0;
while (i<len01||j<len02) {
if(i==len01){
arrayList.add(array02[j]);
j++;
} else if(j==len02){
arrayList.add(array01[i]);
i++;
} else if(array02[j]==array01[i]){
arrayList.add(array01[i]);
arrayList.add(array02[j]);
i++;
j++;
} else if(array02[j]>array01[i]){
arrayList.add(array01[i]);
i++;
} else if(array02[j]<array01[i]){
arrayList.add(array02[j]);
j++;
}
}
System.out.println(arrayList.toString());
}
}
(22)⭐️三数之和
(1)题目描述
给出一个有n个元素的数组S,S中是否有元素a,b,c满足a+b+c=0?找出数组S中所有满足条件的三元组。
注意:三元组(a、b、c)中的元素必须按非降序排列。(即a≤b≤c),解集中不能包含重复的三元组。
输入:[-2,0,1,1,2]
返回值:[[-2,0,2],[-2,1,1]]
(2)问题分析
三个数据,所以原来的双指针思路就不适用了,我们要先确定一个值,然后在剩下的范围内用碰撞指针来找
(3)解法一
1-先使用冒泡排序进行排序
2-然后双层循环,第二层循环用对撞指针来找目标值
public class array_13 {
public static void main(String[] args) {
// int array[] = {-10,0,10,20,-10,-40};
// int array[] = {-2,0,0,2,2};
int array[] = {-4,-2,-2,-2,0,1,2,2,2,3,3,4,4,6,6};
int len = array.length;
ArrayList<ArrayList<Integer>> arrayLists = new ArrayList<ArrayList<Integer>>();
// 先进行排序
int temp = 0;
for (int j = 1; j < array.length; j++) {
for (int i = 0; i < array.length - j; i++) {
if (array[i] > array[i + 1]) {
temp = array[i + 1];
array[i + 1] = array[i];
array[i] = temp;
}
}
}
int left=0;
while (left<len-2) {
// left指针的也要去重
if(left!=0 && array[left]==array[left-1]){
left++;
continue;
}
int target = 0-array[left];
int middle=left+1;
int right=len-1;
while (middle<right) {
if(array[middle]+array[right]==target){
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(array[left]);
arrayList.add(array[middle]);
arrayList.add(array[right]);
arrayLists.add(arrayList);
// 因为前面已经排好序了,所以可以判断相邻的数据是否相等,从而去重
while (middle<right && array[middle]==array[middle+1]) {
middle++;
}
while (middle<right && array[right]==array[right-1]) {
right--;
}
middle++;
right--;
} else if(array[middle]+array[right]<target){
middle++;
} else if(array[middle]+array[right]>target){
right--;
}
}
left++;
}
System.out.println(arrayLists.toString());
}
}
(23)⭐️缺失的第一个正整数
(1)题目描述
给定一个无重复元素的整数数组nums,请你找出其中没有出现的最小的正整数
输入:[1,0,2]
返回值:3
输入:[-2,3,4,1,5]
返回值:2
输入:[4,5,6,8,9]
返回值:1
(2)问题分析
首先,数据是不重复的,其次,数据不是有序的。可以先给数组排序,然后进行遍历,判断各种情况
(3)第一遍解题
使用冒泡排序进行排序,然后根据不同的情况加判断
public static int minNumberDisappeared (int[] nums) {
// write code here
int len = nums.length;
for (int i = 1; i < len; i++) {
for (int j = 0; j < len-i; j++) {
if(nums[j]>nums[j+1]){
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
// Arrays.sort(nums);
int k=0;
// 如果数组第一个值就大于1,那最小正整数就是1
if(nums[0]>1){
return 1;
}
while (k<len-1) {
// 如果相邻的两个数都小于等于1,那就递增
if(nums[k]<=1 && nums[k+1]<=1){
k++;
continue;
} else if(nums[k]<1 && nums[k+1]>1){
// 如果前一个值小于1,后一个数大于1
return 1;
} else if(nums[k]>=1 && nums[k+1]>nums[k]+1){
// 如果前一个数大于等于1,后一个数大于它+1
return nums[k]+1;
} else {
// 其他的情况,例如nums[k+1]=nums[k]+1
k++;
continue;
}
}
// 如果遍历到最后也没有找到,那么这个值就是最后一个数+1
return nums[len-1]+1;
}
(4)第二遍解题
上面使用了冒泡排序,结果时间超时了,直接使用Arrays.sort()方法进行排序,就不超时了。同时对代码进行一些精简
public int minNumberDisappeared (int[] nums) {
// write code here
int len = nums.length;
Arrays.sort(nums);
int k=0;
if(nums[0]>1){
return 1;
}
while (k<len-1) {
if(nums[k]<1 && nums[k+1]>1){
return 1;
} else if(nums[k]>=1 && nums[k+1]>nums[k]+1){
return nums[k]+1;
}
k++;
}
return nums[len-1]+1;
}
(5)另一种思路,使用HashMap
使用哈希表记录数据是否出现过,使用containsKey来判断
public static int minNumberDisappeared2 (int[] nums) {
int len = nums.length;
HashMap<Integer,Integer> hashMap = new HashMap<>();
int i=0;
while (i<len) {
hashMap.put(nums[i],1);
i++;
}
int j=1;
while (hashMap.containsKey(j)) {
j++;
}
return j;
}
这种方式简单,耗时跟上面差不多,但是空间占用大了很多。
【2】二分查找
(1)⭐️升序数组找target
(1)题目描述
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
(2)问题分析
也可以直接遍历判断,但是使用二分查找会更快
(3)解题代码
public static int search(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len-1;
while (left<=right) {
int middle = (right-left)/2+left;
if(nums[middle]>target){
right=middle-1;
} else if(nums[middle]<target){
left=middle+1;
} else {
return middle;
}
}
return -1;
}
(2)搜索插入位置
(1)题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
输入: nums = [1,3,5,6], target = 5
输出: 2
(2)解题代码
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int left = 0, right = n - 1, ans = n;
while (left <= right) {
int mid = ((right - left)/2) + left;
if (target <= nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
(3)《剑指offer》6-回旋数组的最小数字【数组二分查找】
(1)题目描述:
有一个长度为 n 的递增数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
数据范围:1≤n≤10000,数组中任意元素的值: 0≤val≤10000
要求:空间复杂度:O(1) ,时间复杂度:O(logn)
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
(2)旋转数组特性:
1-包含两个有序序列
2-最小数一定位于第二个序列的开头
3-前序列的值都>=后序列的值
(3)解法一:暴力法
主要通过对数组遍历获取最小值(此方法一般不推荐使用)
算法流程:
1、特殊情况,如果数组为空,则直接返回0
2、创建最小值 minx
3、遍历数组每一个元素num,并更新最小值 minx = min(minx,num)
4、遍历结束,直接返回 minx
时间复杂度O(N):N表示数组的长度,遍历整个数组O(N)
空间复杂度O(1):仅使用一个额外空间变量O(1)
int minNumberInRotateArray(int* rotateArray, int rotateArrayLen ) {
if (rotateArrayLen==0 || rotateArray==null) {
return 0;
}
int min = rotateArray[0];
//遍历数组,取出每个元素
for (int i = 0; i < rotateArray.length; i++) {
//遍历到的元素和变量min比较
//如果数组元素小于min
if (arr[i] < min) {
//max记录住大值
min = arr[i];
}
}
return min;
}
(4)二分法
排序数组的查找问题首先考虑使用 二分法 解决,其可将 遍历法 的 线性级别 时间复杂度降低至 对数级别
算法流程:
1、初始化: 声明 i, j 双指针分别指向 array 数组左右两端
2、循环二分: 设 m = (i + j) / 2 为每次二分的中点( “/” 代表向下取整除法,因此恒有 i≤m1、当 array[m] > array[j] 时: m 一定在 左排序数组 中,即旋转点 x 一定在 [m + 1, j] 闭区间内,因此执行 i = m + 1
2、当 array[m] < array[j] 时: m 一定在 右排序数组 中,即旋转点 x 一定在[i, m]闭区间内,因此执行 j = m
3、当 array[m] = array[j] 时: 无法判断 mm 在哪个排序数组中,即无法判断旋转点 x 在 [i, m] 还是 [m + 1, j] 区间中。解决方案: 执行 j = j - 1 缩小判断范围
3、返回值: 当 i = j 时跳出二分循环,并返回 旋转点的值 array[i] 即可。
时间复杂度O(logN):N表示数组的长度,二分查找O(logN)
空间复杂度O(1):仅使用常数(i, j, m)额外空间变量O(1)
public int minNumberInRotateArray(int [] array) {
if(array.length==0){
return 0;
}
int low=0;
int high=array.length-1;
int mid=0;
//当头指针<尾指针时,说明没有遍历完,接着执行
while(low<high){
//当前面的值<后面的值,说明前部分是小,后部分为大
if(array[low]<array[high]){
return array[low];
}
//找到数组的中点
mid=low+(high-low)/2;
//如果中间值大于前面值,说明中间值处在第一个序列里
//那就继续缩小范围,努力往第二个序列靠近
if(array[mid] > array[low]){
low = mid + 1;
} else if(array[mid] < array[high]){
//如果中间值小于后面值,说明中间值处在第二个序列里
//那就继续缩小范围,努力往第一个序列靠近
high = mid;
} else {
//low值往第二个序列靠近,high往一个序列靠近,最终找到两个序列的交接处,那个值就是最小值
low++;
}
}
return array[low];
}
思路分析:
二分查找变种,没有具体的值用来比较。那么用中间值和高低位进行比较,看处于递增还是递减序列,进行操作缩小范围。
处于递增:low上移
处于递减:high下移(如果是high-1,则可能会错过最小值,因为找的就是最小值)
其余情况:low++缩小范围
(5)根据旋转数组的特性进行遍历(如果前一个数比后一个数大,则后一个数就是最小值)
(4)《剑指offer》1-二维数组中的查找【数组、查找】
(1)题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。
数据范围:矩阵的长宽满足 0≤n,m≤5000≤n,m≤500 , 矩阵中的值满足 0 \le val \le 10^90≤val≤10
9
进阶:空间复杂度 O(1),时间复杂度 O(n+m)
解法一:暴力法
使用嵌套循环,遍历数组中的每一个值进行判断,直到匹配到目标值为止
public class Solution {
public boolean Find(int target, int [][] array) {
for(int i=0;i<array.length;i++){
for(int j=0;j<array[0].length;j++){
if(array[i][j]==target){
return true;
}
}
}
return false;
}
}
注意点:
1-array.length表示一行的个数
2-array[0]表示第一列,array[0].length表示第一列的个数
解法二:从左下角开始找
数组的重点一个是排序,一个是查找,题意中的数组已经具备了排序的特点,那就可以更方便的进行查找了,参照二分法
public class Solution {
public boolean Find(int target, int [][] array) {
int rows = array.length;
int cols = array[0].length;
if(rows<=0 || cols<=0){
return false;
}
int row = rows-1;
int col = 0;
while(row>=0 && col<cols){
if(array[row][col]<target){
col++;
} else if(array[row][col]>target){
row--;
} else {
return true;
}
}
return false;
}
}
(9)《剑指offer》37 统计数字在排序数组中出现的次数【二分查找法】
(1)题目描述:
统计一个数字在排序数组中出现的次数。例如,输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于数字3在该数组中出现了4次,所以函数返回4。
(2)解题思路:
既然输入的数组是有序的,所以我们就能很自然的想到用二分查找算法。以题目中给的数组为例,一个比较自然的想法是用二分查找先找到一个3,由于要计算的是输出的次数,所以需要在找到的这个3的左右两边分别再进行顺序扫描,进而得到3的个数,这样最坏的情况下时间复杂度仍然是O(n),和直接顺序扫描的效率相同。
因此,需要考虑怎样更好的利用二分查找算法,由于数组有序,如果知道了第一个k出现的位置和最后一个k出现的位置,那么我们就可以直接算出有多少个k。因此将思路转化为通过二分查找求第一个和最后一个k出现的位置。
以第一个k出现的位置为例,利用二分查找算法可以直接对数组进行二分,而每次总是拿中间的数字和k做比较,如果中间的数字大于k,那么第一个k只有可能出现在左边,下一次直接在数组左半段继续进行二分查找;如果中间的数字小于k,则第一个k只有可能出现在右边,则在右半段再查找;如果中间的数字等于k,我们先判断它前面的一个数字是不是k,如果不是,那么这个中间的数字就是第一个出现的位置,反之,如果中间数字前面的数字是k,那么第一个k仍然在前半段,继续查找。
同理,找最后一个k出现的位置方法类似,可以使用两个函数分别获得。
(3)解法一(推荐):
再有因为数组中全是整数,因此我们可以考虑,用二分查找找到k+0.5k+0.5k+0.5应该出现的位置和k−0.5k-0.5k−0.5应该出现的位置,二者相减就是k出现的次数。
step 1:写一个二分查找的函数在数组中找到某个元素出现的位置。每次检查区间中点值,根据与中点的大小比较,确定下一次的区间。
step 2:分别使用二分查找,找到k+0.5和k-0.5应该出现的位置,中间的部分就全是k,相减计算次数就可以了。
public class Solution {
//二分查找
private int bisearch(int[] data, double k){
int left = 0;
int right = data.length - 1;
//二分左右界
while(left <= right){
int mid = (left + right) / 2;
if(data[mid] < k)
left = mid + 1;
else if(data[mid] > k)
right = mid - 1;
}
return left;
}
public int GetNumberOfK(int [] array , int k) {
//分别查找k+0.5和k-0.5应该出现的位置,中间的部分就全是k
return bisearch(array, k + 0.5) - bisearch(array, k - 0.5);
}
}
(4)解法二:
//看自己写的
public class shuzu_08 {
public static void main(String[] args) {
int[] array = new int[]{1,2,3,3,3,4};
int first = fing_first_key(array,3);
int last = find_last_key(array,3);
int number = last - first + 1;
System.out.println(first);
System.out.println(last);
System.out.println(number);
}
//找第一个K
public static int fing_first_key(int[] array,int key) {
if(array == null || array.length == 0) {
return -1;
}
int low = 0;
int high = array.length-1;
while(low <= high) {
int mid = (low + high) / 2;
if(array[mid] < key) {
low = mid + 1;
}else if(array[mid] > key) {
high = mid -1;
}else {
mid = mid -1;
if(array[mid] == key) {
high = mid;
}else {
return mid + 1;
}
}
}
return -1;
}
//找最后一个K
public static int find_last_key(int[] array,int key) {
if(array == null || array.length == 0) {
return -1;
}
int low = 0;
int high = array.length-1;
while(low <= high) {
int mid = (low + high) / 2;
if(array[mid] < key) {
low = mid + 1;
}else if(array[mid] > key) {
high = mid -1;
}else {
mid = mid + 1;
if(array[mid] == key) {
low = mid;
}else {
return mid - 1;
}
}
}
return -1;
}
}
(5)解法三:
import java.util.Arrays;
public class Practise_09 {
public static void main(String[] args) {
int[] array = new int[]{1,2,3,3,3,4,5};
int times = getNumberOfK(array,3);
System.out.println("原数组:"+Arrays.toString(array));
System.out.println("3出现的次数:"+times);
}
//查找第一个K,和最后一个K,返回二者下标相减+1,即k有多少个
public static int getNumberOfK(int[] array,int k ) {
int first = getFirstNumber(array,k);
int last = getLastNumber(array,k);
if(first == -1 || last == -1 ) {
return 0;
}
return last-first+1;
}
//查找第一个K出现时的下标
public static int getFirstNumber(int[] array,int k) {
int result = -1;
if(array==null || array.length==0) {
return result ;
}
int low = 0,high = array.length-1;
while(low <= high) {
int mid = low+(high-low)/2;
if(array[mid]<k) { //小于 K
low = mid+1;
}
else if(array[mid]>k) { //大于 K
high = mid - 1;
}
else{ //等于 K的时候,因为我们要找的是第一个k,所以不确定中间的数是不是第一个,所以还要怕都难它前面的一个数
mid = mid - 1;
if(mid<low || array[mid]!=k) {
return mid+1;
}
else {
high = mid;
}
}
}
return result;
}
//查找最后一个K出现时的下标
public static int getLastNumber(int[] array,int k) {
int result = -1;
if(array==null || array.length==0) {
return result ;
}
int low = 0,high = array.length-1;
while(low <= high) {
int mid = low+(high-low)/2;
if(array[mid]<k) {
low = mid+1;
}
else if(array[mid]>k) {
high = mid - 1;
}
else{ //上面的函数和这个函数唯一的区别是这里开始
mid = mid + 1;
if(mid>high || array[mid]!=k) {
return mid-1;
}
else {
low = mid;
}
}
}
return result ;
}
}
【3】动态规划、贪心算法
(6)⭐️《剑指offer》30-连续子数组的最大和【动态规划、贪心】
(1)题目:输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。
(2)动态规划(时间复杂度O(n),空间复杂度O(n))
这个3是原数组的3,所以子数组应该从这个位置开始算起
思路分析:最基础的动态规划的题目:最大子数组的和一定是由当前元素和之前最大连续子数组的和叠加在一起形成的,因此需要遍历n个元素,看看当前元素和其之前的最大连续子数组的和能够创造新的最大值。
public class array_10 {
public static void main(String[] args) {
int array[] = {1,-2,3,10,-4,7,2,-5};
int len=array.length;
int i = 1;
int this_val;
int max_val = array[0];
int temp[] = new int[len];
temp[0] = array[0];
while (i<len) {
this_val = array[i]+temp[i-1];
temp[i] = this_val>array[i]?this_val:array[i];
max_val = max_val>temp[i]?max_val:temp[i];
i++;
}
System.out.println(max_val);
}
}
(7)《剑指offer》32 把数组排成最小的数【排序】
(1)题目描述:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
(2)排序
排序就是将一个线性数据的元素按照一定的次序进行重排,这个次序可能是递增序、也可能是递减序,还有可能是自己定义的顺序(重载),常用的方法一般有快速排序、堆排序、归并排序、冒泡排序、选择排序等。再程序中的可以使用sort函数,它是基于优化后的快速排序。
(3)解法一:重载比较的排序(推荐使用)
step 1:优先判断空数组的特殊情况。
step 2:将数组中的数字元素转换成字符串类型。
step 3:重载排序比较为字符串类型的x + y < y + x,然后进行排序。
step 4:将排序结果再按照字符串拼接成一个整体。
当A = 206,B = 1
此时 A+B = 2061 B+A = 1206
我们可以看到 A+B > B+A,所以很明显B需要放在A的前面,即B+A,才能使得拼出来的数字最小。
public String PrintMinNumber(int [] numbers) {
//优先判断空数组的特殊情况。
if(numbers.length==0) return "";
String[] str=new String[numbers.length];
StringBuilder sb=new StringBuilder();
//将数组中的数字元素转换成字符串类型,放进字符串数组str
for(int i=0;i<numbers.length;i++)
{
str[i]=String.valueOf(numbers[i]);
}
//
Arrays.sort(str,new Comparator<String>(){
public int compare(String str1,String str2){
String c1=str1+str2;
String c2=str2+str1;
//小于返回-1
return c1.compareTo(c2);
}
});
for(int i=0;i<str.length;i++){
sb.append(str[i]);
}
return sb.toString();
}
本题最直观的解法就是求出数组中所有数字的全排列,然后比较所有的排列,最后找到最小的排列,但是时间复杂度为O(n!),所以不是一个好的解法。
换一种思路可以发现,本题实际上希望我们找到一个排序规则,数组根据这个排序规则进行重排之后可以连成一个最小的数字。要确定这样的排序规则,也就是对于两个数字m和n,通过一个规则确定哪个应排在前面。
根据题目要求,我们可以发现,两个数字m和n能拼接成mn和nm,如果mn<nm,那m应该在前;如果nm<mn,那么n应该在前。因此,我们得到的排序规则如下:
若mn>nm,则m大于n
若mn<nm,则m小于n
若mn=nm,则m等于n
根据上述规则,我们需要先把数字转换成字符串再进行比较,因为需要拼接起来。比较完之后按顺序连接成一个字符串即可。
①用自定义规则排好序
②然后把排序结果丢到result就可以了
import java.util.Arrays;
import java.util.Comparator;
public class Practise_07 {
public static void main(String[] args) {
int[] array = new int[]{3,2,1};
System.out.println("原来的数组:"+Arrays.toString(array));
System.out.println("处理后的数:"+PrintMinNumber(array));
}
private static String PrintMinNumber(int[] array) {
//result用来接收结果,即最小的数
String result = "";
//判空
if(array==null || array.length==0) {
return result;
}
//用str数组来接收整数数组的数
String[] str = new String[array.length];
for(int i=0;i<array.length;i++) {
str[i] = String.valueOf(array[i]);
}
//因为数据是引用类型数据不是基本数据类型,因此这里的排序需要用自定义的规则来排序,自定义规则需要重写Compare方法
Arrays.sort(str,new Comparator<String>(){
public int compare(String str1,String str2) {
String c1 = str1+str2;
String c2 = str2+str1;
return c1.compareTo(c2);
}
});
//把从小到大排好序的str数组的数逐一加到result中
for(String s:str) {
result+=s;
}
return result;
}
}
(4)冒泡排序(扩展思路)
如果觉得重载比较的方式自己掌握不太熟练,那我们可以直接尝试冒泡排序,冒泡排序过程要求比较数组相邻位置元素的大小,我们可以改成比较数组相邻字符串拼接之后的大小。
step 1:优先判断空数组的特殊情况。
step 2:将数组中的数字元素转换成字符串类型。
step 3:使用冒泡排序,两层遍历数组,每次比较数组相邻位置字符串拼接的大小,如果顺序拼接比逆序拼接更大,则需要交换位置。
step 4:将排序结果再按照字符串拼接成一个整体。
import java.util.*;
public class Solution {
public String PrintMinNumber(int [] numbers) {
//空数组的情况
if(numbers == null || numbers.length == 0)
return "";
String[] nums = new String[numbers.length];
//将数字转成字符
for(int i = 0; i < numbers.length; i++)
nums[i] = numbers[i] + "";
//冒泡排序
for(int i = 0; i < nums.length - 1; i++){
for(int j = 0; j < nums.length - i - 1; j++){
String s1 = nums[j] + nums[j + 1];
String s2 = nums[j + 1] + nums[j];
//比较拼接的大小交换位置
if(s1.compareTo(s2) > 0){
String temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
StringBuilder res = new StringBuilder();
//字符串叠加
for(int i = 0; i < nums.length; i++)
res.append(nums[i]);
return res.toString();
}
}
(8)《剑指offer》35 数组中的逆序对【递归、归并排序】
(1)题目描述:
如2431中,21,43,41,31是逆序,逆序数是4,为偶排列。
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。
输入描述:
题目保证输入的数组中没有的相同的数字数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
(2)暴力统计法
public class Solution {
public int InversePairs(int[] array) {
int cnt = 0;
int len = array.length;
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
if (array[i] > array[j]) {
cnt++;
}
}
}
return cnt;
}
}
(3)解题思路:
本题一个最容易想到的解法是暴力解法,顺序扫描整个数组,每扫描到一个数字时,逐个比较该数字与后面的数字的大小关系,统计逆序对的个数,假设数组中有n个数字,则每个数字都要和O(n)个数字做比较,因此,这个暴力解法的时间复杂度为O(n^2)。
一般情况下,最容易想到的往往不是最优解法。在这里,我们采用分治的思想,类比归并排序算法来分析此题。
首先将数组分隔成子数组,统计出子数组内部逆序对数目,然后再统计相邻子数组之间的逆序对数目,统计过程中还需要对数组进行排序,这实际上就是归并排序的过程。主要考虑的是合并两个有序序列时,计算逆序对数。对于两个有序升序序列,设置两个下标分别指向开始位置,每次比较两个指针对应的值,如果第一个序列当前值大于第二个序列当前值,则有第一个序列“当前长度”个逆序对。
这看起来好像比较拗口,但是从代码中可以直观看出。
public class Solution {
int result=0;
public int inversePairs(int [] array) {
//数组中的逆序对,归并排序
if(array==null || array.length==0) //因为查找的是多少对逆序对,所以如果数组为空,则返回的是0对
return 0;
findInversePairs(array,0,array.length-1);
return result%1000000007;
}
public void findInversePairs(int[] array,int low,int high){
if(low<high){
int mid=low+(high-low)/2;
findInversePairs(array,low,mid); //左一半递归
findInversePairs(array,mid+1,high); //右一半递归
merge(array,low,mid,high); //合并merge
}
}
public void merge(int[] array,int low,int mid,int high){
int i=low,j=mid+1;
int k=0; //k是下面定义数组的索引
int[] temp=new int[high-low+1];
while(i<=mid && j<=high){
if(array[i]<=array[j])
temp[k++]=array[i++];
else { //大于,说明是逆序
temp[k++] = array[j++];
result+= (mid - i + 1); //这是因为,左边的数组是从小到大排的,如果前面的i比后面的j对应的元素大,则i到mid的所有元素都比j要大。(如果第一个序列当前值大于第二个序列当前值,则有第一个序列“当前长度”个逆序对。)因为左边的数组,是从小到大排的,如果第一个比后面的大,则第二个数也会比后面的大,所以有第一个序列“当前长度”个逆序对。
result= result%1000000007;
}
}
while(i<=mid) //比较完之后,左一半递归还剩下的元素,丢到temp数组中
temp[k++]=array[i++];
while(j<=high) //比较完之后,右一半递归还剩下的元素,丢到temp数组中
temp[k++]=array[j++];
for(i=0;i<temp.length;i++) //这里是归并排序的操作,将temp中的元素全部拷贝到原数组中
array[low+i]=temp[i];
}
}
(12)《剑指offer》51 构建乘积数组【三角计算、累乘】
(1)题目描述
给定一个数组 A[0,1,…,n-1] ,请构建一个数组 B[0,1,…,n-1] ,其中 B 的元素 B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1](除 A[i] 以外的全部元素的的乘积)。程序中不能使用除法。(注意:规定 B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2])
对于 A 长度为 1 的情况,B 无意义,故而无法构建,用例中不包括这种情况。
(2)解法一:双向遍历(推荐使用)
B[i]=A[0]∗A[1]∗…∗A[i−1]∗A[i+1]∗…∗A[n−1]如上图所示,矩阵中由对角线1将其分成了上三角和下三角。我们先看下三角,如果我们累乘的时候,B[1]是在B[0]的基础上乘了新增的一个A[0],B[2]是在B[1]的基础上乘了新增的一个A[1],那我们可以遍历数组的过程中不断将数组B的前一个数与数组A的前一个数相乘就得到了下三角中数组B的当前数。同理啊,我们在上三角中,用一个变量存储从右到左的累乘,每次只会多乘上一个数字。这样,两次遍历就可以解决。
step 1:初始化数组B,第一个元素为1.
step 2:先算下三角的乘积
从左到右遍历数组A,将数组B的前一个数与数组A的前一个数相乘就得到了下三角中数组B的当前数。
B[0]=1
B[1]=B[0] * A[0]
B[2]=B[1] * A[1](也就是:B[0] * A[0] * A[1])
B[3]=B[2] * A[2](也就是:B[0] * A[0] * A[1] * A[2])
…
B[i]=B[i-1] * A[i-1](也就是:B[0] * A[0] * A[1] * A[2] * … * A[i-1])
step 3:再算上三角,从右下方开始
使用一个temp作为临时值方便计算
再从右到左遍历数组A,用一个数字记录从右到左上三角中的累乘,每次只会乘上一个数,同时给数组B对应部分也乘上该累乘。
import java.util.ArrayList;
public class Solution {
public int[] multiply(int[] A) {
//初始化数组B
int[] B = new int[A.length];
B[0] = 1;
//先乘左边,从左到右
for(int i = 1; i < A.length; i++)
//每多一位由数组B左边的元素多乘一个前面A的元素
B[i] = B[i - 1] * A[i - 1];
int temp = 1;
//再乘右边,从右到左
for(int i = A.length - 1; i >= 0; i--){
//temp为右边的累乘
B[i] *= temp;
temp *= A[i];
}
return B;
}
}
时间复杂度:O(n),其中n为数组A的长度,遍历两次数组
空间复杂度:O(1),数组B为返回必要空间,不属于额外空间
(3)另一种写法
import java.util.Arrays;
public class Practise_12 {
public static void main(String[] args) {
int[] A = new int[]{1,2,3,4,5};
int[] B = multiply(A);
System.out.println(Arrays.toString(A));
System.out.println(Arrays.toString(B));
}
public static int[] multiply(int[] A) {
/*
思路:分成两部分的乘积,第一部分可以自上而下,第二部分自下而上
*/
if(A==null||A.length<1)
return A;
int len=A.length;
int[] B=new int[len];
B[0]=1;
//计算左三角
for(int i = 1;i <= len-1;i++) { //为什么从1开始,因为B[0]上面已经算了
B[i] = B[i-1] * A[i-1];
}
//计算右三角 temp用来记录有三角每一行的值
int temp=1;
for(int i = len-2;i >= 0;i--){ //第二部分可以自下而上,为什么从len-2开始,本来是从len-1开始的,但是temp=1就是len-1的值为1嘛,所以从len-2开始
temp=temp * A[i+1];
B[i]=B[i] * temp;
}
return B;
}
}
(16)⭐️《剑指offer》买卖股票的最好时机【贪心算法+双指针】
(1)题目描述:
假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天
2.如果不能获取到任何利润,请返回0
3.假设买入卖出均无手续费
(2)注意细节
1-卖出股票之前必须先买入
2-卖出的价格>买入时候的价格(得考虑利润)
3-给一个数组模拟股票,换句话说,就是查找数组中某两个元素差值的最大。
4-如果无利可图,请return 0
(3)解法一:双指针
public class array_09 {
public static void main(String[] args) {
int array[] = {8,9,2,5,4,7,1};
int len = array.length;
int left = 0;
int right = 0;
int this_val = 0;
int max_val = 0;
while (right<len && left<len) {
if(array[left]==array[right]){
this_val = 0;
right++;
} else if(array[left]>array[right]){
this_val = 0;
left++;
} else if(array[left]<array[right]){
this_val = array[right]-array[left];
right++;
}
max_val = max_val>this_val?max_val:this_val;
}
System.out.println(max_val);
}
}
(17)《剑指offer》 礼物的最大价值【递归、动态规划】
(1)题目描述
在一个m\times nm×n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
如输入这样的一个二维数组,
[[1,3,1],
[1,5,1],
[4,2,1]]
那么路径 1→3→5→2→1 可以拿到最多价值的礼物,价值为12
(2)解法一:递归
同样考虑我们在最右下角这个位置,它可以选择来自左边的格子过来,也可以选择来自上边的格子过来,这样我们的问题可以从(m,n)(m,n)(m,n)分解成(m−1,n)(m-1,n)(m−1,n)与(m,n−1)(m,n-1)(m,n−1)这两个子问题,取子问题的最大值加上右下角这个礼物价值,因此可以用递归:
- 终止条件: 到达右上角起点的时候,终止递归,即m与n都等于0.
- 返回值: 将子问题的较大值加上本格子的礼物价值返回。
- 本级任务: 判断是否是矩阵遍历,进入子问题计算。
step 1:使用一个二维辅助数组dp记录递归过程中的中间值。
step 2:递归时先判断是否到起点与是否到边界,边界只有一条递归路线。
step 3:如果dp数组中中有记录的值,直接返回,否则进入子问题求解后加到数组中。
import java.util.*;
public class Solution {
private int recursion(int[][] grid, int m, int n, int[][] dp){
//到达起点
if(m == 0 && n == 0){
dp[0][0] = grid[0][0];
return grid[0][0];
}
//两个边界
if(m == 0)
dp[0][n] = grid[0][n] + recursion(grid, m, n - 1, dp);
if(n == 0)
dp[m][0] = grid[m][0] + recursion(grid, m - 1, n, dp);
//如果有值可以直接返回
if(dp[m][n] == 0)
//递归求左边或者上边的最大值
dp[m][n] = grid[m][n] + Math.max(recursion(grid, m - 1, n, dp), recursion(grid, m, n - 1, dp));
return dp[m][n];
}
public int maxValue (int[][] grid) {
int m = grid.length;
int n = grid[0].length;
//用于记忆递归过程中的值
int[][] dp = new int[m][n];
return recursion(grid, m - 1, n - 1, dp);
}
}
时间复杂度:O(mn),其中m、n分别为矩阵的边长,最坏递归需要填满dp数组
空间复杂度:O(mn),递归栈及记录中间值的辅助数组
(3)解法二:动态规划
step 1:初始化第一列,每个元素只能累加自上方。
step 2:初始化第一行,每个元素只能累加自左方。
step 3:然后遍历数组,对于每个元素添加来自上方或者左方的较大值。
import java.util.*;
public class Solution {
public int maxValue (int[][] grid) {
int m = grid.length;
int n = grid[0].length;
//第一列只能来自上方
for(int i = 1; i < m; i++)
grid[i][0] += grid[i - 1][0];
//第一行只能来自左边
for(int i = 1; i < n; i++)
grid[0][i] += grid[0][i - 1];
//遍历后续每一个位置
for(int i = 1; i < m; i++)
for(int j = 1; j < n; j++)
//增加来自左边的与上边的之间的较大值
grid[i][j] += Math.max(grid[i - 1][j], grid[i][j - 1]);
return grid[m - 1][n - 1];
}
}
(20)⭐️最长无重复子数组【贪心算法+双指针递增】
(1)题目描述
给定一个长度为n的数组arr,返回arr的最长无重复元素子数组的长度,无重复指的是所有数字都不相同。
子数组是连续的,比如[1,3,5,7,9]的子数组有[1,3],[3,5,7]等等,但是[1,3,7]不是子数组
输入:[1,2,3,1,2,3,2,2]
返回值:3
说明:最长子数组为[1,2,3]
(2)问题分析
这一题的要求只是计算最长的不重复长度,并没有要求计算具体的子字符串。我们的思路可以使用双指针来确定子字符串首尾的位置,然后使用HashSet来判断每个位置是否重复。
(3)解法一
1-首先有两个指针,left负责尾部,right负责头部,还有一个HashSet用contains判断是否重复
2-right遍历往前走,每一步判断HashSet是否已存在对象,如果不存在就继续往前走,并且把对象放进HashSet
3-如果right的对象在HashSet中已经存在了,说明left和right之间存在了重复对象,于是left从尾部往前走,挨个remove对象,直到把重复对象之前的对象都删除掉
4-此时没有重复对象了,计算一下这个时候的长度,并且跟已保存的最长长度比较,保留最长的长度
public class array_07 {
public static void main(String[] args) {
int array[] = {1,2,3,1,2,3,2,2};
int len=array.length;
int left=0;
int right=0;
HashSet<Integer> hashSet = new HashSet<>();
int max_len = 0;
while (right<len) {
if(!hashSet.contains(array[right])){
hashSet.add(array[right]);
right++;
} else {
while (hashSet.contains(array[right])) {
hashSet.remove(array[left]);
left++;
}
}
int this_len = right-left;
max_len = max_len>this_len?max_len:this_len;
}
System.out.println(max_len);
}
}
(21)⭐️盛水最多的容器【贪心算法+双指针碰撞】
(1)题目描述
给定一个数组height,长度为n,每个数代表坐标轴中的一个点的高度,height[i]是在第i点的高度,请问,从中选2个高度与x轴组成的容器最多能容纳多少水
1.你不能倾斜容器
2.当n小于2时,视为不能形成容器,请返回0
3.数据保证能容纳最多的水不会超过整形范围,即不会超过231-1
如输入的height为[1,7,3,2,4,5,8,2,7],那么如下图:
输入:[1,7,3,2,4,5,8,2,7]
返回值:49
(2)问题分析
跟上一题求最长无重复子数组长度一样,我们也是要保留一个最大的记录,这里使用贪心算法+双指针,不同的是这次双指针要改成碰撞指针,不能是单向的了
(3)解法一
1-头部指针left,尾部指针right,谁对应的值小谁就前进一步
2-每次前进的时候都计算新的容积,然后跟记录的最大容积比较,保留较大的那一个值
public class array_08 {
public static void main(String[] args) {
int array[] = {1,7,3,2,4,5,8,2,7};
int len=array.length;
if(len<2){
return 0;
}
int left = 0;
int right = len-1;
int this_cap = 0;
int max_cap=0;
while (left<right) {
if(array[left]<array[right]){
this_cap = array[left]*(right-left);
left++;
} else if(array[left]>array[right]){
this_cap = array[right]*(right-left);
right--;
} else {
this_cap = array[left]*(right-left);
left++;
right--;
}
max_cap = max_cap>=this_cap?max_cap:this_cap;
}
System.out.println(max_cap);
}
}
(22)合并区间
(1)题目描述
给出一组区间,请合并所有重叠的区间。请保证合并后的区间按区间起点升序排列。
输入:[[10,30],[20,60],[80,100],[150,180]]
返回值:[[10,60],[80,100],[150,180]]
(2)题目分析
1-首先对每行数组的start值进行排序
2-遍历二维数组,
(3)解题代码
public static ArrayList minmumNumberOfHost (int[][] startEnd) {
// write code here
// 先对二维数组进行排序
Arrays.sort(startEnd, (o1, o2) -> {
return o1[0] - o2[0];
});
// 使用ArrayList存放结果
ArrayList<int[]> res = new ArrayList<>();
// 先把第一个值放进去
res.add(startEnd[0]);
int count = 0;
int len = startEnd.length;
// 从第二个子数组开始遍历
int i=1;
while (i<len) {
if(startEnd[i][0]>res.get(count)[1]){
res.add(startEnd[i]);
count++;
} else {
int start = res.get(count)[0]>startEnd[i][0]?startEnd[i][0]:res.get(count)[0];
int end = res.get(count)[1]>startEnd[i][1]?res.get(count)[1]:startEnd[i][1];
res.remove(count);
int[] temp={start,end};
res.add(temp);
}
i++;
}
return res;
}
(23)主持人调度
(1)题目描述
有 n 个活动即将举办,每个活动都有开始时间与活动的结束时间,第 i 个活动的开始时间是 starti ,第 i 个活动的结束时间是 endi ,举办某个活动就需要为该活动准备一个活动主持人。
一位活动主持人在同一时间只能参与一个活动。并且活动主持人需要全程参与活动,换句话说,一个主持人参与了第 i 个活动,那么该主持人在 (starti,endi) 这个时间段不能参与其他任何活动。求为了成功举办这 n 个活动,最少需要多少名主持人。
输入:2,[[1,2],[2,3]]
返回值:1
说明:只需要一个主持人就能成功举办这两个活动
输入:2,[[1,3],[2,4]]
返回值:2
说明:需要两个主持人才能成功举办这两个活动
(2)问题分析
这题可以理解成判断前后两个数组之间有没有交集,如果有交集的话就算两个独立的,如果没有交集就可以重复使用主持人
1-遍历二维数组的行
2-判断
(3)解题代码
public int minmumNumberOfHost (int n, int[][] startEnd) {
int[] start = new int[n];
int[] end = new int[n];
//分别得到活动起始时间
for(int i = 0; i < n; i++){
start[i] = startEnd[i][0];
end[i] = startEnd[i][1];
}
//单独排序
Arrays.sort(start, 0, start.length);
Arrays.sort(end, 0, end.length);
int res = 0;
int j = 0;
for(int i = 0; i < n; i++){
//新开始的节目大于上一轮结束的时间,主持人不变
if(start[i] >= end[j])
j++;
else
//主持人增加
res++;
}
return res;
}
【4】HashSet、HashMap、Stack等
(11)⭐️《剑指offer》50 如何找到数组中的重复数字【排序算法orHashSet】
(1)题目描述:
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
(2)解题思路:
1-思路一:循环两次一个个去算有没重复,时间复杂度O(n2)
2-思路二:先排号序,然后遍历搜索重复的数字O(logn)
3-思路三:借助数组下标不可以重复,和原数组值为非负数【空间换时间,时间复杂度O(n)+空间复杂度O(n)】
新建一个数组,所有值默认设置为-1,遍历原来的数组,把原来数组的值当做index和value设置到新数组中
如果一个数在原来数组中只出现一次,那么在设置新的数组时,原来的值为-1
如果一个数在原来数组中出现1次以上,那么在设置这个index的value时,原来的value肯定不是-1,因为原来的数组值范围是0~n-1,不可能出现-1值的
4-思路四:借助HashSet或者Set的特性【时间复杂度O(n)】
Set的特性就是值不可以重复,所以如果调用add添加的时候返回值为false,那么就说明这个下标已经存在了,即为重复的数
(3)⭐️解法一:先使用冒泡排序进行数组排序,然后遍历数组判断相邻的数据是否有相等的就可以了
public class Solution {
public int duplicate (int[] numbers) {
// write code here
int temp = 0;
for (int j = 1; j < numbers.length - 1; j++) {
for (int i = 0; i < numbers.length - j; i++) {
if (numbers[i] > numbers[i + 1]) {
temp = numbers[i + 1];
numbers[i + 1] = numbers[i];
numbers[i] = temp;
}
}
}
int k = 0;
while (k < numbers.length - 1) {
if (numbers[k] == numbers[k+1]){
return numbers[k];
}
k++;
}
return -1;
}
}
(4)解法二
//思路三
public static void test3(int[] a, int len) {
// 这个方法没有对数据的有效性进行检测,有需要的自己添加
// 这个数组默认全为-1
int[] b = new int[len];
for (int i = 0; i < len; i++) {
b[i] = -1;
}
// 循环遍历原数组,如果b[a[i]] 的位置存了数据不为-1的话 说明这个数在之前已经出现过了,所以重复
for (int i = 0; i < len; i++) {
if (b[a[i]] != -1) {
System.out.println(a[i]);
}
b[a[i]] = a[i];
}
// 原理就是将这个数组中的每一个数依次放到对应位置上去,如果之前有了,那么就是重复,如果没有,那就放
}
(5)⭐️解法三:使用HashSet不可重复添加的原理,判断添加的结果
public class Solution {
public int duplicate (int[] numbers) {
// write code here
HashSet hashSet = new HashSet();
int i=0;
while(i<numbers.length-1) {
boolean addResult = hashSet.add(numbers[i]);
if(!addResult) {
return numbers[i];
}
i++;
}
return -1;
}
}
(6)⭐️解法四:使用HashMap的key不能重复的原理
(14)⭐️两数之和【辅助哈希表】
(1)题目描述
给出一个整型数组 numbers 和一个目标值 target,请在数组中找出两个加起来等于目标值的数的下标,返回的下标按升序排列。(注:返回的数组下标从1开始算起,保证target一定可以由数组里面2个数字相加得到)
输入:[3,2,4],6
返回值:[2,3]
说明:因为 2+4=6 ,而 2的下标为2 , 4的下标为3 ,又因为 下标2 < 下标3 ,所以返回[2,3]
(2)题目分析
首先这个数组不是默认排好序的,所以上面那一题的双指针碰撞就不能直接使用了。
其次要求的是结果返回下标,所以还不能进行手动的进行排序
还是尽量不要用暴力解法,暴力解法用双层遍历就可以解决
所以思路变成了使用辅助数据结构来做
(3)解题过程
public class array_12 {
public static void main(String[] args) {
int array[] = {3,2,4};
int target = 6;
int len = array.length;
HashMap<Integer,Integer> hashMap = new HashMap<>();
int[] result = new int[2];
int i = 0;
while (i<len) {
if(!hashMap.containsKey(target-array[i])){
hashMap.put(array[i],i);
} else {
int value = hashMap.get(target-array[i]);
int firstIndex = i<value?i:value;
int secondIndex = i>value?i:value;
result[0] = firstIndex+1;
result[1] = secondIndex+1;
break;
}
i++;
}
System.out.println(result.toString());
}
}
(二)⭐️⭐️字符串
字符串的基本知识
(1)遍历字符串
String str="2022 fight";
for(int i=0;i < str.length();i++) {
System.out.println(str.charAt(i));
}
(2)StringBuffer如何拼接和清空
append方法拼接
stringBuffer.setLength(0);直接把长度设置为0就可以清空了
(3)字符串String怎么拼接和清空
str += 'abc';
str = "";
(4)Char类型的字符怎么转换大小写
if(word>='A' && word<='Z'){
stringBuffer.append((char) (word-'A'+'a'));
} else if(word>='a' && word<='z'){
stringBuffer.append((char) (word-'a'+'A'));
}
(5)使用栈Stack的时候怎么遍历存放和取出
1-存放
stack.push(stringBuffer);
2-遍历取出
while (!stack.empty()) {
result.append(stack.pop());
result.append(' ');
}
(1)《剑指offer》2-替换空格【StringBuilder等、函数方法】
(1)题目: 请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
(2)解题思路:
对于这个题目,我们首先想到原来的一个空格替换为三个字符,字符串长度会增加,因此,存在以下两种不同的情况:(1)允许创建新的字符串来完成替换。(2)不允许创建新的字符串,在原地完成替换。
(3)解法一:允许创建新的字符串来完成替换【if判断然后直接替换】
第一种情况比较简单。
先遍历取出字符串里的字符,然后判断,最后使用StringBuilder把字符再拼接在一起
public class Solution {
public String replaceSpace(StringBuffer str) {
StringBuilder sb = new StringBuilder();
for(int i=0; i<str.length(); i++){
char c = str.charAt(i);
if(c==' '){
sb.append("%20");
} else {
sb.append(c);
}
}
return sb.toString();
}
}
注意点:
1-StringBuffer和StringBuilder的区别
(1)String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁
(2)StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。 StringBuffer类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全。
(3) StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。
2-char c = str.charAt(i); 用来取出字符串里的字符
字符串的方法很多,可以取某个位置的字符,可以取一段子字符串,可以比较字符串等等
3-单引号‘ ’和双引号“ ”的区别
单引号表示单个字符,双引号表示一个字符串
(4)解法二:从头到尾遍历字符串,当遇到空格时,后面所有的字符都后移2个【时间复杂度为O(n^2)的解法】
(5)解法三:【时间复杂度为O(n)的解法】
可以先遍历一次字符串,这样可以统计出字符串中空格的总数,由此计算出替换之后字符串的长度,每替换一个空格,长度增加2,即替换之后的字符串长度为原来的长度+2*空格数目。接下来从字符串的尾部开始复制和替换,用两个指针P1和P2分别指向原始字符串和新字符串的末尾,然后向前移动P1,若指向的不是空格,则将其复制到P2位置,P2向前一步;若P1指向的是空格,则P1向前一步,P2之前插入%20,P2向前三步。这样,便可以完成替换,时间复杂度为O(n)。
使用String类自带的replace方法
public class Solution {
public String replaceSpace(StringBuffer str) {
return str.toString().replace(" ","%20");
}
}
(2)《剑指offer》27-字符串的排列
(1)题目描述:输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
(2)解题思路一
都是求元素的全排列,字符串与数组没有区别,一个是数字全排列,一个是字符全排列,因此大致思路与有重复项数字的全排列类似,只是这道题输出顺序没有要求。但是为了便于去掉重复情况,我们还是应该参照数组全排列,优先按照字典序排序,因为排序后重复的字符就会相邻,后续递归找起来也很方便。
使用临时变量去组装一个排列的情况:每当我们选取一个字符以后,就确定了其位置,相当于对字符串中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归。
- 终止条件: 临时字符串中选取了n个元素,已经形成了一种排列情况了,可以将其加入输出数组中。
- 返回值: 每一层给上一层返回的就是本层级在临时字符串中添加的元素,递归到末尾的时候就能添加全部元素。
- 本级任务: 每一级都需要选择一个元素加入到临时字符串末尾(遍历原字符串选择)。
递归过程也需要回溯,比如说对于字符串“abbc”,如果事先在临时字符串中加入了a,后续子问题只能是"bbc"的全排列接在a后面,对于b开头的分支达不到,因此也需要回溯:将临时字符串刚刚加入的字符去掉,同时vis修改为没有加入,这样才能正常进入别的分支。
step 1:先对字符串按照字典序排序,获取第一个排列情况。
step 2:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
step 3:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用,也不需要将其纳入。
step 4:进入下一层递归前将vis数组当前位置标记为使用过。
step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素,
step 6:临时字符串长度到达原串长度就是一种排列情况。
import java.util.*;
public class Solution {
public void recursion(ArrayList<String> res, char[] str, StringBuffer temp, boolean[] vis){
//临时字符串满了加入输出
if(temp.length() == str.length){
res.add(new String(temp));
return;
}
//遍历所有元素选取一个加入
for(int i = 0; i < str.length; i++){
//如果该元素已经被加入了则不需要再加入了
if(vis[i])
continue;
if(i > 0 && str[i - 1] == str[i] && !vis[i - 1])
//当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了
continue;
//标记为使用过
vis[i] = true;
//加入临时字符串
temp.append(str[i]);
recursion(res, str, temp, vis);
//回溯
vis[i] = false;
temp.deleteCharAt(temp.length() - 1);
}
}
public ArrayList<String> Permutation(String str) {
ArrayList<String> res = new ArrayList<String>();
if(str == null || str.length() == 0)
return res;
//转字符数组
char[] charStr = str.toCharArray();
// 按字典序排序
Arrays.sort(charStr);
boolean[] vis = new boolean[str.length()];
//标记每个位置的字符是否被使用过
Arrays.fill(vis, false);
StringBuffer temp = new StringBuffer();
//递归获取
recursion(res, charStr, temp, vis);
return res;
}
}
时间复杂度:O(n∗n!),全排列的全部情况为n!,每次递归过程都是遍历字符串查找元素,这里是O(n)
空间复杂度:O(n),递归栈的最大深度为字符串长度n,临时字符串temp的空间也为O(n),res属于返回必要空间
(3)解题思路二:递归
面对这样的题目,我们需要将复杂问题分解化,分解成一个一个小问题。将一个字符串分为两部分:第一部分为它的第一个字符,第二部分为后面所有的字符,如下图所示:
求整个字符串的全排列,可以看成两步:第一步首先求所有可能出现在第一个位置的字符,即把第一个字符和后面所有的字符交换,上图就是分别把第一个字符a和后面的b、c等字符交换的情形;第二步固定第一个字符,求后面所有字符的排列。这时候仍然把后面的字符分成两部分,后面的第一个字符,和这个字符之后的所有字符,然后把后面的第一个字符和它后面的字符交换。
注:(a)把字符串分成两部分,一部分是字符串的第一个字符,另一部分是第一个字符以后的所有字符(有阴影背景的区域)。接下来我们求阴影部分的字符串的排列。(b)拿第一个字符和它后面的字符逐个交换。
(2)解法二:
import java.util.ArrayList;
import java.util.TreeSet;
public class Solution {
public ArrayList<String> Permutation(String str) {
ArrayList<String> result = new ArrayList<String>() ;
if(str==null || str.length()==0) { return result ; }
char[] chars = str.toCharArray() ;
TreeSet<String> temp = new TreeSet<>() ;
Permutation(chars, 0, temp);
result.addAll(temp) ;
return result ;
}
public void Permutation(char[] chars, int begin, TreeSet<String> result) {
if(chars==null || chars.length==0 || begin<0 || begin>chars.length-1) { return ; }
if(begin == chars.length-1) {
result.add(String.valueOf(chars)) ;
}else {
for(int i=begin ; i<=chars.length-1 ; i++) {
swap(chars, begin, i) ;
Permutation(chars, begin+1, result);
swap(chars, begin, i) ;
}
}
}
public void swap(char[] x, int a, int b) {
char t = x[a];
x[a] = x[b];
x[b] = t;
}
}
(2)解法三:
import java.util.*;
public class Practise_14{
public static void main(String[] args) {
String str = "abc";
permutation(str);
}
//给输入的str字符串中的字符进行全排列
public static void permutation(String str){
if(str == null){ //如果字符串为空,直接返回
return ;
}
permutation(str.toCharArray(), 0); //否则将字符串转换为字符数字,并从字符0位置开始进行全排列
}
public static void permutation(char[] chars, int pos) {
if(pos == chars.length - 1){
System.out.println(chars);
}
for(int i = pos; i < chars.length; i++){
//首部字符和它后面的字符(包括自己)进行交换
char temp = chars[i];
chars[i] = chars[pos];
chars[pos] = temp;
//递归求后面的字符的排列
permutation(chars, pos+1);
//由于前面交换了一下,所以chs的内容改变了,我们要还原回来
temp = chars[i];
chars[i] = chars[pos];
chars[pos] = temp;
}
}
}
(3)《剑指offer》34 如何找到第一个只出现一次的字符【借助哈希表、数组、Set等】
(1)题目描述:
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。
(2)解法一:借助哈希表
step 1:遍历一次字符串,对于每个字符,放入哈希表中统计出现次数。
step 2:再次遍历字符串,对于每个字符,检查哈希表中出现次数是否为1,找到第一个即可。
step 3:遍历结束都没找到,那就是没有,返回-1.
import java.util.*;
public class Solution {
public int FirstNotRepeatingChar(String str) {
HashMap<Character, Integer> mp = new HashMap<>();
//统计每个字符出现的次数
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
map.put(c,map.getOrDefault(c,0)+1);
}
//找到第一个只出现一次的字母
for(int i = 0; i < str.length(); i++)
if(mp.get(str.charAt(i)) == 1)
return i;
//没有找到
return -1;
}
}
(3)解法二:借助队列Set
上述方法一遍历了两次,有些繁琐,我们能不能在统计频率的过程中就找到第一个只出现一次的字符呢?利用先进先出的队列找到第一个位置!
首先我们还是利用了哈希表,但是这次我们不是统计频率,而是统计每个字符出现位置。遍历字符串,如果遇到哈希表中没有的字符,我们入哈希表,同将字符和位置同时各自入队,后续如果遇到了哈希表中出现的字符,那么这个字符势必不可能是我们要找的只出现一次的字符,在哈希表中将其位置置为-1:
//位置置为-1
mp[str[i]] = -1;
然后弹出队列中在前面的哈希表中位置为-1的字符。因为队列是先进先出,因此队列头记录的字符一定是第一次只出现一次的字符。
while(!q.empty() && mp[q.front().first] == -1)
q.pop();
空队列则代表没有找到。
step 1:利用哈希表记录字符串中出现过的字符的位置,利用两个队列分别记录字符与下标位置(C++可以用pair)。
step 2:遍历字符串,如果是没有遇到过的字符,就加入哈希表记录位置,同时字符与下标分别入队。
step 3:遇到出现过的字符,就将其哈希表中的下标置为-1,然后弹出队列首部所有重复的字符,即位置为-1的字符。
step 4:最后队列中剩余的队首就是第一个只出现一次的字符,因为其他的重复字符都被弹出了,队列为空就代表没有不重复的字符。
import java.util.*;
public class Solution {
public int FirstNotRepeatingChar(String str) {
//统计字符出现的位置
HashMap<Character, Integer> mp = new HashMap<>();
Queue<Character> q1 = new LinkedList<>();
Queue<Integer> q2 = new LinkedList<>();
for(int i = 0; i < str.length(); i++){
//没有出现过的字符
if(!mp.containsKey(str.charAt(i))){
mp.put(str.charAt(i), i);
q1.offer(str.charAt(i));
q2.offer(i);
//找到重复的字符
}else{
//位置置为-1
mp.put(str.charAt(i), -1);
//弹出前面所有的重复过的字符
while(!q1.isEmpty() && mp.get(q1.peek()) == -1){
q1.poll();
q2.poll();
}
}
}
return q2.isEmpty() ? -1 : q2.poll();
}
}
时间复杂度:O(n),对字符串进行一次遍历,内循环整个过程中才最多弹出52次
空间复杂度:O(1),哈希表和队列的大小最多不会超过字符集,即52个字符,属于常数空间
(4)解法三:借助数组
更进一步,因为该字符串全部是字母,所以可以用一个数组代替哈希表,数组下标就代表该字母。
//方法二:数组代替哈希表
public int FirstNotRepeatingChar(String str) {
if(str==null || str.length()==0)
return -1;
// A-Z对应的ASCII码为65-90,a-z对应的ASCII码值为97-122
int len=str.length();
int[] count=new int[58]; //122-65+1
for(int i=0;i<len;i++){
char c=str.charAt(i);
count[c-'A']++;
}
for(int i=0;i<len;i++){
char c=str.charAt(i);
if(count[c-'A']==1)
return i;
}
return -1;
}
(5)解法四:模式匹配
使用模式匹配从前(indexOf)和从后(lastIndexOf)匹配每一个字符,相等即为唯一。
//方法三:模式匹配
public int firstUniqChar(String s) {
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
if(s.indexOf(c)==s.lastIndexOf(c))
return i;
}
return -1;
}
(4)《剑指offer》43 如何左旋转字符串【reverse函数、三次反转】
(1)题目描述:
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
(2)解题思路:三次反转
对于本题,从最直观的角度我们首先可以想到暴力解法:每次移动一位,移动k次为止。对于每一次移动,其实就是将字符串第一个字符放到字符串末尾,而为了实现这一目标,需要将字符串其他位置的元素依次前移,因此,暴力解法时间复杂度为O(n^2)。
还是那句话:最容易想到的解法往往不是最优的。
进一步考虑,字符串左移k位,就相当于将字符串分为两部分,第一部分是前k位,另一部分是剩余的其他位,然后将这两部分交换顺序即可得到最后结果。因此,我们可以得到以下的三次反转算法:
将字符串分为两部分,即前k个字符和剩余的其他字符,然后分别对这两部分进行反转,然后再对整个字符串进行一次反转,这样得到的结果就是我们想要的循环左移之后的字符串。事实上,这并不难理解,前后两部分各自经历了两次反转,因此每一部分的顺序并没有改变,只是将前后两部分进行了交换。对字符串进行一次反转,需要一次扫描,因此次算法时间复杂度为O(n)。
举例:
输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab";
第一步:翻转字符串“ab”,得到"ba";
第二步:翻转字符串"cdefg",得到"gfedc";
第三步:翻转字符串"bagfedc",得到"cdefgab";
可以用到reverse函数
编程实现(Java):
//方法一,依次左移,每次移动一位
public String LeftRotateString(String str,int n) {
char[] strArr=str.toCharArray();
int len=strArr.length;
if(len<=0)
return str;
n=n%len;
for(int i=0;i<n;i++){ //只控制循环次数
char c=strArr[0];
for(int j=0;j<len-1;j++) //拿出第一个,后面依次前移,复杂度O(n^2)
strArr[j]=strArr[j+1];
strArr[len-1]=c;
}
return new String(strArr);
}
//方法二:三次反转
public String LeftRotateString(String str,int n) {
char[] strArr=str.toCharArray();
int len=strArr.length;
if(len<=0)
return str;
n=n%len;
reverseStr(strArr,0,n-1);
reverseStr(strArr,n,len-1);
reverseStr(strArr,0,len-1);
return new String(strArr);
}
public void reverseStr(char[] array,int begin,int end){ //反转字符串,前后指针
for(;begin<end;begin++,end--){
char c=array[begin];
array[begin]=array[end];
array[end]=c;
}
}
(3)另一种写法
public class Solution {
public String LeftRotateString(String str,int n) {
//取余,因为每次长度为n的旋转数组相当于没有变化
if(str.isEmpty() || str.length() == 0)
return "";
int m = str.length();
n = n % m;
//第一次逆转全部元素
char[] s = str.toCharArray();
reverse(s, 0, m - 1);
//第二次只逆转开头m个
reverse(s, 0, m - n - 1);
//第三次只逆转结尾m个
reverse(s, m - n, m - 1);
return new String(s);
}
//反转函数
private void reverse(char[] s, int start, int end){
while(start < end){
swap(s, start++, end--);
}
}
//交换函数
private void swap(char[] s, int a, int b){
char temp = s[a];
s[a] = s[b];
s[b] = temp;
}
}
时间复杂度:O(n),三次reverse函数的复杂度都最坏为O(n)
空间复杂度:O(1),C++没有使用额外的辅助空间,java与Python借助了O(n)的空间
(4)解法二:遍历拼接(扩展思路)
既然循环左移是前面nnn个字符平移到了最后,我们就可以分开加入字符串中,先挨个加入后面部分字符,然后再回过头加入前面的字符。
step 1:因为nnn可能大于字符串长度,因此需要对长度mmm取余,因为每次长度为mmm的旋转相当于没有变化。
step 2:先遍历后m−nm-nm−n个字符,依次加入待返回的字符串中。
step 3:再遍历前nnn个字符,依次加入待返回的字符串中。
import java.util.*;
public class Solution {
public String LeftRotateString(String str,int n) {
//取余,因为每次长度为n的旋转数组相当于没有变化
if(str.isEmpty() || str.length() == 0)
return "";
int m = str.length();
//取余,因为每次长度为m的旋转数组相当于没有变化
n = n % m;
StringBuilder res = new StringBuilder();
//先遍历后面的,放到前面
for(int i = n; i < m; i++)
res.append(str.charAt(i));
//再遍历前面的放到后面
for(int i = 0; i < n; i++)
res.append(str.charAt(i));
return res.toString();
}
}
(5)《剑指offer》44 如何反转单词序列【两次反转、栈】
(1)题目描述:
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
(2)解题思路:
本题相对比较简单,但是在面试中经常遇到,流传甚广。其主要思路也简洁明了,主要分为以下两步:
第一步:反转整个序列中所有的字符,这时会发现不但反转了单词的顺序,单词中的字母顺序也被反转,因此需要第二步的调整。
第二步:以空格为分隔,依次反转每个单词,即让每个单词会到原来的正常顺序。
举例:
以字符串“student. a am I”为例:整体反转后变为:“I ma a ,tenduts”,然后再反转每个单词,可以得到最后结果:“I am a student.”。
(3)解法一
class Solution {
public String reverseWords(String s) {
s = s.trim(); // 删除首尾空格
int j = s.length() - 1, i = j;
StringBuilder res = new StringBuilder();
while(i >= 0) {
while(i >= 0 && s.charAt(i) != ' ') i--; // 搜索首个空格
res.append(s.substring(i + 1, j + 1) + " "); // 添加单词
while(i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
j = i; // j 指向下个单词的尾字符
}
return res.toString().trim(); // 转化为字符串并返回
}
}
(4)解法二:两次反转
import java.util.*;
public class Solution {
//字符串反转函数
private void reverse(char [] c, int l, int h){
//双指针反转
while(l < h)
swap(c, l++, h--);
}
//字符交换函数
private void swap(char [] c, int l, int h){
char temp = c[l];
c[l] = c[h];
c[h] = temp;
}
public String ReverseSentence(String str) {
int n = str.length();
char[] c = str.toCharArray();
//第一次整体反转
reverse(c, 0, n - 1);
for(int i = 0; i < n; i++){
int j = i;
//以空格为界找到一个单词
while(j < n && c[j] != ' ')
j++;
//将这个单词反转
reverse(c, i, j - 1);
i = j;
}
return new String(c);
}
}
时间复杂度:O(n),n为整个句子字符串的长度,遍历整个字符串和反转字符串都是O(n)
空间复杂度:O(1),常数级变量,无额外辅助空间(其中java版本有O(n)的辅助空间)
(5)解法三:栈(扩展思路)
我们都知道栈是先进后出的,于是我们可以用方法一中分割单词的方式,在大的句子字符串中分割出一个一个地单词。然后从头到尾遍历单词,将分割出来的单词送入栈中,然后按照栈中弹出的字符串顺序拼接单词即可使单词之间逆序。
step 1:遍历字符串,将整个字符串按照空格分割然后入栈。
step 2:遍历栈,将栈中内容弹出拼接成字符串。
import java.util.*;
public class Solution {
public String ReverseSentence(String str) {
Stack<String> st = new Stack<String>();
String[] temp = str.split(" ");
//单词加入栈中
for(int i = 0; i < temp.length; i++){
st.push(temp[i]);
st.push(" ");
}
StringBuilder res = new StringBuilder();
//去掉最后一个空格
if(!st.isEmpty())
st.pop();
//栈遵循先进后厨,单词顺序是反的
while(!st.isEmpty())
res.append(st.pop());
return res.toString();
}
}
时间复杂度:O(n),nnn为整个句子字符串的长度,遍历整个字符串和弹出栈都是O(n)
空间复杂度:O(n),栈空最坏情况下长度为n
(6)《剑指offer》49 如何把字符串转化为整数【看视频补充】
(1)题目描述:
将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。
输入输出描述:
输入一个字符串,包括字母、数字、符号,可以为空。如果是合法的数值表达则返回该数字,否则返回0。
(2)解题思路:
本题解决起来并不困难,功能实现简单,但是主要的问题是能否把各种不同的特殊情况都考虑进去,也就是代码的鲁棒性和思考的全面,比如空指针、空字符串、正负号、溢出等等问题。
主要需要注意的点有以下几个:
字符串是否为null或者字符串是否为空字符串。
字符串对于正负号的处理,特别是正号,可能有也可能没有,但都代表正数
输入值是否合法,判断除首位可能为正负号外,其他位是否都是数字
int为32位,最大的整数是刚刚超过21亿,也就是10位十进制数
使用错误标志,区分合法值0和非法值0
以下直接给出相应的代码实现。
编程实现(Java):
public class Solution {
public int StrToInt(String str) {
//判断字符串是否为空
if(str==null || str.length()==0)
return 0;
//通过首位进行判断,并标记正负数/是否合法
char c = str.charAt(0); //首位
int flag=0; //标记正负数
boolean isVaild=false; //标记是否合法
if(c=='+') //为正数
flag=1;
else if(c=='-') //为负数
flag=-1;
else if(c>='0'&&c<='9') { //正数,便于统一处理
flag=1;
str="+"+str;
}else { //不是数,不合法
isVaild=true;
return 0;
}
//计算后续数字
int len= str.length();
if(len>11) //最大整数是10位
return 0;
long res=0;
for(int i=1;i<len;i++){
c=str.charAt(i);
if(c<'0'||c>'9'){
isVaild=true;
return 0;
}
res=res*10+(c-'0'); //计算数值大小
}
//根据标志位(flag、isValid)返回值
if(flag==1 && res<=Integer.MAX_VALUE)
return (int)res;
if(flag==-1 && (-1*res)>=Integer.MIN_VALUE)
return (int)(-1*res);
if(isVaild==true)
return 0;
return 0;
}
}
(7)《剑指offer》52 正则化表达式匹配【动态规划】
(1)题目描述
请实现一个函数用来匹配包括’.‘和’‘的正则表达式。
1.模式中的字符’.‘表示任意一个字符
2.模式中的字符’'表示它前面的字符可以出现任意次(包含0次)。
在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
(2)解题思路一:动态规划
为了方便,使用 ss 代指 str,使用 pp 代指 pattern。
整理一下题意,对于字符串 p 而言,有三种字符:
- 普通字符:需要和 s 中同一位置的字符完全匹配
- ‘.’:能够匹配 s 中同一位置的任意字符
- ‘*’:不能够单独使用 ’ * ',必须和前一个字符同时搭配使用,数据保证了 ’ * ’ 能够找到前面一个字符。能够匹配 s 中同一位置字符任意次。
所以本题关键是分析当出现 a* 这种字符时,是匹配 0 个 a、还是 1 个 a、还是 2 个 a …
本题可以使用动态规划进行求解:
-
状态定义:f(i,j) 代表考虑 s 中以 i 为结尾的子串和 p 中的 j 为结尾的子串是否匹配。即最终我们要求的结果为 f[n][m] 。
-
状态转移:也就是我们要考虑 f(i,j) 如何求得,前面说到了 p 有三种字符,所以这里的状态转移也要分三种情况讨论:
(1)p[j] 为普通字符:匹配的条件是前面的字符匹配,同时 s 中的第 i 个字符和 p 中的第 j 位相同。 即 f(i,j) = f(i - 1, j - 1) && s[i] == p[j] 。
(2)p[j] 为 ‘.’:匹配的条件是前面的字符匹配, s 中的第 i 个字符可以是任意字符。即 f(i,j) = f(i - 1, j - 1) && p[j] == ‘.’。
(3)p[j] 为 ’ * ':读得 p[j - 1] 的字符,例如为字符 a。 然后根据 a* 实际匹配 s 中 a 的个数是 0 个、1 个、2 个 …
3.1. 当匹配为 0 个:f(i,j) = f(i, j - 2)
3.2. 当匹配为 1 个:f(i,j) = f(i - 1, j - 2) && (s[i] == p[j - 1] || p[j - 1] == ‘.’)
3.3. 当匹配为 2 个:f(i,j) = f(i - 2, j - 2) && ((s[i] == p[j - 1] && s[i - 1] == p[j - 1]) || p[j] == ‘.’)
…
我们知道,通过「枚举」来确定 * 到底匹配多少个 a 这样的做法,算法复杂度是很高的。
我们需要挖掘一些「性质」来简化这个过程。
import java.util.*;
public class Solution {
public boolean match (String ss, String pp) {
// 技巧:往原字符头部插入空格,这样得到 char 数组是从 1 开始,而且可以使得 f[0][0] = true,可以将 true 这个结果滚动下去
int n = ss.length(), m = pp.length();
ss = " " + ss;
pp = " " + pp;
char[] s = ss.toCharArray();
char[] p = pp.toCharArray();
// f(i,j) 代表考虑 s 中的 1~i 字符和 p 中的 1~j 字符 是否匹配
boolean[][] f = new boolean[n + 1][m + 1];
f[0][0] = true;
for (int i = 0; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 如果下一个字符是 '*',则代表当前字符不能被单独使用,跳过
if (j + 1 <= m && p[j + 1] == '*') continue;
// 对应了 p[j] 为普通字符和 '.' 的两种情况
if (i - 1 >= 0 && p[j] != '*') {
f[i][j] = f[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.');
}
// 对应了 p[j] 为 '*' 的情况
else if (p[j] == '*') {
f[i][j] = (j - 2 >= 0 && f[i][j - 2]) || (i - 1 >= 0 && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.'));
}
}
}
return f[n][m];
}
}
(3)解题思路二
public class Solution {
public boolean match(char[] str, char[] pattern){
/*
思路:比较前两个字符,递归比较
*/
if(str==null || pattern==null)
return false;
return match(str,0,pattern,0);
}
public boolean match(char[] str,int i,char[] pattern,int j){
if(i==str.length && j==pattern.length)//都为空
return true;
if(i<str.length && j==pattern.length)//模式串为空
return false;
//以下j一定是<len
if(j+1<pattern.length && pattern[j+1]=='*'){ //第二个字符是*
if((i<str.length && str[i]==pattern[j]) ||(i<str.length && pattern[j]=='.') ) //第一个字符相等,有三种情况
return match(str,i,pattern,j+2) || match(str,i+1,pattern,j+2) || match(str,i+1,pattern,j);
//分别代表匹配0个,1个和多个
else //第一个字符不等
return match(str,i,pattern,j+2);
}else{ //第二个字符不是*
if((i<str.length && str[i]==pattern[j]) || ( pattern[j]=='.'&& i< str.length))
return match(str,i+1,pattern,j+1);
else
return false;
}
}
}
(8)《剑指offer》53 表示数值的字符串
(9)《剑指offer》54 第一个只出现一次的字符【借助哈希表、数组、Set等】
(1)题目描述:
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
输出描述:
如果当前字符流不存在只出现一次的字符,返回“#”字符。
(2)解法一:借助数组
本题还是相当简单的,有点类似于第34题:第一个只出现一次的字符,只不过本题是字符流序列。解题思路也比较类似,将字节流保存起来,通过哈希表统计每个字符出现的次数,然后再从头遍历字符流,找到第一个次数为1的字符,就是我们要找的目标。
这里,为了简单,可以用数组代替哈希表,将字符的ASCLL码作为数组下标,字符对应出现的次数作为数组的元素进行保存。
public class Solution {
/*
思路:用hashmap保存每个字符出现的次数 或者 用长度为256的数组代替哈希表
还有一种是使用indexof和lastIndexof
*/
String str="";
int[] charToCount=new int[256]; //256个字符
//Insert one char from stringstream
public void Insert(char ch)
{
str+=ch;
charToCount[ch]+=1;
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
for(int i=0;i<str.length();i++){
char c=str.charAt(i);
if(charToCount[c]==1)
return c;
}
return '#';
}
}
(3)解法二:借助哈希表(推荐使用)
既然要找第一个只出现一次的字符,那只要我们统计每个字符在字符串中出现的次数,后续不就可以找到第一个只出现一次的字符了吗?
统计频率可以建立一个哈希表,遍历字符串的同时,统计每个字符出现的频率,然后再从头遍历一次字符串,在哈希表中查看每个字符串的频率,找到第一个只出现一次的字符串,返回位置,如果没找到返回-1即可。
step 1:遍历一次字符串,对于每个字符,放入哈希表中统计出现次数。
step 2:再次遍历字符串,对于每个字符,检查哈希表中出现次数是否为1,找到第一个即可。
step 3:遍历结束都没找到,那就是没有,返回-1.
import java.util.*;
public class Solution {
public int FirstNotRepeatingChar(String str) {
HashMap<Character, Integer> mp = new HashMap<>();
//统计每个字符出现的次数
for(int i = 0; i < str.length(); i++)
mp.put(str.charAt(i), mp.getOrDefault(str.charAt(i), 0) + 1);
//找到第一个只出现一次的字母
for(int i = 0; i < str.length(); i++)
if(mp.get(str.charAt(i)) == 1)
return i;
//没有找到
return -1;
}
}
时间复杂度:O(n),其中nnn为字符串长度,两次单独的遍历
空间复杂度:O(1),哈希表的大小最多不会超过字符集,即52个字符,属于常数空间
(10)《剑指offer》最长不含重复字符的子字符串【动态规划+哈希表】
(1)题目描述:
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
(2)解法一:滑动窗口+哈希表
既然要找一段连续子串的内不重复的长度,我们可以使用滑动窗口,保证窗口内都是不重复的,然后窗口右界不断向右滑,如果窗口内出现了重复字符,说明新加入的元素与之前的重复了,只需要窗口左界也向右收缩就可以保证窗口内都是不重复的。
而保证窗口内的元素不重复,我们可以使用根据key值快速访问的哈希表,key值为窗口内的元素,value为其出现次数,只要新加入窗口的元素出现次数不为1,就是重复。
while(mp.get(s.charAt(right)) > 1)
//窗口左移,同时减去该数字的出现次数
mp.put(s.charAt(left), mp.get(s.charAt(left++)) - 1);
step 1:构建一个哈希表,用于统计字符元素出现的次数。
step 2:窗口左右界都从字符串首部开始,每次窗口优先右移右界,并统计进入窗口的元素的出现频率。
step 3:一旦右界元素出现频率大于1,就需要右移左界直到窗口内不再重复,将左边的元素移除窗口的时候同时需要将它在哈希表中的频率减1,保证哈希表中的频率都是窗口内的频率。
step 4:每轮循环,维护窗口长度最大值。
import java.util.*;
public class Solution {
public int lengthOfLongestSubstring (String s) {
//哈希表记录窗口内非重复的字符
HashMap<Character, Integer> mp = new HashMap<>();
int res = 0;
//设置窗口左右边界
for(int left = 0, right = 0; right < s.length(); right++){
//窗口右移进入哈希表统计出现次数
if(mp.containsKey(s.charAt(right)))
mp.put(s.charAt(right), mp.get(s.charAt(right)) + 1);
else
mp.put(s.charAt(right), 1);
//出现次数大于1,则窗口内有重复
while(mp.get(s.charAt(right)) > 1)
//窗口左移,同时减去该字符的出现次数
mp.put(s.charAt(left), mp.get(s.charAt(left++)) - 1);
//维护子串长度最大值
res = Math.max(res, right - left + 1);
}
return res;
}
}
时间复杂度:O(n),其中nnn为字符串的长度,外循环窗口右界从字符串首右移到数组尾,内循环窗口左界同样如此,因此复杂度为O(n+n)=O(n)
空间复杂度:O(n),最坏情况下整个字符串都是不重复的,哈希表长度就为字符串长度n
(3)解法二:动态规划+哈希表
如果对于某个前面的子串,如果我们新加入一个字符,与前面的都不重复,那么最长无重复子串肯定就是在前面的基础上加1,如果与前面重复了,那就是当前位置减去它重复之前字符出现的位置的长度。因此我们使用动态规划递推。
import java.util.*;
public class Solution {
public int lengthOfLongestSubstring (String s) {
//哈希表记录窗口内非重复的字符及其下标
HashMap<Character, Integer> mp = new HashMap<>();
int res = 0;
//dp[i]表示以下标i结尾的字符串最长不含重复子串的长度
int[] dp = new int[s.length() + 1];
for(int i = 1; i <= s.length(); i++){
dp[i] = 1;
//哈希表中没有,说明不重复
if(!mp.containsKey(s.charAt(i - 1)))
//前一个加1
dp[i] = dp[i - 1] + 1;
//遇到重复字符
else
dp[i] = Math.min(dp[i - 1] + 1, i - mp.get(s.charAt(i - 1)));
//加入哈希表
mp.put(s.charAt(i - 1), i);
//维护最大值
res = Math.max(res, dp[i]);
}
return res;
}
}
时间复杂度:O(n),其中nnn为字符串长度,遍历一次字符串
空间复杂度:O(n),辅助数组dp的大小为字符串长度,哈希表的最大空间为字符串长度
(11)《剑指offer》把数字翻译成字符串【动态规划】
(1)题目描述:
有一种将字母编码成数字的方式:‘a’->1, ‘b->2’, … , ‘z->26’。
现在给一串数字,返回有多少种可能的译码结果
(2)解法一:动态规划
对于普通数组1-9,译码方式只有一种,但是对于11-19,21-26,译码方式有可选择的两种方案,因此我们使用动态规划将两种方案累计。
step 1:用辅助数组dp表示前i个数的译码方法有多少种。
step 2:对于一个数,我们可以直接译码它,也可以将其与前面的1或者2组合起来译码:如果直接译码,则dp[i]=dp[i−1];如果组合译码,则dp[i]=dp[i−2]。
step 3:对于只有一种译码方式的,选上种dp[i−1]即可,对于满足两种译码方式(10,20不能)则是dp[i−1]+dp[i−2]
step 4:依次相加,最后的dp[length]即为所求答案。
import java.util.*;
public class Solution {
public int solve (String nums) {
//排除0
if(nums.equals("0"))
return 0;
//排除只有一种可能的10 和 20
if(nums == "10" || nums == "20")
return 1;
//当0的前面不是1或2时,无法译码,0种
for(int i = 1; i < nums.length(); i++){
if(nums.charAt(i) == '0')
if(nums.charAt(i - 1) != '1' && nums.charAt(i - 1) != '2')
return 0;
}
int[] dp = new int[nums.length() + 1];
//辅助数组初始化为1
Arrays.fill(dp, 1);
for(int i = 2; i <= nums.length(); i++){
//在11-19,21-26之间的情况
if((nums.charAt(i - 2) == '1' && nums.charAt(i - 1) != '0') || (nums.charAt(i - 2) == '2' && nums.charAt(i - 1) > '0' && nums.charAt(i - 1) < '7'))
dp[i] = dp[i - 1] + dp[i - 2];
else
dp[i] = dp[i - 1];
}
return dp[nums.length()];
}
}
时间复杂度:O(n),两次遍历都是单层
空间复杂度:O(n),辅助数组dp
(12)⭐️字符串变形
(1)问题描述
首先这个字符串中包含着一些空格,就像"Hello World"一样,然后我们要做的是把这个字符串中由空格隔开的单词反序,同时反转每个字符的大小写。
比如"Hello World"变形后就变成了"wORLD hELLO"。
输入:“This is a sample”,16
返回值:“SAMPLE A IS tHIS”
(2)解法一:使用栈来存放
使用了subString方法,但是遍历了3次,循环的次数太多了,比较耗时
public class string_01 {
public static void main(String[] args) {
String demo = new String("This is a sample");
int len = 16;
Stack<Object> stack=new Stack<>();
StringBuffer stringBuffer = new StringBuffer();
// 实现大小写的转换
for (int i = 0; i < demo.length(); i++) {
char word = demo.charAt(i);
if(word>='A' && word<='Z'){
stringBuffer.append((char) (word-'A'+'a'));
} else if(word>='a' && word<='z'){
stringBuffer.append((char) (word-'a'+'A'));
} else if(word==' '){
stringBuffer.append((char)word);
}
}
// 遍历单词放进栈
for (int i = 0; i < stringBuffer.length(); i++) {
// j指针用来找空格
int j=i;
while (j<stringBuffer.length()&&stringBuffer.charAt(j)!=' ') {
j++;
}
// 截取子字符串放进栈
stack.push(stringBuffer.substring(i,j));
// 如果j小于字符串长度,就把这个空格也放进栈
if(j<stringBuffer.length()){
stack.push(stringBuffer.charAt(j));
}
i=j;
}
// 遍历从栈中取出数据拼接
StringBuffer result = new StringBuffer();
while (!stack.empty()) {
result.append(stack.pop());
}
System.out.println(result);
}
}
(3)解法二:栈的优化
使用split方法
(13)⭐️最长公共前缀
(1)题目描述
给你一个大小为 n 的字符串数组 strs ,其中包含n个字符串 , 编写一个函数来查找字符串数组中的最长公共前缀,返回这个公共前缀。
输入:[“abca”,“abc”,“abca”,“abc”,“abcc”]
返回值:“abc”
(2)解法一:把它看成是二维数组来遍历
public static String method(String[] demo) {
int rows = demo.length;
if (rows==0) {
return "";
}
int cols = demo[0].length();
// 先把第一个字符串找出来,遍历每一个字符,依次与后面的每个字符串的相对位置的字符比较
for (int i = 0; i < cols; i++) {
char word = demo[0].charAt(i);
for (int j = 1; j < rows; j++) {
// 如果后面有字符串遍历长度到头了,或者出现跟第一个字符串相对字符不同时
if(demo[j].length()==i || demo[j].charAt(i)!=word){
// 说明出现了不同的字符,可以截取字符串,返回结果了
return demo[0].substring(0,i);
}
}
}
// 如果第一个字符串遍历完了也没有找到不同,说明第一个字符串就是完整的公共前缀
return demo[0];
}
(14)⭐️有效括号序列
(1)题目描述
给出一个仅包含字符’(‘,’)‘,’{‘,’}‘,’[‘和’]',的字符串,判断给出的字符串是否是合法的括号序列
括号必须以正确的顺序关闭,"()“和”()[]{}“都是合法的括号序列,但”(]“和”([)]"不合法。
(2)问题分析
类似于数组的思路,使用对撞指针,如果不匹配,就是错误的(这个思路不对)
应该是遍历字符串数组,然后遇到括号就放进栈里,遇到对应就判断出栈是不是对应的括号。要注意的是栈pop之前要判断是不是空的,否则会抛出异常
(3)解法
public class Solution {
/**
*
* @param s string字符串
* @return bool布尔型
*/
public boolean isValid (String string) {
// write code here
int len = string.length();
int i = 0;
Stack<Character> stack = new Stack<Character>();
while (i<len) {
char word = string.charAt(i);
if(word=='(' || word=='{' ||word=='['){
stack.push(word);
} else if(word==')'){
if(!stack.empty()){
if(stack.pop()!='('){
System.out.println("false");
return false;
}
} else {
System.out.println("false");
return false;
}
} else if(word==']'){
if(!stack.empty()){
if(stack.pop()!='['){
System.out.println("false");
return false;
}
} else {
System.out.println("false");
return false;
}
} else if(word=='}'){
if(!stack.empty()){
if(stack.pop()!='{'){
System.out.println("false");
return false;
}
} else {
System.out.println("false");
return false;
}
}
i++;
}
if(!stack.empty()){
System.out.println("false");
} else {
System.out.println("true");
}
if(!stack.empty()){
return false;
} else {
return true;
}
}
}
(4)优化解法
原来是遇到括号就放入栈,后面再出栈判断。可以优化改一下,遇到括号,就入栈一个相反的括号,后面遍历到相反的括号时就从栈里取出来判断是不是相等的
public class Solution {
public boolean isValid (String s) {
//辅助栈
Stack<Character> st = new Stack<Character>();
//遍历字符串
for(int i = 0; i < s.length(); i++){
//遇到左小括号
if(s.charAt(i) == '(')
//期待遇到右小括号
st.push(')');
//遇到左中括号
else if(s.charAt(i) == '[')
//期待遇到右中括号
st.push(']');
//遇到左打括号
else if(s.charAt(i) == '{')
//期待遇到右打括号
st.push('}');
//必须有左括号的情况下才能遇到右括号
else if(st.isEmpty() || st.pop() != s.charAt(i))
return false;
}
//栈中是否还有元素
return st.isEmpty();
}
}
(15)⭐️反转字符串
(1)题目描述
写出一个程序,接受一个字符串,然后输出该字符串反转后的字符串。(字符串长度不超过1000)
输入:“abcd”
返回值:“dcba”
(2)题目分析
可以使用类似数组的双指针对撞来解决
注意的是:
1-字符串转数组:string.toCharArray(); 或者 str.split(“”);
2-数组转字符串:String.copyValueOf(arr); 或者 new String(result);
(3)解法
public static String demo(String string) {
int len = string.length();
if(len<=1){
return string;
}
int left=0;
int right=len-1;
char result[] = string.toCharArray();
while (left<right) {
char temp = result[left];
result[left] = result[right];
result[right] = temp;
left++;
right--;
}
return new String(result);
}
(16)⭐️判断是否为回文字符串
(1)题目描述
给定一个长度为 n 的字符串,请编写一个函数判断该字符串是否回文。如果是回文请返回true,否则返回false。字符串回文指该字符串正序与其逆序逐字符一致。
输入:“absba”
返回值:true
(2)问题分析
使用对撞指针判断就行了
(3)解题代码
public static boolean demo(String string) {
int len = string.length();
int left = 0;
int right = len-1;
while (left<=right) {
if(string.charAt(left)==string.charAt(right)){
left++;
right--;
} else {
return false;
}
}
return true;
}
(17)最长回文子串
(1)题目描述
对于长度为n的一个字符串A(仅包含数字,大小写英文字母),请设计一个高效算法,计算其中最长回文子串的长度。
输入:“ababc”
返回值:3
说明:最长的回文子串为"aba"与"bab",长度都为3
(2)问题分析
这种情况仅仅使用指针是不行的了,需要借助其他数据结构来实现了
(18)🌶最长的括号子串
(1)题目描述
给出一个长度为 n 的,仅包含字符 ‘(’ 和 ‘)’ 的字符串,计算最长的格式正确的括号子串的长度。
例1: 对于字符串 “(()” 来说,最长的格式正确的子串是 “()” ,长度为 2 .
例2:对于字符串 “)()())” , 来说, 最长的格式正确的子串是 “()()” ,长度为 4 .
(2)问题分析
这题不是仅仅判断格式是否正确,还要记录最长的长度是什么,但是简单的是这里只有一种括号。还是要用栈才行,但是比较抽象
(3)解题代码
public static int demo(String string) {
int len = string.length();
Stack<Integer> stack = new Stack<>();
int i=0;
int top=-1;
int max_val = 0;
int this_val = 0;
while (i<len) {
if(string.charAt(i)=='('){
stack.push(i);
} else if(string.charAt(i)==')'){
if(stack.empty()){
top=i;
} else {
stack.pop();
if(!stack.empty()){
this_val = i-stack.peek();
} else {
this_val = i-top;
}
}
max_val = max_val>this_val?max_val:this_val;
}
i++;
}
return max_val;
}
(19)🌶最小覆盖子串
(1)题目描述
给出两个字符串 s 和 t,要求在 s 中找出最短的包含 t 中所有字符的连续子串。
输入:“XDOYEZODEYXNZ”,“XYZ”
返回值:“YXNZ”
(2)问题分析
首先把给的目标子串遍历放到一个数据结构里,考虑用HashMap,方便判断和删除,然后遍历长串,每命中一个就是remove一个,当最后HashMap为空的时候,计算长度
(3)解题代码
(四)二叉树
(1)二叉树遍历的技巧
(2)二叉树的前序遍历
(1)题目描述
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
(2)问题分析
首先树的节点结构如下,这个是给定的。前序遍历的逻辑就是left->middle->right
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
(3)解题代码
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* public TreeNode(int val) {
* this.val = val;
* }
* }
*/
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型一维数组
*/
public int[] preorderTraversal (TreeNode root) {
// write code here
List<Integer> list = new ArrayList();
preorder(list,root);
int[] res=new int[list.size()];
for(int i=0;i<list.size();i++) {
res[i]=list.get(i);
}
return res;
}
public void preorder(List list,TreeNode root) {
if(root==null){
return;
}
list.add(root.val);
preorder(list,root.left);
preorder(list,root.right);
}
}
(3)二叉树的中序遍历
(1)题目描述
给定一个二叉树的根节点root,返回它的中序遍历结果。
(2)问题分析
什么是二叉树的中序遍历,简单来说就是“左根右”,展开来说就是对于一棵二叉树,我们优先访问它的左子树,等到左子树全部节点都访问完毕,再访问根节点,最后访问右子树。
(3)解题代码
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* public TreeNode(int val) {
* this.val = val;
* }
* }
*/
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型一维数组
*/
public int[] inorderTraversal (TreeNode root) {
// write code here
List<Integer> list = new ArrayList();
traOrder(list,root);
int[] result = new int[list.size()];
for(int i=0;i<list.size();i++) {
result[i] = list.get(i);
}
return result;
}
public void traOrder(List list,TreeNode root) {
if(root==null) {
return;
}
traOrder(list,root.left);
list.add(root.val);
traOrder(list,root.right);
}
}
(4)二叉树的后序遍历
(1)题目描述
给定一个二叉树,返回他的后序遍历的序列。
后序遍历是值按照 左节点->右节点->根节点 的顺序的遍历。
(3)解题代码
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* public TreeNode(int val) {
* this.val = val;
* }
* }
*/
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return int整型一维数组
*/
public int[] postorderTraversal (TreeNode root) {
// write code here
List<Integer> list = new ArrayList();
postOrder(list,root);
int result[] = new int[list.size()];
for(int i=0;i<list.size();i++) {
result[i] = list.get(i);
}
return result;
}
public void postOrder(List list,TreeNode root) {
if(root==null) {
return;
}
postOrder(list,root.left);
postOrder(list,root.right);
list.add(root.val);
}
}
(5)二叉树的最大深度
(1)题目描述
给定一棵二叉树的根节点,求这棵树的最大深度
深度是指树的根节点到任一叶子节点路径上节点的数量
最大深度是所有叶子节点的深度的最大值
叶子节点是指没有子节点的节点
(2)解题代码
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* }
*/
public class Solution {
/**
*
* @param root TreeNode类
* @return int整型
*/
public int maxDepth (TreeNode root) {
// write code here
if(root==null) {
return 0;
}
int left_dep = maxDepth(root.left);
int right_dep = maxDepth(root.right);
int result = left_dep>right_dep?left_dep:right_dep;
return result+1;
}
}
(五)斐波那契数列
(1)斐波那契数列
(1)题目描述
(3)解题代码
public class Solution {
public int Fibonacci(int n) {
if (n<=1) {
return n;
} else {
return Fibonacci(n-1)+Fibonacci(n-2);
}
}
}
(2)跳台阶
(1)题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
(2)题目分析
(3)解题代码
public class Solution {
public int jumpFloor(int target) {
if(target<=1) {
return 1;
} else {
return jumpFloor(target-1)+jumpFloor(target-2);
}
}
}
(3)最小花费爬楼梯
(1)题目描述
给定一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用,下标从0开始。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
(3)解题代码
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param cost int整型一维数组
* @return int整型
*/
public int minCostClimbingStairs (int[] cost) {
// write code here
int[] dp=new int[cost.length+1];
for(int i=2;i<=cost.length;i++) {
dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[cost.length];
}
}
(六)链表
(1)反转链表
(1)题目描述
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
(2)解题代码
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
// 第一个指针,指明前节点
ListNode pre=null;
// 第二个指针,指向当前节点
ListNode cur=head;
while(null!=cur) {
// 第三个指针,确定当前节点的下一个节点
ListNode cur_next = cur.next;
cur.next=pre;
pre=cur;
cur=cur_next;
}
return pre;
}
}