目录
1.两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
关键思想:哈希表
- vector初始化
vector<int> b(2,-1);//b中有2个元素,每个值都是-1 利用构造函数进行初始化
vector<int> b{2,-1};//b中有2个元素,分别为2和-1 列表初始化
- 关联容器,假设a是map关联容器,
a.count(k)//返回关键值等于k的数量
所以可以通过下面代码来判断map容器中是否存在键值k。
if(a.count(k)>0)
- find算法
find(beg,end,val)//beg,end为迭代器,val为查找的值
//返回值:查找到,则返回迭代器,指向第一个等于val的元素
//查找失败,返回end迭代器,即等于第二个参数,例如
vector<int> a;
find(a.begin(),a.end(),2);
//若查找失败,返回a.end()
题解程序:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
map<int,int> a;
vector<int> b(2,-1);
for(int i=0;i<nums.size();++i)
{
if(a.count(target-nums[i])>0)//如果条件成立,说明已经找到了
{
b[0]=a[target-nums[i]];//target-nums[i]为a中已经有的键值 通过键值查找索引
b[1]=i;
return b;
}
a[nums[i]]=i;//还未找到,就添加到map容器
}
return b;
}
};
2.盛最多水的容器
给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
主要思想:双指针,关键点在于如果我们试图将指向较长线段的指针向内侧移动,矩形区域的面积将受限于较短的线段而不会获得任何增加。但是,在同样的条件下,移动指向较短线段的指针尽管造成了矩形宽度的减小,但却可能会有助于面积的增大。因为移动较短线段的指针会得到一条相对较长的线段,这可以克服由宽度减小而引起的面积减小。也就是说向内侧移动指向较长线段的指针,只会使面积减小,而移动指向较短线段的指针,则有可能导致面积增大。
题解程序:
#define min(a,b) ((a < b) ? a : b)
class Solution {
public:
int maxArea(vector<int>& height) {
int start=0,end=height.size()-1;
int maxarea=(end-start)*min(height[start],height[end]);
for(int i=0;i<height.size()-1;++i)
{
if(height[start]<height[end])
{
start++;
}
else
{
end--;
}
int temparea=(end-start)*min(height[start],height[end]);
if(temparea>maxarea)
maxarea=temparea;
}
return maxarea;
}
};
3.三数之和
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
关键思想:排序算法加双指针,而且因为有了排序算法,可以比较方便的去除重复解同时可以很方便的设置截止条件。特别要学习其break与continue处代码的思想。
另外,有时要构建一个vector<vector<int>>的容器,其不必要临时创建一个vector<int> temp,然后push_back(temp),而是直接用列表初始化的方式直接push_back,例如push_back({1,2,3})。
话外:一开始我使用了STL库中的unique函数进行去重,后来算法复杂度太高,不过还是有必要介绍一下unique函数去重。
vector<int> a={3,2,2,4,1,3,4,5};
sort(a.begin(),a.end());//a={1,2,2,3,3,4,4,5}
auto it=unique(a.begin(),a.end());
a.erase(it,a.end());//a={1,2,3,4,5}
需要注意,unique函数并不是删除重复的元素,而是把(相邻的)重复的元素放到容器的后面。因此,为了去除重复的元素,应该,首先对vector进行排序,这样保证重复元素在相邻的位置。然后调用unique函数,该函数返回重复元素的首地址。所以接下来调用erase方法将重复元素删去。
题解程序:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> threesum;
if(nums.size()<3)
return threesum;
sort(nums.begin(),nums.end());
for(int k=0;k<nums.size()-2;++k)
{
int i=k+1;
int j=nums.size()-1;
if(nums[k]>0)//最小元素都大于0了 肯定不会再有满足条件的了
break;
if(k>0&&nums[k]==nums[k-1])//nums[k-1]已经考虑到所有情况,本次只会重复解,跳过
continue;
while(i<j)
{
int sum=nums[k]+nums[i]+nums[j];
if(sum<0)
{
i++;
}
else if(sum>0)
{
j--;
}
else
{
threesum.push_back({nums[k],nums[i],nums[j]});
i++;
j--;
while(i<j&&nums[i]==nums[i-1])//跳过重复的解
{
i++;
}
while(i<j&&nums[j]==nums[j+1])
{
j--;
}
}
}
}
return threesum;
}
};
4.最接近的三数之和
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
关键思想:排序加双指针。三个数之和使用双指针可以每次for循环固定一个数,然后用双指针一个start指向该数的后一位,一个end指向容器的最后一位,然后利用排好的序向内移动指针,关键思想就是sum如果大于target,说明值大了,只有向内移动end指针才有可能使值变小。sum如果小于target,说明值小了,只有向内移动start指针才有可能使值变大。同时需要设置一个变量,用于存储最近产生的最接近target的值。
编程中遇到一个很傻的问题,但有必要记录一下,一开始我以为leetcode不包含abs函数所在的库,然后自己宏定义了abs,结果问题来了,后来发现其实leetcode已经把我们常用的库都给包含进去了,所以一般情况下我们能想到的库函数leetcode都已经默认包含了。且看下述代码:
#define abs(x) (x>0)?(x):(-x)
该宏定义乍一看好像没啥问题,实现了绝对值功能,但是实际上,宏定义是完全替换,不会给你额外加括号。该宏定义在单变量的情况下问题不大,关键问题出在多变量的情况下,例如abs(b-c)会被替换成下面这样
(b-c>0)?(b-c):(-b-c)
看到问题所在了吗?宏定义只会把你中间的x直接以b-c进行替换,这就造成了-x变成了-b-c,而不是-(b-c),也就是说宏定义不会给你额外加括号。所以在宏定义里,每项运算都要加括号,因为括号的优先级最高,保证表达式的运算顺序不会发生改变。因此正确的宏定义如下:
#define abs(x) ((x)>0?(x):(-(x)))
被这个问题坑了半天,现在想想好傻好天真。。。。
题解程序:
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
int sum;
int ans=nums[0]+nums[1]+nums[2];
int start;
int end;
for(int i=0;i<nums.size()-2;++i)
{
start=i+1;
end=nums.size()-1;
while(start!=end)
{
sum=nums[i]+nums[start]+nums[end];
if(abs(target-sum)<abs(target-ans))
ans=sum;
if(sum==target)
return sum;
else if(sum>target)
--end;
else
++start;
}
}
return ans;
}
};
5.四数之和
给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:
答案中不可以包含重复的四元组。
关键思想:双指针。其去三数之和那道题很类似,比较新颖的地方在于其对每一个循环内部计算当前循环所能达到的最小值与最大值,通过与target做比较,可以提前结束循环,很是巧妙,注意学习。当然了,三数之和也可以这样做,不过当时写的时候没有想到。
题解程序:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ans;
if(nums.size()<4)
return ans;
sort(nums.begin(),nums.end());
int length=nums.size();
for(int i=0;i<nums.size()-3;++i)
{
if(i>0&&nums[i]==nums[i-1])//防止重复解
continue;
int min=nums[i]+nums[i+1]+nums[i+2]+nums[i+3];
if(min>target)//当前最小值都大于target 肯定不满足条件了 整个循环结束
break;
int max=nums[i]+nums[length-1]+nums[length-2]+nums[length-3];
if(max<target)//当前最大值都大于target 肯定不满足条件了 只有递增i才有可能 所以continue
continue;
for(int k=i+1;k<nums.size()-2;++k)//第2层循环
{
if(k>i+1&&nums[k]==nums[k-1])//防止重复解 k-1的时候就把k的所有情况都考虑到了
continue;
int min1=nums[i]+nums[k]+nums[k+1]+nums[k+2];
if(min1>target)//当前最小值都大于target 肯定不满足条件了 整个for循环结束
break;
int max1=nums[i]+nums[k]+nums[length-1]+nums[length-2];
if(max1<target)//当前最大值都大于target 只有递增k才有可能 所以continue
continue;
int m=k+1;
int n=length-1;
while(m<n)
{
int sum=nums[i]+nums[k]+nums[m]+nums[n];
if(sum>target)//右指针向左移
{
n--;//这里面不需要考虑重复解的情况 因为这里注定不会生成答案
}
else if(sum<target)//左指针向右移
{
m++;
}
else
{
ans.push_back({nums[i],nums[k],nums[m],nums[n]});
m++;
n--;//这里左右指针都向内移 因为只移一个没有意义 要么下一次会重复解 要么无解
while(m<n&&nums[m]==nums[m-1]){m++;}//防止重复解
while(m<n&&nums[n]==nums[n+1]){n--;}
}
}
}
}
return ans;
}
};
6.删除排序数组中的重复项
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
主要思想:双指针。新颖的地方在于一个慢指针,一个快指针,这不同于以往的前后夹击的双指针。慢指针i主要去记录不重复的项,快指针j去遍历整个数组。nums[j]是单调递增的,只要遇到nums[i]!=nums[j]的情况,就说明新找到的nums[j]为不重复的项,将i+1后赋值给nums[i]。我自己实现的版本主要本质上也是双指针,但是我在找到新的不重复项的值的时候对数组进行了交换,这样做导致我不能对相邻元素判断是否相等来确定是否有不重复项,而是始终要与当前访问过的最大的不重复项去比较,只有大于该不重复项,说明才有新的不重复项。实际上对于题干要求来说这样做没有必要,直接赋值替换即可,因为题干没有要求不能改变数组的内容。
题解程序:
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.size()==0)
return 0;
int i=0;//i为慢指针 i等于不重复的数的个数减1
for(int j=1;j<nums.size();++j)//j为快指针 nums[j]一直在单调递增
{
if(nums[i]!=nums[j])//一旦出现不等了 ++i然后更新nums[i]的值
{
++i;
nums[i]=nums[j];
}
}
return i+1;
}
};
我的程序(算法复杂度与官方题解差不多,但是没有官方题解代码精简):
void swap(vector<int>&nums,int a,int b)
{
int temp=nums[a];
nums[a]=nums[b];
nums[b]=temp;
}
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.size()==0)
return 0;
int ans=1;
for(int i=1;i<nums.size();)
{
if(nums[i]<=nums[ans-1])//nums[ans-1]当中永远保存着访问过的最大的元素
{
++i;
}
else//最新的nums[i]比访问过的最大元素还要大 说明要增加计数
{
swap(nums,ans,i);
++ans;
++i;
}
}
return ans;
}
};
7.移除元素
给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
主要思想:双指针。慢指针与快指针可以(官方思路)(逆向思维),前后夹击的双指针也可以(我的思路)。来看官方思路,其主要是判断不等于val的值,然后用慢指针去记录所有并不等于val的值。官方的思路有点逆向思维的感觉,让找所有等于val的元素,它去找了所有不等于val的值,这种逆向思维值得学习。而我的思路在于前后夹击的双指针,我是去寻找所有等于val的值,一遇到就由右向左夹击寻找第一个不等于val的值进行交换,这样左侧指针i之前的值都不等于val,而右侧指针之后的都等于val。注意理解这两种思想,当然了,官方思路程序更加简洁明了。
题解程序:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
if(nums.size()==0)
return 0;
int i=0;//慢指针
for(int j=0;j<nums.size();++j)//快指针
{
if(nums[j]!=val)
{
nums[i++]=nums[j];
}
}
return i;
}
};
我的程序:
void swap(vector<int>& nums,int a,int b)
{
int temp=nums[a];
nums[a]=nums[b];
nums[b]=temp;
}
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
if(nums.size()==0)
return 0;
int i=0;
int j=nums.size()-1;
while(i<j)//双指针 i在左 j在右
{
if(nums[i]==val)
{
while(i<j&&nums[j]==val)//从右往左寻找第一个不等于val的值的索引
{
j--;
}
if(i==j)//由于i左侧都不等于val j右侧都等于val 因此i==j就可以结束了
{
return i;
}
swap(nums,i,j);//否则进行交换
i++;
j--;
}
else//不等于val 左侧i递增
{
i++;
}
}
if(nums[i]==val)//最中间的值是否等于val
return i;
else
return i+1;
}
};
8.下一个排列
实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须原地修改,只允许使用额外常数空间。
以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1
关键思想:从后往前扫描寻找第一个右侧大于左侧的值,只有这样才有可能找到下一个排列。找到这样的索引之后,索引之后的值必然是非严格单调递减的,接下来只需要两种操作,一种是索引左侧的值比索引右侧的所有值都大,只能将索引左侧值与索引处的值交换,然后排序。另一种是索引右侧存在比索引左侧的那个值大的值,我们要找到索引右侧比索引左侧的那个值大且最接近它的那个值,这样将其交换再排序,必然可以得到下一个排列。巧妙的是,经过从后往前扫描寻找第一个右侧大于左侧的值这样的操作之后,索引之后的值必然是非严格单调递减的,这样我们只需要从右向左遍历,寻找到第一个大于索引左侧的那个值,这个值就是我们要寻找的。
题解程序(也是我的程序):
void swap(vector<int>&nums,int a,int b)
{
int temp=nums[a];
nums[a]=nums[b];
nums[b]=temp;
}
class Solution {
public:
void nextPermutation(vector<int>& nums) {
if(nums.size()<=1)
return ;
int len=nums.size();
if(nums[len-1]>nums[len-2])//如果最后一位比倒数第二位大 直接交换就OK
{
swap(nums,len-1,len-2);
return;
}
int j=len-2;
while(j>0)//从倒数第二个往前寻找第一个右侧大于左侧的值的索引
{
if(nums[j]>nums[j-1])
break;
--j;
}//经过这样比较之后j右侧的数从j往右是非严格单调递减的
if(j!=0)
{
int m=j-1;
auto it_max=max_element(nums.begin()+j+1,nums.end());//j右侧最大值的索引
if(nums[m]>=*it_max)//大于j右侧的最大值 而nums[m]必然小于nums[j] 与j交换再重新排序即可
{
swap(nums,j,j-1);
sort(nums.begin()+j,nums.end());
}
else//否则 在j右侧从右向左寻找第一个大于nums[m]的索引 该值是必然存在的 交换再重新排序
{
int n=len-1;
while(n>j)
{
if(nums[n]>nums[m])
break;
--n;
}
swap(nums,n,m);
sort(nums.begin()+j,nums.end());
}
}
else//数组原来是完全逆序的
{
sort(nums.begin(),nums.end());
}
}
};
9.搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
主要思想:二分法。一般要求时间复杂度为O(logN),首先要想到用二分法。当然了,本题二分法其实很容易想到,但是其中的规律却不容易发现,这个规律就是对于这种排序好的数组从任意一点进行旋转得到的数组,无论你从在数组中间的某个点往两侧看,都是一侧是有序的,一侧是无序的。然后就是我们要充分利用有序的那一侧数组去决定二分法下一步该往哪走。
另外,一定要注意,二分法的while循环一定要为<=,这个等于号一定要包含,举个例子,假设下面的例子:
vecotr<int> a={1,2,3};
int start=0;
int end=a.size()-1;
int val=1;
while(start<=end)
{
int mid=start+(end-start)/2;
if(a[mid]==val)
return mid;
if(a[mid]>val)
end=mid-1;
else
start=mid+1;
}
上面的例子中,主要是寻找有序vector中值为0的下标,显然,一开始start=0,end=2,mid=0+(2-0)/2=1,接下来end=mid-1=0,如果不加等于号,while(start<end)不成立,相当于我们没有找到值为1的下标,这显然是错误的,因此,等于号必须要加。
题解程序:
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.size()==0)
return -1;
int start=0;
int end=nums.size()-1;
while(start<=end)//务必要有等于号
{
int mid=start+(end-start)/2;
if(nums[mid]==target)
{
return mid;
}
if(nums[mid]>=nums[start])//左侧有序 注意等于号要加 因为mid有可能等于start
{
if(target<nums[mid]&&target>=nums[start])
{
end=mid-1;
}
else
{
start=mid+1;
}
}
else//右侧有序
{
if(target>nums[mid]&&target<=nums[end])
{
start=mid+1;
}
else
{
end=mid-1;
}
}
}
return -1;
}
};
10.在排序树组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 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]
主要思想:二分法。首先需要利用二分法找到一个mid,其作为下标对应的值等于target。找到这样一个mid后,就分别把它分为两侧,左侧必然都小于等于target,右侧必然都大于等于target。对这两个区间再用二分法寻找。二分法初始的left_start与right_end的初始值要充分利用之前的二分查找,所以不是0和numms.size()-1,而是start和end,因为对于先前的二分法,start之前的必定都小于target,把left_start置为0没有太大的意义,同理,right_end也是。
另外,程序原本执行时间大概12ms,仅击败了50%左右的人,加了 nums[start]>target与nums[end]<target这两个不可能找到结果的情况判断,提前结束整个程序,程序执行时间缩短为4ms,击败了99.18%的人,所以以后刷题的时候务必要想一想有没有一些显而易见不可能有结果的情况,在程序开头就把它提前结束以大大缩短程序执行时间。
题解程序(也是我的程序):
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans{-1,-1};
int start=0;
int end=nums.size()-1;
if(nums.size()==0||nums[start]>target||nums[end]<target)
return ans;
while(start<=end)
{
int mid=(start+end)/2;
if(nums[mid]<target)
start=mid+1;
else if(nums[mid]>target)
end=mid-1;
else
{
ans={mid,mid};
int left_start=start;//这个地方写start而不是0 可以充分利用之前二分法的搜索情况
int left_end=mid;
int right_start=mid;
int right_end=end;//这个地方写end而不是nums.size()-1 可以充分利用之前二分法的搜索情况
while(left_start<=left_end)//寻找左侧等于target的索引 mid左侧所有值必定都小于等于target
{
int left_mid=(left_start+left_end)/2;
if(nums[left_mid]==target)
{
ans[0]=left_mid;
left_end=left_mid-1;
}
else
{
left_start=left_mid+1;
}
}
while(right_start<=right_end)//寻找右侧等于target的索引 mid右侧所有值必定都大于等于target
{
int right_mid=(right_start+right_end)/2;
if(nums[right_mid]==target)
{
ans[1]=right_mid;
right_start=right_mid+1;
}
else
{
right_end=right_mid-1;
}
}
break;//结束整个while循环
}
}
return ans;
}
};
11.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
主要思想:二分法。注意的是还是要把一些很容易的情况提前判断了,一定要养成这样的思维。在二分法的程序中,最终左右两侧向内收一定会有start与end相等的情况,我的两个实现版本:第一个实现版本在start==end后就不再循环了,第二个版本在start==end后还会再进入一次循环,这就造成了第一个版本while过后start一定是等于end的,而第二个版本必然出现start==mid或者end==mid的情况。因此我们需要依据这些情况做特定的判断。
题解程序(我的实现版本1):
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
if(nums.size()==0)
return 0;
int start=0;
int end=nums.size()-1;
int mid=(start+end)/2;
if(target>nums[end])
return nums.size();
if(target<nums[start])
return 0;
while(start<end)
{
if(nums[mid]==target)
{
return mid;
}
else if(nums[mid]>target)
{
end=mid-1;
}
else
{
start=mid+1;
}
mid=(start+end)/2;
}
if(nums[mid]>=target)//此时start=mid=end
return mid;
else
return mid+1;
}
};
题解程序(我的实现版本2):
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
if(nums.size()==0)
return 0;
int start=0;
int end=nums.size()-1;
if(target>nums[end])
return nums.size();
if(target<nums[start])
return 0;
while(start<=end)
{
int mid=(start+end)/2;
if(nums[mid]==target)
{
return mid;
}
else if(nums[mid]>target)
{
end=mid-1;
}
else
{
start=mid+1;
}
}
//到这里说明target不在nums中
//此时start要么与mid相等 说明target<nums[mid] 插在start处
//要么start比mid大1 说明target>nums[mid] 还是插在start处
return start;
}
};
12.组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
说明:所有数字(包括 target)都是正整数。解集不能包含重复的组合。
示例 1:输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
主要思想:回溯法+剪枝。首先这是一道需要剪枝的回溯法。因为后面的节点对前面的节点有依赖关系。本题首先需要排序,以避免重复解。个人感觉本题最难的还是回溯部分。走到回溯部分说明经过当前节点的这条路径是不通的,要把这个节点给pop_back。另外非常重要的一点在于防止重复解,注意是i>begin而不是i>0,i>begin代表着一次新的循环,并且如果candidates[i]==candidates[i-1]的话,那么之前的循环已经把当前的这个情况都考虑过了,只可能产生重复解。
有剪枝的回溯法思路:
1.找到满足题目要求的一个答案,返回。
2.满足递归终止的条件,结束递归 相当于剪枝操作。
3.否则,说明走经过当前节点的这条路是有可能找到答案的,递归。
4.回溯。走到了第4步,说明之前第3步的递归也最终没有找到答案,说明走这经过当前节点的这条路是不通的,因此要回溯到上一个可能行得通的节点。
题解程序:
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> path;
if(candidates.empty())
return ans;
sort(candidates.begin(),candidates.end());
DFS(candidates,0,candidates.size(),path,ans,target);
return ans;
}
void DFS(vector<int>& candidates,int begin,int size,vector<int>& path, vector<vector<int>>& ans,int target)
{
if(target==0)//找到了满足条件的 添加到ans中 并终止递归
{
ans.push_back(path);
return;
}
int temp;
for(int i=begin;i<size;++i)
{
if(i>begin&&candidates[i]==candidates[i-1])//防止重复解
continue;
temp=target-candidates[i];
if(temp<0)//递归终止 已经不可能满足条件了 相当于剪枝操作
break;
path.push_back(candidates[i]);//走到这边说明还是有可能满足条件的
DFS(candidates,i,size,path,ans,temp);//继续递归操作 因为数字允许重复选取 所以这个地方是i 不是i+1
path.pop_back();//走到这边说明之前压入path的candidates[i]其实是不符合要求的 删去
//pop_back的原因:仅考虑本次DFS内部
//case1:找到一个answer 直接返回
//case2:for循环中 不可能有满足条件的 break
//case3:还有可能找到满足条件的answer 开启下一次的DFS
//真的走到这一步,只能说明经过candidates[i]的路是不通的 所以要pop_back 相当于回溯到前一个节点 因为如果经过这个节点是行得通的 其就会调用DFS,开启下一次递归 不会走到这个地方来
}
}
};
13.组合总和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
主要思想:回溯法(剪枝)。本题与上一题几乎差不多,唯一的区别在于每个数字在每个组合中只能使用一次,所以只要DFS函数中原本的i改为i+1即可。
题解程序:
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> path;
if(candidates.empty())
return ans;
sort(candidates.begin(),candidates.end());
DFS(candidates,0,candidates.size(),path,ans,target);
return ans;
}
void DFS(vector<int>& candidates,int begin,int size,vector<int>& path, vector<vector<int>>& ans,int target)
{
if(target==0)//找到了满足条件的 添加到ans中 并终止递归
{
ans.push_back(path);
return;
}
int temp;
for(int i=begin;i<size;++i)
{
if(i>begin&&candidates[i]==candidates[i-1])//防止重复解
continue;
temp=target-candidates[i];
if(temp<0)//递归终止 已经不可能满足条件了 相当于剪枝操作
break;
path.push_back(candidates[i]);//走到这边说明还是有可能满足条件的
DFS(candidates,i+1,size,path,ans,temp);//继续递归操作 每个数字只能使用一次 所以为i+1
path.pop_back();//走到这边说明之前压入path的candidates[i]其实是不符合要求的 删去
//pop_back的原因:仅考虑本次DFS内部
//case1:找到一个answer 直接返回
//case2:for循环中 不可能有满足条件的 break
//case3:还有可能找到满足条件的answer 开启下一次的DFS
//真的走到这一步,只能说明经过candidates[i]的路是不通的 所以要pop_back 相当于回溯到前一个节点 因为如果经过这个节点是行得通的 其就会调用DFS,开启下一次递归 不会走到这个地方来
}
}
};
14.旋转图像
给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
说明:
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例 1:
给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
主要思想:转置加翻转或者单次循环旋转4个矩形。本题的难点在于在原矩阵上直接修改,不能使用额外的矩阵。
法一:先将矩阵转置,为了直接在矩阵上修改,考虑使用STL中的swap函数直接进行对应元素相交换,然后再翻转每一个vector。注意,swap的for循环中j=i+1,即对对角线一侧的所有元素操作,这样就把所有元素相当于交换了一遍。
法二:把给定的矩阵分成四个小的矩形,对这四个矩形的元素进行旋转即可。注意下图中,在3*3的矩阵中,可以选取左顶点1*2的或者2*1的小矩形,在4*4的矩阵中,可以选取左顶点2*2的小矩形,在5*5的矩阵中,选择左顶点2*3或者3*2的小矩形进行依次旋转。注意到每个小矩形中其实包括了所有颜色,也即包括了所有要旋转的元素。然后就是元素旋转了,[i,j]->[j,n-1-i];[j,n-1-i]->[n-1-i,n-1-j];[n-1-i,n-1-j]->[n-1-j,i];[n-1-j,i]->[i,j]。注意元素的小标,因为是旋转90度,所以行标、列标都是错开的,如4*4的矩阵1对应的行标为i,而4对应的行标为j,1对应的列标为j,而4对应的列标为n-1-i,可以发现i,j都是错开的,这是因为旋转90度的缘故。
法一题解程序:
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
if(matrix.size()==0||matrix.size()==1)
return;
int size=matrix[0].size();
for(int i=0;i<size;++i)
for(int j=i+1;j<size;++j)//对角线一侧所有元素
swap(matrix[i][j],matrix[j][i]);//转置
for(int i=0;i<size;++i)
reverse(matrix[i].begin(),matrix[i].end());//翻转
}
};
法二题解程序:
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n=matrix.size();
for(int i=0;i<n/2;++i)
for(int j=0;j<(n+1)/2;++j)//以5*5为例,选取的是左顶点2*3的小矩形
{
int temp=matrix[i][j];
matrix[i][j]=matrix[n-1-j][i];
matrix[n-1-j][i]=matrix[n-1-i][n-1-j];
matrix[n-1-i][n-1-j]=matrix[j][n-1-i];
matrix[j][n-1-i]=temp;
}
}
};
15.缺失的第一个正数(经典)
给定一个未排序的整数数组,找出其中没有出现的最小的正整数。
示例 1:
输入: [1,2,0]
输出: 3
示例 2:输入: [3,4,-1,1]
输出: 2
示例 3:输入: [7,8,9,11,12]
输出: 1
说明:你的算法的时间复杂度应为O(n),并且只能使用常数级别的空间。
主要思想:桶排序+抽屉原理。本题的难点在于时间复杂度为O(n),因此直接进行普通的排序必然是不行的。这道题具体的思路参考网站https://leetcode-cn.com/problems/first-missing-positive/solution/tong-pai-xu-python-dai-ma-by-liweiwei1419/,该网站提供了步骤详解。个人认为这是一道蛮好的题目,其用到了桶排序与抽屉原理。桶排序就是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有 n + 1 个元素放到 n 个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理。这两种方法都有一种一个萝卜一个坑的思想。具体到本题,我们认为索引为i的位置上应该存放的数字是i+1。我们需要关注那些落在索引范围+1的那些数字。例如:[3, 4, -1, 1],一共 4 个数字,那么如果这个数组中出现 “1”、“2”、“3”、“4”,就是我们重点要关注的数字了;又例如:[7, 8, 9, 11, 12] 一共 5 个数字,每一个都不是 “1”、“2”、“3”、“4”、“5” 中的一个,因此我们无须关注它们。
题解程序:
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int size=nums.size();
if(size==0)
return 1;
for(int i=0;i<size;++i)
{//数字必须大于0且小于等于数组长度且数字值与索引不对应且与要交换的数字不一样 那么就要进行交换
while(nums[i]>0&&nums[i]<=size&&nums[i]!=i+1&&nums[nums[i]-1]!=nums[i])//最后的这个条件一定要加 否则可能陷入死循环 例如[1,1]
{
swap(nums[i],nums[nums[i]-1]);
}
}
for(int i=0;i<size;++i)//寻找第一个与索引不对应的
{
if(nums[i]!=i+1)
return i+1;
}
return size+1;//执行到这边 说明前面一一对应 那显然缺失的第一个正数就是数组长度加1了
}
};
16.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1] 输出: 6
主要思想:动态规划法或栈或双指针。
动态规划法:对于每一列,我们利用动态规划的思想分别求取其左右两侧最高的墙,分别将其存放在两个数组当中。然后求取出每一列所积水的量。因为每一列所积水的量是由其左侧最大值与右侧最大值中较小的那一个以及当前这一列的高度决定的。
栈的思想:我们用栈保存每堵墙。当遍历墙的高度的时候,如果当前高度小于栈顶的墙高度,说明这里会有积水,我们将墙的高度的下标入栈。如果当前高度大于栈顶的墙的高度,说明之前的积水到这里停下,我们可以计算下有多少积水了。计算完,就把当前的墙继续入栈,作为新的积水的墙。总体的原则就是,当前高度小于等于栈顶高度,入栈,指针后移。当前高度大于栈顶高度,出栈,计算出当前墙和栈顶的墙之间水的多少,然后计算当前的高度和新栈的高度的关系,重复第 2 步。直到当前墙的高度不大于栈顶高度或者栈空,然后把当前墙入栈,指针后移。注意while循环里面还有一个while循环,因为只要当前高度大于栈顶高度且栈不为空,就说明还有积水,还要进行计算(栈中的元素从栈底到栈顶是非严格单调递减的)。例如本例中3个积水段中间的那一段。
双指针:与动态规划法中维护两个数组不同,这里只需维护两个值,分别为left_max与right_max,从两侧往内收,在遍历过程中维护这两个值。结合双指针的程序看,如果左指针指向的墙的高度小于右指针指向的墙的高度,我们可以分两种情况,第一种就是左指针指向的墙的高度本身就是左侧最大的,那说明这一列积不了水,更新左侧最大值,另一种情况就是左指针指向的墙的高度小于左侧最大值,那么此时这堵墙被夹在左侧最大值与右侧最大值之间了,自然这一列是有积水的,且积水高度由left_max与这堵墙的高度决定,因为left_max必小于等于height[right],只要满足了那个if条件,left_max必小于等于height[right],否则其会进入到else。例如[3,2,5,4],一开始height[0]<height[4],进入if里面,left_max=3,height[right]=4,左指针一直右移,右指针不动,因此left_max小于等于height[right],直到left=3,即height[left]=5>height[right]=4,此时由于进入了else里面,right_max必小于等于height[left],右指针左移,结束。我们可以想象一下如果没有进入else的话,那么left_max=5,而right不动,就不满足left_max小于等于height[right]。
动态规划法题解程序:
class Solution {
public:
int trap(vector<int>& height) {
int size=height.size();
if(size<=1)
return 0;
int ans=0;
vector<int> left_max(size),right_max(size);
left_max[0]=height[0];
for(int i=1;i<size;++i)
left_max[i]=max(left_max[i-1],height[i]);// left_max[i]存放包括i在内的i左侧最大值
right_max[size-1]=height[size-1];
for(int i=size-2;i>0;--i)
right_max[i]=max(height[i],right_max[i+1]);//right_max[i]存放包括i在内的i右侧最大值
for(int i=1;i<size-1;++i)
ans+=min(left_max[i],right_max[i])-height[i];//每一个i对应的能存放的水的容量
return ans;
}
};
栈思想题解程序:
class Solution {
public:
int trap(vector<int>& height) {
int size=height.size();
if(size<=1)
return 0;
stack<int> a;
int current=0;
int ans=0;
int distance=0;
while(current<size)
{
while(!a.empty()&&height[current]>height[a.top()])//积水在这里停下 可以计算这一区域积了多少水
{//while循环的原因是只要当前高度大于栈顶元素 就说明还有积水
int top=a.top();
a.pop();
if(a.empty())
break;
distance=current-a.top()-1;
ans+=distance*(min(height[current],height[a.top()])-height[top]);
}
a.push(current++);
}
return ans;
}
};
双指针题解程序:
class Solution {
public:
int trap(vector<int>& height) {
int size=height.size();
if(size<=1)
return 0;
int left_max=0,right_max=0;
int left=0,right=size-1;
int ans=0;
while(left<right)
{
if(height[left]<height[right])//height[left]必然小于右侧最大值
{//如果height[left]<left_max,那么相当于height[left]被夹在了左侧最大值与右侧最大值中间 自然可以求积水量了
//且这个时候left_max必然小于等于height[right] 因此水量由left_max与height[left]决定
height[left]>=left_max?(left_max=height[left]):(ans+=left_max-height[left]);
++left;
}
else//height[right]必然小于左侧最大值
{//如果height[right]<right_max,那么相当于height[right]被夹在了左侧最大值与右侧最大值中间 自然可以求积水量了
//且这个时候right_max必然小于等于height[left] 因此水量有right_max与height[right]决定
height[right]>=right_max?(right_max=height[right]):(ans+=right_max-height[right]);
--right;
}
}
return ans;
}
};
17.跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
示例 2:输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。
主要思想:贪心算法。本题使用贪心算法即可解决问题,不太推荐使用动态规划,因为本题贪心算法思路简单且效率高。本题可以从前往后进行贪心算法,即从第一个格子开始往后跳,看看最远能跳到哪,如果最远的跳的格子已经大于最后的位置了,肯定可大了,否则不可达。也可以从后往前进行贪心算法,我们首先判断由倒数第二个位置能否到达倒数第一个位置,如果可以,我们就不考虑倒数第一个位置,因为如果能到达倒数第二个,必然能到倒数第一个,如果他连倒数第二个都到达不了,那必然到不了倒数第一个,所以这个时候只需关注能否到达倒数第二个,然后依此往前推。
从前往后的贪心算法题解程序:
class Solution {
public:
bool canJump(vector<int>& nums) {
int k=0;//最远能跳到的距离
for(int i=0;i<nums.size();++i)
{
if(i>k) return false;//最远连i都到不了 更别谈数组末尾了
k=max(k,i+nums[i]);//更新最远能跳到的距离
if(k>=nums.size()-1)//最远距离能到数组最后一位 即可终止循环
return true;
}
return true;
}
};
从后往前的贪心算法题解程序:
class Solution {
public:
bool canJump(vector<int>& nums) {
int size=nums.size();
if(size==0)
return false;
int LastPosition=size-1;
for(int i=size-2;i>=0;--i)
{
if(i+nums[i]>=LastPosition)//从后往前推
LastPosition=i;
}
return LastPosition==0;
}
};
18.跳跃游戏II
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
说明:假设你总是可以到达数组的最后一个位置。
主要思想:贪心算法。本题是上题的升级版,这一题还是需要用贪心算法看看最远能跳到哪,与上一题不同的地方在于,这道题的一个关键点是要设定一个边界,在遇到边界的时候更新边界并且步数加一。什么是边界呢,如下图,在起始位置处,其最远能到达索引2处的1,所以对索引0来说,橙色的1是一个边界。在索引1处,其最远能到达索引4处的4,所以对索引1来说,橙色的4是一个边界。在索引2处,其最远还是到达索引4处的4,所以对索引2来说,其边界还是橙色的4.另外,注意i<nums.size()-1而不是i<nums.size(),因为最后一个元素不需要考虑。
题解程序:
class Solution {
public:
int jump(vector<int>& nums) {
int MaxPos=0;
int end=0;//边界
int steps=0;
for(int i=0;i<nums.size()-1;++i)
{
MaxPos=max(MaxPos,i+nums[i]);//当前能跳到最远的距离
if(i==end)//访问到了边界,就要更新边界,并将步数加一
{
end=MaxPos;
steps++;
}
}
return steps;
}
};