目录
- 元戎启行 面试 编程
- 1、数组 (二分)
- 1.1 前缀和
- 1.2 质数
- 1.3 重叠区间
- 2、滑动窗口
- 3、链表
- 4、哈希表
- 5、字符串
- 6、栈与队列
- 7、二叉树
-
-
- 144.二叉树的前序遍历
- 102.二叉树的层序遍历
- 116.填充每个节点的下一个右侧节点指针
- 226.翻转二叉树
- 101.对称二叉树
- 572.另一棵树的子树
- 104.二叉树的最大深度
- 111.二叉树的最小深度
- 222.完全二叉树的节点个数
- 求普通二叉树节点个数
- 110.平衡二叉树
- 257.二叉树的所有路径
- 404.左叶子之和
- 513.找树左下角的值
- 112.路径总和
- 113.路径总和II
- 105.从前序与中序遍历序列构造二叉树
- 617.合并二叉树
- 700.二叉搜索树中的搜索
- 98.验证二叉搜索树BST(双指针/前驱节点)
- 236.二叉树的最近公共祖先
- 235.二叉搜索树的最近公共祖先
- 450.删除二叉搜索树中的节点
- 删除普通二叉树中的节点
- 669.修剪二叉搜索树
- offer47.二叉树剪枝
- offer48.序列化与反序列化二叉树
- offer49.从根节点到叶子节点的路径数字之和
- offer51.节点之和的最大路径(二叉树中的最大路径和)
- offer52.展平二叉搜索树
- offer53.二叉搜索树中的中序后继
- offer54.所有大于等于节点的值之和
- offer55.二叉搜索树迭代器
- offer56.二叉搜索树中两个节点之和
-
- 8、回溯算法
- 9、贪心算法
- 10、动态规划
元戎启行 面试 编程
1、木棒切割问题
// 元戎启行 木棒切割问题:
/*
给你n根木棍,长度为li,使得这些木棍能够砍出k块最长的长度x,得到至少K段长度相等的木棒
==============即问所有木棒最少能切k段,得到的长度相等的木棒的最长长度x是多少==========
木棒切割问题,给出N根木棒,长度已知,现在希望通过分割他们来得到至少K段长度相等的木棒
(长度必须为整数),问这些长度相等的木棒最长能有多长
*/
// 计算每段木棒切割长度为x时对应的总的木棒段数
int woodNum(const vector<int>& woods, int x)
{
int count = 0;
for(int i = 0; i < woods.size(); ++i)
{
// woods[i] / x 计算每一根木棒切割为x长度时的段数
count = count + woods[i] / x;
}
return count;
}
// k为要求木棒的段数
int bSearch(const vector<int>& woods, int k)
{
// 二分法
int left = 0, right = woods.size() - 1;
int mid = 0;
while(left < right)
{
mid = left + (right - left) / 2;
// 以长度为mid切割每一根木棒,总的木棒段数小于k,说明只要x够小还能切
// 长度越短,木棒数越多
if(woodNum(woods, mid) < k)
{
right = mid;// 缩小right,使得mid更小,因为我切割出的木棒总段数未达到k数量
}
// 总的木棒段数大于k,试试x更大一些能否切割出更多的木棒
// 木棒数太多,试试扩大x的大小,相当于缩小x的右边界
else
{
// 移动left,使得mid增大,因为木棒总段数足够,需要找到符合k的最大长度mid
left = mid + 1;
}
}
return right - 1;
}
int main()
{
// 木棒切割问题,主函数中需要参数 n:木棒数量 li:木棒长度 k:至少切k段
int n = 5;
int li = 10;
int k = 2;
vector<int> woods(n, li);// 创建数组,每个元素为木棒对应的长度
int ret = bSearch(woods, k);
cout << ret << endl;
return 0;
}
1、数组 (二分)
C++中,二维数组在地址空间上是连续的。
void test_arr() {
int array[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}
int main() {
test_arr();
}
704.二分查找
注意:二分查找尽量不用else,只用else if代替。
需要注意的是区间的范围,如果最初确定好是左闭右闭区间,那就需要在while循环的判断条件添加=,因为left == right是有意义的。
class Solution {
public:
int search(vector<int>& nums, int target) {
// 给定有序数组 搜索目标值 使用二分法
// 时间复杂度O(logn) 空间复杂度O(1)
// 注意左闭右闭区间
// 声明双指针
int left = 0, right = nums.size() - 1;
while(left <= right)
{
// 中间下标位置
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
left = mid + 1;
}
else if(target < nums[mid])
{
right = mid - 1;
}
else if(target == nums[mid])
return mid;
}
return -1;
}
};
35.搜索插入位置
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
// 二分法在排序数组内寻找目标值
// 时间复杂度O(logn) 空间复杂度O(1)
int left = 0, right = nums.size() - 1;
// 左闭右闭区间
while(left <= right)
{
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
left = mid + 1;
}
else if(target < nums[mid])
{
right = mid - 1;
}
else if(target == nums[mid])
return mid;
}
// 跳出循环,target不存在数组中,指针left指向合适的插入位置
return left;
}
};
题解如下:
class Solution {
private:
// 寻找左侧边界(左闭右闭区间)
int getLeftBorder(vector<int>& nums, int target)
{
// 寻找左侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
// 搜索区间为[left, right]
while(left <= right)
{
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
// 搜索区间变为[mid + 1, right]
left = mid + 1;
}
else if(target < nums[mid])
{
// 搜索区间变为[left, mid - 1]
right = mid - 1;
}
else if(target == nums[mid])
{
// 别返回,收缩左侧边界
right = mid - 1;
}
}
// 循环结束还没有返回值,还需要判断索引是否越界(这里left == right + 1会结束循环)
// 检查出界情况 || (不出界判断是否是target)
if(left >= nums.size() || nums[left] != target)
{
return -1;
}
return left;
}
// 寻找右侧边界
int getRightBorder(vector<int>& nums, int target)
{
// 寻找右侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
while(left <= right)
{
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
// 搜索区间变为[mid + 1, right]
left = mid + 1;
}
else if(target < nums[mid])
{
// 搜索区间变为[left, mid - 1]
right = mid - 1;
}
else if(target == nums[mid])
{
// 别返回,收缩右侧边界
left = mid + 1;
}
}
// 循环结束,判断出界情况或者是否是target
if(right < 0 || nums[right] != target)
{
return -1;
}
return right;
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
vector<int> ret(2, -1);
int leftRet = getLeftBorder(nums, target);
int rightRet = getRightBorder(nums, target);
if(leftRet == -1 || rightRet == -1)
return ret;
// 找到了左右边界
ret[0] = leftRet;
ret[1] = rightRet;
return ret;
}
};
34.在排序数组中查找元素的第一个和最后一个位置 (二分查找左右边界)
(在排序数组内搜索左右边界)
思路:labuladong
① 搜索一个元素时,搜索区间两端闭;while条件带等号,if相等就返回;mid必须加减一,因为区间两端闭;while结束就凉了,凄凄惨惨返-1。
② 搜索左右边界时,搜索区间要阐明;左闭右开最常见,其余逻辑便自明;while要用小于号,这样才能不漏掉;if相等别返回,利用mid锁边界;mid加一或减一?要看区间开或闭;while结束不算完,因为你还没返回;索引可能出边界,if检查保平安。
将right初始化为nums.size() - 1, while的终止条件为left == right + 1,也就是left > right时循环结束,那么while的循环条件就应该为 <= 。这样就将搜索区间统一成左闭右闭区间。
// 寻找左侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
// 搜索区间为[left, right]
while(left <= right)
{
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
// 搜索区间变为[mid + 1, right]
left = mid + 1;
}
else if(target < nums[mid])
{
// 搜索区间变为[left, mid - 1]
right = mid - 1;
}
else if(target == nums[mid])
{
// 别返回,收缩左侧边界
right = mid - 1;
}
}
// 循环结束还没有返回值,还需要判断索引是否越界(这里left == right + 1会结束循环)
// 检查出界情况 || (不出界判断是否是target)
if(left >= nums.size() || nums[left] != target)
{
return -1;
}
return left;
// 寻找右侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
while(left <= right)
{
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
// 搜索区间变为[mid + 1, right]
left = mid + 1;
}
else if(target < nums[mid])
{
// 搜索区间变为[left, mid - 1]
right = mid - 1;
}
else if(target == nums[mid])
{
// 别返回,收缩右侧边界
left = mid + 1;
}
}
// 循环结束,判断出界情况或者是否是target
if(right < 0 || nums[right] != target)
{
return -1;
}
return right;
class Solution {
private:
// 寻找左侧边界(左闭右闭区间)
int getLeftBorder(vector<int>& nums, int target)
{
// 寻找左侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
// 搜索区间为[left, right]
while(left <= right)
{
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
// 搜索区间变为[mid + 1, right]
left = mid + 1;
}
else if(target < nums[mid])
{
// 搜索区间变为[left, mid - 1]
right = mid - 1;
}
else if(target == nums[mid])
{
// 别返回,收缩左侧边界
right = mid - 1;
}
}
// 循环结束还没有返回值,还需要判断索引是否越界(这里left == right + 1会结束循环)
// 检查出界情况 || (不出界判断是否是target)
if(left >= nums.size() || nums[left] != target)
{
return -1;
}
return left;
}
// 寻找右侧边界
int getRightBorder(vector<int>& nums, int target)
{
// 寻找右侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
while(left <= right)
{
int mid = left + (right - left) / 2;
if(target > nums[mid])
{
// 搜索区间变为[mid + 1, right]
left = mid + 1;
}
else if(target < nums[mid])
{
// 搜索区间变为[left, mid - 1]
right = mid - 1;
}
else if(target == nums[mid])
{
// 别返回,收缩右侧边界
left = mid + 1;
}
}
// 循环结束,判断出界情况或者是否是target
if(right < 0 || nums[right] != target)
{
return -1;
}
return right;
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
vector<int> ret(2, -1);
int leftRet = getLeftBorder(nums, target);
int rightRet = getRightBorder(nums, target);
if(leftRet == -1 || rightRet == -1)
return ret;
// 找到了左右边界
ret[0] = leftRet;
ret[1] = rightRet;
return ret;
}
};
27.移除元素
移除元素思想:遇到与val值不相等的元素,交换到数组前面位置,相等则不做处理,最后数组前面元素都是与val值不相等的元素,由慢指针控制其范围,快指针只负责遍历。
// 快慢指针同向解法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 使用快慢指针移除数组内指定元素
int slowIndex = 0;
// 快指针先移动,只要找到不等于val的元素,就进行快慢指针的元素交换,然后慢指针移动一步
// 时间复杂度O(n) 空间复杂度O(1)
for(int fastIndex = 0; fastIndex < nums.size(); ++fastIndex)
{
if(nums[fastIndex] != val)
{
swap(nums[slowIndex], nums[fastIndex]);
++slowIndex;
}
}
return slowIndex;
}
};
// 快慢指针双向解法:确保了移动最少元素
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
// 找左边等于val的元素
while (leftIndex <= rightIndex && nums[leftIndex] != val)
{
++leftIndex;
}
// 找右边不等于val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
-- rightIndex;
}
// 将右边不等于val的元素覆盖左边等于val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
return leftIndex; // leftIndex一定指向了最终数组末尾的下一个元素
}
};
977.有序数组的平方
时间复杂度O(n) 不考虑结果集所占用空间,空间复杂度O(1)
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
// 双指针解法
// 因为非递减且数组内存在正负元素,负数的平方也可能比正数的平方更大(数组两端元素的平方会最大),所以需要双指针从数组的首尾位置开始向中间进行遍历,将指定位置元素平方的大者存储在数组末尾,最终结果也为非递减顺序
// 创建新数组
vector<int> result(nums.size(), 0);
int index = nums.size() - 1;// 新数组索引,从后向前存储元素
// 前后双指针
int beginIndex = 0, endIndex = nums.size() - 1;
while(beginIndex <= endIndex)
{
if(nums[beginIndex] * nums[beginIndex] <= nums[endIndex] * nums[endIndex])
{
// 将双指针指向的元素的平方中的大值,赋值到新数组中
result[index] = nums[endIndex] * nums[endIndex];
// 更新所指元素平方最大的指针
--endIndex;
}
else if(nums[beginIndex] * nums[beginIndex] > nums[endIndex] * nums[endIndex])
{
// 同理
result[index] = nums[beginIndex] * nums[beginIndex];
++beginIndex;
}
--index;// 结果集上每循环一次,需要更新一下索引,指向下一个待赋值的位置
}
return result;
}
};
59.螺旋矩阵II <二维数组>
双指针固定始末位置,然后根据旋转矩阵元素的性质,利用start与end向矩阵进行赋值操作. 时间复杂度O(n^2) 空间O(1)
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int num = 1;
// 创建 n x n 正方形矩阵
vector<vector<int>> matrix(n, vector<int>(n, 0));
// 确定边界 初始位置和终止位置下标
int start = 0, end = n - 1;
while(start < end)
{
// 注意旋转矩阵赋值的时候为 左闭右开 区间或者 左开右闭 区间,即 不能同时取到边界值
// 以n = 3为例,第一次界限下标为[0, 2],但是赋值的时候按照旋转矩阵的性质只能赋值[0, 1],索引2交由下一个循环赋值,以此类推
// [1, 2][3]
// [8][ ][4]
// [7][6, 5]
for(int i = start; i < end; ++i)
{
matrix[start][i] = num++;
}
for(int i = start; i < end; ++i)
{
matrix[i][end] = num++;
}
for(int i = end; i > start; --i)
{
matrix[end][i] = num++;
}
for(int i = end; i > start; --i)
{
matrix[i][start] = num++;
}
// 外圈赋值完毕,边界同时向内圈缩进
++start;
--end;
}
// n是奇数,需要对中间位置进行赋值操作
if(n % 2 != 0)
{
matrix[start][end] = num;
}
// for(int i = 0; i < matrix.size(); ++i)
// {
// for(int j = 0; j < matrix[0].size(); ++j)
// {
// cout<<matrix[i][j]<<" ";
// }
// cout<<endl;
// }
return matrix;
}
};
54.螺旋矩阵
矩阵的顺时针遍历,注意不是n x n的矩阵,所以要设置四个变量来对要遍历的矩阵边界进行限定. 时间复杂度O(mn) 空间复杂度O(1)
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
// 确定矩阵的行与列
int rows = matrix.size();
int cols = matrix[0].size();
// 创建结果集
vector<int> result(rows * cols, 0);
int index = 0;// 结果集中的索引
// 因为是矩阵,所以需要4个变量确定矩阵的索引边界
int up = 0, down = rows - 1;
int left = 0, right = cols - 1;
// 顺时针 遍历"矩阵": 注意一定要按照顺时针进行遍历
// 按照顺时针顺序,每遍历完某个边界,对应边界要向内圈缩进更新
while(true)
{
// 先遍历第up行
for(int i = left; i <= right; ++i)
{
result[index++] = matrix[up][i];
}
// 第up行遍历完,更新初始up,即up边界下移,并判断是否符合规范,是否越界
if(++up > down)
break;
// 接着遍历right列
for(int i = up; i <= down; ++i)
{
result[index++] = matrix[i][right];
}
// 更新right,right边界左移
if(--right < left)
break;
// 接着遍历down行
for(int i = right; i >= left; --i)
{
result[index++] = matrix[down][i];
}
// 更新down,down边界上移
if(--down < up)
break;
// 接着遍历left列
for(int i = down; i >= up; --i)
{
result[index++] = matrix[i][left];
}
// 更新left列,left边界右移
if(++left > right)
break;
}
return result;
}
};
剑指 Offer 03. 数组中重复的数字
优化解法,利用该题元素的性质
时间复杂度为O(n) 空间复杂度优化为O(1)
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
// 找出数组中任意一个重复的数字
// 如果这个数组中没有重复数字,那么对数组进行排序之后,数字i将出现在下标为i的位置;如果数组中有重复数字,有些位置可能存在多个数字,有些位置可能没有数字
// 对未排序的数组进行遍历
for(int i = 0; i < nums.size(); ++i)
{
// i元素应该出现在i位置(无重复的情况)
// 例:2应该在下标为2的位置
// 若当前位置的元素不是第i个位置应该存在的元素
// 比较当前遍历到的元素是否等于i
while(nums[i] != i)
{
// 不相等,进入while循环
// 比较nums[i]和索引nums[i]处的元素值是否相等
if(nums[i] == nums[nums[i]]) return nums[i];// 相等,则找到第一个重复的数字
// 不相等,则交换,将nums[i]放到属于它的地方(排序)
swap(nums[i], nums[nums[i]]);// [0, 1, 2, 3, 2, 5, 3]
}
}
return -1;
}
};
剑指 Offer 04. 二维数组中的查找
方法2:以右上角(左下角)位置元素为根节点不断搜索目标元素
取右上角或者左下角的目的在于,每次比较后进行移动的时候,可以保证只能移动一个方向,另一个方向严格不能移动,每一次都在数组的查找范围中剔除一行或者一列,这样每一步都可以缩小查找的范围,直到找到要查找的数字。
如果取左上角或者右下角为起始搜索位置,则无法缩小查找的范围。
算法时间复杂度为O(n + m) 空间复杂度为O(1)
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
// 方法1:逐列遍历,在每一列上都使用二分查找 时间复杂度O(m * logn)
// 方法2:以右上角或者左下角位置元素为根节点,进行搜索,这里的搜索每次只能原则一个方向前进(取其它元素不能确定搜索的方向)
// 方法2:以右上角位置元素为根节点不断搜索目标元素
if(matrix.size() == 0) return false;
int n = matrix.size();
int m = matrix[0].size();
// 记录根节点的位置
int row = 0, col = m - 1;
// 根节点的位置
// 行可以++操作,所以需要设定上界;列进行--操作移动,所以需要设定上界
while(row < n && col >= 0)
{
// 目标值比根节点值要小,因为当前列的元素是升序,所以不能向同列的下一行进行移动;当前行从前向后为升序,从后向前为降序,所以需要移动列,行不变
if(target < matrix[row][col])
{
--col;
}
else if(target > matrix[row][col])
{
++row;
}
else
{
return true;
}
}
return false;
}
};
方法1:<二分查找>虽然使用剪枝操作,但是平均的时间复杂度还是为O(mlogn) 空间复杂度为O(1)
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
// 方法1:逐列遍历,在每一列上都使用二分查找 时间复杂度O(m * logn)
// 方法2:以右上角或者左下角位置元素为根节点,进行搜索,这里的搜索每次只能原则一个方向前进(取其它元素不能确定搜索的方向)
// 方法1:时间复杂度O(mlogn)
// 特例:矩阵为空
if(matrix.size() == 0) return false;
int n = matrix.size();// 行数
// 遍历矩阵的每一列
int m = matrix[0].size();// 列数
for(int j = 0; j < m; ++j)
{
// 剪枝
if(target < matrix[0][0]) break;
// 因为每一行都是升序,每一列都是升序
// 对在每一列中使用二分查找搜索target 左闭右闭区间
int left = 0, right = n - 1;
int mid = 0;
while(left <= right)
{
mid = left + (right - left) / 2;
if(target < matrix[mid][j])
{
right = mid - 1;
}
else if(target > matrix[mid][j])
{
left = mid + 1;
}
// 在第j列中找到了目标值
else
{
// target == matrix[mid][j]
return true;
}
}
// 跳出循环,说明未找到target,则继续遍历列
}
// 执行到这里说明在每一列都没有找到目标值,返回false
return false;
}
};
15.三数之和
三数之和:一层for循环 + 双指针 时间复杂度O(n^2) 结果集所占用的空间不记录的话,空间复杂度为O(1)
四数之和:两层for循环 + 双指针操作 时间复杂度O(n^3) 空间复杂度O(1)
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// target为0
// 先对数组内元素进行排序操作,方便后续使用下标进行去重
sort(nums.begin(), nums.end());
vector<vector<int>> result;
for(int i = 0; i < nums.size(); i++)
{
// 对排序后的数组进行剪枝操作
if(nums[i] > 0)
break;
// 去重
if(i > 0 && nums[i] == nums[i - 1])
continue;
// 这里使用双指针对数组剩余元素进行操作
int left = i + 1;
int right = nums.size() - 1;
int sum = 0;// 存储三个数之和
// 注意这里的while循环只能是<,不能加=
// 因为三个元素的下标各不相同,所以这里的双指针 不能取<=
while(left < right)
{
sum = nums[i] + nums[left] + nums[right];
if(sum > 0) --right;
else if(sum < 0) ++left;
else
{
// sum符合条件,添加到结果集中
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 对双指针进行去重操作,判断双指针之间是否还存在符合条件的元素
while(left < right && nums[left] == nums[left + 1])
++left;
while(left < right && nums[right] == nums[right - 1])
--right;
// 若双指针存在重复元素,则两个指针都需要再进行一次移动才能走出重复区间
// 若不存在重复元素,则两个指针也需要移动一次开始下一次sum判断
++left;
--right;
}
}
}
return result;
}
};
16.四数之和
四数之和:两个for循环+一对双指针前后去重(时间复杂度O(n^3))
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
// 创建结果集
vector<vector<int>> result;
// 对数组内元素进行排序,方便后续使用下标进行去重
sort(nums.begin(), nums.end());// O(nlogn)
// 双重for循环 + 双指针
for(int i = 0; i < nums.size(); ++i)
{
// 去重
if(i > 0 && nums[i] == nums[i - 1])
continue;
// 剪枝处理
if (nums[i] > target && nums[i] >= 0) break;
for(int j = i + 1; j < nums.size(); ++j)
{
// 剪枝
// 当target为负数不能这样剪枝
if(nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;
// 去重
if(j > i + 1 && nums[j] == nums[j - 1])
continue;
// 声明双指针,对剩下的元素进行判断
// 注意这里可能会int overflow,所以使用long类型的变量
long sum = 0;
int left = j + 1;
int right = nums.size() - 1;
while(left < right)
{
sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
if(sum > target) --right;
else if(sum < target) ++left;
else
{
// sum符合条件,添加结果集
result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
// 判断双指针之间的区间内是否还存在符合条件的元素
while(left < right && nums[left] == nums[left + 1]) ++left;
while(left < right && nums[right] == nums[right - 1]) --right;
++left;
--right;
}
}
}
}
return result;
}
};
X.N数之和
class Solution {
public:
vector<vector<int>> nSum(vector<int>& nums, int target, int n, int startIndex) {
// 创建结果集
vector<vector<int>> result;
int size = nums.size();
// 至少是2Sum,且数组大小不能小于n
if(n < 2 || size < n) return result;
// 对数组内元素进行排序,方便后续使用下标进行去重
sort(nums.begin(), nums.end());// O(nlogn)
if(n == 2)
{
// 在数组中找到和为target的两个整数
// 创建哈希表 key为元素,value为下标
unordered_map<int, int> hash;
// 遍历数组
for(int i = 0; i < nums.size(); ++i)
{
// 找到了,返回两个元素对应下标 => 数组当前元素之前存在x, x + nums[i] == target
if(hash.find(target - nums[i]) != hash.end())
{
result.push_back(vector<int>{hash[target - nums[i]], i});
}
// 未找到,将当前元素添加到哈希表中
else
{
hash[nums[i]] = i;
}
}
}
else
{
// n > 2时,递归计算 (n - 1)Sum 的结果
// nSum(nums, target - nums[i], n - 1, i + 1);
}
return result;
}
};
offer4.只出现一次的数字
137.只出现一次的数字II
算法思路:
如果数组中所有数字的 第i个数位相加之和 能
被3整除
,那么只出现一次的数字的 第i个数位一定是0;如果数组中所有数字的 第i个数位相加之和被3除余1
,那么只出现一次的数字的 第i个数位一定是1.知道一个整数任意一位是0还是1之后,最终就演变成了使用二进制还原十进制。
以此类推,如果其它元素都出现k次,找只出现一次的元素,也是一样的道理
class Solution {
public:
int singleNumber(vector<int>& nums) {
// 使用额外空间 那么时间复杂度O(n) 空间复杂度O(n)
// 要求不使用额外空间来实现 那么就应该想到使用常数空间
// int类型固定为32位,使用长度为32的数组,记录所有数值二进制的每一位共出现了多少次1
vector<int> cnt(32, 0);
for(int num : nums)
{
// 记录每个数字二进制的每一位共出现了多少次1
for(int i = 0; i < 32; ++i)
{
// 得到整数num的二进制形式中从右起第i个数位
if((num >> i) & 1 == 1) ++cnt[i];
}
}
// 对cnt数组的每一位进行 mod 3 的操作,重新拼凑出只出现一次的数值
int ans = 0;
for(int i = 0; i < 32; ++i)
{
// 如果当前位对3取余后值为1,那么说明 只出现一次的那个数字的二进制在cnt[i]的位置值也为1
// 例如(cnt[2] % 3) & 1 == 1 那么对应的二进制位为 1 0 0 ,即 1 << 2
// 又cnt数组中有多个 (cnt[i] % 3) & 1 == 1 存在,所以这里使用累加和求出最终的结果
if((cnt[i] % 3) & 1 == 1) ans += (1 << i);
}
return ans;
}
};
offer70.排序数组中只出现一次的数字
给定一个只包含整数的有序数组
nums
,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字
二分法,针对排序数组,时间复杂度为O(log n)
如果是不排序的数组,搜索只出现一次的数字,可以进行位操作,同offer4.只出现一次的数字
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
// 每个元素都会出现两次,只有一个数只会出现一次
int left = 0, right = nums.size() - 1;
// 如果mid为奇数索引,那么如果mid和mid-1指向的元素相等,说明只出现一次的元素在当前mid的右侧;如果mid为偶数索引,判断mid和mid+1是否相等,相等,则唯一出现的元素在mid+1的右侧
int mid = 0;
while(left <= right)
{
if(left == right) break;
mid = left + (right - left) / 2;
if(mid % 2 == 1)
{
if(nums[mid] == nums[mid - 1]) left = mid + 1;
// 不相等,说明唯一出现的元素在mid的左侧
else right = mid;
}
else
{
if(nums[mid] == nums[mid + 1]) left = mid + 1;
else right = mid;
}
}
return nums[left];
}
};
offer5.单词长度的最大乘积
时间复杂度O(n^2) 空间复杂度O(n)
可进行优化:该解法是用一个长度为26的bool型数组记录字符串中出现的字符,bool型要么为true,要么为false,在二进制中数字的每个数位要么是0要么是1,因此可以将长度为26的bool型数组用26个二进制的数位代替,二进制的0对应false,1对应true
class Solution {
public:
int maxProduct(vector<string>& words) {
// 判断两个字符串中是否出现相同的字符,只需要从'a'到'z'判断某个字符是否在两个字符串对应的哈希表中都出现了
// 记录某个字符串中对应字符出现的位置
vector<vector<bool>> flags(words.size(), vector<bool>(26, false));
for(int i = 0; i < words.size(); ++i)
{
for(char ch : words[i])
{
flags[i][ch - 'a'] = true;
}
}
int result = 0;
// 比较两个字符串是否包含相同字符
for(int i = 0; i < words.size(); ++i)
{
for(int j = i + 1; j < words.size(); ++j)
{
int k = 0;
for( ; k < 26; ++k)
{
// 说明两个字符串出现了相同的字符
if(flags[i][k] == true && flags[j][k] == true) break;
}
// 说明两个字符串不包含相同字符
if(k == 26)
{
int prod = words[i].size() * words[j].size();
result = max(result, prod);
}
}
}
return result;
}
};
offer6.排序数组中两个数字之和
对于排序数组,首先要想到双指针实现,这里的双指针实现时间复杂度为O(n) 空间复杂度O(1)
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
// 两数之和 + 排序数组
// 双指针实现:双指针,不是语言中的指针,而是一个能定位数据容器中某个数据的手段
int left = 0, right = numbers.size() - 1;
while(left < right)
{
if(numbers[left] + numbers[right] > target) --right;
else if(numbers[left] + numbers[right] < target) ++left;
else return vector<int>{
left, right};
}
return vector<int>{
-1, -1};
}
};
1.1 前缀和
offer10.和为k的子数组
利用前缀和求和为k的连续子数组的个数
时间复杂度O(n) 空间复杂度O(n)
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
// 定义pre[i]为[0..i]所有数之和
// 那么[j..i]连续子数组和为k 可以转换为 pre[i] - pre[j - 1] == k
// 即pre[j - 1] == pre[i] - k
// 相当于只要在哈希表中找到了pre[j - 1],那么就找到了连续子数组[j..i]和为k,统计对应连续子数组出现的个数
// 创建哈希表,key为[0..i]连续子数组之和;value为连续子数组之和 出现的次数
unordered_map<int, int> hash;
// 这里一定要初始化哈希表,当preSum和为k时,会在哈希表中寻找pre[j-1],也即是preSum-k == 0,如果哈希表未初始化,就会错过这个符合条件的子数组
hash[0] = 1;
int count = 0; // 符合条件的子数组的个数
int preSum = 0;// 定义preSum为[0..i]所有数之和
for(int i = 0; i < nums.size(); ++i)
{
preSum += nums[i];
// 找到了符合条件的连续子数组
if(hash.find(preSum - k) != hash.end())
{
// 因为连续子数组和相同的子数组在此之前不一定只出现一次,所以当符合条件的时候,不能使用++count,而应累加key对应的value
count += hash[preSum - k];
}
// 将当前连续子数组和添加到哈希表中
++hash[preSum];
}
return count;
}
};
offer11.0和1个数相同的子数组
关键思路同和为k的子数组,但是本题是求最长连续子数组的长度,所以还需要注意很多细节方面的问题,如:哈希表的初始化问题,最长连续子数组如何使得其长度最长问题等
时间复杂度O(n) 空间复杂度O(n)
class Solution {
public:
int findMaxLength(vector<int>& nums) {
// 求0和1个数相同的最长连续子数组的长度,难点在于连续子数组0和1的个数统计
// 将0转换为-1,那么题目就转换为 和为0的最长连续子数组的长度
// 问题就转换为前缀和问题
// 本题 key为"第一次出现"连续子数组之和preSum;value为"第一次出现"连续子数组之和为preSum的最后一个元素对应的下标(这样才能保证最长连续子数组)
unordered_map<int, int> hash;
// 同上一题求和为k的连续子数组,上一题求符合条件的连续子数组出现的次数,所以初始化hash[0] = 1,本题是求长度,需要将hash[0] = -1,初始化为-1
// 例如[0, 1] 遍历到1,符合条件,那么连续子数组的长度为1 - (-1) = 2
hash[0] = -1;
int maxLength = 0;
int preSum = 0;
for(int i = 0; i < nums.size(); ++i)
{
preSum += nums[i] == 0 ? -1 : 1;
// 找到了和为k的连续子数组 k == 0
if(hash.find(preSum - 0) != hash.end())
maxLength = max(maxLength, i - hash[preSum - 0]);
// hash.find(preSum) == hash.end() 未找到的时候,才添加键值对
else
hash[preSum] = i;
}
return maxLength;
}
};
offer12.左右两边子数组的和相等
利用前缀和思路求解子数组和类面试题
时间复杂度O(n) 空间复杂度O(1)
class Solution {
public:
int pivotIndex(vector<int>& nums) {
// 求数组元素总和
int sum = 0;
for(int num : nums) sum += num;
// [0..i-1]所有数之和 等于 [i+1..nums.size()-1]所有数之和
// pre[i-1]为[0..i-1]所有元素之和 [i..nums.size()-1]所有元素和为sum-pre[i-1]
// [i+1..nums.size()-1]所有元素和为sum-pre[i-1]-nums[i]
// 遍历数组
int preSum = 0;// preSum表示[0..i-1]数组元素之和(不能包含中心下标元素)
// sum - preSum 则为[i..nums.size()-1]数组元素之和
// 因为要求中心下标左右两边元素和相等,这里不能包含中心下标
for(int i = 0; i < nums.size(); ++i)
{
// 注意中心下标右侧所有元素相加之和为sum - preSum - nums[i]
if(preSum == sum - preSum - nums[i]) return i;
preSum += nums[i];
}
// 不存在中心下标,返回-1
return -1;
}
};
offer50.向下的路径节点之和
同offer10求和为k的子数组的个数原理类似,前缀和,只不过这里是二叉树的某个路径符合条件的数目,同时这里需要注意的是,向哈希表中添加键值对的时候,在递归回溯的时候,对相应的键值对也需要进行回溯撤销处理
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
// 将哈希表创建为全局变量,因为要进行递归
// 创建哈希表,key为[0..i]连续子数组之和;value为连续子数组之和 出现的次数
unordered_map<long long, int> hash;
int dfs(TreeNode* root, int targetSum, long long preSum)
{
int ret = 0;
if(root == nullptr) return ret;
// 根节点不为空
preSum += root->val;
// 找到符合条件的连续子数组,更新路径数目
if(hash.find(preSum - targetSum) != hash.end())
{
ret += hash[preSum - targetSum];
}
// 将前缀和添加到哈希表中
++hash[preSum];
ret += dfs(root->left, targetSum, preSum);
ret += dfs(root->right, targetSum, preSum);
// 注意回溯的时候需要将对应的前缀和从哈希表中去除
--hash[preSum];
return ret;
}
public:
int pathSum(TreeNode* root, int targetSum) {
// 转换为求和为k的子数组的个数
// 定义pre[i]为[0..i]所有数之和
// 那么[j..i]连续子数组和为k,可以转换为 pre[i] - pre[j - 1] == k
// 即pre[j - 1] == pre[i] - k
// 相当于只要找到了pre[j - 1],即pre[i] - k,就找到了连续子数组[j..i]和为k,统计相应子数组出现的次数即可
// 这里一定要初始化哈希表,当preSum和为k时,会在哈希表中寻找pre[j-1],也即是preSum-k == 0,如果哈希表未初始化,就会错过这个符合条件的子数组
hash[0] = 1;
return dfs(root, targetSum, 0);
}
};
offer13.二维子矩阵的和
求二维子矩阵和,思路同利用前缀和求一维子数组和;在初始化一次NumMatrix对象后,后续不断调用函数,不需要再重新对矩阵内元素进行搜索,每次调用是需要做几次加减法,调用操作的时间复杂度为O(1)
时间复杂度O(mn) 空间复杂度O(mn)
class NumMatrix {
public:
// 利用前缀和思路求解二维子矩阵的和
/*
f(i, j)表示以[i,j]为右下角元素的矩阵之和:
f(i, j) = f(i-1, j) + f(i, j-1) - f(i-1, j-1) + matrix[i][j];
sumRegion(int r1, int c1, int r2, int c2)求给定子矩阵元素之和:
= f(r2, c2) - f(r1-1, c2) - f(r2, c1-1) + f(r1-1, c1-1)
如果坐标值(r1, c1)值为0,f(r1-1, c1-1) = f(-1, -1)下标会失效,那么可以在矩阵的最上面增加一行,最左面增加一列
// 将前缀和矩阵统一向右下移动一个单位(横纵坐标统一 + 1)
注:为什么后面要加上f(r1-1, c1-1),因为减去两个子矩阵和的时候,减了两次f(r1-1, c1-1),所以要加上一个f(r1-1, c1-1)
*/
// sum[i][j]表示以[i, j]为右下角元素的矩阵元素总和
vector<vector<int>> sum;
NumMatrix(vector<vector<int>>& matrix) {
int row = matrix.size();
// 行不符合条件
if(row <= 0) return;
// 行符合条件
int col = matrix[0].size();
// 这里初始化二维矩阵大小分别为原数组的行列数 + 1,为了避免求f(0,0)等出现负数下标
sum.resize(row + 1, vector<int>(col + 1, 0));
for(int i = 0; i < matrix.size(); ++i)
{
for(int j = 0; j < matrix[0].size(); ++j)
{
sum[i + 1][j + 1] = sum[i][j + 1] + sum[i + 1][j] - sum[i][j] + matrix[i][j];
}
}
// for(int i = 0; i < sum.size(); ++i)
// {
// for(int j = 0; j < sum[0].size(); ++j)
// {
// cout << sum[i][j] << " ";
// }
// cout << endl;
// }
}
int sumRegion(int row1, int col1, int row2, int col2) {
// 求f(i, j)对应sum下标分别+1
return sum[row2 + 1][col2 + 1] - sum[row1][col2 + 1] - sum[row2 + 1][col1] + sum[row1][col1];
}
};
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix* obj = new NumMatrix(matrix);
* int param_1 = obj->sumRegion(row1,col1,row2,col2);
*/
1.2 质数
204.计数质数
给定整数
n
,返回 所有小于非负整数n
的质数的数量 。
class Solution {
public:
int countPrimes(int n) {
// 厄拉多塞筛法
// 表示第i个数是否是质数
vector<int> isPrime(n, 1);
int ans = 0;
for (int i = 2; i < n; ++i)
{
if (isPrime[i])
{
ans += 1;
// 如果x是质数,那么大于x的x的倍数 2x, 3x, 4x ... ( < n)一定不是质数
if ((long long)i * i < n)
{
for (int j = i * i; j < n; j += i)
{
isPrime[j] = 0;
}
}
}
}
return ans;
}
};
2761.和等于目标值的质数对
先声明大小为n的数组将所有的质数都标记出来,然后接下来利用条件x + y == n && 都是质数 && x <= y 求出所有的质数对
计算质数的时间复杂度O(nloglogn),空间复杂度O(n)
求质数对的时间复杂度为O(n)
class Solution {
public:
vector<vector<int>> findPrimePairs(int n) {
// 创建结果集
vector<vector<int>> result;
if(n <= 2) return result;
// 厄拉多塞筛法
// 表示第i个数是否是质数 : 质数是大于 1 的自然数,并且只有两个因子,即它本身和 1
vector<int> isPrime(n, 1);
isPrime[0] = 0;
isPrime[1] = 0;
for (int i = 2; i < n; ++i)
{
if (isPrime[i])
{
// 如果x是质数,那么大于x的x的倍数 2x, 3x, 4x ... ( < n)一定不是质数
if ((long long)i * i < n)
{
for (int j = i * i; j < n; j += i)
{
isPrime[j] = 0;
}
}
}
}
// x + y == n && 都是质数
for(int i = 1; i <= n; ++i)
{
int temp = n - i;
if(i > temp) break;// 1 <= x <= y <= n
if(isPrime[i] == 1 && isPrime[temp] == 1)
{
result.push_back(vector<int>{
i, temp});
}
}
return result;
}
};
1.3 重叠区间
6929.数组的最大美丽值(重叠区间个数)
转换为计算所有区间内的最大重叠区间个数问题(优先队列小顶堆实现),同类问题可都使用该解法
时间复杂度O(nlogn) 空间复杂度O(n)
如果使用暴力解法,双循环计算重叠区间个数,会超时,时间复杂度O(n^2)
class MyComp
{
public:
// 将所有区间按照从小到大的顺序排序
bool operator()(const vector<int>& v1, const vector<int>& v2)
{
if(v1[0] == v2[0]) return v1[1] < v2[1];
return v1[0] < v2[0];
}
};
class Comp
{
public:
// 实现小顶堆
bool operator()(const int& n1, const int& n2)
{
return n1 > n2;
}
};
class Solution {
public:
int maximumBeauty(vector<int>& nums, int k) {
// 区间重叠问题
// ranges中存储的是每一个区间,转换为判断最多有多少个重复区间问题
vector<vector<int>> ranges(nums.size(), vector<int>(2, 0));
vector<int> range(2, 0);
for(int i = 0; i < nums.size(); ++i)
{
range[0] = nums[i] - k;
range[1] = nums[i] + k;
ranges[i] = range;
}
if(ranges.size() == 1) return 1;
// 首先对每个区间进行从小到大排序
// 这里如果不进行排序的话,那么后续对区间进行遍历,在维持队列内区间重叠的时候,可能导致某个区间end边界pop,但是后续仍有区间与之重叠这种情况发生
// 进行排序,如果当前遍历到的区间与堆顶对应的区间不重叠,那么与之前的所有区间一定不重叠
sort(ranges.begin(), ranges.end(), MyComp());
// 创建优先队列 小顶堆
priority_queue<int, vector<int>, Comp> pri;
//利用小顶堆解决重叠区间个数的问题:
/*
目的:***始终维持优先队列内的区间是重叠的***
如何保证:借助小顶堆 小顶堆内元素为区间的结束end边界
1. 如果待添加区间的开始位置 小于等于 优先队列堆顶元素(前面某个区间的最小end边界),说明队列内所有的区间都重叠 ,即start <= pri.top(),则将当前区间end边界添加到队列中,不断更新ans为队列的最大size()
2. 如果待添加区间的开始位置 大于 优先队列堆顶元素,说明两个区间不重叠,则while循环不断的弹出堆顶元素,直到满足条件1
问:为什么是小顶堆实现?
如果是大顶堆,如果当前区间与堆顶元素对应的区间不重叠的情况下,我们不能保证与队列内所有的区间都不重叠,同理,重叠也一样;只有小顶堆才能保证,队列内的区间始终是重叠的
*/
pri.push(ranges[0][1]);//结束时间
int ans = 0;// 记录重叠区间的个数
for(int i = 1; i < ranges.size(); i++)
{
int start = ranges[i][0]; //当前区间的开始边界
// 两个区间不重叠,不满足队列条件,弹出堆顶元素,始终维持队列内区间是重叠的
while(!pri.empty() && start > pri.top())
pri.pop();
pri.push(ranges[i][1]);
int count = pri.size();
ans = max(ans, count);
}
return ans;
}
};
offer58.日程表 (判断是否可以插入某个区间类问题)
判断是否可以插入某个区间类问题
使用map集合实现添加日程,map与set的底层实现都是红黑树,增删查的时间复杂度为O(logn)
使用map将key作为日程开始时间,则底层会默认对开始时间进行排序,可以优化查找的效率O(logn)
总的时间复杂度为O(logn) 空间复杂度O(n)upper_bound()函数返回指向第一个大于给定值的元素的迭代器,也就是说,它返回的是大于目标值的最小元素的位置
lower_bound()函数返回指向第一个大于或等于给定值的元素的迭代器,也就是说,它返回的是大于等于目标值的最小元素的位置
class MyCalendar {
public:
// 使用map存储键值对
map<int, int> mp;
MyCalendar() {
}
bool book(int start, int end) {
// 如果待插入的时间段与集合中某个时间段重叠,则直接返回false
// 若未重叠,则将对应的时间段添加到集合中,并返回true
// 待插入的区间为[start, end) 需要在集合中找到:
// 开始时间小于start的所有事项中最晚的一个
// 开始时间大于start的所有事项中最早的一个
// 当前区间与后一个时间段重叠
auto it = mp.lower_bound(start);
if(it != mp.end() && it->first < end) return false;
// 移动迭代器,找到当前迭代器指向时间的前一个时间
if(it != mp.begin() && (--it)->second > start) return false;
// 未出现重叠
mp[start] = end;
return true;
}
};
/**
* Your MyCalendar object will be instantiated and called as such:
* MyCalendar* obj = new MyCalendar();
* bool param_1 = obj->book(start,end);
*/
offer57.值和下标之差都在给定的范围内 (220.存在重复元素 III)
需要使用一个大小为k的滑动窗口,在窗口内判断值之差是否符合条件
判断滑动窗口中是否存在某个数 y 落在区间 [x−t,x+t]中,只需要判断滑动窗口中所有 大于等于x−t 的元素中的 最小元素 是否 小于等于 x+t即可
// upper_bound()函数返回指向第一个大于给定值的元素的迭代器,也就是说,它返回的是大于目标值的最小元素的位置
// lower_bound()函数返回指向第一个大于或等于给定值的元素的迭代器,也就是说,它返回的是大于等于目标值的最小元素的位置
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
// 下标之差为k
// 值之差为t
// abs(nums[i] - nums[j]) <= t 可以推导出 nums[j] - t <= nums[i] <= nums[j] + t 并且i,j下标的绝对值小于等于k
// 那么就需要使用一个大小为k的滑动窗口,在窗口内判断值之差是否符合条件
// 判断滑动窗口中是否存在某个数 y 落在区间 [x−t,x+t]中,只需要判断滑动窗口中所有 大于等于x−t 的元素中的 最小元素 是否 小于等于 x+t即可
set<long> st;
for(int i = 0; i < nums.size(); ++i)
{
// 在集合中找到 >= x-t 的最小元素 y
auto it = st.lower_bound((long)nums[i] - t);
if(it != st.end() && *it <= (long)nums[i] + t)
return true;
// 若是集合中不存在这个元素y,则将当前遍历到的元素添加到集合中
st.insert(nums[i]);
// 始终保证滑动窗口的大小为k
if(i >= k)
st.erase(nums[i - k]);// 删除的是指定窗口外的元素
// set底层是红黑树,默认排序,但是这里没有使用到其排序的功能,只是按照元素值进行增删查操作
// 时间复杂度O(nlogk) 红黑树增删查的时间复杂度为O(logk) k为红黑树节点数量 空间复杂度O(k)
}
return false;
}
};
2、滑动窗口
tips:
使用双指针解决滑动窗口类问题,需要注意一个细节,就是使用双指针的解法基于如下假设:
- 向右移动快指针相当于在子数组中添加一个新的数字,从而得到更大的子数组的数字之和。
- 如果新添加的数字是正数,那么这个假设是成立的。
- 如果题目的数组没有明确的说明数组是由正整数组成的,因此不能保证在子数组中添加新的数字就能得到和更大的子数组,同样也不能保证删除子数组最左边的数字就能得到和更小的子数组。
209.长度最小的子数组
使用滑动窗口方法 (不断调节子序列的起始位置和终止位置) 求得长度最小的子数组。
每个元素在滑动窗口进来操作一次,出去操作一次,每个元素被操作2次,所以时间复杂度是O(2n) = 时间复杂度O(n) 空间复杂度O(1)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// 滑动窗口
int minLength = INT_MAX;
// 声明两个变量,分别表示滑动窗口的左右侧
int slow = 0;
int fast = 0;
int sum = 0;// 窗口内元素之和
while(fast < nums.size())
{
sum += nums[fast];
// 当前窗口符合条件
// 这里使用while循环,是为了多次判断缩小左侧窗口时出现sum符合条件的情况
while(sum >= target)
{
// 添加结果
minLength = min(minLength, fast - slow + 1);
// 缩小左侧窗口,同时更新窗口内元素之和
sum -= nums[slow];
++slow;
}
++fast;// 更新快指针,即窗口右侧右移
}
// 根据初始值判断是否存在符合条件的子数组
return minLength == INT_MAX ? 0 : minLength;
}
};
904.水果成篮(☆☆☆)
通过滑动窗口限定窗口内水果种类,通过哈希表查找指定元素是否在集合中出现过以及出现的次数,两者相结合。时间复杂度O(n) 空间复杂度O(1)
class Solution {
public:
int totalFruit(vector<int>& fruits) {
// 滑动窗口解法
int maxFruits = 0;
// 双指针控制窗口的大小
int slow = 0;
int fast = 0;
// 创建哈希表,查询一个元素是否在集合中出现过
unordered_map<int, int> hash;
while(fast < fruits.size())
{
++hash[fruits[fast]];
// 窗口大小符合收缩条件,因为需要收集水果的最大数目,所以按照上限3确定窗口界限(篮子只能装2种类型的水果)
while(hash.size() > 2)
{
// 缩小左侧窗口
--hash[fruits[slow]];
if(hash[fruits[slow]] == 0)
{
hash.erase(fruits[slow]);
}
// 应该先判断当前慢指针指向元素对应哈希表中的数据,然后慢指针再进行移动
++slow;
}
// 窗口大小 <= 2,添加结果
maxFruits = max(maxFruits, fast - slow + 1);
// 窗口大小不符合收缩条件,移动窗口右侧,扩大窗口
++fast;
}
return maxFruits;
}
};
76.最小覆盖子串(☆☆☆)
时间复杂度O(n) 空间复杂度O(m + n) 两个字符串都存储到哈希表中 可以声明128大小的数组将空间复杂度优化为O(1)
slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度,在进行滑动窗口时,向哈希表中添加字符为减,删除字符为加。降低空间复杂度可以用26大小的数组,代替哈希表
class Solution {
public:
string minWindow(string s, string t) {
// 滑动窗口解法
// 求字符串s中涵盖t所有字符的最小子串
// 子串的最小长度
int length = INT_MAX;
// 最小长度子串的起始索引
int startIndex = 0;
// 创建哈希表,将t所有字符存储到哈希表中 key为t中字符,value为字符出现频率
unordered_map<char, int> hash;
for(char ch : t)
{
++hash[ch];
}
int needCount = t.size();// 窗口内子字符串所需要指定字符的个数
// 声明快慢指针,限定窗口大小
// slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度
int slow = 0;
for(int fast = 0; fast < s.size(); ++fast)
{
// 步骤1:不断增加fast使得窗口增大,使得窗口内包含t中所有字符
// 遍历到的是t中的字符
if(hash[s[fast]] > 0)
--needCount;
--hash[s[fast]];// 向哈希表中添加字符,扩大窗口
// 2、不断增加慢指针,使得窗口缩小,直到遇到一个必须包含的元素
if(needCount == 0)
{
while(true)
{
// 这里必须找到t中的字符并且判断其在哈希表中的value,若不为0,则说明其在窗口内出现多次,还可以进行缩小窗口操作;只有为0,则窗口为最小窗口
if(hash.find(s[slow]) != hash.end() && hash[s[slow]] == 0)
break;
// 不是必须包含的元素,缩小窗口,++
++hash[s[slow]];
++slow;
}
// 添加结果
if(fast - slow + 1 < length)
{
length = fast - slow + 1;
startIndex = slow;// 更新起始位置
}
// 步骤3、让slow慢指针在增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤
// 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
++hash[s[slow]];
++needCount;
++slow;
}
}
return length == INT_MAX ? "" : s.substr(startIndex, length);
}
};
offer17.含有所有字符的最短字符串
同76题最小覆盖子串 求字符串s中包含t的所有字符的最短子字符串,返回符合条件的子字符串
时间复杂度O(n) 空间复杂度O(m + n)
因为这里的两个字符串都由英文字母(大小写字母)组成 26 + 6 + 26 = 58 可通过创建大小为58的数组优化空间复杂度O(1)
class Solution {
public:
string minWindow(string s, string t) {
// 同76题最小覆盖子串
// 求字符串s中包含t的所有字符的最短子字符串,返回符合条件的子字符串
// 记录符合条件的最短子字符串的长度
int length = INT_MAX;
// 记录符合条件的最短子字符串对应的起始下标,方便后续返回最短子字符串
int startIndex = 0;
// 创建哈希表,记录字符串t中字符出现的频率
unordered_map<char, int> hash;
for(char ch : t) ++hash[ch];
int needCount = t.size();// 窗口中必须包含的t的字符的个数
// 创建双指针,限制窗口大小,进行滑动窗口
int slow = 0;
for(int fast = 0; fast < s.size(); ++fast)
{
// 步骤1:移动快指针,扩大窗口大小,直到窗口内包含t中所有字符
if(hash[s[fast]] > 0) --needCount;
// 扩大窗口,则向哈希表中添加窗口内元素(扩大窗口--,缩小++)
--hash[s[fast]];
// 当needCount为0,则表示窗口内包含了t中所有字符,开始执行步骤2
// 步骤2:移动慢指针,缩小窗口,直到慢指针指向窗口内必须包含的元素
if(needCount == 0)
{
while(true)
{
// 当前慢指针指向窗口内必须包含的字符,若其哈希值不为0,则说明该字符在窗口中多次出现,不是窗口内必须包含的字符,还需要进行缩小窗口;若为0,则当前窗口不能再缩小,跳出循环
if(hash.find(s[slow]) != hash.end() && hash[s[slow]] == 0)
break;
// 不是窗口必须包含的字符,还需要进行缩小窗口
++hash[s[slow]];
++slow;
}
// 添加结果
if(fast - slow + 1 < length)
{
length = fast - slow + 1;
startIndex = slow;
}
// 步骤3:破坏当前窗口满足的条件,移动一步慢指针,则继续重复三个步骤
// 因为慢指针指向的元素是窗口内必须包含的元素,所以移动慢指针进行缩小一个单位窗口时,需要先更新哈希表中对应的值,然后再更新needCount,然后再移动慢指针
++hash[s[slow]];
++needCount;
++slow;
}
}
// if(length == INT_MAX) return ""; 未找到符合条件的子字符串
return length == INT_MAX ? "" : s.substr(startIndex, length);
}
};
438.找到字符串中所有字母异位词
同76求最小覆盖子串类问题,相同的解法,这里只需要将长度为p.size()的最小子串的初始位置添加到结果集中就可以解决此类问题。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> result;
// 特判
if(s.size() < p.size())
return result;
// 创建哈希表 key为字符串p字符,value为p中字符出现频率
unordered_map<char, int> hash;
for(char ch : p)
{
++hash[ch];
}
int needCount = p.size();// 窗口内子字符串所需要指定字符的个数
// 声明快慢指针,限定窗口大小
// slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度
int slow = 0;
for(int fast = 0; fast < s.size(); ++fast)
{
// 1.fast指针移动,增大窗口,使得窗口内包含t中所有字符
if(hash[s[fast]] > 0)
{
--needCount;
}
--hash[s[fast]];
// 2.慢指针移动,缩小窗口,直到遇到一个必须包含的元素
if(needCount == 0)
{
while(true)
{
// 这里必须找到t中的字符并且判断其在哈希表中的value,若不为0,则说明其在窗口内出现多次,还可以进行缩小窗口操作;只有为0,则窗口为最小窗口
if(hash.find(s[slow]) != hash.end() && hash[s[slow]] == 0)
break;
// 不是必须包含的字符,++
++hash[s[slow]];
++slow;
}
// 添加结果
if(fast - slow + 1 == p.size())
{
result.push_back(slow);
}
// 3、让slow慢指针在增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤
// 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
++hash[s[slow]];
++needCount;
++slow;
}
}
return result;
}
};
28.找出字符串中第一个匹配项的下标
滑动窗口解法,同76覆盖最小子串,时间复杂度O(m) 最坏情况下为O(mn),即两个字符串相同。
空间复杂度O(n) 可进行优化.
虽然不是最佳解法,但是滑动窗口可以解决这一类问题,即统一解法。
class Solution {
private:
// 判断闭区间内的子串和needle字符串是否相同,注意needle字符串要从头开始
bool isValid(string& haystack, int slow, int fast, string& needle)
{
int index = 0;
while(slow <= fast)
{
if(haystack[slow] != needle[index]) return false;
++slow;
++index;
}
return true;
}
public:
int strStr(string haystack, string needle) {
// 同76最小覆盖子串类问题
// 创建哈希表,key为needle字符,value为字符出现频率
unordered_map<int, int> hash;
for(char ch : needle)
{
++hash[ch];
}
// 所需覆盖字符串长度
int needCount = needle.size();
int start = -1;// 记录起始位置
int slow = 0;
for(int fast = 0; fast < haystack.size(); ++fast)
{
// 1、不断扩大窗口,直到包含needle字符串中所有字符
if(hash[haystack[fast]] > 0)
{
--needCount;
}
--hash[haystack[fast]];
// 包含了needle字符串中所有字符,开始缩小窗口
if(needCount == 0)
{
// 2、开始缩小左侧窗口,直到左侧窗口遍历到的字符是窗口内必须包含的元素,即needle中的字符
while(true)
{
// 必须包含的元素:是needle中的字符,并且在哈希表中的频率为0,说明此时不能再缩小左侧窗口了
if(hash.find(haystack[slow]) != hash.end() && hash[haystack[slow]] == 0)
{
break;
}
// 否则,还能缩小窗口 扩大窗口--,缩小就++
++hash[haystack[slow]];
++slow;
}
// 添加结果这一步需要具体问题具体分析***
// 跳出while循环,判断是否符合条件,若符合,添加到结果集中
// 该题的符合条件是指找到子串为needle字符串
// 该题为了避免 ip 与 pi 两种结果的出现,再添加一个判断条件
if(fast - slow + 1 == needle.size() && isValid(haystack, slow, fast, needle) == true)
{
// 记录符合条件的子字符串起始位置
start = slow;
break;
}
// 3、让slow慢指针再增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤(***第三步注意移动先后顺序,很重要)
// 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
++hash[haystack[slow]];
++needCount;
++slow;
}
}
return start;
}
};
优化空间复杂度O(1)
class Solution {
private:
// 判断闭区间内的子串和needle字符串是否相同,注意needle字符串要从头开始
bool isValid(string& haystack, int slow, int fast, string& needle)
{
int index = 0;
while(slow <= fast)
{
if(haystack[slow] != needle[index]) return false;
++slow;
++index;
}
return true;
}
public:
int strStr(string haystack, string needle) {
// 同76最小覆盖子串类问题
// 创建哈希表,key为needle字符,value为字符出现频率
// unordered_map<int, int> hash;
vector<int> hash(26, 0);
for(char ch : needle)
{
++hash[ch - 'a'];
}
// 所需覆盖字符串长度
int needCount = needle.size();
int start = -1;// 记录起始位置
int slow = 0;
for(int fast = 0; fast < haystack.size(); ++fast)
{
// 1、不断扩大窗口,直到包含needle字符串中所有字符
if(hash[haystack[fast] - 'a'] > 0)
{
--needCount;
}
--hash[haystack[fast] - 'a'];
// 包含了needle字符串中所有字符,开始缩小窗口
if(needCount == 0)
{
// 2、开始缩小左侧窗口,直到左侧窗口遍历到的字符是窗口内必须包含的元素,即needle中的字符
while(true)
{
// 必须包含的元素:是needle中的字符,并且在哈希表中的频率为0,说明此时不能再缩小左侧窗口了
if(hash[haystack[slow] - 'a'] == 0)
{
break;
}
// 否则,还能缩小窗口 扩大窗口--,缩小就++
++hash[haystack[slow] - 'a'];
++slow;
}
// 跳出while循环,判断是否符合条件,若符合,添加到结果集中
// 该题的符合条件是指找到子串为needle字符串
// 该题为了避免 ip 与 pi 两种结果的出现,再添加一个判断条件
if(fast - slow + 1 == needle.size() && isValid(haystack, slow, fast, needle) == true)
{
// 记录符合条件的子字符串起始位置
start = slow;
break;
}
// 3、让slow慢指针再增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤
// 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
++hash[haystack[slow] - 'a'];
++needCount;
++slow;
}
}
return start;
}
};
offer14.字符串中的变位词
同76 最小覆盖子串类问题,统一解法
class Solution {
public:
bool checkInclusion(string s1, string s2) {
// 与76最小覆盖子串同解法
// 转换为求在字符串s2中涵盖s1所有字符的最小子串问题 ==> 滑动窗口
// 记录最小子串的长度
int length = INT_MAX;
// 创建哈希表,记录字符串s1中字符出现的频率
unordered_map<char, int> hash;
for(char ch : s1) ++hash[ch];
// 窗口的大小,即为s1中字符个数
int needCount = s1.size();
// 创建双指针,限定窗口大小,进行滑动窗口
// slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度
int slow = 0;
for(int fast = 0; fast < s2.size(); ++fast)
{
// 步骤1:不断增加fast使得窗口增大,使得窗口内包含t中所有字符
// 如果当前遍历到的s2中的字符在哈希表中存在
if(hash[s2[fast]] > 0) --needCount;
// 向哈希表中添加字符,扩大窗口(添加字符是--,减少字符是++)
--hash[s2[fast]];
// 步骤2:不断增加慢指针,使得窗口缩小,直到遇到一个必须包含的元素
if(needCount == 0)
{
while(true)
{
// 在窗口内找到字符串s1中的字符,若其哈希值不为0,则说明该字符在窗口中多次出现,不是窗口内必须包含的元素,还需要进行缩小窗口;若为0,则窗口为最小窗口
if(hash.find(s2[slow]) != hash.end() && hash[s2[slow]] == 0)
break;
// 不是窗口必须包含的元素,缩小窗口,++
++hash[s2[slow]];
++slow;
}
// 添加结果
if(fast - slow + 1 < length)
{
length = fast - slow + 1;
if(length == s1.size()) return true;
}
// 步骤3:破坏当前窗口满足的条件,让slow指针再增加一个位置,此时当前窗口一定不满足条件,继续重复三个步骤
// 当前slow指针指向必须包含的元素,所以缩小窗口时需要将该指针指向的元素添加到哈希表中,同时更新needCount,然后再移动慢指针
++hash[s2[slow]];
++needCount;
++slow;
}
}
return false;
}
};
offer15.字符串中的所有变位词
同76题最小覆盖子串统一解法,时间复杂度O(n) 空间复杂度O(mn),可对空间复杂度进行优化
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
// 同76题最小覆盖子串统一解法
// 转换为求字符串s中涵盖p中所有字符的最小子串问题 ==> 滑动窗口
vector<int> result;
// 记录最小子串的长度
int length = INT_MAX;
// 创建哈希表,记录字符串p中字符出现的频率(因为是求s中涵盖p)
unordered_map<char, int> hash;
for(char ch : p) ++hash[ch];
// 窗口大小为p中字符个数
int needCount = p.size();
// 创建双指针,限定窗口大小,进行滑动窗口
int slow = 0;
for(int fast = 0; fast < s.size(); ++fast)
{
// 步骤1:移动快指针,扩大窗口,使得窗口内包含p中所有字符
if(hash[s[fast]] > 0) --needCount;
--hash[s[fast]];
// 步骤2:缩小滑动窗口,使得窗口内包含p中必须包含的字符
if(needCount == 0)
{