本文最后更新于:2024年4月16日 下午
数组和字符串
数组简介 寻找数组的中心索引 思路 :前缀和——中心索引左右两边和相等,所以先sum求出总和,然后计算右边是否等于左边left == sum-left-nums[i]
,满足就返回,不满足就右移,直到结束。时间复杂度O(N)空间复杂度O(1)
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int pivotIndex (int [] nums) { int sum=0 ,left=0 ; for (int s:nums) sum+=s; for (int i=0 ;i<nums.length;i++){ if (left== sum-left-nums[i]){ return i; } left+=nums[i]; } return -1 ; } }
搜索插入位置 思路 :简单的二分查找代码。(当然由于数组已经有序了,直接遍历一遍也行)
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int searchInsert (int [] nums, int target) { int left=0 ,right=nums.length-1 ; while (left<=right){ int mid = left+(right-left)/2 ; if (nums[mid]==target) return mid; else if (nums[mid]<target) left = mid+1 ; else right = mid-1 ; } return right+1 ; } }
合并区间 思路:题目是要判断重叠区间,类似算法笔记贪心
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int [][] merge(int [][] intervals) { Arrays.sort(intervals,(v1,v2)-> v1[0 ]-v2[0 ] ); int [][] ans = new int [intervals.length][2 ]; int index = -1 ; for (int [] interval:intervals){ if (index==-1 || interval[0 ]>ans[index][1 ]) ans[++index] = interval; else ans[index][1 ] = Math.max(ans[index][1 ],interval[1 ]); } return Arrays.copyOf(ans, index + 1 ); } }
二维数组简介 旋转矩阵 对于矩阵中第 i 行的第 j 个元素,在旋转后,它出现在倒数第 i 列的第 j 个位置。
matrix [row] [col] –> matrix [col] [n-1-row]
思路一:用辅助数组存储临时旋转的结果
复杂度:时间O(N^2) 空间O(N^2)
思路二:原地旋转,前一个的列是旋转结果的行,前一个的行倒数过来是结果的列
一次旋转交换四个位置
[row,col] –> [col,n-1-row] –> [n-1-row,n-1-col] –> [n-1-col,row] –> [row,col]
n为偶数:n^2/4 = n/2 * n/2,左上角四分之一区域
n为奇数:(n^2-1)/4 = (n-1)/2 * (n+1)/2,左上角(n-1)/2,(n+1)/2区域
注:赋值的时候反过来
思路三:用翻转代替旋转,做两次翻转结果也是一样
水平翻转 :matrix [row] [col] = matrix [n-1-row] [col]
+主对角线翻转 :matrix [row] [col] = matrix [col] [row]
= 联立结果 :matirx [row] [col] = matrix [col] [n-1-row]
注:水平+对角线–>顺时针 对角线+水平–>逆时针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution { public void rotate (int [][] matrix) { int n = matrix.length; for (int row=0 ;row<n/2 ;row++){ for (int col=0 ;col<(n+1 )/2 ;col++){ int tmp = matrix[row][col]; matrix[row][col] = matrix[n-1 -col][row]; matrix[n-1 -col][row] = matrix[n-1 -row][n-1 -col]; matrix[n-1 -row][n-1 -col] = matrix[col][n-1 -row]; matrix[col][n-1 -row] = tmp; } } } }class Solution { public void rotate (int [][] matrix) { int n = matrix.length; for (int i=0 ;i < n/2 ;i++){ for (int j=0 ;j<n;j++){ int tmp = matrix[i][j]; matrix[i][j] = matrix[n-1 -i][j]; matrix[n-1 -i][j] = tmp; } } for (int i=0 ;i<n;i++){ for (int j=0 ;j<i;j++){ int tmp = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = tmp; } } } }
零矩阵 思路:数组中只要某个位置matrix[i][j] = 0
那么对应i
行j
列全置0,所以可以用两个数组记录行列中出现0的情况,遍历二维数组做处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution { public void setZeroes (int [][] matrix) { boolean [] row = new boolean [matrix.length]; boolean [] col = new boolean [matrix[0 ].length]; for (int i=0 ;i<matrix.length;i++){ for (int j=0 ;j<matrix[0 ].length;j++){ if (matrix[i][j]==0 ){ row[i] = true ; col[j] = true ; } } } for (int i=0 ;i<matrix.length;i++){ if (row[i]){ for (int j=0 ;j<matrix[0 ].length;j++) matrix[i][j] = 0 ; } } for (int i=0 ;i<matrix[0 ].length;i++){ if (col[i]){ for (int j=0 ;j<matrix.length;j++) matrix[j][i] = 0 ; } } } }
字符串简介 最长公共前缀 思路一:横向扫描方式,一次遍历比较字符串,更新前缀prefix,遍历完后就可以得到最长前缀。
时间复杂度O(mn),m是字符串平均长度,n是字符串数量;空间复杂度O(1)
思路二:纵向扫描,从前往后遍历所有字符串的第一列,相同列上字符相同就继续,否则就返回
还有其他方法例如分治、二分查找等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Solution { public String longestCommonPrefix (String[] strs) { if (strs == null || strs.length ==0 ){ return "" ; } String prefix = strs[0 ]; int count = strs.length; for (int i=1 ;i<count;i++){ prefix = lcp(prefix,strs[i]); if (prefix.length() == 0 ) break ; } return prefix; } public String lcp (String s1,String s2) { int len = Math.min(s1.length(),s2.length()); int index = 0 ; while (index<len && s1.charAt(index)==s2.charAt(index)) index++; return s1.substring(0 ,index); } }class Solution { public String longestCommonPrefix (String[] strs) { if (strs == null || strs.length == 0 ) { return "" ; } int length = strs[0 ].length(); int count = strs.length; for (int i = 0 ; i < length; i++) { char c = strs[0 ].charAt(i); for (int j = 1 ; j < count; j++) { if (i == strs[j].length() || strs[j].charAt(i) != c) { return strs[0 ].substring(0 , i); } } } return strs[0 ]; } }
最长回文子串 思路:暴力法枚举所有子串,验证是否为回文串,可以添加剪枝j-i+1>maxLen
,只有串长比maxLen大才判断。时间复杂度O(N^3),这个暴力法自己写的都有问题。。参考一下别人的改出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution { public String longestPalindrome (String s) { int len = s.length(); if (len<2 ){ return s; } int maxLen = 1 ; int begin = 0 ; char [] charArray = s.toCharArray(); for (int i=0 ;i<len-1 ;i++){ for (int j=i+1 ;j<len;j++){ if (j-i+1 >maxLen && validPalindromic(charArray,i,j)){ maxLen = j-i+1 ; begin = i; } } } return s.substring(begin,begin+maxLen); } private boolean validPalindromic (char [] charArray,int left,int right) { while (left<right){ if (charArray[left]!=charArray[right]){ return false ; } left++; right--; } return true ; } }
别人的思路:动态规划。回文串具有天然的状态转移性质——一个回文串去掉首尾两个字符还是回文串。所以可以从两端判断:两端不等必然不是,相等再看里面
定义状态:dp[i][j]
表示s[i...j]
是否为回文子串
状态转移方程:dp[i][j]==(s[i]==s[j]) and dp[i+1][j-1]
动态规划就是填二维表,由于i<=j
所以只看对角线以上
还要考虑dp[i+1][j-1]
可能存在边界情况。边界条件:(j-1)-(i+1)+1<2 --> j-i<3
即字符串长度为2或3的时候直接判断
判断结论:如果子串 s[i+1..j-1]
只有 1 个字符,即去掉两头,剩下中间部分只有1个字符,显然是回文;如果子串s[i+1..j-1]
为空串,那么子串s[i, j]
一定是回文子串。
初始化:单个字符肯定是回文串,所以对角线即dp[i][i]=true
,不过但单个字符也不会被其他状态值参考,基本可以去除
输出:只要得到dp[i][j]=true
就记录字串长度和起始位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution { public String longestPalindrome (String s) { int len = s.length(); if (len>2 ) return s; int manLen = 1 ; int begin = 0 ; boolean [][] dp = new boolean [len][len]; char [] charArray = s.toCharArray(); for (int i=0 ;i<len;i++) do [i][i] = true ; for (int j=1 ;j<len;j++){ for (int i=0 ;i<j;i++){ if (charArray[i]!=charArray[j]) dp[i][j] = false ; else { if (j-i<3 ) dp[i][j]=true ; else dp[i][j]=dp[i+1 ][j-1 ]; } if (dp[i][j]&&j-i+1 >maxLen){ maxLen = j-i+1 ; begin = i; } } } return s.substring(begin,begin+maxLen); } }
别人的思路:中心扩散。除了从左右两端来判断,回文串也可以从中间往两边扩散。基本想法是遍历每一个索引,以该索引为中心利用回文串对称的特点往两边扩散
翻转字符串里的单词 思路:一开始没想到用库函数,想着直接遍历s,开一个String数组存放单词,遇到空格就表示一个单词结束,所以只要s.charAt(i)!=' '
的话就word[num]+=s.charAt(i)
,遇到空格单词入栈和num++
,然后将栈中元素拼接,思路没错,就是太麻烦了,效率也非常低
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { public String reverseWords (String s) { s+=" " ; int len = s.length(); String[] word = new String [10010 ]; Stack<String> stack = new Stack <>(); int num=0 ; for (int i=0 ;i<10010 ;i++){ word[i]="" ; } for (int i=0 ;i<len;i++){ if (s.charAt(i)!=' ' ){ word[num] += s.charAt(i); }else { stack.push(word[num]); num++; } } String ss="" ; while (!stack.isEmpty()){ ss+=stack.peek(); if (stack.size()>1 &&stack.peek()!="" ){ ss+=" " ; } stack.pop(); } ss=ss.trim(); return ss; } }
问题一:遇到" hello world!"
会输出" world! hello "
而不是预期的"world! hello"
。。。在后面处理一下ss=ss.trim()
问题二:遇到"a good example"
时,在while循环中把word[num]=""
加了一次到stack中,所以在拼接ss时多了一个空格,导致输出"example good a"
,将判断条件由stack.size()!=0
改为stack.size()>1&&stack.peek()!=" "
就行
word数组设大一点,最后有一个输入用例非常多
别人的思路:可以直接用库函数(属实不太熟悉。。。就很致命)trim去首位空格、spilt分割、reverse翻转、join再连接。一波操作带走
1 2 3 4 5 6 7 class Solution { public String reverseWords (String s) { String[] words = s.trim().split(" +" ); Collections.reverse(Arrays.asList(words)); return String.join(" " , words); } }
别人的思路二:上面库函数面试可能不会让用,所以参考了一下别人的发现,可以考虑改进我的思路,从后往前遍历就不需要栈了,用一个双指针索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public String reverseWords (String s) { s=s.trim(); StringBuilder res = new StringBuilder (); int end=s.length()-1 ,start=end; while (start>=0 ){ while (start>=0 &&s.charAt(start)!=' ' ) start--; res.append(s.substring(start+1 ,end+1 ) +" " ); while (start>0 &&s.charAt(start)==' ' ) start--; end = start; } return res.toString().trim(); }
实现 strStr() 思路一:直接调用库函数indexOf
思路二:双指针暴力遍历检测
思路三:KMP算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution { public int strStr (String haystack, String needle) { if (needle.length()==0 ) return 0 ; if (needle.length()>haystack.length()) return -1 ; int i=0 ,j=0 ; int index = -1 ; while (i<haystack.length() && j<needle.length()){ if (haystack.charAt(i) == needle.charAt(j)){ if (index == -1 ) index = i; if (j == needle.length()-1 ) return index; j++; }else { if (index != -1 ){ i = index; j = 0 ; index = -1 ; } } i++; } return -1 ; } }
双指针技巧 常用的有:
左右指针:左右两端点开始,反向运动,常用于二分查找之类问题
快慢指针:同一起点,通向运动,一快一慢,典型的判定链表中是否包含环
反转字符串 【题目要求原地修改输入数组、并用O(1)的额外空间解决问题】
思路:很显然放在双指针下自然想到双指针方法。。这也算是最典型的应用了,双指针left和right从两边往中间靠拢,每次交换对应的s[left]和s[right]即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public void reverseString (char [] s) { if (s == null || s.length == 0 ) return ; int left = 0 ; int right = s.length-1 ; while (left < right){ char temp = s[left]; s[left] = s[right]; s[right] = temp; left ++; right --; } } }
数组拆分 思路:将长度为2n的数组分成n对,取每队的最小值min相加,使得最后的值sum最大。那我们就应该尽可能的保留大的值——假如最大和最小、第二大和第二小。。。这样每次取min都是小的;应该最大与第二大、第三大与第四大。。。这样才能做到保留大值。【实现上就是排序取奇数位】
1 2 3 4 5 6 7 8 9 10 class Solution { public int arrayPairSum (int [] nums) { Arrays.sort(nums); int ans = 0 ; for (int i=0 ;i<nums.length;i+=2 ){ ans+=nums[i]; } return ans; } }
两数之和 II - 输入有序数组 思路:因为数组已经有序了,那这就跟二分查找差不多了,只不过它多了一步——返回是下标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public int [] twoSum(int [] numbers, int target) { int left = 0 ,right=numbers.length-1 ; while (left<right){ int sum = numbers[left]+numbers[right]; if (sum==target){ return new int []{left+1 ,right+1 }; }else if (sum>target){ right--; }else { left++; } } return new int []{-1 ,-1 }; } }
移除元素 【原地移除等于目标值的元素,控制常量空间】
思路:一般思路是遇到不等于目标值的就放入新数组,这样一次遍历得到的新数组即为所需的。现在要求常数空间,那就可以用快慢指针:快指针每次都移动,慢指针只有快指针的值不为目标值val才移动,若相同则停止,等下一个不同的来覆盖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int removeElement (int [] nums, int val) { if (nums==null ||nums.length==0 ){ return 0 ; } int slow=0 ; for (int fast=0 ;fast<nums.length;fast++){ if (nums[fast]!=val){ nums[slow] = nums[fast]; slow++; } } return slow; } }
最大连续1的个数 思路:由0、1组成的数组中连续1的最大个数。之前没用双指针,感觉差不多
原来思路:变量count记录当前连续1的个数,遇到0就将count存入tmp然后count置0,下次再遇到就和tmp比较,遍历一遍得到结果
双指针:快指针判断,慢指针只有判断为1才前移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { public int findMaxConsecutiveOnes (int [] nums) { int count=0 ,tmp=0 ; for (int i=0 ;i<nums.length;i++){ if (nums[i]==1 ){ count++; }else { tmp = Math.max(tmp,count); count=0 ; } } return Math.max(tmp,count); } }class Solution { public int findMaxConsecutiveOnes (int [] nums) { int slow=0 ,count=0 ,fast; for (fast=0 ;fast<nums.length;fast++){ if (nums[fast]==0 ){ count = Math.max(fast-slow,count); slow = fast+1 ; } if (fast==nums.length-1 && nums[fast]==1 ) count = Math.max(fast-slow+1 ,count); } return count; } }
长度最小的子数组 思路:题目要找出满足和>=s的长度最小的连续 子数组,我第一反应就是暴力求解,不过复杂度会比较高
暴力想法:枚举nums每个元素nums[i]
,作为子数组的起始,然后从i
开始往后,直到找到j
使得nums[i]到nums[j]
元素之和大于等于s,更新子数组长度。复杂度O(n2 )
题解的双指针方法:双指针start和end,每轮sum+=nums[end]
,满足大于等于s,就更新长度,然后执行sum -= nums[start]
【start右移,类似滑动窗口,直到sum<s,其中每次也更新长度】一轮完成后end++
,开始下一轮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int minSubArrayLen (int s, int [] nums) { if (nums.length==0 ){ return 0 ; } int len = 10010 ; int start = 0 ,end = 0 ; int sum = 0 ; for (end=0 ;end<nums.length;end++){ sum += nums[end]; while (sum>=s){ len = Math.min(len,end-start+1 ); sum -= nums[start++]; } } return len==10010 ? 0 : len; } }
小结 杨辉三角 思路:打印杨辉三角嘛,两边是1直接add(1)
,中间是正上面的左上方和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public List<List<Integer>> generate (int numRows) { List<List<Integer>> res = new ArrayList <List<Integer>>(); for (int i=0 ;i<numRows;i++){ ArrayList<Integer> sub = new ArrayList <Integer>(); for (int j=0 ;j<=i;j++){ if (j==0 ||j==i){ sub.add(1 ); }else { sub.add(res.get(i-1 ).get(j-1 )+res.get(i-1 ).get(j)); } } res.add(sub); } return res; } }
杨辉三角 II 思路:上一题返回整个,这个返回杨辉三角的第k行【进阶:空间O(k)】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public List<Integer> getRow (int rowIndex) { ArrayList<Integer> res = new ArrayList <Integer>(); for (int i=0 ;i<=rowIndex;i++){ for (int j=i;j>=0 ;j--){ if (j==0 ||j==i){ res.add(1 ); }else { res.set(j,res.get(j)+res.get(j-1 )); } } } int size = res.size(); for (int j=size-1 ;j>=size-rowIndex;j--){ res.remove(j); } return res; } }
反转字符串中的单词 III 之前做过反转句子输出:i love you
输出you love i
这种,这题是反转每个单词,但是单词间位置不变:i love you
输出i evol uoy
思路:正在刷双指针,所以一下就想到双指针方法,一个slow用于定位每个单词的开始位置,fast==' '
时前一个就是单词的结束位置,然后将单词的字符反转。
【写完编译不通过,才想起来Java中的String是不可变的。。。改用C++思路不变】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : string reverseWords (string s) { int fast=0 ,slow=0 ; for (int fast=0 ;fast<s.length ();fast++){ if (s[fast]==' ' ){ int j = fast-1 ; while (slow<j){ swap (s[slow], s[j]); slow++; j--; } slow = fast+1 ; } if (fast==s.length ()-1 &&s[fast]!=' ' ){ int j = fast; while (slow<j){ swap (s[slow], s[j]); slow++; j--; } } } return s; } };
寻找旋转排序数组中的最小值 看题目输入输出,好像就是找数组最小值。。。它说在某个点旋转了我也没整明白咋旋转的:[0,1,2,4,5,6,7]
转成[4,5,6,7,0,1,2]
。可能是说数组变成了4,5,6,7
和0,1,2
两个区域然后旋转了方向,所以数组不再有序了
思路:
第一眼就直接遍历一遍:复杂度O(N)
改进方式:采用二分优化。但是二分需要数组有序,旋转了之后数组是乱序的,要改进一下,假设两个区域分别叫大区间和小区间
找到中间元素mid,如果nums[mid]>nums[right]
说明mid是大区间的值,那我们就应该去小区间搜索;反之表示mid是小区间的值,那就缩小范围继续
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int findMin (int [] nums) { int left=0 ,right=nums.length-1 ; if (nums[right]>nums[0 ]){ return nums[0 ]; } while (right>left) { int mid=left+(right-left)/2 ; if (nums[mid]>nums[right]) left=mid+1 ; else right=mid; } return nums[left]; } }
删除排序数组中的重复项 思路:有序数组去重,也是双指针比较好想
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int removeDuplicates (int [] nums) { if (nums==null ||nums.length==1 ){ return nums.length; } int slow=0 ,fast; for (fast=1 ;fast<nums.length;fast++){ if (nums[fast] != nums[slow]){ slow++; nums[slow] = nums[fast]; } } return slow+1 ; } }
移动零 将数组[0,1,2,0,3]
中0移到最后且保持原数组顺序,即[1,2,3,0,0]
。
最简单的思路就是再开一个数组遍历,不过题目要求原地操作,所以比较容易想到双指针操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public void moveZeroes (int [] nums) { int slow=0 ,fast=0 ; while (fast<nums.length){ if (nums[fast]!=0 ){ swap(nums,slow,fast); slow++; } fast++; } } public void swap (int [] nums,int i,int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } }