LeetCode
11. 盛最多水的容器
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
题解
双指针法:
两个指针分别指向左右两端,每次水的容量为两个指针指向的数字中较小值∗指针之间的距离,
每次记录下当次计算得的容量,移动指向数字较小的那个指针,向中间靠拢,计算得容量,若比原来的大则覆盖,否则保持原来的容量。
对于左右指针相等的情况,因为此时左右指针都是“瓶颈”,无论移动哪一个,因为x轴方向的长度变小,移动之后的计算的面积一定减小,故应该两个指针同时移动,而代码实现时任意移动一边的指针即可。
用到algorithm中的min()和max()函数。
class Solution {
public:
// 双指针法
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int area = 0;
while (left < right) {
int temp = min(height[left], height[right]) * (right - left);
area = max(area, temp);
// if (area < min(height[left], height[right]) * (right - left))
// area = min(height[left], height[right]) * (right - left);
if (height[left] > height[right])
right--;
else if (height[left] <= height[right])
left++;
}
return area;
}
};
时间复杂度:O(n),向量中每个元素最多访问一次;
空间复杂度:O(1),额外使用常数级别的空间。
15. 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
题解
双指针法
先对数组进行排序,使用algorithm库的sort(nums.begin(), nums.end()),排序begin()到end()-1的数。
为得到不重复的三元组,需要进行相等判断,相等则移动指针,本题中是在边界之后判断数组某个index的值是否等于前一个使用的下标的值。
由于有外层循环first++条件,判断语句中使用continue,不能用first++,否则程序继续执行出现越界错误。
if (first > 0 && nums[first] == nums[first - 1])
continue;
第二重循环内部,由于有second++的递增条件,可以不用显示判断左指针移动的情况,只需要在循环内部判断右指针移动的情况,同时进行左右指针位置的检验(left < right)。
while (second < third && nums[second] + nums[third] > target)
third--;
左右指针边界条件的判断:
if (second == third)
break;
//相等,说明当前first值无法获取满足条件的三元组,停止当前循环
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
int n = nums.size();
for (int first = 0; first < n; ++first) {
// skip index that has the same value
if (first > 0 && nums[first] == nums[first - 1])
continue;
// 不可为first++:下面程序会出现越界错误
int target = -nums[first];
int third = n - 1;
for (int second = first + 1; second < n; ++second) {
// skip index that has the same value
if (second > first + 1 && nums[second] == nums[second - 1])
continue;
// 双指针典型用法
while (second < third && nums[second] + nums[third] > target)
third--;
// 左右指针相等,first指针++
if (second == third)
break;
if (nums[second] + nums[third] == target) {
ans.push_back({nums[first], nums[second], nums[third]});
}
}
}
return ans;
}
时间复杂度:O(N2),N为nums数组的长度,对每一个值,在其右侧进行双指针判断(O(N));
空间复杂度:O(logN),额外的排序空间(sort函数)。由于改变了nums数组的值,如果需要另外存储nums数组,则为O(N)。
34.在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
错误方法
顺序查找,时间复杂度为O(n),不满足O(logn)条件。
int left = 0, right = nums.size() - 1;
int ptr = 0, flag = 0;
if (left == right && nums[0] == target)
return { 0, 0 };
//二分法找到target位置
while (left < right) {
ptr = (left + right) / 2;
if (nums[ptr] > target) {
right = ptr - 1;
}
else if (nums[ptr] < target) {
left = ptr + 1;
}
else {
flag = 1;
break;
}
}
if (flag == 0)
return {-1, -1};
left = ptr;
right = ptr;
//但判断的时候不满足O(logn),仍为O(n)
while (nums[left] == target)
left--;
while (nums[right] == target)
right++;
return { left + 1, right - 1 };
题解
要求O(logn),故考虑二分法。
由于要找出第一个和最后一个位置,在二分位置的值和target相等时,不停止,而是继续移动左右指针,最终找到第一个和最后一个位置。
本次代码实现较多,可以将第一个位置和最后一个位置的获取封装成两个函数。
int left = 0, right = nums.size() - 1;
int ptr = 0;
while (left <= right) {
ptr = (left + right) / 2;
if (nums[ptr] > target) {
right = ptr - 1;
}
else if (nums[ptr] < target) {
left = ptr + 1;
}
//==========================关键代码===========================
//==时,不停止循环,
//right向左移动,结束时,如果数组nums中存在target,则left指向target的第一个出现的位置
else {
right = ptr - 1;
}
}
int first_pos, last_pos;
//注意两个条件顺序,nums[left] == target条件不可前置,否则会出错,涉及短路判断
if (left < nums.size() && nums[left] == target) {
first_pos = left;
}
else
first_pos = -1;
left = 0; right = nums.size() - 1;
while (left <= right) {
ptr = (left + right) / 2;
if (nums[ptr] > target) {
right = ptr - 1;
}
else if (nums[ptr] < target) {
left = ptr + 1;
}
//==========================关键代码===========================
//==时,不停止循环,
//left向右移动,结束时,如果数组nums中存在target,则right指向target的最后一个出现的位置
else {
left = ptr + 1;
}
}
last_pos = right;
if (first_pos == -1) {
return { -1, -1 };
}
return { first_pos, last_pos };
48.旋转图像
给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
说明:
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例 1:
给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],
原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
示例 2:
给定 matrix =
[
[ 5, 1, 9,11],
[ 2, 4, 8,10],
[13, 3, 6, 7],
[15,14,12,16]
],
原地旋转输入矩阵,使其变为:
[
[15,13, 2, 5],
[14, 3, 4, 1],
[12, 6, 8, 9],
[16, 7,10,11]
]
关于vector
初始化和输入
一维向量:
vector<int> nums;
int r;
cin >> r;
//说明nums向量的长度大小,才能对向量元素进行赋值操作
nums.resize(r);
for (int i = 0; i < r; ++i) {
cin >> nums[i];
//nums[i] = i * i...
}
二维向量:
int row;
int column;
cout << "请输入数组的行数和列数:";
cin >> row >> column;
//下面给向量分配存储空间
arr.resize(row);
for (int i = 0; i < row; i++)
{
arr[i].resize(column);
}
将矩阵的输入和输出封装起来,之后本地调用函数即可进行输入和输出:
void inputMatrix(vector<vector<int>>& matrix) {
int r, c;
cout << "row: ";
cin >> r;
cout << "column: ";
cin >> c;
matrix.resize(r);
for (int i = 0; i < r; ++i) {
matrix[i].resize(c);
//需要先确定向量的长度,之后才能对确定下标的元素操作赋值/输入
//否则只能使用push_back
}
//输入r*c的矩阵
for (int i = 0; i < r; ++i) {
for (int j = 0; j < c; ++j) {
cin >> matrix[i][j];
}
}
}
void outputMatrix(vector<vector<int>>& matrix) {
//输出r*c的矩阵
for (int i = 0; i < matrix.size(); ++i) {
for (int j = 0; j < matrix[0].size(); ++j) {
cout << matrix[i][j];
if (j != matrix[0].size() - 1)
cout << ", ";
else
cout << endl;
}
}
}
题解
由于要原地翻转,即空间复杂度为O(1),不可以使用一个与输入矩阵长度相关联的空间存储中间值。
有的题解使用逐层旋转的方法,比较难操作比较繁琐。
为了实现顺时针旋转90°,这里使用矩阵的翻转操作:
- 先对矩阵上下翻转;
- 对矩阵做转置。
注意:1.2.顺序不调换,若先转置,后面左右翻转操作需要对二维操作,而先上下翻转只需要一位操作即可。
void rotate(vector<vector<int>>& matrix) {
int r = matrix.size(), c = matrix[0].size();
//先上下翻转,不左右,因为上下翻转只需要到行坐标,更方便
for (int i = 0; i < r / 2; ++i) {
swap(matrix[i], matrix[r - i - 1]);
}
//再转置
for (int i = 0; i < r; ++i) {
for (int j = i; j < c; ++j) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
54.螺旋矩阵
给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例 1:
输入:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]
示例 2:
输入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
题解
如图所示,看成一层一层的矩阵。先输出最外层,在逐步输出内层,直到结束。
如何判断边界呢?
设定4个值即可:left、top、right、bottom,即左上角和右下角的坐标(top,left)和(bottom,right)。
- 先输出(top,left)到(top,right)的;
- 再输出(top,right)到(bottom,right);
- 此时需要判断该层内部是否“包着下一层”,是则进入4,否则该层就是一个行向量或者列向量,进行了12之后已经遍历完成;
- 输出(bottom,right - 1)到(bottom,left);
- 输出(bottom - 1, left)到(top + 1, left);
- top++,bottom–,left++,right–,重复上述步骤。
注意:边界条件的判断,bottom和right是逐次递减的,top和left是逐次递增的,不要弄错。
vector<int> spiralOrder(vector<vector<int>>& matrix) {
//如果矩阵为空,返回一个空向量,向量用{}包起来。
if (matrix.size() == 0 || matrix[0].size() == 0)
return {};
int row = matrix.size(), col = matrix[0].size();
vector<int> ans;
int top = 0, left = 0, bottom = row - 1, right = col - 1;
while (top <= bottom && left <= right) {
for (int i = left; i <= right; ++i) {
ans.push_back(matrix[top][i]);
}
for (int j = top + 1; j <= bottom; ++j) {
ans.push_back(matrix[j][right]);
}
if (left < right && top < bottom) {
for (int ii = right - 1; ii > left; --ii) {
ans.push_back(matrix[bottom][ii]);
}
for (int jj = bottom; jj > top; --jj) {
ans.push_back(matrix[jj][left]);
}
}
top++;
bottom--;
left++;
right--;
}
return ans;
}
55.跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。
题解
使用贪心算法。记录下当前能到达的最远下标farthest,在[当前下标, farthest]里移动,不断更新farthest,若farthest大于最右下标,则返回true,否则若遍历完成,则返回false。
注意:
- 更新farthest容易漏掉,使用algorithm库的max()函数判断当前farthest和nums[i]+i的较大值;
- 只有下标满足index<farthest条件时,才能更新farthest,否则index为无效值,不参与更新;
- 由于2,可以在遍历循环中添加if条件,满足条件才更新farthest。
bool canJump(vector<int>& nums) {
int index = 0, farthest = 0;
//遍历数组
for (int i = 0; i < nums.size(); ++i) {
//只有满足<farthest条件才更新farthest值
if (i <= farthest) {
farthest = max(farthest, nums[i] + i);
if (farthest >= nums.size() - 1)
return true;
}
}
//遍历结束,说明farthest始终<nums.size()-1,故返回false
return false;
}
56.合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: intervals = [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
题解
-
sort对二维vector的作用:按照二维数组中每一行的第一个元素进行大小排序,该元素大,则该元素所在一维数组就大。
#include <iostream> #include <vector> #include <algorithm> #include <time.h> using namespace std; int main() { srand(time(NULL)); vector<vector<int> > a(10); for (int i = 0; i < 10; i++) for (int j = 0; j < 10; j++) { a[i].push_back(rand() % 100); } for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { cout << a[i][j] << "\t"; } cout << endl; } cout << "After 2D sorting:" << endl; sort(a.begin(), a.end()); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { cout << a[i][j] << "\t"; } cout << endl; } system("pause"); return 0; }
结果如下:
-
贪心算法:
-
先对区间按上述方法排序,初始时将第一个区间加入到结果数组results数组中,随后按顺序考虑之后的每个区间;
-
若当前区间的左端点大于results最后一个区间的右端点,则它们不会重合,将当前区间直接加入到results的末尾;
-
否则它们重合,更新results中最后一个区间的右端点,设为二者的较大值。
-
对于第一次循环要单独拉出来考虑,这里第一次循环的操作和判断不会重合的情况一致,故将第一次循环的条件和不会重合的条件放在一起。
class Solution { public: // 贪心算法 // 1.先对所有区间按照左端点大小排序; // 2.逐个对比当前区间左端点和results中最后一个区间右端点大小。(初始时将第一个区间直接加入到results) vector<vector<int>> merge(vector<vector<int>>& intervals) { if (intervals.size() == 0) return {}; sort(intervals.begin(), intervals.end()); vector<vector<int>> results; for (int i = 0; i < intervals.size(); ++i) { int L = intervals[i][0], R = intervals[i][1]; if (!results.size() || results.back()[1] < L) { results.push_back({L, R});//添加一个二元数组 } else { results.back()[1] = max(results.back()[1], R); } } return results; } };
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
输入:m = 3, n = 7
输出:28
题解
- 递归
最开始想到的是递归。每次到达之后相当于求解(m-1, n)或(m, n-1)规模的问题。
但是因为递归中,重复计算太多的值,导致超时。int uniquePaths(int m, int n) { if (m == 1 || n == 1) return 1; return uniquePaths(m - 1, n) + uniquePaths(m, n - 1); }
- 动态规划(dynamic programming)
之前不了解dp,发现思想和递归很像,都是大规模问题转换为更小规模的问题来解决。 - 组合数学
高中数学问题。m*n的方格,左上角到达右下角,只能向右向下。
共走了m+n-2次,其中向右n-1次,向下m-1次,组合数问题。
C m + n − 2 m − 1 = ( m + n − 2 ) ( m + n − 3 ) . . . n ( m − 1 ) ! . C{^{m-1} _{m+n-2}} = \frac{(m+n-2)(m+n-3)...n}{(m-1)!}. Cm+n−2m−1=(m−1)!(m+n−2)(m+n−3)...n.int uniquePaths(int m, int n) { long int ans = 1; for (int x = 1, y = n; x < m; ++x, ++y) { ans = ans * y / x; //注意不要写成ans *= y / x,有小数舍去。 } return ans; }
73. 矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例 1:
输入:
[
[1,1,1],
[1,0,1],
[1,1,1]
]
输出:
[
[1,0,1],
[0,0,0],
[1,0,1]
]
示例 2:
输入:
[
[0,1,2,0],
[3,4,5,2],
[1,3,1,5]
]
输出:
[
[0,0,0,0],
[0,4,5,0],
[0,3,1,0]
]
题解
不能直接对原矩阵作置0操作,否则会改变后面要遍历的元素的值,导致错误。
- 暴力。
最简单的方法。直接复制一个matrix2,遍历原矩阵matrix1,对matrix2操作,需要的额外的空间O(mn)。 - 标记要置0的行和列。
方法1的改进。不存储整个矩阵,存储需要置0的行和列的标志,每行每列都有一个标志位,需要的额外空间O(m+n)。// 不能一边遍历一边更改,后面的数据需要用原本矩阵的值。 // 2.m+n的额外存储空间,标记需要置0的行和列。 void setZeroes(vector<vector<int>>& matrix) { int row = matrix.size(), col = matrix[0].size(); // 初始化为true。 vector<bool> rowReset(row, true); vector<bool> colReset(col, true); for (int i = 0; i < row; ++i) { for (int j = 0; j < col; ++j) { if (matrix[i][j] == 0) { rowReset[i] = 0; colReset[j] = 0; } } } // 修改标记为false的行。 for (int i = 0; i < row; ++i) { if (rowReset[i] == 0) for (int j = 0; j < col; ++j) matrix[i][j] = 0; } // 修改标记为false的列。 for (int i = 0; i < col; ++i) { if (colReset[i] == 0) for (int j = 0; j < row; ++j) matrix[j][i] = 0; } }
- 方法2的改进:用第一行和第一列来存储标志位,第一行和第一列的标志位用两个变量存储即可。
用第一行和第一列存储标志位,注意修改元素值的时候,先从第二行第二列开始,即先不改动标志位的第一行和第一列,最后在根据两个标记变量修改。// 3.方法2的改进:用第一行和第一列来存储标志位,第一行和第一列的标志位用两个变量存储即可。 void setZeroes(vector<vector<int>>& matrix) { int row = matrix.size(), col = matrix[0].size(); // 初始化为true。 bool firstRow = true, firstCol = true; for (int i = 0; i < col; ++i) { if (matrix[0][i] == 0) { firstRow = false; break; } } for (int i = 0; i < row; ++i) { if (matrix[i][0] == 0) { firstCol = false; break; } } // 从第二行和第二列开始,第一行和第一列若有0,则标志位就是0,不用操作。 for (int i = 1; i < row; ++i) { for (int j = 1; j < col; ++j) { if (matrix[i][j] == 0) { matrix[i][0] = 0; matrix[0][j] = 0; } } } // 置0,注意下标从1开始,否则改变了标记位的值,导致出错。 for (int i = 1; i < row; ++i) { for (int j = 1; j < col; ++j) { if (matrix[i][0] == 0 || matrix[0][j] == 0) { matrix[i][j] = 0; } } } if (firstRow == 0) { for (int i = 0; i < col; ++i) matrix[0][i] = 0; } if (firstCol == 0) { for (int i = 0; i < row; ++i) matrix[i][0] = 0; } }
75. 颜色分类
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
进阶:
你可以不使用代码库中的排序函数来解决这道题吗?
你能想出一个仅使用常数空间的一趟扫描算法吗?
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
示例 3:
输入:nums = [0]
输出:[0]
示例 4:
输入:nums = [1]
输出:[1]
题解
- 直接用algorithm库进行排序即可。
sort(nums.begin(), nums.end());
- 单指针法。(当想改变数组,又想标记数组本身,考虑指针)
两遍遍历,第一次把0移到前面,第二次把1移到0的后面。
用一个指针表示头部尾部(不包括头部),一开始为0,表示没有头部。
使用标准库中的swap()交换。
时间:O(n).
空间:O(1).int n = nums.size(); int ptr = 0; for (int i = 0; i < n; ++i) { if (nums[i] == 0) { swap(nums[i], nums[ptr]); ptr++; } } for (int i = ptr; i < n; ++i) { if (nums[i] == 1) { swap(nums[i], nums[ptr]); ptr++; } }
- 双指针法。(略)
78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
题解
- 子集二进制掩码枚举。
可以用1~ 2 n 2^n 2n来表示n个元素的选/不选,有 2 n 2^n 2n个mask,集合中每一个元素有自己唯一的位置,用(1 << 下标)表示。
用位操作&来判断。vector<vector<int>> subsets(vector<int>& nums) { vector<vector<int>> ans; vector<int> tmp; int n = nums.size(); for (int mask = 0; mask < (1 << n); ++mask) { tmp.clear(); for (int i = 0; i < n; ++i) { if (mask & (1 << i)) tmp.push_back(nums[i]); } ans.push_back(tmp); } return ans; }
- 回溯(TODO)