LeetCode
- 数组
- 二分法
- 35.搜索插入位置
- 278. 第一个错误的版本
- 724.寻找中心索引
- 前缀和
- 56.合并区间
- 48.旋转图像
- 面试题 01.08. 零矩阵
- 498.对角线遍历(不太好理解)
- 解题思路:
- 代码
- 118. 杨辉三角
- 119. 杨辉三角 II
- 189. 旋转数组
- 217. 存在重复元素(哈希表)
- 136. 只出现一次的数字(位运算)
- 350. 两个数组的交集 II(双指针,哈希表)
- 66. 加一
- 1. 两数之和(哈希表)
- 7. 整数反转(栈)
- 88. 合并两个有序数组(排序)
- 剑指 Offer 03. 数组中重复的数字
- 思路
- 字符串
- 14.最长公共前缀(简单,自己做出来了)
- 5.最长回文串(动态规划)
- 暴力破解
- 动态规划
- 中心扩散(双指针)
- 387. 字符串中的第一个唯一字符
- 8. 字符串转换整数 (atoi)
- 38. 外观数列
- 3. 无重复字符的最长子串(双指针+滑动窗口)
- 567. 字符串的排列(滑动窗口+哈希表)
- 双指针
- 151.翻转字符串里的单词
- 557. 反转字符串中的单词 III
- 167. 两数之和 II - 输入有序数组
- 209. 长度最小的子数组
- 485. 最大连续1的个数
- 153. 寻找旋转排序数组中的最小值
- 26. 删除排序数组中的重复项
- 283. 移动零
- 122. 买卖股票的最佳时机 II
- 125. 验证回文串
- 268. 丢失的数字
数组
二分法
核心思想:「减而治之」,即「不断缩小问题规模」
两种思路:
1.在循环体内部查找元素
-
while(left <= right) 这种写法表示在循环体内部直接查找元素;
-
退出循环的时候 left 和 right 不重合,区间 [left, right] 是空区间。
2.在循环体内部排除元素(重点)
-
while(left < right) 这种写法表示在循环体内部排除元素;
-
退出循环的时候 left 和 right 重合,区间 [left, right] 只剩下成 1 个元素时退出循环,这个元素 有可能 就是我们要找的元素。
-
左右边界往中间走,两边夹
二分法重点 -
分析题意,挖掘题目中隐含的 单调性(有序数组,递增或递减);
-
while (left < right) 退出循环的时候有 left == right 成立,因此无需考虑返回 left 还是 right;
-
始终思考下一轮搜索区间是什么,如果是 [mid, right] 就对应 left = mid ,如果是 [left, mid - 1] 就对应 right = mid - 1,是保留 mid 还是 +1、-1就在这样的思考中完成;
-
从一个元素什么时候不是解开始考虑下一轮搜索区间是什么 ,把区间分为 2个部分(一个部分肯定不存在目标元素,另一个部分有可能存在目标元素),问题会变得简单很多(逐渐缩小范围),这是一条 非常有用 的经验;
-
每一轮区间被划分成 2 部分,理解 区间划分 决定中间数取法( 无需记忆,需要练习 + 理解 ),在调试的过程中理解 区间和中间数划分的配对关系:
划分 [left, mid] 与 [mid + 1, right] ,mid 被分到左边 ,对应 int mid = left + (right - left) / 2;
划分 [left, mid - 1] 与 [mid, right] , mid 被分到右边 ,对应 int mid = left + (right - left + 1) / 2; -
至于为什么划分是这种对应关系,原因在于区间只有 2 个数的时候,如果中间数的取法不对,一旦进入的分支不能使得区间缩小,会出现 死循环。暂时不理解问题不大,需要在练习中进行调试;
-
退出循环的时候有 left == right 成立,此时如果能确定问题一定有解,返回 left 即可,如果不能确定,需要单独判断一次。
35.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
解题思路:
- 由于给定的是顺序数组,所以可以用二分法的思想。
- 首先是二分法查找,定义查找区间边界范围为 [left , right] 将目标元素 t 与数组中间位置元素 m 进行比较,对于升序数组,如果t的值小于m,则说明t位于左半部分,而不会在右半部分,查找区间变成 [left , m-1] ,由此来缩小查找区间。
- 如果数组中没有目标元素,则要求出它插入的位置,插入位置是第一个大于t的数组元素的位置。
- 继续缩小区间的条件设为while(left<=right),即当left>right时会退出循环,此时的left的值就刚好是第一个大于t的位置,也就是程序应该返回的值。
因此,严格小于 target 的元素一定不是解,在循环体中将左右边界 left 和 right 逐渐向中间靠拢,最后 left 和 right 相遇,则找到了插入元素的位置。根据这个思路,可以写出如下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution { public: int searchInsert(vector<int>& nums, int target) { int left=0; int right=nums.size()-1; int m=0; while(left<=right) { m=(left+right)/2; if(nums[m]==target) return m; else if(nums[m]>target) right=m-1; else left=m+1; } //当left>right时,说明数组中没有目标元素,返回left的值为插入位置 return left; } };
278. 第一个错误的版本
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有n
个版本[1, 2, ..., n]
,你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version)
接口来判断版本号version
是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
思路:
找第一个true
的位置,利用二分法,逐渐缩小搜索区间,当left
大于right
时,输出左边界位置,即为答案。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// The API isBadVersion is defined for you. // bool isBadVersion(int version); class Solution { public: int firstBadVersion(int n) { //二分查找,找第一个true long left=1,right=n,mid; //当left>right时返回left while(left<=right) { //取中间值 mid=(left+right)/2; //当为true时,将搜索区间缩小为左半部分 if(isBadVersion(mid)==true) right=mid-1; //当为false时,搜索区间缩小为右半部分 else left=mid+1; } return left; } };
724.寻找中心索引
给定一个整数类型的数组 nums,请编写一个能够返回数组 “中心索引” 的方法。
我们是这样定义数组 中心索引 的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和。
如果数组不存在中心索引,那么我们应该返回 -1。如果数组有多个中心索引,那么我们应该返回最靠近左边的那一个。
解题思路: 利用 前缀和的思想,首先计算数组的总和sum,然后循环计算前 i 个元素的和leftsum,对于第i个元素,如果满足nums[i]=sum-2*leftsum,即 i 两边的元素和相等,此时的 i 就是要求的中心索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution { public: int pivotIndex(vector<int>& nums) { int sum=0; int leftsum=0; //计算数组总和 for(int i:nums) sum+=i; for(int i=0;i<nums.size();i++) { //寻找中心索引,左边部分和等于右边部分和,满足nums[i]==sum-2*leftsum if(nums[i]==sum-2*leftsum) return i;//满足条件的话返回中心索引 leftsum+=nums[i];//不满足的话就继续累加 } return -1; } };
前缀和
前缀和是数列的前n项和,Sn = a1+a2+a3+...an
。比如S5 = a1 + a2 + a3 + a4 + a5; S2 = a1 + a2
。所以我们完全可以通过S5-S2
得到 a3+a4+a5
的值,这个过程就和我们做题用到的前缀和思想类似。前缀和数组里保存的就是前 n 项的和。
通过下面这段代码得到前缀和数组
1
2
3
4for (int i = 0; i < nums.length; i++) { presum[i+1] = nums[i] + presum[i]; }
56.合并区间
给出一个区间的集合,请合并所有重叠的区间。
解题思路:先将intervals
中的区间按照做端点进行升序排列。创建一个新的二维数组result
来存储结果,首先放入intervals
中的第一个元素,然后逐个考察剩下的元素。如果intervals[i]
中的左端点小于result
中最后一个元素的右端点,则说明两区间有重合部分,更新result中最后一个元素的右端点为两者右端点中的较大值;否则,直接将当前区间放入result中,因为它和result中最后一个元素不重合。
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
45class Solution { public: vector<vector<int>> merge(vector<vector<int>>& intervals) { if (intervals.size() == 0) { return {}; } vector<vector<int>> result; //冒泡排序,时间超出限制,不用 /*int m=size;//intervals的长度 int flag=1; vector<int> temp; //先对intervals中的区间按左边界进行排序 while((m>0)&&(flag==1)) { flag=0; //冒泡排序(太慢了) for(int i=0;i<m-1;i++)//这里要注意边界范围,不能是m,因为后面会索引到i+1这个位置,会超出范围,导致溢出 { if(intervals[i][0]>intervals[i+1][0]) { flag=1; temp=intervals[i]; intervals[i]=intervals[i+1]; intervals[i+1]=temp; } } --m; }*/ //排序 sort(intervals.begin(),intervals.end()); result.push_back(intervals[0]); for(int j=1;j<intervals.size();j++) { if(intervals[j][0]<=result.back()[1])//当前区间左边界小于result中最后一个区间的右边界 //更新result中的区间 result.back()[1]=max(result.back()[1],intervals[j][1]); else result.push_back(intervals[j]); } return result; } };
48.旋转图像
给定一个 n × n
的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
必须在原地旋转图像,这意味着需要直接修改输入的二维矩阵。而不要使用另一个矩阵来旋转图像。
解题思路: 原地九十度旋转图像的过程,可以分成两个部分进行,第一步是以主对角元素作为对称轴,交换两边对称位置的元素;第二步是进行左右镜像翻转。
本题的重点在于应对哪些元素进行操作,交换操作只需对一边的元素操作即可,左边交换了右边再交换一次就相当于没有交换。
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
32class Solution { public: void rotate(vector<vector<int>>& matrix) { int size=matrix.size(); int temp=0; //以主对角线为对称轴交换元素 for(int i=0;i<size;i++) { for(int j=0;j<=i;j++)//对j,循环条件注意是小于i,因为只需对一边做交换 { if(i==j) { continue; } temp=matrix[i][j]; matrix[i][j]=matrix[j][i]; matrix[j][i]=temp; } } //沿竖直对称轴镜像交换元素 for(int i=0;i<size;i++)//循环条件注意是小于号 { for(int j=0;j<size/2;j++)//循环条件注意是小于矩阵列数的二分之一,列数为奇数时,中间列不做交换,只需要做一边的交换就好了 { temp=matrix[i][size-1-j]; matrix[i][size-1-j]=matrix[i][j]; matrix[i][j]=temp; } } } };
面试题 01.08. 零矩阵
编写一种算法,若M × N矩阵中某个元素为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
30
31
32
33class Solution { public: void setZeroes(vector<vector<int>>& matrix) { int r=matrix.size();//行数 int c=matrix[0].size();//列数 vector<bool> r_ind(r,0); vector<bool> c_ind(c,0); for(int i=0;i<r;i++) { for(int j=0;j<c;j++) { if(matrix[i][j]==0) { r_ind[i]=1; c_ind[j]=1; } } } for(int i=0;i<r;i++) { for(int j=0;j<c;j++) { if(r_ind[i]||c_ind[j]) { matrix[i][j]=0; } } } } };
498.对角线遍历(不太好理解)
给定一个含有 M x N 个元素的矩阵(M 行,N 列),请以对角线遍历的顺序返回这个矩阵中的所有元素,对角线遍历如下图所示。
解题思路:
- 总共会进行M+N-1趟,每一趟中元素的坐标(x,y)相加的和i=x+y是递增的
1
2
3
4
5第一趟:1 的坐标(0, 0)。x + y == 0。 第二趟:2 的坐标(1, 0),4 的坐标(0, 1)。x + y == 1。 第三趟:7 的坐标(0, 2), 5 的坐标(1, 1),3 的坐标(2, 0)。第三趟 x + y == 2。 第四趟:……
- 第二趟:2 的坐标(1, 0),4 的坐标(0, 1)。x 每次-1,y 每次+1。
1
2
3第二趟:2 的坐标(1, 0),4 的坐标(0, 1)。x 每次-1,y 每次+1。 第三趟:7 的坐标(0, 2), 5 的坐标(1, 1),3 的坐标(2, 0)。x 每次 +1,y 每次 -1。
- 确定初始值。当往左下遍历时,x从大到小,x的初始值尽可能取到最大(受限于x+y=i和x<N),当x的初始值取到上限时,不足i的部分交给y(因为y=i-x)
1
2
3第二趟:2 的坐标(1, 0),4 的坐标(0, 1)。x + y == 1,x 初始值取 1,y 取 0。 第四趟:6 的坐标(2, 1),8 的坐标(1, 2)。x + y == 3,x 初始值取 2,剩下的加到 y上,y 取 1。
- 确定结束值。当往右上遍历时,x从小到大,这趟的结束判断是,x加到上限或y减到0;
1
2
3第二趟:2 的坐标(1, 0),4 的坐标(0, 1)。x 减到 0 为止。 第四趟:6 的坐标(2, 1),8 的坐标(1, 2)。x 虽然才减到 1,但是 y 已经加到上限了。
- 奇数趟和偶数趟只是方向相反,x和y各自的上限值相反,其他逻辑处理是一样的
1
2
3x 从大到小,第二趟:2 的坐标(1, 0),4 的坐标(0, 1)。x + y == 1,x 初始值取 1,y 取 0。结束值 x 减到 0 为止。 x 从小到大,第三趟:7 的坐标(0, 2),5 的坐标(1, 1),3 的坐标(2, 0)。x + y == 2,y 初始值取 2,x 取 0。结束值 y 减到 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
30
31
32
33
34
35
36vector<int> findDiagonalOrder(vector<vector<int>>& matrix) { vector<int> nums; //存储结果 int m = matrix.size(); //行数 if (m == 0) return nums; int n = matrix[0].size(); //列数 if (n == 0) return nums; //将往右上和往左下的代码合在一起,用一个标记值标记 bool bXFlag = true; //1代表右上 for (int i = 0; i < m + n; i++)//i为每趟中x和y的和 { int pm = bXFlag ? m : n;//pm为x的上限 int pn = bXFlag ? n : m;//y的上限是列数 //确定初始值,x尽量取到最大 int x = (i < pm) ? i : pm - 1; //剩下的给y int y = i - x; //开始对角线遍历,将元素存到数组 while (x >= 0 && y < pn) { //往左下遍历,横坐标从大到小(——),纵坐标从小到大(++) //往右上遍历,相反 nums.push_back(bXFlag ? matrix[x][y] : matrix[y][x]); x--; y++; } bXFlag = !bXFlag; } return nums; }
118. 杨辉三角
给定一个非负整数 numRows
,生成杨辉三角的前numRows
行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
解题思路: 暂时没想到什么技巧,就对数组每行遍历赋值,这道题的关键操作是vector的应用,用resize()
分配空间为每行数组创建合适长度的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Solution { public: vector<vector<int>> generate(int numRows) { vector<vector<int>> result(numRows); if(numRows==0) return result; for(int i=0;i<numRows;i++) { //resize()既分配空间,也创建对象 result[i].resize(i+1); //每行第一个和最后一个元素为1 result[i][0]=result[i][i]=1; for(int j=1;j<i;j++) { //对中间的数进行赋值,利用上一行的结果 result[i][j]=result[i-1][j]+result[i-1][j-1]; } } return result; } };
119. 杨辉三角 II
给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。
解题思路:
- O(K)的空间复杂度的话,那就只创建一个
(k+1)
长度的数组,并且直接在数组上修改数值,依次计算杨辉三角形每行的值,返回第k行。 - 关键点在于求中间每个元素的值得时候应该从后往前计算(从前往后的话会将发生更新了的值纳入计算)(图片来自【笨猪爆破组】题解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Solution { public: vector<int> getRow(int rowIndex) { //构建结果数组,大小为rowIndex+1,初始值都为0 vector<int>result(rowIndex+1); result[0]=1; for(int i=1;i<=rowIndex;i++) { //i代表对每一行进行计算,每一行都是根据上一行的计算结果来的 //只在一个数组里修改元素,不用另外创建 //所以要从后往前加 for(int j=i;j>0;j--) { result[j]=result[j]+result[j-1]; } } return result; } };
189. 旋转数组
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
进阶:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
我的解法: 利用vector
的操作函数,逐个地将数组nums
的后面k
个元素插入到前部去,然后再逐个删除后面的k个元素(插入的同时删除)
空间倒是小了,但是超级超级慢,只超过了5%,呜呜呜。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Solution { public: void rotate(vector<int>& nums, int k) { //方法二,将后面k个元素依次插入数组前部,然后删掉,还是很慢 int tmp; for(int i=0;i<k;i++) { tmp=nums.back(); nums.pop_back(); nums.insert(nums.begin(),tmp); } } };
官方神仙题解: 又快又好理解,用过都说好
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Solution { public: void rotate(vector<int>& nums, int k) { //官方题解三,这个方法好妙 //先整体全部翻转,然后再在前半段和后半段内部翻转 //k大于size时取余,因为移动size步相当于没有移 k=k%nums.size(); reverse(nums.begin(),nums.end()); reverse(nums.begin(),nums.begin()+k); reverse(nums.begin()+k,nums.end()); } };
呜呜呜我太菜了
217. 存在重复元素(哈希表)
给定一个整数数组,判断是否存在重复元素。
如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。
解题思路: 利用哈希表 ,通过unordered_set.创建一个哈希表,利用函数find()
找表中 是否存在目标元素,存在的话会返回指向目标元素的迭代器(指针),不存在的话返回end()
值。如果找到相同的元素,返回true
,没找到的话就将该元素插入表中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Solution { public: bool containsDuplicate(vector<int>& nums) { if(nums.size()<2) return false; unordered_set<int> numset; for(int i:nums) { if(numset.find(i)!=numset.end()) return true; numset.insert(i); } return false; } };
136. 只出现一次的数字(位运算)
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
解题思路:
这道题没经验的怎么可能想到要用位运算里的异或啦,啊不过异或算法真的太强了,两行代码就能搞定。
异或运算的主要原理是:
1
2
3
4x XOR x=0 x XOR 0=x 交换律: x XOR y XOR x=x XOR x XOR y
也就是说,数组中如果存在两个值相同的元素,只有一个只出现一次,如果将数组内全部元素都进行异或运算,那么相同的元素会变成0,剩下的元素就是那个单出来的。
太强了,这个想法无敌
代码
1
2
3
4
5
6
7
8
9
10
11
12class Solution { public: int singleNumber(vector<int>& nums) { int tmp=nums[0]; for(int i=1;i<nums.size();i++) { tmp=tmp ^ nums[i]; } return tmp; } };
350. 两个数组的交集 II(双指针,哈希表)
给定两个数组,编写一个函数来计算它们的交集。
说明:
- 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
- 我们可以不考虑输出结果的顺序。
进阶:
- 如果给定的数组已经排好序呢?你将如何优化你的算法?(双指针)
- 如果
nums1
的大小比nums2
小很多,哪种方法更优? - 如果
nums2
的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
方法一 双指针
思路: 先对两个数组 进行排序,然后两个指针分别指向数组头部,比较所指元素大小,如果相等,就将元素加入result数组,不相等的话将所指数值较小的指针向右移,直到两指针中有一个指向了尾部。
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
26class Solution { public: vector<int> intersect(vector<int>& nums1, vector<int>& nums2) { vector<int>result; //双指针解法 int i=0,j=0; //先排序 sort(nums1.begin(),nums1.end()); sort(nums2.begin(),nums2.end()); while(i<nums1.size()&&j<nums2.size()) { if(nums1[i]==nums2[j]) { result.push_back(nums1[i]); i++; j++; } else if(nums1[i]<nums2[j]) i++; else j++; } return result; } };
方法二 哈希表
思路: 构造一个哈希表,存入较短数组的元素值及出现次数,然后遍历较长数组,当哈希表中存在相同元素时,将其加入result,然后哈希表中对应元素的次数减1,直到减为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
29class Solution { public: vector<int> intersect(vector<int>& nums1, vector<int>& nums2) { if (nums1.size() > nums2.size()) { //这里很妙,通过再调用自己来避免写重复代码 return intersect(nums2, nums1); } //哈希表 unordered_map <int, int> m; //遍历较短的数组,将元素加到哈希表,元素值作为键,出现次数作为值 for (int num : nums1) { ++m[num]; } vector<int> intersection; for (int num : nums2) { //当存在相同元素,就将该数加到结果数组,然后哈希表中的次数减1 if (m.count(num)) { intersection.push_back(num); --m[num]; //当减为0时,表中删掉这个元素 if (m[num] == 0) { m.erase(num); } } } return intersection; } };
66. 加一
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
解题思路:
- 由于加一之后可能导致进位,并且可能还会将进位往前传(比如
9999
这种数字),所以要先做一个判断 - 指针
i
先指向数组digits
尾部,如果当前位置加一之后是小于10
的,那么该位置加一后输出结果; - 如果等于
10
,那么该位置置为0
,然后继续循环,指针往前移,继续看当前位置加一后的结果,直到遍历完整个数组(i=-1
)时跳出循环 - 而此时没有返回结果,说明最高位还需要再进一位,此时在
digits
数组前面再插入一个1
就好。 - 呜呜呜我自己做出来的(虽然是道
简单
),100% 93% 但是代码写的很简洁,也有一点点技巧在,呜呜呜我成长了,我出息了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution { public: vector<int> plusOne(vector<int>& digits) { //考虑进位 for(int i=digits.size()-1;i>=0;i--) { if(digits[i]+1<10) { digits[i]++; return digits; } digits[i]=0; } digits.insert(digits.begin(),1); return digits; } };
1. 两数之和(哈希表)
又回到了梦开始的地方
给定一个整数数组 nums
和一个整数目标值target
,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
你可以按任意顺序返回答案。
解题思路:利用哈希表 ,遍历数组利用find()
查找表中是否存在键为target-nums[i]
的元素,存在的话即返回结果,不存在的话将元素值nums[i]
和它的下标i
存入表中。
知道做法的话这道题还是挺简单的,当时第一次做的时候真的一脸懵逼,啥???哈希表???这是什么鬼东西,现在已经大概会使用了,可以可以,我又成长了,寒假这段时间的题没白做啊,真好。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { unordered_map<int,int>pair; for(int i=0;i<nums.size();i++) { auto itr=pair.find(target-nums[i]); if(itr!=pair.end()) //无需另外创建数组,可直接将结果返回 return {itr->second,i}; else //没有匹配的数,就将当前数插入哈希表,键为元素值,值为下标 pair[nums[i]]=i; } //如果没有答案,则返回空 return {}; } };
7. 整数反转(栈)
给你一个 32 位
的有符号整数 x
,返回 x 中每位上的数字反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围[2^31, 2^31− 1]
,就返回 0。
假设环境不允许存储 64 位整数(有符号或无符号)。
思路:
- 利用栈,首先从x低位开始提取数字(对10取余),相当于出栈,然后再压入另外一个栈中,低位变高位。
- 这道题的关键是溢出的判断,32位有符号整数的取值范围是
-2147483648 ~ +2147483647
,因此需要在压入栈之前做一个判断,看是否会溢出。
代码(官方题解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution { public: int reverse(int x) { int rev = 0; while (x != 0) { //从低位取数字,类似出栈 int pop = x % 10; x /= 10; //溢出判断 //int的范围:-2147483648 ~ +2147483647 //如果rev*10+pop导致溢出,那么一定有rev*10+pop > INT_MAX,即有rev >= INT_MAX/10 if (rev > INT_MAX/10 || (rev == INT_MAX / 10 && pop > 7)) return 0; if (rev < INT_MIN/10 || (rev == INT_MIN / 10 && pop < -8)) return 0; //将低位数字放到前面,相当于压入栈 //下面这个类似入栈的操作挺妙的 rev = rev * 10 + pop; } return rev; } };
88. 合并两个有序数组(排序)
给你两个有序整数数组nums1
和 nums2
,请你将nums2
合并到nums1
中,使 nums1
成为一个有序数组。
初始化nums1
和nums2
的元素数量分别为 m
和n
。你可以假设 nums1
的空间大小等于m + n
,这样它就有足够的空间保存来自 nums2
的元素。
解法一 :
将nums1
后的0
全部由nums2
替换,然后再将nums1
进行排序,简单简单
1
2
3
4
5
6
7
8
9
10
11class Solution { public: void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) { //先将nums2放到nums1后面然后再排序? //复制 copy(nums2.begin(),nums2.end(),nums1.begin()+m); //排序 sort(nums1.begin(),nums1.end()); } };
**解法二: ** 官方题解 解法三
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
28class Solution { public: void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) { int p1 = m - 1, p2 = n - 1; int tail = m + n - 1; int cur; while (p1 >= 0 || p2 >= 0) { //nums1遍历完了 //将p2剩下的部分放在前面 if (p1 == -1) { cur = nums2[p2--]; } //nums2遍历完了 else if (p2 == -1) { cur = nums1[p1--]; } //将较大的元素先放入 else if (nums1[p1] > nums2[p2]) { cur = nums1[p1--]; } else { cur = nums2[p2--]; } nums1[tail--] = cur; } } };
剑指 Offer 03. 数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums
里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
思路
有三种方法:
- 用哈希表,将没有出现过到的元素依次加入表中,遇到出现过的元素直接返回,时间O(N),空间O(N)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public: int findRepeatNumber(vector<int>& nums) { //遍历数组元素,如果哈希表中没有这个数,就将其放入哈希表 //如果有,就返回这个数 unordered_set<int>S; for(int i:nums) { auto find_s=S.find(i); if(find_s!=S.end()) return i; else S.insert(i); } return 0; }
- 排序,然后看相邻元素中是否有重复的,运用排序算法
sort()
的时间O(NlogN)
,空间O(1)
1
2
3
4
5
6
7
8
9
10
11int findRepeatNumber(vector<int>& nums) { //排序,然后挨个看有没有重复的 sort(nums.begin(),nums.end()); for(int i=1;i<nums.size();i++) { if(nums[i-1]==nums[i]) return nums[i]; } return 0; }
- 原地置换 ,利用题目中的约束信息:长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内,这说明数组元素的索引和值是一对多的关系,利用鸽巢原理,重复的元素无法实现索引与值的一一对应,参考题解的方法二
1
2
3
4
5
6
7
8
9
10
11
12
13
14int findRepeatNumber(vector<int>& nums) { int i = 0; while(i < nums.size()) { if(nums[i] == i) { i++; continue; } if(nums[nums[i]] == nums[i]) return nums[i]; swap(nums[i],nums[nums[i]]); } return -1; }
字符串
字符串比较
- 字符串有自己的比较函数,在
C++
和python
中可以使用==
来比较两个字符串,Java
中不可以。(支持运算符重载的语言才可以)
字符串连接
-
对于不同的编程语言中,字符串可能是可变的,也可能是不可变的。不可变意味着一旦字符串被初始化,你就无法改变它的内容。
-
在某些语言(如 C ++)中,字符串是可变的。 也就是说,你可以像在数组中那样修改字符串。
-
在其他一些语言(如 Java、Python)中,字符串是不可变的。
-
不可变字符串无法被修改,哪怕你只是想修改其中的一个字符,也必须创建一个新的字符串。
14.最长公共前缀(简单,自己做出来了)
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""
。
解题思路:
-
将字符串数组中第一个字符串
strs[1]
作为基准,逐个拿出strs[1]
中的每个字母与字符串数组中剩下字符串的相应位置字母进行比较。 -
设置一个标志位
flag=1
,如果在比较过程中发现有一个字母对应不上(即有一个字母不在公共前缀中),则将flag置为0,然后退出循环。 -
一轮比较结束后,如果flag始终为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
27class Solution { public: string longestCommonPrefix(vector<string>& strs) { if(!strs.size()) return ""; string result=""; string a=strs[0]; int flag=1; for(int i=0;i<a.length();i++) { for(int j=1;j<strs.size();j++) { if(a[i]!=strs[j][i]) { flag=0; break; } } if(flag) result+=a[i]; } return result; } };
5.最长回文串(动态规划)
给你一个字符串 s
,找到 s
中最长的回文子串
解题思路:
暴力破解
- 列举出所有可能的字符串,然后判断它是否为回文串
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
46
47
48
49
50#include <iostream> #include <string> #include <vector> using namespace std; class Solution { private: bool valid(string s, int left, int right) { // 验证子串 s[left, right] 是否为回文串 while (left < right) { if (s[left] != s[right]) { return false; } left++; right--; } return true; } public: string longestPalindrome(string s) { // 特判 int size = s.size(); if (size < 2) { return s; } int maxLen = 1; string res = s.substr(0, 1); // 枚举所有长度大于等于 2 的子串 for (int i = 0; i < size - 1; i++) { for (int j = i + 1; j < size; j++) { //valid(s, i, j)判断是否是回文串 //j - i + 1 > maxLen判断该回文串是否是目前找到的最长的 if (j - i + 1 > maxLen && valid(s, i, j)) { maxLen = j - i + 1; res = s.substr(i, maxLen); } } } return res; } };
动态规划
详细介绍:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
- 确定dp数组(dp table)以及下标的含义
布尔类型的dp[i][j]
:表示区间范围[i,j]
(注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]
为true
,否则为false
。
确定递推公式
在确定递推公式时,就要分析如下几种情况。
整体上是两种,就是s[i]
与s[j]
相等,s[i]
与s[j]
不相等这两种。
当s[i]
与s[j]
不相等,那没啥好说的了,dp[i][j]
一定是false。
当s[i]
与s[j]
相等时,这就复杂一些了,有如下三种情况
1
2
3
4情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串 情况二:下标i 与 j相差为1,例如aa,也是文子串 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i+1] [j-1]是否为true。
以上三种情况分析完了,那么递归公式如下
1
2
3
4
5
6
7
8if (s[i] == s[j]) { if (j - i <= 1) { // 情况一 和 情况二 dp[i][j] = true; } else if (dp[i + 1][j - 1]) { // 情况三 dp[i][j] = true; } }
在得到[i,j]区间是否是回文子串的时候,直接保存最长回文子串的左边界和右边界
dp[i] [j初始化为FALSE
,由于右边界j大于左边界i,左移dp数组只用填充右上半部分。
遍历顺序
由于dp[i] [j]
的值需要参考dp[i+1] [j-1]
的值,dp[i+1] [j-1]
位于dp[i] [j]
左下角,所以遍历顺序应该是从上往下或从下往上先遍历完一列后再往右遍历下一列。
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
32class Solution { public: string longestPalindrome(string s) { //创建并初始化dp数组 vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0)); //记录最长回文串的长度和起始位置就好,最后返回在原字符串上截取的子串 int maxlenth = 0; int left = 0; int right = 0; //填表 for (int i = s.size() - 1; i >= 0; i--) { for (int j = i; j < s.size(); j++) { if (s[i] == s[j]) { if (j - i <= 1) { // 情况一 和 情况二 dp[i][j] = true; } else if (dp[i + 1][j - 1]) { // 情况三 dp[i][j] = true; } } //查找最长(maxlengh最大)回文串(dp[i][j]为ture if (dp[i][j] && j - i + 1 > maxlenth) { maxlenth = j - i + 1; left = i; right = j; } } } return s.substr(left, right - left + 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
34class Solution { public: int left = 0; int right = 0; int maxLength = 0; string longestPalindrome(string s) { int result = 0; //遍历可能的中心点,向两边扩散看是否存在回文数 for (int i = 0; i < s.size(); i++) { extend(s, i, i, s.size()); // 以i为中心 extend(s, i, i + 1, s.size()); // 以i和i+1为中心 } //截取子串 return s.substr(left, maxLength); } //定义中心扩散的函数,作用在给定字符串和中心位置的情况下,从中心位置开始向外扩散,找到最长回文串的左右边界 void extend(const string& s, int i, int j, int n) { // 中心点两边的元素要满足条件s[i] == s[j] //左边界扩散的极限是0,有便捷的极限是n-1 while (i >= 0 && j < n && s[i] == s[j]) { if (j - i + 1 > maxLength) { left = i; right = j; maxLength = j - i + 1; } i--; j++; } } };
387. 字符串中的第一个唯一字符
给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。
只含小写字母。
思路:
- 利用哈希表,遍历两遍字符串,第一遍将字符串中的字符以及出现次数存到表中
- 第二遍时看每个字符在哈希表中所记录的次数,返回次数为1的索引
- 这道题不难,但是我的代码一遍通过(除了没找到不满足条件的数时返回值写错),就很爽
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Solution { public: int firstUniqChar(string s) { //利用哈希表,先将字符串整个存入表中 //键为字符,值为出现次数 unordered_map<char,int>str; for(int i=0;i<s.length();i++) ++str[s[i]]; for(int j=0;j<s.length();j++) { auto itr=str.find(s[j]); //当在哈希表中找到当前字符,并且出现次数为1,即为答案 if(itr!=str.end()&&itr->second==1) return j; } return -1; } };
8. 字符串转换整数 (atoi)
请你来实现一个 myAtoi(string s)
函数,使其能将字符串转换成一个 32 位有符号整数(类似C/C++
中的 atoi
函数)。
函数 myAtoi(string s)
的算法如下:
- 读入字符串并丢弃无用的前导空格
' '
。(去掉前面所有的空格) - 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
- 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。(只读取记录一个符号,之后遇到非数字的字符就退出)
- 将前面步骤读入的这些数字转换为整数(即,
"123" -> 123
,"0032" -> 32
)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。(利用压入的思想得到整数) - 如果整数数超过 32 位有符号整数范围
[−2^31, 2^31 − 1]
,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被固定为−2^31
,大于2^31 − 1
的整数应该被固定为2^31 − 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
39class Solution { public: int myAtoi(string s) { bool symbol=true; int i=0; int num=0; //读入字符 //舍弃前面的空格 while(s[i]==' ') i++; //检查是否有负号,只需要读取一个符号,后面再出现将作为非数字字符处理,没有符号就默认为正号。 //确定最终结果为正数还是负数 if(s[i]=='-') { symbol=false; i++; } //遇到正号就将指针往后挪一位 else if(s[i]=='+') i++; //读入数字,直到到达末尾,或者遇到非数字字符 while(i<s.size()&&isdigit(s[i])) { //溢出判断 //上边界 if(symbol && (num>INT_MAX/10 ||(num==INT_MAX/10 && (s[i]-'0'>6)))) return INT_MAX; //下边界 if(!symbol && (num>INT_MAX/10 ||(num==INT_MAX/10 && (s[i]-'0'>7)))) return INT_MIN; //后面那个s[i]-'0'是将数字字符转化成了s[i]对应的整数 num=num*10+(s[i]-'0'); i++; } if(symbol) //正 return num; else //负 return -num; } };
38. 外观数列
给定一个正整数 n ,输出外观数列的第 n 项。
「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。
你可以将其视作是由递归公式定义的数字字符串序列:
countAndSay(1) = “1”
countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。
前五项如下:
1
2
3
4
5
6
7
8
9
10
11
121. 1 2. 11 3. 21 4. 1211 5. 111221 第一项是数字 1 描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11" 描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21" 描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211" 描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221" 要 描述 一个数字字符串,首先要将字符串分割为 最小 数量的组,每个组都由连续的最多 相同字符 组成。然后对于每个组,先描述字符的数量,然后描述字符,形成一个描述组。要将描述转换为数字字符串,先将每组中的字符数量用数字替换,再将所有描述组连接起来。
例如,数字字符串 “3322251” 的描述如下图:
解题思路: 利用递归的思想,逐层向下直到n
为1
,返回“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
34class Solution { public: string countAndSay(int n) { //当减到n为1时,触发递归退出的条件,返回“1”,返回给tmp变量 //然后再利用tmp字符串逐层求出上层项的描述字符串 if(n==1) return "1"; //每次调用返回的字符串 string result; //应用递归,向下求出底层项的描述字符串 string tmp=countAndSay(n-1); //i是遍历指针 //c用于计数,记录每个组中连续的最多相同字符数 int c=1; for(int i=0;i<tmp.length();i++) { //相邻字符相同时计数器加1 if(tmp[i]==tmp[i+1]) c++; //不同时i指向的是最后一个,比如“3322251”,第一次不同时,i指向第二个3 else { result+=to_string(c)+tmp[i]; //重置计数器 c=1; } } //返回每次调用产生的字符串 return result; } };
3. 无重复字符的最长子串(双指针+滑动窗口)
给定一个字符串s
,请你找出其中不含有重复字符的 最长子串 的长度。
思路: 根据官方题解的提示
- 对于给定字符串,找出从每一个字符开始的,不包含重复字符的最长子串
- 当依次递增枚举子串的起始位置时,子串的结束位置也是递增的,从上一轮找到的最长子串开始
- 假设我们选择字符串中的第
k
个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为rk
,那么当我们选择第k+1
个字符作为起始位置时,首先从k+1
到rk
的字符显然是不重复的,并且由于少了原本的第k
个字符 - 从上一轮的结束位置
rk
开始继续向右扩充,直到出现重复字符后记录这一轮的子串长度
滑动窗口
- 使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针
i
代表着上文中「枚举子串的起始位置」,而右指针即为上文中的rk
- 在每一步的操作中,会将左指针向右移动一格,表示 开始枚举下一个字符作为起始位置,然后不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。记录下这个子串的长度;
- 用C++中的哈希表
unordered_set
来判断是否出现重复元素 - 在枚举结束后,找到的最长的子串的长度即为答案。
代码
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
28class Solution { public: int lengthOfLongestSubstring(string s) { //双指针,活动窗口 //左右指针指向滑动窗口的左右边界 //用一个哈希表来判断是否有重复字符 unordered_set<char>set; int i=0,rk=0,res=0; while(i<s.size()) { for(;rk<s.size();rk++) { if(!set.count(s[rk])) { set.emplace(s[rk]); } else break; } int n=set.size(); res=max(n,res); //左边界向右移动时,从哈希表中移除相应字符 set.erase(s[i++]); } return res; } };
567. 字符串的排列(滑动窗口+哈希表)
给你两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。
换句话说,s1
的排列之一是 s2
的 子串
思路:
- 关键是在
s2
中找到一个子串,这个子串中字符的种类和出现次数要和s1
中一致 - 利用一个s1大小的滑动窗口,在
s2
中找符合要求的子串 - 一开始的想法是用
c++
的哈希表unordered_map
,但是超出了时间限制 - 看题解学习到一个新思路,字符集是确定的,即
26个小写字母
,可以通过长度为26的数组来记录字符串中每个字符出现的次数 s1[i]-'a'
的作用是将字符的ASCII
码归到0~25
的范围内,作为数组访问下标,来记录不同字符出现次数
算法步骤:
- 先判断
s1
是否比s2
短,异常输入判断 - 用哈希表记录
s1
中每种字符的出现次数,同时也记录s2
中初始滑动窗口中的每种字符的出现次数 - 比较两表是否相等
- 将滑动窗口向右移,新进来的字符次数加1,出去的字符次数减1,每次移动完窗口都要比较此时窗口中的字符是否和
s1
中一致
代码
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
29class Solution { public: bool checkInclusion(string s1, string s2) { int n = s1.length(), m = s2.length(); if (n > m) { return false; } vector<int> cnt1(26), cnt2(26); //s1中字符出现次数,s2初始窗口中字符出现次数 for (int i = 0; i < n; ++i) { ++cnt1[s1[i] - 'a']; ++cnt2[s2[i] - 'a']; } if (cnt1 == cnt2) { return true; } //i为滑动窗口右边界,是新进入窗口的字符,i-n是窗口左边界,是要从窗口中移除的字符 for (int i = n; i < m; ++i) { ++cnt2[s2[i] - 'a']; --cnt2[s2[i - n] - 'a']; //每次移动窗口都比较两个哈希表是否相等 if (cnt1 == cnt2) { return true; } } return false; } };
复杂度:
- 时间
O(n+m+∣Σ∣)
,其中n
是字符串s1
的长度,m
是字符串s2
的长度,Σ
是字符集,这道题中的字符集是小写字母,∣Σ∣=26
。 - 空间
O(∣Σ∣)
双指针
151.翻转字符串里的单词
给定一个字符串,逐个翻转字符串中的每个单词。
说明:
- 无空格字符构成一个 单词 。
- 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
- 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
提示:
- 1 <= s.length <= 104
- s 包含英文大小写字母、数字和空格 ’ ’
- s 中 至少存在一个 单词
思路:
- 要原地进行翻转的话,就不能另外创建一个字符串来从后往前读取保存单词。
- 可以先将字符串整个翻转,然后再在每个单词内进行翻转。
- 代码为官方题解提供的C++代码,代码很简短但是设计非常妙,妙到我看了两天并且在余某同学的帮助下才看懂(勉强看懂)。
- 巧妙的点在于充分利用索引下标,避开了字符串两端以及单词中间多余的空格,直接在原字符串空间中进行修改,而不用再创建一个,空间效率为O(1),时间效率上也是秒杀了91%的人,很强,感觉学到了新的思路。
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
40class Solution { public: string reverseWords(string s) { // 反转整个字符串 reverse(s.begin(), s.end()); int n = s.size(); int idx = 0;//利用新索引idx直接在原字符串上进行修改,以及作为找到的每个单词的末端索引 for (int start = 0; start < n; ++start) { //只有s[start]为非空格时才开始执行,这样就能避开前面的空格 if (s[start] != ' ') { // 第一次进入if时,还没有读到过单词,idx为0,此时不用在前面补空格 //之后再进入if的时候,已经读到了一个完整单词,单词后面应该有一个空格,此时idx处填一个空白字符,然后将idx移动到下一个单词的开头位置 if (idx != 0) s[idx++] = ' ';//单词之间的空格 // 从非空格字符s[start]开始,循环遍历至单词的末尾 int end = start; while (end < n && s[end] != ' ') //这里是在找字符串中属于单词的部分,当s[end]为空格时暂停遍历,一轮遍历得到的是一个完整单词 //将end遍历到的每个字符以idx存储 s[idx++] = s[end++]; //以上工作相当于将原字符串前后两端以及中间多余的空格舍去后重新保存,并且使用的是原字符串空间,没有另创空间。 // 反转整个单词 //reverse函数用于反转在[first,last)范围内的顺序 //(包括first指向的元素,不包括last指向的元素),reverse函数无返回值 reverse(s.begin() + idx - (end - start), s.begin() + idx); // 更新start,去找下一个单词,此时的s[end]是空格 //进行下一轮的时候同样,会先避开下一个单词之前的空格,只有s[start]非空格时才会进入if,开始遍历单词 start = end; } } s.erase(s.begin() + idx, s.end()); return s; } };
557. 反转字符串中的单词 III
给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
只在单词内部进行翻转,单词顺序不变
解题思路: 利用双指针,一个指针 i 指向单词开头,一个 j 指向单词末尾。一开始都指向字符串开头,用 j 去 寻找属于一个单词的子串,然后利用 reverse()
翻转单词的字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Solution { public: string reverseWords(string s) { if(s.length()==0) return s; int i=0,j=0; while(j<s.length()) { if(s[j]!=' ') { j++; } else if(s[j]==' ') { reverse(s.begin()+i,s.begin()+j); i=j+1; j++; } } //跳出循环时,最后一个单词还没有翻转,需要再做一次 reverse(s.begin()+i,s.end()); return s; } };
167. 两数之和 II - 输入有序数组
给定一个已按照 升序排列 的整数数组 numbers
,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length
。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
解题思路:利用双指针,初始时左指针指向numbers
的第一个元素,右指针指向最后一个元素;如果两数相加为target
,即为答案,如果大于target
,则右指针向左移;如果和小于target
左指针右移。
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
28class Solution { public: vector<int> twoSum(vector<int>& numbers, int target) { vector<int>answer; int i=0,j=numbers.size()-1; int tolt=0; while(i<j) { tolt=numbers[i]+numbers[j]; if(tolt==target) { answer.push_back(i+1); answer.push_back(j+1); break; } else if(tolt>target) { j--; } else { i++; } } return answer; } };
209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target
。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0 。
解题思路:
- 利用双指针,定义两个遍历指针i,j,初始都为0,最小数组长度n,初始为无穷大。
- 按j递增将元素进行相加到元素和sum,当加到sum>=target时,记录下此时子串(i到j之间的元素)的长度(j-i+1),然后按i递增用sum减去nums[i](减去左边界元素)
- 看减后的sum是否还大于target,如果还是大于,则再次更新子串长度,直到减后的子串元素和小于target,终止循环,返回满足条件的最小数组长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Solution { public: int minSubArrayLen(int target, vector<int>& nums) { int i=0,j=0,sum=0,n=INT_MAX; while(j<nums.size()) { //元素和 sum+=nums[j]; while(sum>=target) { //n始终取较小的那个 n=min(n,j-i+1); //减去左边元素,看是否还大于target sum-=nums[i]; i++; } j++; } //当数组中不存在满足条件的子串,返回0 return n==INT_MAX?0:n; } };
485. 最大连续1的个数
给定一个二进制数组, 计算其中最大连续1的个数。
解题思路: 一个指针i遍历,一个j计数,一个max记录最大的j。遇到1,j递增,遇到0,取max和j中的最大值,然后j回到0。返回max。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution { public: int findMaxConsecutiveOnes(vector<int>& nums) { int i=0,j=0; int max=0; while(i<nums.size()) { if(nums[i]==1) j++; else if(nums[i]==0) j=0; max=max>j?max:j; i++; } return max; } };
153. 寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] 。
请找出其中最小的元素。
解题思路:
这道题本来我是准备用二分法的,折腾了半天码好了代码,但是一提交发现居然有很多测试用例没通过,哭了,大概是我二分法的边界条件没设置好,不能算出这样的例子,所以想试试其他解法,想到一个利用双指针的解法
- 初始数组
nums
的首尾两个指针left、right
,比较这两个位置值nums[left]
、nums[right]
的大小。 - 如果此时
nums[left]<nums[right]
,说明数组并没有旋转,还是原来的升序,返回nums[left]
就好。 - 当同时满足
nums[right]<nums[left]
、nums[right]>nums[right-1]
(右指针指向的值没有降到最小)和nums[left]<nums[left+1]
(左指针指向的值没有升到最大)时,左右指针同时向中间靠拢(left++,right--
) - 当不满足上述条件时,说明指针到了一个梯度位置,此时取
nums[right]
和nums[left+1]
中较小者即为答案
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution { public: int findMin(vector<int>& nums) { //利用双指针,找到梯度位置 if(nums.size()==1) return nums[0]; int left=0,right=nums.size()-1; if(nums[left]<nums[right]) return nums[left]; while(nums[right]<nums[left]&&nums[right]>nums[right-1]&&nums[left]<nums[left+1]) { left++; right--; } return min(nums[left+1],nums[right]); } };
26. 删除排序数组中的重复项
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
解题思路: 利用双指针i
和j
,当遇到nums[j]
不等于nums[i]
时,将i向后移并赋值为nums[j]
。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution { public: int removeDuplicates(vector<int>& nums) { //利用快慢双指针 if(nums.size()==0) return 0; int i=0,j=1; while(j<nums.size()) { if(nums[j]==nums[i]) j++; else nums[++i]=nums[j++]; } return i+1; } };
283. 移动零
给定一个数组nums
,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
解题思路: 利用双指针i和j
,i
指向非零元素,j用于遍历,当nums[j]
非零时,赋值给nums[i]
。j
到达数组末尾后,所有的非零元素都放到了前面,最后只需将i
之后的元素全部置为0就好了。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution { public: void moveZeroes(vector<int>& nums) { //利用双指针,i来指向正确的数组,j来遍历 int i=0,j=0; //将非零元素放到前面 while(j<nums.size()) { if(nums[j]==0) j++; else nums[i++]=nums[j++]; } //将i之后的元素全部置为0 while(i<nums.size()) { nums[i++]=0; } } };
122. 买卖股票的最佳时机 II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
解题思路:
最近真的沉迷双指针,好多题第一反应都是用双指针来解。这道题我用双指针解出来了,速度还行,占内存有点大。
两个指针,一个i指向买入时间,一个j用于遍历和指向卖出时间。当j遇到局部最低点时,把位置交给i,j继续往后遍历,直到遇到局部最大点,求出此时的price[j]-price[i],然后j继续去找局部最小、最大,直到遍历到数组尾部。
代码
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
26class Solution { public: int maxProfit(vector<int>& prices) { //利用双指针 if(prices.size()==1) return 0; int i=0,j=0,sum=0; while(j<prices.size()-1) { //从当前位置找第一个最小值,找应该买入股票时的低价 while((j<prices.size()-1)&&(prices[j]>=prices[j+1])) j++; //遇到前一个小于后一个时退出,此时找到一个局部最低点 i=j; //从j当前位置开始找局部最高点,找到应该卖出股票的时候 if(j!=prices.size()-1) { while((j<prices.size()-1)&&(prices[j]<prices[j+1])) j++; } sum+=prices[j]-prices[i]; } return sum; } };
贪心法
leetcode官方题解
1
2
3
4
5
6
7
8
9
10
11
12class Solution { public: int maxProfit(vector<int>& prices) { int ans = 0; int n = prices.size(); for (int i = 1; i < n; ++i) { ans += max(0, prices[i] - prices[i - 1]); } return ans; } };
125. 验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
解题思路: 利用双指针 ,从两边开始遍历字符串,当指针指向的字符非字母(标点符号或者空格)时,单独移动左指针或右指针,当同时指向字母,且两字母相等时,两指针同时往中间移动,如果两字母不等,则返回false
,直到两指针相遇。
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
32class Solution { public: bool isPalindrome(string s) { //利用双指针 int i=0,j=s.length()-1; while(i<j) { //左指针跳过除字母的部分 while(i<j&&!isalnum(s[i])) { i++; } //右指针跳过除字母的部分 while(i<j&&!isalnum(s[j])) { j--; } //当左右两个字母相同时,两指针同时向中间移,不相同的话返回false if(tolower(s[i])==tolower(s[j])) { i++; j--; } else return false; } //当左右指针相遇还没有返回false 时,说明字符串是回文串,返回true return true; } };
268. 丢失的数字
给定一个包含 [0, n]
中n
个数的数组 nums
,找出 [0, n]
这个范围内没有出现在数组中的那个数。
能否实现时间O(n)
,空间O(1)
思路:
- 先对数组进行排序
- 然后遍历排序后的数组,看前后两个元素中是否存在差值不为
1
的情况,存在的话就返回两数的中间数 - 还要考虑丢失的是
0
或者n
的情况
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Solution { public: int missingNumber(vector<int>& nums) { int n=nums.size(); int result; //先排个序,然后遍历的时候看哪个地方的差不为1 sort(nums.begin(),nums.end()); for(int i=1;i<n;i++) { if((nums[i]-nums[i-1])!=1) result=(nums[i]+nums[i-1])/2; } if(nums[n-1]!=n) return n; if(nums[0]!=0) return 0; return result; } };
最后
以上就是阳光帽子最近收集整理的关于Leetcode——数组和字符串数组字符串双指针的全部内容,更多相关Leetcode——数组和字符串数组字符串双指针内容请搜索靠谱客的其他文章。
发表评论 取消回复