文章目录
一、旋转数组问题
旋转数组,都用二分法的思路解决。
1、轮转数组
思路:这是408里面经典的一道题目,主要考虑用3次翻转操作,实现空间复杂度O(1)。
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int len = nums.size();
k = k%len; // 这里取模,因为有可能k会大于数组长度
int res = len - k;
reverse(nums.begin(), nums.begin()+res); // 翻转前面部分
reverse(nums.begin()+res, nums.end()); // 翻转后面k个数
reverse(nums.begin(), nums.end()); // 整体翻转
}
};
2、寻找旋转数组的最小值
思路:因为是有序数组进行旋转,所以旋转数组会变成两边有序,而中间断开,可以用二分查找来进行。
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size()-1;
while(left<right) //这里控制条件没取等号,取等号大多是为了在while中直接return mid,不取等号就跳出while返回l的值。
{
int mid = left + (right - left) / 2;
if(nums[mid]>nums[right]) // 说明左侧是递增的,最小值在右边
{
left = mid + 1;
}else{ // 说明右侧是递增的,最小值在左边
right = mid;
}
}
return nums[left];
}
};
3、寻找旋转数组中最小值(2)
思路:这一题和上一题的做法是大致相同的,只不过这里有重复值出现,因此要考虑到左、中、右边界会相等的情况,而不仅仅是比大小。
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0;
int right = nums.size()-1;
int ans = nums[0];
while(left<=right)
{
int mid = left + (right - left) / 2;
ans = min(ans, nums[mid]);
if(nums[mid]>nums[right]) // 左侧递增
{
left = mid+1;
}else if(nums[mid]<nums[right]) // 右侧递增,最小值在左边
{
right = mid - 1;
}else{ // nums[mid]==nums[right],此时最小值在中间,不能轻易判断,只能让右边界递减
right--;
}
}
return ans;
}
};
4、搜索旋转排序数组
思路:同样用二分查找,这里只进行一次旋转,所以会存在两段有序,因此每次二分查找要判断左侧有序还是右侧有序。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
while(left<=right)
{
int mid = left + (right - left) / 2;
if(nums[mid]==target)
{
return mid;
}else if(nums[mid]<nums[right]) // 右侧递增。
{
if(target>nums[mid]&&target<=nums[right])
{
left = mid+1;
}else{
right = mid-1;
}
}else{ // 左侧递增
if(nums[left]<=target&&target<nums[mid])
{
right = mid-1;
}else{
left = mid+1;
}
}
}
return -1;
}
};
5、搜索旋转排序数组2
思路:就是在上一题的基础上,出现了重复的可能性,因此这里讨论的情况更多。
class Solution {
public:
bool search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
while(left<=right)
{
int mid = left + (right - left) / 2;
if(nums[mid]==target)
{
return true;
}else if(nums[mid]<nums[right])
{
if(nums[mid]<target&&target<=nums[right])
{
left = mid+1;
}else{
right = mid-1;
}
}else if(nums[mid]==nums[right]) // 有可能想等,这时候让右边界递减
{
right--;
}else if(nums[left]<nums[mid])
{
if(nums[mid]>target&&target>=nums[left])
{
right = mid-1;
}else{
left = mid+1;
}
}else if(nums[left]==nums[mid]) // 可能和左边界想等,不能判断递增,因此左边界➕1
{
left++;
}
}
return false;
}
};
6、搜索旋转数组
思路:这里也是旋转多次,有重复,同时还要求返回索引值最小的一个,因此这里从左侧开始寻找。
//旋转多次和旋转一次没有区别,最终只有一个旋转点
class Solution {
public:
int search(vector<int>& arr, int target) {
int left=0;
int right=arr.size()-1;
while(left<=right)
{
int mid = left+(right-left)/2;
if(arr[left]==target) // 先和左边界比,因为找最小索引
{
return left;
}
if(arr[mid]==target) // 肯定往左边找,因为最小索引
{
right=mid;
}else if(arr[left]<arr[mid]) // 左侧递增
{
if(target>=arr[left] && target<arr[mid])
{
right=mid-1;
}else{
left=mid+1;
}
}else if(arr[left]>arr[mid])
{
if(target>arr[mid] && target<=arr[right])
{
left=mid+1;
}else{
right=mid-1;
}
}else if (arr[left]==arr[mid])
{
left++;
}
}
return -1;
}
};
二、排列、组合问题
统一是回溯的思想,只是处理好条件。
1、组合
思路:组合中(1,2)和(2,1)会视为同一个,为了避免重复,在回溯的过程中以从小到大的顺序进行遍历,并且进入下一轮时是cur+1。
class Solution {
public:
vector<vector<int> > ans;
vector<int> tmp;
vector<int> nums;
void dfs(int cur, vector<int>& nums, int k)
{
if(tmp.size() + (nums.size() - cur)<k) // 剪枝
{
return;
}
if(tmp.size()==k)
{
ans.push_back(tmp);
return;
}
for(int i=cur;i<nums.size();i++) // 索引不断递增
{
tmp.push_back(nums[i]);
dfs(i+1, nums, k); // 进入下一个索引
tmp.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
for(int i=1;i<=n;i++)
{
nums.push_back(i);
}
dfs(0, nums, k);
return ans;
}
};
但如果,在进入下一个递归中,写成了dfs(cur, nums, k),那就会出现(i,i)的情况,因为下个递归还是从 i 开始遍历。
输入:3 3
输出:[[1,1,1],[1,1,2],[1,1,3],[1,2,2],[1,2,3],[1,3,3],[2,2,2],[2,2,3],[2,3,3]]
不会出现[2,1,1]这样的组合,因为索引顺序在递增。
2、全排列
思路:这里和组合不一样,全排列允许出现(1,0)和(0,1)这样的情况。因此遍历索引需要从头遍历,每个数字都可能再次出现。那么为了避免重复选取数字,需要另外一个vis数组来标记。
class Solution {
public:
vector<vector<int> > ans;
vector<int> tmp;
void dfs(vector<int>& nums, vector<int>& vis)
{
if(tmp.size()==nums.size())
{
ans.push_back(tmp);
return;
}
for(int i=0;i<nums.size();i++)
{
if(vis[i]==0)
{
vis[i]=1;
tmp.push_back(nums[i]);
dfs(nums, vis);
tmp.pop_back();
vis[i]=0;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int> vis(nums.size(), 0);
dfs(nums, vis);
return ans;
}
};
3、全排列2
思路:这里是有重复数字,和上一题没有重复数字的全排列有些不同。因为如果直接使用上一题的代码,对于[1,1,2]:
输出:[[1,1,2],[1,2,1],[1,1,2],[1,2,1],[2,1,1],[2,1,1]]。为什么会有两个[1,1,2]呢?其实是第一个[1,1,2]中,第1个1是来自nums[0],第2个1来自nums[1];第二个[1,1,2]是相反的情况。表现在vector中就是两个相同的[1,1,2]。
1、粗暴的方法可以直接上set,来暴力去重。
2、让重复数字只用一次
class Solution {
public:
vector<vector<int> > ans;
vector<int> tmp;
void dfs(vector<int>& nums, vector<int>& vis)
{
if(tmp.size()==nums.size())
{
ans.push_back(tmp);
return;
}
for(int i=0;i<nums.size();i++)
{
if(i>0 && nums[i]==nums[i-1] && vis[i-1]==1) // 去重,相邻两个数字相同,并且前一个数字已经用过了,那么当前 i 不再使用
{
continue;
}
if(vis[i]==0)
{
vis[i]=1;
tmp.push_back(nums[i]);
dfs(nums, vis);
tmp.pop_back();
vis[i]=0;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 先排序,让重复的数字相邻
vector<int> vis(nums.size(), 0);
dfs(nums, vis);
return ans;
}
};
4、允许重复选择元素的组合
思路:乍一看是组合题,但是这里组合中元素个数是不限制的,限制条件改成了和为target。
但组合的性质(1,2)和(2,1)相同,因此for中遍历要以当前索引递增(和之前的组合一样);另外,数组中的选择能够重复选择,因此递归中索引不能递增dfs(i,…)。
class Solution {
public:
vector<vector<int> > ans;
vector<int> tmp;
void dfs(vector<int>& candidates, int target, int sum, int idx)
{
if(sum>target)
{
return;
}
if(sum==target) // 返回条件不再是 数量=k,而是和=target
{
ans.push_back(tmp);
return;
}
for(int i=idx;i<candidates.size();i++) // 和组合问题一样,要从当前索引遍历,不能走回头路
{
tmp.push_back(candidates[i]);
dfs(candidates, target, sum+candidates[i], i); // 这里索引不能增加,因为可以重复选择数字
tmp.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
dfs(candidates, target, 0, 0);
if(ans.size()==0)
{
return {};
}
return ans;
}
};
5、含有重复元素集合的组合
思路:这里数组中会有重复元素,而每个数字只能用一次,和上面的重复选取不同,因此这里在递归的索引中应该是递增索引dfs(i+1,…)。
class Solution {
public:
vector<vector<int> > ans;
vector<int> tmp;
void dfs(vector<int>& candidates, int target, int sum, int idx)
{
if(sum>target)
{
return ;
}
if(sum==target)
{
ans.push_back(tmp);
return;
}
for(int i=idx;i<candidates.size();i++)
{
if(i>idx && candidates[i]==candidates[i-1]) // 上一层已经使用过candidatescandidates[i-1],因此要跳过相同值
{
continue;
}
tmp.push_back(candidates[i]);
dfs(candidates, target, sum+candidates[i], i+1); // 索引递增
tmp.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
dfs(candidates, target, 0, 0);
return ans;
}
};
6、组合总和
class Solution {
public:
vector<vector<int> > ans;
vector<int> tmp;
void backtrace(vector<int>& candidates, int cur, int sum, int target)
{
if(sum>target)
{
return;
}
if(sum==target)
{
ans.push_back(tmp);
}
for(int i=cur; i<candidates.size(); i++)
{
tmp.push_back(candidates[i]);
backtrace(candidates, i, sum+candidates[i], target);
tmp.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtrace(candidates, 0, 0, target);
return ans;
}
};
7、组合总和2
class Solution {
public:
vector<vector<int> > ans;
void backtrace(vector<int>& candidates, int cur, vector<int> tmp, int sum, int target)
{
if(sum>target)
{
return;
}
if(sum==target)
{
ans.push_back(tmp);
return;
}
for(int i=cur;i<candidates.size();i++)
{
if(i>cur && candidates[i]==candidates[i-1])
{
continue;
}
sum+=candidates[i];
tmp.push_back(candidates[i]);
backtrace(candidates, i+1, tmp, sum, target);
tmp.pop_back();
sum-=candidates[i];
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrace(candidates, 0, {
}, 0, target);
return ans;
}
};
8、组合总和3
class Solution {
public:
vector<vector<int> > ans;
vector<int> tmp;
void backtrace(int cur, int sum, int k, int n)
{
if(sum>n || tmp.size()>k)
{
return;
}
if(tmp.size()==k && sum==n)
{
ans.push_back(tmp);
}
for(int i=cur;i<=9;i++)
{
tmp.push_back(i);
backtrace(i+1, sum+i, k, n);
tmp.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtrace(1, 0, k, n);
return ans;
}
};
四、二分查找
二分查找的前置条件:升序,时间复杂度O(logn)
1、在排序数组中查找元素的第一个和最后一个位置
思路:题目条件升序 + 时间复杂度O(logn),明显是要用二分来做。但是数组中会出现重复元素,因此和普通二分在判断条件上有所不同。
要求查找元素的第一个位置:即大于等于target的第一个位置;
最后一个位置:即大于target的第一个位置,再-1。
需要两次二分来分别获得两个位置索引。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans(2, -1);
if(nums.size()==0 || (nums.size()==1 && nums[0]!=target))
{
return ans;
}
int left_idx=-1, right_idx=nums.size(); // 右边界初始化为数组最后,因为如果数组中存在target,那么左边界必然!=-1;而又可能target是数组的最大值,因此不一定存在大于target的位置,此时右边界就是数组的最后
// 找第一个大于等于target的位置
int left=0;
int right=nums.size()-1;
while(left<=right)
{
int mid = left + (right - left) / 2;
if(nums[mid]>=target) // 此时mid也满足大于等于target的条件,暂记左边界
{
right=mid-1;
left_idx=mid;
}else{
left=mid+1;
}
}
// 找第一个大于target的位置
left=0;
right=nums.size()-1;
while(left<=right)
{
int mid = left + (right - left) / 2;
if(nums[mid]>target) // 此时mid已经满足条件,再将右边界左移,试着还有没有满足条件的mid
{
right=mid-1;
right_idx=mid; // 先暂记mid是满足条件的右边界
}else{
left=mid+1;
}
}
if(left_idx!=-1 && nums[left_idx]==target && nums[right_idx-1]==target)
{
ans[0]=left_idx;
ans[1]=right_idx-1;
}
return ans;
}
};
2、x的平方根 (md文档中有详细解法)
思路:x的范围是0~ 2 31 − 1 2^{31}-1 231−1,又要求算术平方根,可以利用二分来加速查找。值得注意的是,查找过程中需要判断mid*mid,可能会超出int范围。题目的要求中,如果开根号得到小数,需要强制向下取整,翻译过来其实就是找一个整数K,满足 K ∗ K < = x K*K<=x K∗K<=x且K能取到最大。
class Solution {
public:
int mySqrt(int x) {
int left=0, right=x;
int ans=-1;
while(left<=right)
{
int mid = left + (right - left) / 2;
if((long long)mid*mid<=x) // 和上一题一样,此时mid是满足条件的,暂记下来
{
ans=mid;
left=mid+1;
}else{
right=mid-1;
}
}
return ans;
}
};
3、完成旅途的最少时间
思路:这里随着趟数的增加,所需的最少时间也必然增加,符合单调性,可用二分法来查找。
class Solution {
public:
long long minimumTime(vector<int>& time, int totalTrips) {
sort(time.begin(), time.end());
int min_time = time[0];
long long left = min_time; // 最少时间的下界:只跑一趟,那就用最快的车跑
long long right = (long long)min_time * totalTrips; // 最少时间的上界:只用最快的车,跑完所有趟需要的时间
while(left<=right)
{
long long mid = left + (right - left) / 2;
long long total=0; // 记录在当前时间下,最多能跑几趟
for(auto t: time)
{
total+= mid / t;
}
if(total>=totalTrips)
{
right = mid - 1;
}else{
left = mid + 1;
}
}
return left;
}
};
五、双指针
常见于原地操作数组
1、移动0
思路:由于要保持非0元素的相对顺序,因此不能直接把0往后扔,要找到0和第一个非0交换位置。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int i=0;
while(i<nums.size()) // 找到第一个0的位置
{
if(nums[i]==0)
{
break;
}else{
i++;
}
}
for(int j=i+1;j<nums.size();j++) // 从0后开始找第一个不为0的数,然后和i交换位置,i始终指向0的位置
{
if(nums[j]!=0)
{
swap(nums[i++], nums[j]);
}
}
}
};
2、有序数组的平方
思路:用双指针实现比较插入法。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> ans(nums.size(), 0);
int k=nums.size()-1;
for(int i=0,j=nums.size()-1;i<=j;)
{
if(nums[i]*nums[i]>nums[j]*nums[j])
{
ans[k--]=nums[i]*nums[i];
i++;
}else
{
ans[k--]=nums[j]*nums[j];
j--;
}
}
return ans;
}
};
六、排序
熟练掌握各种排序方法。
1、基本排序算法总结
(1)选择排序 平均、最好、最坏复杂度O(n2) 不稳定
void selectSort(vector<int>& nums)
{
for(int i=0;i<nums.size()-1;i++)
{
int min = i;
for(int j=i+1;j<nums.size();j++)
{
if(nums[min]>nums[j])
{
min=j;
}
}
swap(nums[min], nums[i]);
}
}
(2)插入排序 平均O(n2),最坏O(n2),最好O(n),稳定
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
void insertsort(vector<int>& nums)
{
for(int i=1;i<nums.size();i++)
{
for(int j=i-1;j>=0 && nums[j]>nums[j+1];j--)
{
swap(nums[j], nums[j+1]);
}
}
}
(3)冒泡排序 平均O(n2),最坏O(n2),最好O(n),稳定
越小的元素会经由交换慢慢“浮”到数列的顶端。
void sort(vector<int>& nums)
{
/*
如果用一个flag来判断一下,当前数组是否已经有序,
有序就退出循环,可以提高冒泡排序的性能。
*/
int n = nums.size();
for (int i = 1; i<n; i++) {
bool flag = true;
for (int j = 0; j < n - i; j++) {
if (nums[j]>nums[j+1]) {
swap(nums[j], nums[j+1]);
flag=false;
}
}
if (flag) {
break;
}
}
}
(4)归并排序 平均、最好、最坏复杂度O(nlogn),稳定
void merge(vector<int>& nums, int left, int mid, int right)
{
vector<int> cnt(right-left+1, 0);
int i = 0;
int pFirst = left;
int pSecond = mid+1;
while (pFirst <= mid && pSecond <= right) {
cnt[i++] = nums[pFirst] < nums[pSecond] ? nums[pFirst++]:nums[pSecond++];
}
while (pFirst <= mid) {
cnt[i++] = nums[pFirst++];
}
while (pSecond <= right) {
cnt[i++] = nums[pSecond++];
}
for (int j = 0; j < (right-left+1); j++) {
nums[left+j] = cnt[j];
}
}
void mergeSort(vector<int>& nums, int left, int right)
{
if(left==right)
{
return;
}
int mid = (left+right)/2;
mergeSort(nums, left, mid);
mergeSort(nums, mid+1, right);
merge(nums,left,mid,right);
}
(5)快排(随机)平均、最好复杂度O(nlogn),最坏O(n2),空间O(logn),不稳定
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
int partition(vector<int>& nums, int left, int right) {
int pivot = nums[right];
int i = left;
for (int j = left ; j <= right - 1; ++j) //注意这里的<=边界条件!
{
if (nums[j] < pivot) {
swap(nums[i], nums[j]);
++i;
}
}
swap(nums[i], nums[right]);
return i;
}
void quicksort(vector<int>& nums, int left, int right)
{
if(left<right)
{
int picked = rand() % (right - left + 1) + left; // 取一个随机数,将数组打乱
swap(nums[picked], nums[left]);
int p = partition(nums, left, right);
quicksort(nums, left, p - 1);
quicksort(nums, p + 1, right);
}
}
非递归版本:
用栈来存放每次的left和right
class Solution {
public:
int partion(vector<int>& nums, int left, int right)
{
int pivot = nums[right];
int i=left;
for(int j=left;j<=right-1;j++)
{
if(nums[j]<pivot)
{
swap(nums[i], nums[j]);
i++;
}
}
swap(nums[i], nums[right]);
return i;
}
vector<int> sortArray(vector<int>& nums) {
stack<int> st;
int k;
int low = 0;
int high = nums.size()-1;
if(low<high)
{
st.push(low);
st.push(high);
while(!st.empty())
{
int j=st.top();
st.pop();
int i=st.top();
st.pop();
int rand_p = rand()%(j-i+1)+i;
swap(nums[i], nums[rand_p]);
k=partion(nums,i,j);
if(i<k-1)
{
st.push(i);
st.push(k-1);
}
if(k+1<j)
{
st.push(k+1);
st.push(j);
}
}
}
return nums;
}
};
(6)堆排序 平均、最好、最坏复杂度O(nlogn),不稳定
//向上走
void heapInsert(vector<int>& nums,int index){
while (nums[index] > nums[(index-1)/2]) {
swap(nums[index], nums[(index-1)/2]);
index = (index -1)/2;
}
}
//向下走
//size为最右的边界,size是取不到的.
void heapify(vector<int>& nums,int index ,int size){
int leftChild = index*2 + 1;
while (leftChild < size) {
int maxChild = leftChild + 1 < size && nums[leftChild+1] >nums[leftChild] ? leftChild+1 : leftChild;
int maxAll = nums[maxChild] > nums[index] ? maxChild: index;
if (maxAll == index) {
break;
}
swap(nums[maxAll], nums[index]);
index = maxAll;
leftChild = index*2 +1;
}
}
int main(){
vector<int> nums;
nums.push_back(2);
nums.push_back(11);
nums.push_back(9);
nums.push_back(6);
nums.push_back(5);
for(int i = 0;i < nums.size();i++)
{
heapInsert(nums, i); // 大顶堆
}
int size = nums.size();
swap(nums[0], nums[--size]); // 此时在0位置的是最大值,把它放到最后去,就固定在最后的位置
while (size > 0){
//heapify时间复杂度为O(logN)
heapify(nums, 0, size); // 调整堆,从头开始向下调整
swap(nums[0], nums[--size]); // 每次都把当前的最大值放到后面去
}
return 0;
}
(7)计数排序 平均、最好、最坏复杂度O(n+k)
int max = arr[0];
int lastIndex= 0;
for (int i = 1; i<length; i++) {
max = arr[i]>max ? arr[i]:max;
}
int* sortArr = new int[max+1](); // 设置哈希表
for (int j = 0; j< length; j++) {
sortArr[arr[j]]++; // 记录每个元素出现的个数
}
for (int k = 0; k<max+1; k++) {
while (sortArr[k]>0) { // 根据索引大小,然后把这个索引出现了几次都放到数组中
arr[lastIndex++] = k;
sortArr[k]--;
}
}
2、前K个高频元素
思路1:堆排序。这里用小顶堆,将出现次数少的元素放在堆顶,这样便于pop。
class Solution {
public:
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second; // 出现次数多的元素放在后面,少的放在堆顶
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> occurrences;
for (auto& v : nums) {
occurrences[v]++;
}
// pair 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
for (auto& [num, count] : occurrences) {
if (q.size() == k) { // 当已经有k个元素,那就要把出现次数少的给移除
if (q.top().second < count) {
q.pop();
q.emplace(num, count);
}
} else { // 当堆中数量少于k,说明还能加入
q.emplace(num, count);
}
}
vector<int> ret;
while (!q.empty()) {
ret.emplace_back(q.top().first);
q.pop();
}
return ret;
}
};
思路2:快排
class Solution {
public:
void qsort(vector<pair<int, int>>& v, int start, int end, vector<int>& ret, int k) {
int picked = rand() % (end - start + 1) + start; // 取一个随机数,将数组打乱
swap(v[picked], v[start]);
int pivot = v[start].second;
int index = start;
for (int i = start + 1; i <= end; i++) {
if (v[i].second >= pivot) { // 高频的都往前扔
swap(v[index + 1], v[i]);
index++;
}
}
swap(v[start], v[index]);
if (k <= index - start) { //此时只要在【start, index-1】的范围内找k个数即可
qsort(v, start, index - 1, ret, k);
} else {
for (int i = start; i <= index; i++) { // 说明左侧全是前k个高频中的元素
ret.push_back(v[i].first);
}
if (k > index - start + 1) { // 还有剩余的就在右边,然后递归右侧找剩余的元素
qsort(v, index + 1, end, ret, k - (index - start + 1));
}
}
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> occurrences;
for (auto& v: nums) {
occurrences[v]++;
}
vector<pair<int, int>> values;
for (auto& kv: occurrences) {
values.push_back(kv);
}
vector<int> ret;
qsort(values, 0, values.size() - 1, ret, k);
return ret;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/top-k-frequent-elements/solution/qian-k-ge-gao-pin-yuan-su-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3、根据字符出现频率排序
思路:和上一题一样,用哈希记录出现频率,再构造堆。
class Solution {
public:
static bool cmp(pair<char, int>& a, pair<char, int>& b)
{
return a.second < b.second;
}
string frequencySort(string s) {
unordered_map<char, int> mp;
for(int i=0;i<s.size();i++)
{
mp[s[i]]++;
}
// 构建大顶堆,出现频率高的字母在堆顶
priority_queue<pair<char, int>, vector<pair<char, int> >, decltype(&cmp) > q(cmp);
for(auto iter=mp.begin();iter!=mp.end();iter++)
{
q.push(make_pair(iter->first, iter->second));
}
string ans="";
while(!q.empty())
{
int num = q.top().second;
for(int i=0;i<num;i++)
{
ans+=q.top().first;
}
q.pop();
}
return ans;
}
};
4、颜色分类
思路:一开始的思路是两次排序,第一次将0全扔到最前面,然后记录非0的第一个位置;第二次将1扔到最前面,就不用管2了。后来看了评论,发现可以将这两次合成一次遍历(利用双指针)。每次遍历到0,就将0扔到前面,遍历到2将2扔到后面,而需要注意的是,扔完2后,如果当前值不是1(是0或2),那需要将遍历指针回退一下,不然就会漏掉一个0或2。
class Solution {
public:
void sortColors(vector<int>& nums) {
int ptr_0=0, ptr_2=nums.size()-1;
for(int k=ptr_0;k<=ptr_2;k++) // 注意遍历到2指针的位置即可,不用遍历到数组末尾
{
if(nums[k]==0)
{
swap(nums[k], nums[ptr_0++]);
}else if(nums[k]==2)
{
swap(nums[k], nums[ptr_2--]);
if(nums[k]!=1) // 当前指针不是1,要回退,下次进入循环再判断是0还是2
{
k--;
}
}
}
}
};
七、贪心
要保证局部操作是最优的,那么最后的结果是全局最优的。
1、无重叠区间
思路:题目问移除区间的最小数量,反向思维,求不重叠区间的最多数量,那么答案=总数-不重叠区间数量。在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。此外贪心体现在排序,让局部情况尽早出现。
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size()==0)
{
return 0;
}
sort(intervals.begin(), intervals.end(),
[](const vector<int>& a, const vector<int>& b)
{
return a[1] < b[1]; // 让右端点小的排在前面
});
int ans=1; // 记录不重叠区间个数,第一个区间自然没人重叠
int right = intervals[0][1]; // 第一个区间的右端点
for(int i=1;i<intervals.size();i++)
{
if(intervals[i][0]>=right) // 下一个区间的左端点只要不小于右端点,那么这两个区间不会重叠
{
ans++;
right = intervals[i][1];
}
}
return intervals.size() - ans; // 总数-不重叠区间 = 重叠区间数量
}
};
2、用最少数量的箭引爆气球
思路:这和上一题是一样的思路,但在条件处理上相反。这里要求最少的箭射爆所有的气球,意味着求最少的不重叠区间数量(因为区间若重叠,那么只需要一支箭就能射爆),上一题要求最多的不重叠区间数量。因此在排序时,要让左端点尽可能大,这样留给左边的空间更小。
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
if(points.size()==1)
{
return 1;
}
sort(points.begin(), points.end(),
[](const vector<int>& a, const vector<int>& b)
{
return a[0] > b[0]; // 左边大
});
int left=points[0][0];
int ans=1; // 最少的不重叠区间数量
for(int i=1;i<points.size();i++)
{
if(points[i][1]<left) // 让前面区间的右端点小于当前区间的左端点,不能取等号,因为取等号的话箭可以同时射爆这两个区间
{
ans++;
left = points[i][0];
}
}
int n = points.size();
return ans;
}
};
3、买卖股票的最佳时机2
思路:这里能够允许多次买卖,因此要尽可能抓住每次能获得利润的机会(局部最优),才能使最终最大利润(全局最优),满足贪心。根据 121. 买卖股票的最佳时机,可以通过一个最低价格变量,来保存 i 位置之前的最低价格,这样如果之后有高于该价格,那么就可卖出,获得当前的最大利润。又鉴于题目允许同一天买卖,因此当天如果卖出了股票可以立即买,这样仍能保持现在情况的最低价格变量,等待后续更大的价格来卖出。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()==1)
{
return 0;
}
int min_value = prices[0];
int ans=0;
for(int i=1;i<prices.size();i++)
{
if(min_value>prices[i])
{
min_value = prices[i];
}else if(prices[i] > min_value){
ans += prices[i] - min_value;
min_value = prices[i];
}
}
return ans;
}
};
4、根据身高重建队列
思路:二维数组,贪心体现在对每一维都要利用排序,构成最符合题意的序列,在遍历重建队列。这里先让身高降序排,若身高想等,则让k升序排。此时排完序后,之前人的身高必然大于等于当前的人,且k也符合要求,那么只要根据k值来将每个人插入队列即可(让其前面恰好有k个人)。
class Solution {
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(),
[](const vector<int>& a, const vector<int>& b)
{
if(a[0]!=b[0])
{
return a[0]>b[0]; // 让身高从大到小排序
}else{
return a[1]<b[1]; // 身高一样,那么让k小的在前面。
}
}); // 此时排完序后,身高是降序,并且如果身高相同,那么先排k小的,再排k大的,满足题目条件。这样的话,无论哪个人的身高都小于等于他前面人的身高。所以接下来只要按照K值将他插入相应的位置就可以了。
int n = people.size();
vector<vector<int> > ans;
for (const vector<int>& person: people) {
int pos = person[1];
ans.insert(ans.begin()+pos, person);
}
return ans;
}
};
5、种花问题
思路:在数组的两边界加上0,这样就不用考虑边界是1开始还是0开始的。另外,如果出现连续3个位置是0,那么位置中间可种下一朵花。
class Solution {
public:
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
flowerbed.insert(flowerbed.begin(), 0);
flowerbed.push_back(0);
for(int i=1;i<flowerbed.size()-1;i++)
{
if(flowerbed[i-1]==0&&flowerbed[i]==0&&flowerbed[i+1]==0) // 连续3个位置为0
{
flowerbed[i]=1;
n--;
}
}
if(n<=0)
{
return true;
}else{
return false;
}
}
};
补:视频拼接
思路一:将区间排序,维护一个最小的覆盖最长的数组。
class Solution {
public:
int videoStitching(vector<vector<int>>& clips, int time) {
sort(clips.begin(), clips.end(),
[](const vector<int>& a, const vector<int>& b)
{
if(a[0]!=b[0])
{
return a[0]<b[0];
}else{
return a[1]<b[1];
}
});
if(clips[0][0]!=0) // 开头不是0,左端肯定不满足
{
return -1;
}
vector<vector<int> > vec;
vec.push_back({
clips[0][0], clips[0][1]});
if(clips[0][1]>=time) // 第一个区间直接覆盖time了,那就返回
{
return 1;
}
for(int i=1;i<clips.size();i++)
{
int right = vec.back()[1];
if(clips[i][0]<=right && clips[i][1]>right) // 新来的区间,一定要左端小于等于数组末端的右端,说明能重叠;并且右端比数组末端的右端大,不然没加进去的必要
{
// 这里有两种情况让数组末端退出
// 1.新来的左端小于等于末端的左端,说明覆盖范围更大
// 2.新来的左端小于等于数组末端前一个的右端,说明能过和前一个重叠,那就不需要数组末端了
if(clips[i][0]<=vec.back()[0])
{
vec.pop_back();
}else if(vec.size()>=2 && clips[i][0]<=vec[vec.size()-2][1])
{
vec.pop_back();
}
vec.push_back(clips[i]);
if(vec.back()[1]>=time)
{
break;
}
}
}
if(vec.back()[1]>=time)
return vec.size();
else
return -1;
}
};
思路二:dp
设dp[i]表示覆盖[0,i)的最少区间数量。
class Solution {
public:
int videoStitching(vector<vector<int>>& clips, int time) {
vector<int> dp(time + 1, INT_MAX - 1);
dp[0] = 0;
for (int i = 1; i <= time; i++) {
for (auto& it : clips) {
if (it[0] < i && i <= it[1]) {
// 说明当前区间能包含i,那么dp[it[0]]表示覆盖0~it[0]的最少数量,可以进行当前状态的更新
dp[i] = min(dp[i], dp[it[0]] + 1);
}
}
}
return dp[time] == INT_MAX - 1 ? -1 : dp[time];
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/video-stitching/solution/shi-pin-pin-jie-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
思路三:贪心
可以理解为跳格子的问题。需要知道在当前位置能够跳到的位置,然后用最少的次数跳到目的地。
class Solution {
public:
int videoStitching(vector<vector<int>>& clips, int time) {
vector<int> maxn(time);
for(auto t:clips)
{
if(t[0]<time)
{
maxn[t[0]]=max(maxn[t[0]], t[1]); // 确定每个区间左端点,能够碰到的最右端点
}
}
int ans=0;
int last=0;
int pre=0;
for(int i=0;i<time;i++)
{
last = max(last, maxn[i]); // 从该位置能碰到的最右边
if(i==last) // 说明该位置能碰到的最右边还是该位置,不能继续往后
{
return -1;
}
if(i==pre) // 要走到上一步能到的最后位置,才能更新所跳的步数
{
ans++;
pre=last;
}
}
return ans;
}
};
补:跳跃游戏
思路:在每一个格子时,就能知道自己能跳的最远距离,不断更新最远距离,如果能到达最后的索引,说明跳到了。
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
int rightmost=0;
for(int i=0;i<n;i++)
{
if(i<=rightmost) // 说明在能跳到的范围内
{
rightmost = max(rightmost, i+nums[i]);
if(rightmost>=n-1)
{
return true;
}
}
}
return false;
}
};
反向dp(通过169/166,竟然超时)
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
if(n==1)
{
return true;
}
vector<int> dp(n, 0);
dp[n-1]=1;
for(int i=n-2;i>=0;i--)
{
for(int j=nums[i];j>=1;j--) // 只要在当前格子的跳跃范围内,有一个格子是1,那么当前格子肯定也是1.
{
if(i+j<nums.size())
{
dp[i]|=dp[i+j];
if(dp[i]==1)
{
break;
}
}
}
}
return dp[0];
}
};
补:跳跃游戏2
思路:贪心的想法,每一步要尽可能远,但是对于当前位置 i ,不能直接跳到nums[i]+i,因为有可能在 i ~ nums[i]+i的区间中,有更远的距离。因此分为当前到达的最远位置,下一步到达的最远位置。每一次走到当前的最远位置时,步长再++,而在走的过程中不断更新下一步的最远位置。
class Solution {
public:
int jump(vector<int>& nums) {
int ans=0;
int end=0; // 当前能走到的终点位置
int maxpos=0;
for(int i=0;i<nums.size()-1;i++)
{
maxpos = max(maxpos, nums[i]+i); // 更新能跳到的最远位置。其实这里在算下一步能跳到的最远位置
if(i==end) // 什么时候跳跃数才++,因为还没跳到最后,还有下一步的跳跃空间
{
end=maxpos; // 将当前能走到的最远位置更新
ans++;
}
}
return ans;
}
};
补:加油站
思路:贪心。根据题目,可以得出两个推论:
1)将每个加油站的剩余油量累加给left,即left+=gas[i]-cost[i],如果left<0,那么从出发站到i都不是起点。
2)总油量要>=总消耗量。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum=0, totalSum=0; // 一开始油量状态是0
int start=0; // 假设从0位置出发
for(int i=0;i<gas.size();i++)
{
curSum+=gas[i]-cost[i]; // 记录当前剩余油量
totalSum+=gas[i]-cost[i]; // 记录总共剩余油量
if(curSum<0) // 说明油量不足,因此0~i都不能作为起始点
{
curSum=0;
start=i+1;
}
}
if(totalSum<0) // 总共剩余油量都不够,那就肯定走不完一圈
{
return -1;
}
return start;
}
};
补:会议室2
思路:这样看,最少的会议室数量,其实就是同一时间使用的最多会议室数量,因为必须满足这一个时间,那么多会议都有会议室用。那我们可以思考在这个区间内,有会议就表示区间都+1(需要一个会议室),那么只要看看那个时间点对应的值,就表示这个时间点所需的会议室数量。这种区间增量的问题,可以用差分数组+前缀和。
/**
* Definition of Interval:
* classs Interval {
* int start, end;
* Interval(int start, int end) {
* this->start = start;
* this->end = end;
* }
* }
*/
class Solution {
public:
/**
* @param intervals: an array of meeting time intervals
* @return: the minimum number of conference rooms required
*/
int minMeetingRooms(vector<Interval> &intervals) {
// Write your code here
int last = INT_MIN;
for(int i=0;i<intervals.size();i++)
{
last = max(last, intervals[i].end);
}
vector<int> f(last+1, 0);
for(int i=0;i<intervals.size();i++)
{
int left = intervals[i].start;
int right = intervals[i].end;
f[left]++; // 差分
f[right]--;
}
int ans=f[0];
for(int i=1;i<=last;i++)
{
f[i]+=f[i-1]; // 前缀和
ans = max(ans, f[i]);
}
return ans;
}
};
八、分治
分治的本质就是拆分成最小的部分进行操作。
1、为运算表达式设计优先级
思路:分治的思想将表达式向下不断切成更小的表达式(最小的表达式就是只有数字,不包含运算符),切完之后回溯的过程中进行表达式的计算,逐渐向上传递结果。
class Solution {
public:
vector<int> diffWaysToCompute(string expression) {
vector<int> count;
for(int i=0;i<expression.size();i++)
{
char c = expression[i];
if(c=='+' || c=='-' || c=='*')
{
vector<int> left = diffWaysToCompute(expression.substr(0, i)); // 计算左侧表达式结果
vector<int> right = diffWaysToCompute(expression.substr(i+1)); // 计算右侧表达式结果
for(auto & l : left)
{
for(auto &r : right)
{
if(c=='+')
{
count.push_back(l+r);
}else if(c=='-')
{
count.push_back(l-r);
}else if(c=='*')
{
count.push_back(l*r);
}
}
}
}
}
if(count.size()==0) // 没有运算符,已经切成最小的表达式,只包含数字
{
count.push_back(stoi(expression));
}
return count;
}
};
2、不同的二叉搜索树2
思路:一开始的想法是,将1~n全排列,然后对每一种排列情况进行建树,但是会出现重复的情况,剪完树再去重的话成本太高。想到其实建树的问题可以很好的用递归来解决(和上一题非常像)。
/**
* 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 {
public:
vector<TreeNode*> create(int left, int right) // 表示left~right的范围内能建的树
{
vector<TreeNode*> ans;
if(left>right) // 下标不符,只能返回空
{
ans.push_back(nullptr);
return ans;
}
for(int i=left;i<=right;i++)
{
vector<TreeNode*> left_vec = create(left, i-1); // 左子树
vector<TreeNode*> right_vec = create(i+1, right); // 右子树
for(auto l : left_vec) // 分别遍历,因为可以有不同的组合方式
{
for(auto r : right_vec)
{
TreeNode* root = new TreeNode(i);
root->left = l;
root->right = r;
ans.push_back(root);
}
}
}
return ans;
}
vector<TreeNode*> generateTrees(int n) {
return create(1, n);
}
};
九、树状数组
动态维护前缀和,就利用树状数组。
1、计算右侧小于当前元素的个数
思路:为什么这道题能用树状数组?例如[5,5,2,3,6],我们可以列出其对应的哈希表:
而要考虑右侧小于当前元素的个数,应该从右往左遍历。一开始哈希表全为0。
(1)从6遍历开始,此时6索引下+1,而6的前缀和(1~5)是0,因此表示没有小于6的元素出现过;
此时index:12345678
此时value:00000100
(2)再遍历到3,前缀和1~2也是0;
此时index:12345678
此时value:00100100
(3)再遍历到2,前缀和1~1也是0;
此时index:12345678
此时value:01100100
(4)再遍历到5,前缀和1~4是2,表示在5的右侧出现过2个小于5的数;
此时index:12345678
此时value:01101100
(5)最后遍历到5时,同样前缀和也是1~4是2,因为要严格小于5,因此重复的5不算。
此时index:12345678
此时value:01102100
现在可以发现前缀和正好有利于求解题目,而问题是如果直接列哈希表,范围太大,因为可能出现很多0值,导致成本过高;此外,题目要求严格小于,因此重复出现并不会影响计数,可以去重。因此最终的解决方法是:排序+去重,得到一个新的数列,用来初始化树状数组。
这里只能用树状数组来求前缀和,不然求1~i-1用遍历来求会超时。树状数组是变成了,如果有小于v的元素出现,直接让c[v]+1,这样直接就能获得有多少小于v的元素了。
class Solution {
private:
//原数组为nums,
//将nums离散化,此处是排序+去重,转化为数组a
vector<int> a;
//将nums对应a的元素update到树状数组c
vector<int> c;
//resize树状数组大小
void init(int len) {
c.resize(len);
}
//lowbit为二进制中最低位的1的值
int lowbit(int x) {
return x & (-x);
}
//单点更新,从子节点更新到所有父节点(祖父节点等一直往上到上限c.size())
void update(int pos) {
while (pos < c.size()) {
c[pos] += 1; // 后面位置上每个元素都要+1,表示有小元素出现了
pos += lowbit(pos); // 往后遍历
}
}
//查询,实际是求和[0,...,pos],即求1~pos的元素数量
//如c[8],在update时,a[1],a[2],a[3],...,a[8]都会使c[8]增加一个value(该题中我们设置为1)
//res += c[8],然后8减去lowbit为0。
//也可以拿c[6]举例,c[6] =a[5]+a[6],lowbit后,c[4] = a[1]+a[2]+a[3]+a[4]
int query(int pos) {
int res = 0;
while (pos) {
res += c[pos]; // 求前缀和
pos -= lowbit(pos); // 往前遍历
}
return res;
}
//离散化处理
void Discretization(vector<int>& nums) {
//拷贝数组 [5,4,5,3,2,1,1,1,1,1]
a.assign(nums.begin(), nums.end());
//排序[1,1,1,1,1,2,3,4,5,5]
sort(a.begin(), a.end());
//去重[1,2,3,4,5]
a.erase(unique(a.begin(), a.end()), a.end());
}
int getId(int x) {
//lower_bound返回第一个不小于x的迭代器
//[1,2,3,4,5]中1,减去begin()再加1,得到id(1-5)
return lower_bound(a.begin(), a.end(), x) - a.begin() + 1; //+1的目的是不要让id出现0,从1开始
}
public:
vector<int> countSmaller(vector<int>& nums) {
vector<int> res;
//将nums转化为a
Discretization(nums);
//题解是+5,其实+1就够了,树状数组中我们不使用0下标,所以需扩展1位空间
init(a.size()+1); // 对离散化去重后得到的a,构建树状数组
int n = nums.size(); // 从右往左遍历nums
for (int i=n-1; i>=0; --i) {
//倒序处理
int id = getId(nums[i]);
//查询严格小于id的元素数量,所以使用id-1
res.push_back(query(id-1));
//更新id,其实更新也可以提前,因为查询是id-1,所以更新操作不影响当前结果
update(id);
}
//倒序处理再倒序回来。如果不是用push_back,直接用下标可以不用在这里再倒序
reverse(res.begin(), res.end());
return res;
}
};
十、搜索
BFS:广度优先搜索,可以求解无权图的最短路径。
使用BFS的注意事项:
- 队列:用来存储每一轮遍历得到的节点;
- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
1、二进制矩阵中的最短路径
思路:用bfs来求最短路径。由于bfs每次按层遍历,不像dfs会走回头路,因此当设完访问标记后,不用恢复状态。
BFS过程:
- 1、创建队列,将起点入队,并设置访问状态
- 2、遍历当前队列中的元素,出队一个元素,然后for循环遍历该元素可能走的下一步,将下一步再入队
- 3、每出队一个元素,根据终止条件进行判断
不过这个流程有点像dfs。。。
class Solution {
public:
int x[8] = {-1, 1, 0, 0, -1, -1, 1, 1};
int y[8] = {0, 0, -1, 1, -1, 1, -1, 1};
int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
if(grid[0][0]==1) // 开头第一个就是1,说明不能走
{
return -1;
}
int n = grid.size();
queue<vector<int> > q;
vector<int> tmp = {0, 0, 1}; // 保存i, j, 当前路径长度
q.push(tmp);
grid[0][0]=1; // 表示已经通过了
int ans=INT_MAX;
while(!q.empty())
{
vector<int> node = q.front();
q.pop();
if(node[0]==n-1&&node[1]==n-1) // 到达终点时,比较路径长度,取最短的
{
ans = min(ans, node[2]);
}
for(int k=0;k<8;k++) // 可以走8个方向
{
int newX = node[0] + x[k];
int newY = node[1] + y[k];
if(newX>=0&&newX<n&&newY>=0&&newY<n && grid[newX][newY]==0) // 能走的格子,再入队
{
grid[newX][newY]=1; // 说明这个格子已经走过了
vector<int> new_node = {newX, newY, node[2]+1};
q.push(new_node);
}
}
}
return ans==INT_MAX ? -1 : ans;
}
};
另一种写法
class Solution {
public:
int X[8] = {-1, 1, 0, 0, -1, -1, 1, 1};
int Y[8] = {0, 0, -1, 1, -1, 1, -1, 1};
int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
if(grid[0][0]==1)
return -1;
int n=grid.size(),ans=1;
queue<pair<int,int> > q;
q.emplace(0,0); //从0,0开始
grid[0][0]=1; //标记为1代表走过
while(!q.empty()){ //bfs
int size=q.size();
while(size--){
auto [x,y]=q.front();
q.pop();
if(x==n-1&&y==n-1)
return ans;
for(int i=0;i<8;i++) //遍历八个方向的
{
int nx=x+X[i];
int ny=y+Y[i];
if(nx<0||ny<0||nx>=n||ny>=n)
continue; //判断是否越界
if(grid[nx][ny]==0) //判断是否能走
{
q.emplace(nx,ny);
grid[nx][ny]=1; //标记
}
}
}
ans++; //记录循环次数
}
return -1;
}
};
2、完全平方数
思路:这里用bfs来记录当前和,而不是记录当前入队的完全平方数。用vis来标记所经过的当前和,用来剪枝。
不同于上一题的BFS步骤:
- 建队,记步骤=1
- 遍历当前队列中的所有节点,这样避免和下一层节点混在一起
- 判断当前节点是否满足终止条件,如果不是且没访问过,那就再次入队
class Solution {
public:
int numSquares(int n) {
unordered_set<int> visited;
queue<int> q{
{0}}; // 一开始和为0
int steps = 1;
while (!q.empty()) {
auto size = q.size();
while (size--) { // 遍历当前层的节点,不要让当前层和下一层的节点混在一起
auto cur = q.front(); q.pop();
for (int i = 1; i * i + cur <= n; i++) { // 为当前层中每一个节点,再找一个能满足条件的完全平方数
auto next = i * i + cur; // 更新当前的和
if (next == n) {
return steps;
}
if (!visited.count(next)) { // 记忆化,如果当前和已经出现过,说明已经尝试使用当前和,但失败了。因此做标记来避免重复搜索。
visited.insert(next);
q.push(next);
}
}
}
steps++;
}
return -1; // should never reach here.
}
};
3、单词接龙
思路:其实想法比较直接,可以上BFS。
给定的条件也比较明显:
(1)开头、结尾都给出了
(2)下一次遍历该选谁入队也给出了
但简单实用BFS造成了超时。这里一个剪枝的方法,因为下一个单词和当前单词只有一个字符的差别(更改这个字符后,两者完全一样),那么可以尝试用26个字母分别替换当前单词的每个位置,然后判断该单词是否在wordList中,可以利用哈希查找,加快遍历速度。
class Solution {
public:
bool judge(string tmp, string str)
{
int num=0;
for(int i=0;i<tmp.size();i++)
{
if(tmp[i]!=str[i])
{
num++;
}
}
if(num==1)
return true;
else{
return false;
}
}
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
queue<string> q;
q.push(beginWord);
int step=1;
unordered_set<string> wordSet(wordList.begin(), wordList.end()); // 变成哈希表,因为每个单词各不相同
unordered_map<string, int> visted; // 访问情况
visted[beginWord]=1;
while(!q.empty())
{
int size = q.size();
while(size--)
{
auto str = q.front();
q.pop();
if(str==endWord)
{
return step;
}
for (int j = 0; j < str.size(); j++) //遍历单词的每个位置
{
string lWord = str;
for (int k = 0; k < 26; k++) //26个字母替换第j个位置
{
lWord[j] = k + 'a';
// 字典中有这个字符串,并且没有被访问过
if (wordSet.count(lWord) && !visted.count(lWord))
{
q.push(lWord);
visted[lWord]=1;
}
}
}
}
step++;
}
return 0;
}
};
DFS
BFS是一层一层遍历,将每层的新节点遍历完后,才遍历下一层节点。DFS是在得到一个新节点时立即对新节点进行遍历,直到没有新节点才返回。
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
DFS需要注意的问题:
- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
4、岛屿的最大面积
思路:就是计算连通1的最大数量,体现了dfs的可达性(究竟能到达多少个1),因此是典型的dfs。
class Solution {
public:
int X[4] = {-1, 1, 0, 0};
int Y[4] = {0, 0, -1, 1};
int ans=0;
int num=0;
void dfs(vector<vector<int>>& grid, int i, int j)
{
int m = grid.size();
int n = grid[0].size();
ans = max(num, ans);
for(int k=0;k<4;k++)
{
int newX = i + X[k];
int newY = j + Y[k];
if(newX>=0&&newX<m&&newY>=0&&newY<n && grid[newX][newY]==1)
{
grid[newX][newY]=0;
num++;
dfs(grid, newX, newY);
}
}
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(grid[i][j]!=0)
{
num=1;
grid[i][j]=0;
dfs(grid, i, j);
}
}
}
return ans;
}
};
补:统计封闭岛屿的数目
思路:就是加一个标记位,如果这个水域有碰到边界,那就不计数。
class Solution {
public:
int ans=0;
int X[4] = {
-1, 1, 0, 0};
int Y[4] = {
0, 0, -1, 1};
void dfs(vector<vector<int>>& grid, int x, int y, int& f_mark)
{
int m = grid.size();
int n = grid[0].size();
if(x==0||x==m-1||y==0||y==n-1)
{
f_mark=0;
return;
}
if(grid[x][y]==0)
{
grid[x][y]=1;
}
for(int i=0;i<4;i++)
{
int newX = x+X[i];
int newY = y+Y[i];
if(newX>=0&&newX<m && newY>=0&&newY<n && grid[newX][newY]==0)
{
dfs(grid, newX, newY, f_mark);
}
}
}
int closedIsland(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(grid[i][j]==0)
{
int f_mark=1;
dfs(grid, i, j, f_mark);
if(f_mark==1)
{
ans++;
}
}
}
}
return ans;
}
};
补:飞地的数量
思路:从边界开始,把1变成0,那么剩下的1都在0的包围里。
class Solution {
public:
int X[4] = {
-1, 1, 0, 0};
int Y[4] = {
0, 0, -1, 1};
void dfs(vector<vector<int> >& grid, int x, int y)
{
int m = grid.size();
int n = grid[0].size();
grid[x][y]=0;
for(int i=0;i<4;i++)
{
int newX = x+X[i];
int newY = y+Y[i];
if(newX>=0&&newX<m &&newY>=0&&newY<n && grid[newX][newY]==1)
{
dfs(grid, newX, newY);
}
}
}
int numEnclaves(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
for(int i=0;i<m;i++)
{
if(grid[i][0]==1)
{
dfs(grid, i, 0);
}
if(grid[i][n-1]==1)
{
dfs(grid, i, n-1);
}
}
for(int j=0;j<n;j++)
{
if(grid[0][j]==1)
{
dfs(grid, 0, j);
}
if(grid[m-1][j]==1)
{
dfs(grid, m-1, j);
}
}
int ans=0;
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(grid[i][j]==1)
{
ans++;
}
}
}
return ans;
}
};
补:统计子岛屿
思路:只要遍历grid2,如果该位置有1,且grid1对应位置也是1,那就ok,继续递归;如果grid1不是1,那就直接返回。
class Solution {
public:
int X[4] = {
-1, 1, 0, 0};
int Y[4] = {
0, 0, -1, 1};
void dfs(vector<vector<int>>& grid1, vector<vector<int>>& grid2, int x, int y, int& f_mark)
{
int m = grid2.size();
int n = grid2[0].size();
if(grid2[x][y]==1 && grid1[x][y]==1)
{
grid2[x][y]=0;
}else{
f_mark=0;
return;
}
for(int i=0;i<4;i++)
{
int newX = x+X[i];
int newY = y+Y[i];
if(newX>=0&&newX<m&&newY>=0&&newY<n&&grid2[newX][newY]==1)
{
dfs(grid1, grid2, newX, newY, f_mark);
}
}
}
int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
int m = grid1.size();
int n = grid1[0].size();
int ans=0;
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(grid2[i][j]==1)
{
int f_mark=1;
dfs(grid1, grid2, i, j, f_mark);
if(f_mark)
{
ans++;
}
}
}
}
return ans;
}
};
补:不同的岛屿数量2
思路:先用dfs,找到所有的岛屿坐标,然后将坐标的不同方向都转化成字符串,进行去重。
class Solution {
public:
/**
* @param grid: the 2D grid
* @return: the number of distinct islands
*/
int X[4] = {
-1, 1, 0, 0};
int Y[4] = {
0, 0, -1, 1};
void dfs(vector<vector<int>> &grid, int x, int y, vector<pair<int,int> > & land)
{
int m = grid.size();
int n = grid[0].size();
if(grid[x][y]==1)
{
grid[x][y]=0;
land.push_back(make_pair(x, y));
}
for(int i=0;i<4;i++)
{
int newX = x+X[i];
int newY = y+Y[i];
if(newX>=0&&newX<m&&newY>=0&&newY<n && grid[newX][newY]==1)
{
dfs(grid, newX, newY, land);
}
}
}
string getstring(vector<pair<int, int>>& island)
{
sort(island.begin(), island.end(),
[](pair<int, int>& a, pair<int, int>& b)
{
if (a.first != b.first)
return a.first < b.first;
else if (a.first == b.first)
{
return a.second < b.second;
}
});
// 算出左上角的点,然后进行标准化
int ox = island[0].first;
int oy = island[0].second;
string res = "";
for (auto p : island)
{
res += to_string(p.first - ox) + " " + to_string(p.second - oy);
}
return res;
}
string getpatten(vector<pair<int, int>>& islandset)
{
int dx[4] = {
1, -1, 1, -1};
int dy[4] = {
1, 1, -1, -1};
set<string> res;
for (int i = 0; i < 4; i++) // 这里主要得到旋转的不同结果
{
vector<pair<int, int>> p1;
vector<pair