1. 题号3:无重复字符的最长子串
滑动窗口,相当于以每个字符都作为起始位置找寻最长值,只需遍历一次
用哈希集合储存之前的字符串,无需重复遍历也可判断是否已经存在
public int lengthOfLongestSubstring(String s) {
// 哈希集合,记录每个字符是否出现过
Set<Character> occ = new HashSet<Character>();
int n = s.length();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
for (int i = 0; i < n; i++) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.remove(s.charAt(i - 1));
}
while (rk + 1 < n && !occ.contains(s.charAt(rk + 1))) {
// 符合条件时不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
}
return ans;
}
而上种方法 r初始值为-1,用 r+1判断,r正好指向右边界,此时需要加1
右指针初始值为0时,r指针实际指向右区间的下一个所以 r - l不用加1
int lengthOfLongestSubstring(string s) {
unordered_set<char> occ;
int length=0,r=0,n=s.size();
for(int i=0;i<n;i++){
if(i!=0){
occ.erase(s[i-1]);
}
while(!occ.count(s[r]) && r<n){
occ.insert(s[r]);
r++;
}
if(length<(r-i+1)){
length = r-i;
}
}
return length;
}
2. 题号11:盛水最多的容器
左指针从第一个元素开始,右指针从最后一个元素开始
移动二者之中元素较小的指针,因为储水量是由较小元素 t 决定,如果移动大指针储水量不会超过原有的tx
还是要深刻找寻规律才能找出最优解法
int maxArea(vector<int>& height) {
int l=0,r=height.size()-1,max=0,ans=0;
while(l<r){
if(height[l]<height[r]){
ans=(r-l)*height[l];
}else{
ans=(r-l)*height[r];
}
if(ans>max){
max=ans;
}
if(height[l]<=height[r]){
l++;
}else{
r--;
}
}
return max;
}
3. 题号15:三数之和
本质双指针求两数之和,但有好多细节
先排序防止重复,进while循环 l=i+1,r=n-1,出while循环后判断nums[i-1]与nums[i]一样就跳过,注意的是当i>0时才可以
根据sum值和零的关系判断移动哪个指针
当前元素k大于零直接跳过,因为在之后i、j所指元素都大于当前元素
[0]和[0,0,0]这两个测试用例绝了
[0,0,0]决定了不能事先排除重复元素,要不然相当于数组里只有一个元素了,只能第一个重复元素进入while之后continue
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<vector<int>> ans;
int n=nums.size();
for(int i=0;i<n-2;i++){ n-2原因当数组剩余元素小于3时不用看了
if(nums[i]>0){
break;
}
if(i>0&&nums[i]==nums[i-1]){ 与上一个元素相同直接跳过
continue;
}
int l=i+1,r=n-1;
while(l<r){
int sum = nums[i]+nums[l]+nums[r];
if(sum<0){ 这两个if可以合并到while里
while(l<r&&nums[l]==nums[++l]); 相同元素就++
}
else if(sum>0){
while(l<r&&nums[r]==nums[--r]); 相同元素就--
}
else{
ans.push_back({nums[i],nums[l],nums[r]});
while(l < r && nums[l] == nums[++l]);
while(l < r && nums[r] == nums[--r]);
}
}
}
return ans;
}
- 题号42:接雨水
在某个位置i处,它能存的水,取决于它左右两边的最大值中较小的一个减去自身高度,再将每个位置水相加就是答案
方法一:暴力遍历
找到每一个位置的左右两边最大高度
时间复杂度O(n2),空间复杂度O(1)
public int trap(int[] height) {
int ans = 0;
int size = height.length;
for (int i = 1; i < size - 1; i++) {
int max_left = 0, max_right = 0;
for (int j = i; j >= 0; j--) { //Search the left part for max bar size
max_left = Math.max(max_left, height[j]);
}
for (int j = i; j < size; j++) { //Search the right part for max bar size
max_right = Math.max(max_right, height[j]);
}
ans += Math.min(max_left, max_right) - height[i];
}
return ans;
}
方法二:动态规划
运用两次单层循环记录每个位置左右大小值,降低了时间复杂度
时间复杂度O(n),空间复杂度O(n)
public int trap(int[] height) {
if (height == null || height.length == 0)
return 0;
int ans = 0;
int size = height.length;
int[] left_max = new int[size];
int[] right_max = new int[size];
left_max[0] = height[0];
for (int i = 1; i < size; i++) {
left_max[i] = Math.max(height[i], left_max[i - 1]);
}
right_max[size - 1] = height[size - 1];
for (int i = size - 2; i >= 0; i--) {
right_max[i] = Math.max(height[i], right_max[i + 1]);
}
for (int i = 1; i < size - 1; i++) {
ans += Math.min(left_max[i], right_max[i]) - height[i];
}
return ans;
}
方法三:双指针遍历
当我们从左往右处理到left下标时,左边的最大值left_max对它而言是可信的,但right_max对它而言是不可信的。
当我们从右往左处理到right下标时,右边的最大值right_max对它而言是可信的,但left_max对它而言是不可信的。
左边0到left是小于右边最大值的
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int ans = 0;
int left_max = 0, right_max = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= left_max) {
left_max = height[left];
} else {
ans += (left_max - height[left]);
}
++left;
} else {
if (height[right] >= right_max) {
right_max = height[right];
} else {
ans += (right_max - height[right]);
}
--right;
}
}
return ans;
}
- 题号209:长度最小的子数组
从头开始遍历,sum+=尾指针元素。sum>目标值,首指针++,用while的原因是加上尾指针后,前面可能少两个数的情况下也大于目标值,只要大于sum就滑动
有些时候if,else可以被一个if或者while代替
要思考什么时候滑动窗口,本题为sum>s时滑动窗口
时间n,空间1
int minSubArrayLen(int s, vector<int>& nums) {
int sum=0,mlength=INT_MAX,n=nums.size(),l=0;
for(int i=0;i<n;i++){
sum+=nums[i];
while(sum>=s){
if((i-l+1)<mlength){
mlength=(i-l+1);
}
sum-=nums[l++];
}
}
return mlength==INT_MAX? 0:mlength;
}
6.题号88:合并两个有序数组
用从后向前插入可以节省复制的空间
尤其要注意数组m–=-1的这种情况
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int i = n + m - 1;
m--;
n--;
while (n >= 0 && m>=0) { 防止n或m=-1的情况
把两者大的数放到后面
if (nums1[m] > nums2[n]) {
swap(nums1[m--], nums1[i--]);
}
else {
swap(nums2[n--], nums1[i--]);
}
}
while(n>=0){ 把剩余nums2插入
nums1[i--]=nums2[n--];
}
}
7.剑指 Offer 04: 二维数组中的查找
找好分界线,以右上角为分界线,当前值大于taget就向下找(比他大的值都在下面),当前值小于target就向左找(比他小的值都在左边)
要注意空数组的情况
判空时matrix.size()==0要当在前面,不然 [] 时会matrix[0]超索引报错
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if(matrix.size()==0||matrix[0].size()==0){
return false;
}
int row = matrix.size(),columns=matrix[0].size();
int trow=0,tcolumns=columns-1;
while(trow<row && tcolumns>=0){
if(matrix[trow][tcolumns]==target){
return true;
}else if(matrix[trow][tcolumns]<target){
trow++;
}else{
tcolumns--;
}
}
return false;
}
8. 题号925:长按键入
双指针分别指向name和typed
当两个字符一致时都加加,不一致时判断是否与name上一个字符相同(是否长按键入),相同只t++,不同直接返回false
最后可能typed结束了,如“asdfasd”和“asdf”,前半部分一致但name没有遍历完,所以return n==name.size(),判断name是否完全遍历了
要注意数组越界问题,可以把n>0放在if前面
bool isLongPressedName(string name, string typed) {
int n=0,t=0,namel=name.size(),typel=typed.size();
while(t<typel){
if(n<name.size() && name[n]==typed[t]){
n++;
t++;
}
else if(n>0 && typed[t] == name[n-1]){
t++;
}
else{
return false;
}
}
return n==name.size();
}
9.题号424: 替换后的最长重复字符
滑动窗口,替换后也不符合要求就滑动
需要被替换的字符(窗口长度-窗口内最多字符的数量)<=k,满足条件
int characterReplacement(string s, int k) {
int l=0,cmax=INT_MIN,result=0;
vector<int> arr(26);
for(int i=0;i<s.size();i++){
arr[s[i]-'A']++;
cmax=max(cmax,arr[s[i]-'A']);
if(i-l+1-cmax>k){ 替换后也不符合要求
arr[s[l]-'A']--;
l++;
}else{ 本题else不写也可以,都无法超过当时的最大值
result=max(result,i-l+1);
}
}
return result;
}
10.题号1004. 最大连续1的个数 III
与上题类似
本题 r-l+1-count可换为0的数量(需要被替换的数)小于K
int longestOnes(vector<int>& A, int K) {
int count=0,mlength=0,l=0;
for(int r=0;r<A.size();r++){
if(A[r]==0){
count++;
}
if(count>K){
if(A[l]==0){
count--;
} 这个要放在l++的前面,要不然就不是原来的值了
l++;
}
mlength=max(mlength,r-l+1);
}
return mlength;
}
11.题号28. 实现 strStr()
只有当首字母相同时才开始比较,不同时跳出循环,判断j-i与目标字符串长度是否一致
int strStr(string haystack, string needle) {
int h=haystack.size(),n=needle.size();
if(n==NULL){
return 0;
}
for(int i=0;i<=h-n;i++){ 剩余长度小于目标字符串不用继续遍历
if(haystack[i]==needle[0]){
int j=i+1,k=1;
while(j<h && haystack[j]==needle[k]){
j++;
k++;
}
if((j-i)==n){
return i;
}
}
}
return -1;
}
12.题号202. 快乐数
不快乐数会进循环,快慢指针破循环
用dowhile可以不用赋初值,当两数相等或快指针等于1了,出循环
返回快指针是否等于1(确认是因为=1出循环,而不是两指针相遇了)
int bitSquareSum(int n) {
int sum = 0;
while(n)
{
int bit = n % 10;
sum += bit * bit;
n = n / 10;
}
return sum;
}
bool isHappy(int n) {
int slow = n, fast = n;
do{
slow = bitSquareSum(slow);
fast = bitSquareSum(fast);
fast = bitSquareSum(fast);
}while(slow != fast && fast!=1);
return fast == 1;
}
13.题号26. 删除排序数组中的重复项
不满足条件left就不动,对的才赋值,这样left之前都是满足条件的数
要注意right、left开始时要指向同一元素
int removeDuplicates(vector<int>& nums) {
if (nums.size() == 0) {
return 0;
}
int left = 0, right = 1;
for (int right = 0; right < nums.size();right++) {
不满足条件不动
if (nums[left]!=nums[right]) { 满足条件赋值给left
nums[++left] = nums[right];
}
}
return left+1;
}
14.题号80. 删除排序数组中的重复项 II
与上题一致,不过条件变化了
int removeDuplicates(vector<int>& nums) {
int j=1,n=nums.size(),count=1;
for(int i=1;i<n;i++){
if(nums[i]==nums[i-1]){
count++;
}else{
count=1;
}
if(count<=2){ 满足条件了,赋值并left右移
nums[j++]=nums[i];
}
}
return j;
}
15.题号283. 移动零
太棒了! 把0都放在后面就是把非零数都放到前面
实现方法也与上题类似,满足条件就赋值
void moveZeroes(vector<int>& nums) {
int n=nums.size(),left=0;
for(int j=0;j<n;j++){
if(nums[j]!=0){ 满足条件了,赋值并left右移
swap(nums[j],nums[left++]);
}
}
}
收获与体会
- 注意数组索引 - - 之后等于-1的情况
- 判空尽可能放在程序最前面
- 滑动窗口题,主要思考什么时候滑动窗口(一般是满足条件,扩充后又不满足时),左指针滑动到再次满足条件时
- 快慢指针破循环
- 把0都放在后面,就是把非零数都放在前面