1定长滑动窗口

1进入窗口,更新当前符合条件的值

2更新答案

3离开窗口,更新当前符合条件的值

2不定长滑动窗口

2.1求最长最大

当窗口向右扩增检测到不符合条件时,将窗口左侧不断缩小直到符合条件,记录窗口在变化过程中的最大值

2.2求最小

当窗口向右扩增检测到满足条件时,将left指针右移直到不满足条件,每次右移更新ans

使用双指针,循环结束后将两个指针都移动到最右端,满足条件时,左指针右移,但不能超过右指针,直到不满足条件,每次右移检查左右指针切割产生的子数组大小和答案的比较,取最小值,每一轮循环结束时将右指针右移

ans = move(t);字符串的直接赋值

2.3求子数组个数

2.3.1越长越合法

写ans+=left,每次循环读入新的元素,然后检查是否满足合法条件,使用一个while循环不断++left数值,并更新条件变量直到不满足,再将ans+=left即可,一般有合法字眼

2.3.2越短越合法

一般要写 ans += right - left + 1,每次循环读入新的元素然后检查是否满足合法条件,使用while循环不断删去left的数值,更新条件变量直到满足条件,接下来+=right-left+1,这个right一般用i来代替

2.3.3恰好型滑动窗口

计算元素相加恰好满足条件k

int subarraySum(vector<int>& nums, int k) {
    unordered_map<int, int> mp;
    int count = 0, pre = 0;
    for (int i = 0;i<nums.size();i++) {
        mp[pre]++;
        pre += nums[i];
        if (mp.find(pre - k) != mp.end()) {
            count += mp[pre - k];
        }

    }
    return count;
}

可以分解成两个数组,满足至少为k和至少为k+1,即两个越长越合法问题,再用至少为k的ans减去至少为k+1的ans即可

也可以变为两个至多,即满足至多为k和至多为k-1

热题100

1哈希

t1数组找相加为target的元素下标,关键是find target-numi

做法:使用哈希表储存已经寻找过target-nums[i]的元素,每次储存前使用find函数寻找是否存在对应元素,若存在则直接返回,否则继续存入,只要On即可

t2字母异位词分组 排序并作为关键字存入哈希表

求一个字符串数组中字母异位词的分组,字母异位词即字母相同,字母的数量相同,但是字母位置不同

做法:对字符串进行排序,如果两个词是字母异位词,则排序结果相同,将排序结果作为key存入哈希表即可

t3最长连续序列,set储存原数组并遍历

求一个非顺序数组中最长的连续整数序列(不要求连续)

不要求在原数组中连续,这里不能用滑动窗口解(好像可以直接sort然后使用滑动窗口,不过需要on时间复杂度所以可能有点问题)

做法:先用set储存原数组,方便后续查找;遍历set,使用count作为查找函数,如果存在元素自身-1的对象,则跳过该元素,如果不存在,则从该元素开始向后枚举,每次+1直到不存在对象,同时记录长度,使用一个ans保存最大长度最后返回

//遍历一次使用set储存全部数字
unordered_set<int> num_set;
for (const int& num : nums) {
    num_set.insert(num);
}

int longestStreak = 0;
//遍历set,使用count作为查找函数,如果存在count会返回1
for (const int& num : num_set) {
    //如果当前数字不存在前缀数
    if (!num_set.count(num - 1)) {
        //从当前数字开始向后枚举计算序列长度
        int currentNum = num;
        int currentStreak = 1;
        //如果能找到后缀数,则继续向后枚举
        while (num_set.count(currentNum + 1)) {
            currentNum += 1;
            currentStreak += 1;
        }
        //将序列长度和最长长度比较得到结果
        longestStreak = max(longestStreak, currentStreak);
    }
}

return longestStreak;

2双指针

t1移动零,slow维护第一个0的位置

将所有0移动到数组最右且不影响其他元素的相对顺序

以下是高级解法,只需要一个指针,另一个指针用i代替,正常的双指针是使用while和两个指针

SLOW初值为0,每次交换后++,得到下一个要被交换的对象,如果numi非0则进行交换,确保了slow位置前是正常顺序且没有0

从左向右遍历,遇到非0时,将非0元素和slow对应的0交换位置即可

int slow = 0;
for (int i = 0; i < nums.size(); i++) {
    if (nums[i] != 0) {
        // 每次保证是0和非0的交换
        swap(nums[slow++], nums[i]);
    }
}

正常解法,使用while循环,结束条件是right>=nums.size,即右指针指到数组结尾,左指针一直指向正确序列的末尾,右指针指向待处理序列的头部即左右指针中间部分是0,左指针一直指向第一个0

int left =0,right = 0;
while(right<nums.size()){
    if(nums[right]){//右指针指到非0数,说明右指针右侧还可以被整理
        swap(nums[right],nums[left]);
        left++;//左边序列可以增大一位

    }
    //右指针增加
    right++;
} 

t2盛最多水的容器,停止条件是垂线长度大于高

计算一个垂线长度数组中由任意两条垂线和它们之间的x轴构成的容器最大值

解法:使用left=0,right=size-1代表左和右的垂线位置,while的条件是(left<right)每次循环都计算容器的高(为l和r的垂线长度最小值)和底(r-l),将计算出的容器大小和ans比较取最大值,接下来使用两个while循环,当left对应垂线<=高时,left右移,移至垂线>高为止,right同理左移,需要注意两个while循环还有额外的结束条件即left<right

int ans = 0;
        int left = 0,right = height.size()-1;
        int high = 0;
        while(left<right){
            high=min(height[left],height[right]);
            ans = max(ans,(right-left)*high);
            while(height[left]<=high&&left<right)left++;
            while(height[right]<=high&&left<right)right--;
        }
        return ans;

t3三数之和,关键是去重

求一个数组中三数之和为0的所有子序列,且不能重复

解法:将数组排序,从起始点开始for循环遍历,遍历的元素若大于0,则break;去重,若起始值等于上一个元素,则continue,因为得到的结果会与上一次相同;

前面条件都满足之后,设置两个指针l=i+1,r=n-1,while循环条件l<r,循环开始

判断lri三者相加是否为0

为0则加入结果集,进入下一步两个while循环去重,如果l+1=l,则l++,如果r-1=r则r--直到不符合条件,最后再次将l++,r--;

如果不为0,则结果<0则l++;结果>0则r--(这里也可以使用去重);

if(nums.size()<3)return{};
vector<vector<int>>ans;
sort(nums.begin(),nums.end());

for(int i=0;i<nums.size();i++){
    if(nums[i]>0)break;
    if(i>0&&nums[i]==nums[i-1])continue;
    int l=i+1,r=nums.size()-1;
    while(l<r){
        if(nums[i]+nums[l]+nums[r]==0){
            ans.push_back({nums[i],nums[l],nums[r]});
            while(l<r&&nums[l]==nums[l+1])l++;
            while(l<r&&nums[r]==nums[r-1])r--;
            l++;
            r--;
        }
        else if(nums[i]+nums[l]+nums[r]<0)l++;
        else r--;

    }
}
return ans;

t4接雨水,双指针,用层数作为停止条件

非常好解法使我大脑旋转

定义一个双指针,以及层数h=1,双指针在两头往中间移动,只要指针大于等于h,就停下来
目的:当两边指针都停下来的时候,计算第一层的面积(直接左指针减右指针+1),然后h++计算第二层的面积,以此类推计算每一层的面积,然后用这个面积减去height的和,剩下的就是水量了

        int l=0,r=height.size()-1;
        int sumh = 0;
		//计算边缘线段长
        for(int i =0;i<r+1;i++){
            sumh+=height[i];
        }
        int sum =0;
        int h =1;
		//每次上升1层,当lr都大于等于该层时计算该层的大小,相当于吧整个图看做金字塔,一层层计算
        while(l<=r){
            while(l<=r&&height[l]<h){
                l++;
            }
            while(l<=r&&height[r]<h){
                r--;
            }
            sum+=r-l+1;
            h++;
        }
        sum-=sumh;
        return sum;

3滑动窗口

t1无重复字符的最长子串,不定长滑动窗口,求最大

使用最大滑动窗口,使用一个哈希表记录字符数量,记录一个l,当li之间的内容不满足要求(当前的i在哈希表中可以找到,说明i字符重复)时,使用while清除哈希表中,l代表的字符并l++;每个for循环比较一次ans和i-l+1的最大值,并插入当前i代表的字符;

int n = s.length();
unordered_set<char> set;
int sum = 0;
int left = 0;
for(int i =0;i<n;i++){
    while(set.find(s[i])!=set.end())set.erase(s[left++]);
    sum=max(sum,i-left+1);
    set.insert(s[i]);
}
return sum; 

t2找字母异位词,定长滑动窗口,vector比较功能

定长滑动窗口,这里确定ans的条件是使用vector的比较功能,vector如果相同说明字符的数量相同

建立两个vector,一个用来储存给定的目标字符串各个字符的数量,一个用来储存滑动窗口内字符的数量,使用for循环遍历源字符串,滑动窗口大小是目标字符串的长度,if判断两个vector是否相等;使用定长的模版即可

vector<int> c(26);
vector<int> sc(26);
vector<int>ans;
int k = p.size();
for(int i = 0;i<p.size();i++){
    c[p[i]-'a']++;
}
for(int i =0;i<s.size();i++){
    sc[s[i]-'a']++;
    if(i<k-1)continue;
    if(c==sc)ans.push_back(i-k+1);
    sc[s[i-k+1]-'a']--;
}
return ans;

4子串

t1和为k的子数组,前缀和

初看可以视作一个恰好为k的滑窗问题,这里编写新函数采用至少为k进行计算(这里不能用滑窗)

image-20250712154342377

所以本题采用前缀和

这里给出前缀和的基础写法

1首先你有一个随机的数组

2建立一个vector或者数组p,将首位设为0,即p[0]=0

3遍历你的数组,i设为1,i<size+1;每次循环p[i]=p[i-1]+nums[i-1]

END

我的写法:先计算前缀和,再嵌套for循环,外循环和内循环都遍历p,j从i+1开始,每个内循环都检查ij前缀和相减的结果是否等于k,若相等则ans++,这种方法和枚举实际上并无区别,甚至连时间都差不多

高级写法:通过前缀和和hash表得到答案,建立pre和map,遍历数组,每次都让pre加上当前元素,检查mp中是否存在pre-k的元素(只要当前前缀和减去这个前缀和就能得到k),若存在,则ans+mp[pre-k],不存在则不操作,循环结束前mp[pre]++

t2滑动窗口最大值(优先队列或双端队列)

滑动窗口每次向右一格,给出整个滑窗过程中每次移动后的元素最大值

这里采用优先队列做,使用pair<int,int>创建优先队列,优先队列会根据第一个int自动排序,第二个int作为寿命进行储存,每次向右滑动时,将新元素进入优先队列,同时加载其寿命,再对栈顶元素进行检测,如果寿命到了滑窗之外则进行pop,最后将符合条件的栈顶元素记录进结果集中

vector<int>ans;
//单调队列维护
priority_queue<pair<int, int>> q;
for(int i =0;i<nums.size();i++){
    q.emplace(nums[i],i);
    if(i<k-1)continue;
    while(q.top().second<i-k+1){
        q.pop();
    }
    ans.push_back(q.top().first);

}
return ans;

解法2 On 使用双端队列,队列内储存的是元素的入栈位置,但是按照元素大小进行排序,每个新入栈的元素都需要和栈尾元素比较,如果大于栈尾元素,则栈尾出栈(因为原来的栈尾元素寿命肯定小于新入栈元素,如果小于新元素则永远不会轮到它做最大值)直到找到新元素适合的栈尾,结束循环并让新元素入栈,此时再对栈顶进行处理,如果栈顶元素的寿命已尽,则使其出栈,直到栈顶寿命合适,结束循环,最后将栈顶位置代表的值记录进结果集中

deque<int>q;
vector<int>ans;
for(int i = 0;i<nums.size();i++){
    while(!q.empty()&&nums[i]>=nums[q.back()])q.pop_back();
    q.push_back(i);
    while(q.front()<i-k+1)q.pop_front();
    if(i<k-1)continue;
    ans.push_back(nums[q.front()]);
}
return ans;

t3最小覆盖子串(不定长滑动窗口)

给出字符串s和t,返回s中覆盖t的最小子串(不要求顺序)

编写一个judge函数,用来判断map1是否覆盖map2

使用map2来储存t中所有字母的数量

主函数:

1初始化ans,left,和ansl(ans和ansl是用来返回最小覆盖子串的,left记录当前窗口的最左位置)

2遍历整个源字符串,每次将新字符加入map1中,并检查judge条件,如果符合,更新ans和ansl,将left增加缩小窗口

3使用ans和ansl返回最小覆盖子串

5普通数组

t1最大子数组和(分治或者动规)

这里使用分治法做,有三种可能

1最大值在mid左侧

2最大值在mid右侧

3最大值包含mid

这里需要单独给第三种情况写一个计算函数,设l mid r,计算包含mid的左区间最大值,再计算包含mid的右区间最大值,最后返回两者相加

分治函数的退出条件是l==r,直接返回num[l]即可

int maxSubArray(vector<int>& nums) {
    int n = int(nums.size());
    int result =0;
    result = rtmaxlr(nums,0,n-1);
    return result;
}

int rtmaxlr(vector<int>&nums,int l,int r){
    if(l==r){
        return nums[l];
    }
    int mid = (l+r)/2;
    int lmax = rtmaxlr(nums,l,mid);
    int rmax = rtmaxlr(nums,mid+1,r);
    int midmax = rtmaxcross(nums,l,mid,r);
    int result = max(lmax,rmax);
    result = max(result,midmax);
    return result;

}
int rtmaxcross(vector<int>&nums,int l,int mid,int r){
    int leftsum = INT_MIN;

    int sum = 0;
    for(int i =mid;i>=l;i--){
        sum+=nums[i];
        leftsum=max(leftsum,sum);
    }
    int rightsum= INT_MIN;
    sum=0;
    for(int i =mid+1;i<=r;i++){
        sum+=nums[i];
        rightsum=max(rightsum,sum);
    }
    return (leftsum+rightsum);
}    

也可以采用动态规划法做:

需要消除后效性,dp的元素表示以i结尾的连续子数组的最大值,可以得知dpi=dpi-1+nums[i]或dpi=nums[i],取决于dpi-1是否小于0

在动规过程中记录最大值即可

dp[i]=max(dp[i-1]+nums[i],nums[i])

因此可以写出

//动规,dpi是包含numsi的子数组最大和
int dp[100010]; 
dp[0]=nums[0];
int ans = dp[0];
for(int i = 1;i<nums.size();i++){
    dp[i]=max(dp[i-1]+nums[i],nums[i]);
    ans=max(dp[i],ans);
}
return ans;

t2合并区间,暴力法(先排序再根据左边界合并)

直接使用暴力也很快,设置一个start和end记录区间开头和结尾,初始值设为给出的第1个区间

将所有区间按照左边界排序

遍历所有区间,被遍历的区间如果左边界大于end,则原来的start和end记录进结果集,start和end设为该区间

否则end值取end和当前区间右边界的最大值

//暴力
//将所有区间按照左边界排序
sort(intervals.begin(),intervals.end());
vector<vector<int>>ans;
int start = intervals[0][0];
int end = intervals[0][1];
for(int i = 1 ;i<intervals.size();i++){
    if(intervals[i][0]>end){
        ans.push_back({start,end});
        start = intervals[i][0]; 
    }
    end = max(end,intervals[i][1]);
}
ans.push_back({start,end});
return ans; 

t3轮转数组,三次反转法

暴力解法,直接建立新数组,将原数组和新数组的位置对应调换即可

//暴力,建立一个新数组,将旧数组导入新数组
int n  = nums.size();
vector<int>ans;
for(int i = 0;i<n;i++){
    ans.push_back(nums[(n-k%n+i)%n]);
}
nums = ans;

空间复杂度1的解法

三次反转法

先将整个数组反转,再将前k个和后n-k个分别反转即可

int n = nums.size();
k=k%n;
int l =0,r=n-1;
while(l<r){
    swap(nums[l++],nums[r--]);
}
l=0;
r=k-1;
while(l<r){
    swap(nums[l++],nums[r--]);
}
l=k;
r=n-1;
while(l<r){
    swap(nums[l++],nums[r--]);
}

t4除自身以外数组的乘积

vector<int>ans;
int pre =1;//map<int,int>dp;
int suf =1;//map<int,int>pp;
//dp[0]=1;
for(int i = 0;i<nums.size();i++){
    //dp[i]=dp[i-1]*nums[i-1];
    ans.push_back(pre);
    pre*=nums[i];
}
for(int i = nums.size()-1;i>-1;i--){
    //pp[i]=pp[i+1]*nums[i+1];
    ans[i]*=suf;
    suf*=nums[i];
}
/*
for(int i = 0;i<nums.size();i++){
    ans.push_back(dp[i]*pp[i]);
}
*/
return ans;

t5缺失的第一个正数

int n = nums.size();
for(int i = 0;i<n;i++){
    while(nums[i]>0&&nums[i]<=n&&nums[nums[i]-1]!=nums[i])swap(nums[nums[i]-1],nums[i]);
}
for(int i = 0;i<n;i++){
    if(nums[i]!=i+1)return i+1;
}
return n+1;

6矩阵

t1矩阵置0

要求Om*n 空间复杂度常数

方法是利用第0行和第0列作为标记列,将它们自身的0留到最后处理,第一次遍历整个矩阵,遇到0就在第0行和第0列的对应位置标记,第二次遍历根据第0行和第0列对矩阵进行处理,最后根据一开始0行0列是否有0,对0行0列进行置零

bool row =false,col = false;
int m = matrix.size();
int n = matrix[0].size();
for(int i=0;i<n;i++){
    if(matrix[0][i]==0){
        row=true;
        break;
    }
}
for(int i=0;i<m;i++){
    if(matrix[i][0]==0){
        col = true;
        break;
    }
}
for(int i=1;i<m;i++){
    for(int j = 1;j<n;j++){
        if(matrix[i][j]==0){
            matrix[i][0]=0;
            matrix[0][j]=0;
        }
    }
}
for(int i=1;i<m;i++){
    for(int j = 1;j<n;j++){
        if(matrix[i][0]==0||matrix[0][j]==0){
            matrix[i][j]=0;
        }
    }
}
if(row){
    for(int i = 0;i<n;i++){
        matrix[0][i]=0;
    }
}
if(col){
    for(int i = 0;i<m;i++){
        matrix[i][0]=0;
    }
}

t2螺旋矩阵

顺时针螺旋返回矩阵中所有元素

直接暴力遍历一次即可,需要的是编写好边界条件,这个方法为模拟法

int l = -1,r = matrix[0].size(),t = -1,b = matrix.size();
int condition =0;
int maxsize = r*b;
int count = 0;
int i=0,j=0;
vector<int>ans;
while(count<maxsize){
    ans.push_back(matrix[i][j]);
    count++;
    if(condition==0){
        j++;
        if(j==r){
            j--;
            i++;
            t++;
            condition=1;
        }
    }else if(condition==1){
        i++;
        if(i==b){
            i--;
            j--;
            r--;
            condition=2;
        }
    }else if(condition==2){
        j--;
        if(j==l){
            j++;
            i--;
            b--;
            condition=3;
        }
    }else if(condition==3){
        i--;
        if(i==t){
            i++;
            j++;
            l++;
            condition=0;
        }
    }
}
return ans;

t3旋转图像

要求是原地旋转,这题是数学问题,需要将整个矩阵按照对角线翻转,再逐行逆序即可

for(int i = 0;i<matrix.size();i++){
    for(int j = i;j<matrix.size();j++){
        swap(matrix[i][j],matrix[j][i]);
    }
}
for(int i =0;i<matrix.size();i++){
    for(int j = 0;j<matrix.size()/2;j++){
        swap(matrix[i][j],matrix[i][matrix.size()-1-j]);
    }
}

t4 搜索二维矩阵

从左到右升序,从上到下升序

从右上角开始,向左则变小,向下则变大,可知如果ij大于target则向左,小于则向下;

由此可以通过二叉搜索树得到答案

int m = matrix.size();
int n = matrix[0].size();
int i=0,j=n-1;
while(j>-1&&i<m){
    if(target==matrix[i][j])return true;
    else if(target<matrix[i][j])j--;
    else i++;
}
return false;

7链表

t1相交链表,双指针循环相交点

让两个指针不断向后循环,如果循环到空指针则跳到另一个链表的起点,直到两个指针相等

ListNode *a = headA;
ListNode *b = headB;
while(a!=b){
    a=a->next;
    b=b->next;
    if(a==NULL&&b!=NULL)a=headB;
    else if(b==NULL&&a!=NULL)b=headA;

}
return a;

t2反转链表,pre暂存上一个头部

给出head,要求反转链表并返回

使用一个pre暂存已反转链表头部(初始时为NULL),使用cur指针暂存未反转链表头部即可

ListNode* pre = nullptr;
ListNode* cur = head;
while (cur) {
    ListNode* nxt = cur->next;
    cur->next = pre;
    pre = cur;
    cur = nxt;
}
return pre;

评论说可以使用头插法,每次将cur的next插到head前,其实和这个是一样的

t3回文链表,快慢指针加反转链表

判断一个链表是否为回文链表

我想到的方法是使用两个指针,一个指向链表尾部(单向链表行不通,放弃)

使用栈,会有特殊情况导致失效

最简单的方法是输出到数组中,再通过双指针遍历到中心检查是否为回文

看题解发现双指针可行,但是需要翻转链表和寻找链表中心值(中心值可以用快慢指针去找)

ListNode* middleNode(ListNode* head) {//快慢指针查找中心值
        ListNode* slow = head, *fast = head;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
ListNode* reverseList(ListNode* head) {//翻转链表
        ListNode* pre = nullptr, *cur = head;
        while (cur) {
            ListNode* nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
bool isPalindrome(ListNode* head) {//利用快慢指针和反转链表获取和数组一样的效果,从两端开始遍历
    ListNode* mid = middleNode(head);
    ListNode* head2 = reverseList(mid);
    while (head2) {
        if (head->val != head2->val) { // 不是回文链表
            return false;
        }
        head = head->next;
        head2 = head2->next;
    }
    return true;
}

t4环形链表,快慢指针

给你一个链表的头节点 head ,判断链表中是否有环。

初步想法是利用快慢指针,如果快指针能走到NULL则说明没有环,如果快指针和慢指针重合说明有环

ListNode *f = head;
ListNode *s = head;
while(f!=NULL){
    if(f->next!=NULL)f=f->next->next;
    else break;
    s=s->next;
    if(s==f)return true;
}
return false;

t5环形链表2,快慢指针

需要返回入环开始的节点

快慢指针,由于P2=2*P1当他们相遇时走的长度是3*P1,设环的长度是y,环前的长度是x,x+y是全长,可知p1=x+c,p2=x+c+ny,所以p1=ny,同时可以得知x+ny也是入环点,此时将快指针设置为head,和p1一起next循环,相遇时就可以得到入环点

if(head==NULL)return head;
ListNode *s = head;
ListNode *f = head;
do{
    if(f->next==NULL||f->next->next==NULL)return NULL;
    s=s->next;
    f=f->next->next;
}while(s!=f);
f=head;
while(s!=f){
    s=s->next;
    f=f->next;
}
return s;

t6合并两个有序链表,new节点加递归

使用递归进行插入

if(list1==NULL)return list2;
if(list2==NULL)return list1;
ListNode *tmp = new ListNode();
if(list1->val>list2->val){
    tmp->val=list2->val;
    tmp->next=mergeTwoLists(list1,list2->next);
}else{
    tmp->val=list1->val;
    tmp->next=mergeTwoLists(list1->next,list2);
}
return tmp;

t7两数相加,使用pre指针保证特殊条件,使用j储存进位

单纯的两数相加

int j = 0;
ListNode *p=l1;
ListNode *pre;
while(p!=NULL&&l2!=NULL){
    int sum = p->val+l2->val+j;
    j=sum/10;
    p->val=sum%10;
    pre = p;
    p=p->next;
    l2=l2->next;
}
if(l2!=NULL){
    pre->next=l2;
    p = l2;
}
while(p!=NULL){
    int sum =p->val+j;
    j=sum/10;
    p->val=sum%10;
    pre = p;
    p=p->next;
}
if(j!=0){
    ListNode* tmp = new ListNode();
    tmp->val=j;
    pre->next=tmp;
}
return l1;

t8删除链表的倒数第n个结点

感觉也可以递归

感觉可以用快慢指针实现,倒数第几个就向后n几次,特殊情况是第n个结点刚好是头部,则需要将head向后移一位

ListNode* f=head;
ListNode* s=head;
ListNode* pre;
for(int i = 0;i<n;i++){
    f=f->next;
}
while(f!=NULL){
    f=f->next;
    pre = s;
    s=s->next;
}
if(s!=head)pre->next=s->next;
else head = s->next;
return head;

t9两两交换链表中的节点

递归即可,递归退出条件是head后为空或者head为空

if(head==NULL||head->next==NULL)return head;
ListNode *p1=head;
ListNode *p2=head->next;
p1->next=swapPairs(p2->next);
p2->next=p1;
return p2;

t10K个一组翻转链表

使用递归

首先使用计数器验证当前头部向后k位是否存在,如果不存在则直接返回当前头部

其次优先翻转后面的,所以直接用第k位当头部递归

再处理当前部分,使用计数器和反转链表方法(pre和cur)翻转前k位

最后返回pre即可

ListNode *pre=head;
ListNode *q = head;
int count = 0;
while(count<k&&pre!=NULL){
    pre = pre->next;
    count++;
}
if(count<k)return head;
pre = reverseKGroup(pre,k);
count=0;
while(count<k){
    ListNode *nxt=q->next;
    q->next=pre;
    pre = q;
    q=nxt;
    count++;
}
return pre;

t11随机链表的复制(深拷贝)

使用哈希表进行源地址到新地址的映射,所以第一次遍历原链表时,只建立新的节点而不给新节点中的next和random赋值

第二次遍历再进行赋值

unordered_map<Node*,Node*>map;
Node* p=head;
while(p!=NULL){
    Node*ptr=new Node(p->val);
    map[p]=ptr;
    p=p->next;
}
p=head;
while(p!=NULL){
    Node*ptr = map[p];
    if(p->next!=NULL)ptr->next=map[p->next];
    if(p->random!=NULL)ptr->random=map[p->random];
    p=p->next;
}
return map[head];

t12排序链表

感觉可以使用优先队列进行排序,实践可行,因为没有要求(但是很可能不让用优先队列,所以还是需要下面的题解)

typedef pair<int,ListNode*> pairint;
ListNode* sortList(ListNode* head) {
    if(!head)return head;
    priority_queue<pairint,vector<pairint>,greater<pairint>>q;
    ListNode*p=head;
    while(p){
        q.push({p->val,p});
        p=p->next;
    }
    head=q.top().second;
    q.pop();
    p=head;
    while(!q.empty()){
        p->next=q.top().second;
        q.pop();
        p=p->next;
    }
    p->next=NULL;
    return head;
}

如果要求常数级空间,则需要快慢指针归并法

1找中点middle

2cut

3merge

其中middle和merge都需要编写对应函数封装

快慢指针用来寻找中点,cut过程为断开中点左右节点防止乱序

public ListNode sortList(ListNode head) {
    // 1、递归结束条件
    if (head == null || head.next == null) {
        return head;
    }

    // 2、找到链表中间节点并断开链表 & 递归下探
    ListNode midNode = middleNode(head);
    ListNode rightHead = midNode.next;
    midNode.next = null;

    ListNode left = sortList(head);
    ListNode right = sortList(rightHead);

    // 3、当前层业务操作(合并有序链表)
    return mergeTwoLists(left, right);
}

//  找到链表中间节点(876. 链表的中间结点)
private ListNode middleNode(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    ListNode slow = head;
    ListNode fast = head.next.next;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }

    return slow;
}

// 合并两个有序链表(21. 合并两个有序链表)
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode sentry = new ListNode(-1);//假的头结点
    ListNode curr = sentry;

    while(l1 != null && l2 != null) {
        if(l1.val < l2.val) {
            curr.next = l1;
            l1 = l1.next;
        } else {
            curr.next = l2;
            l2 = l2.next;
        }

        curr = curr.next;
    }
	//有一方遍历完直接接上另一个链表的后段
    curr.next = l1 != null ? l1 : l2;
    return sentry.next;
}

t13合并k个升序链表

使用递归,每次合并两个链表并将新的链表插入list中

需要特判输入为空的情况,需要用到stl的部分函数

ListNode* mergeKLists(vector<ListNode*>& lists) {
    if(lists.size()==0)return NULL;
    if(lists.size()==1)return lists[0];
    ListNode* l1 = lists[0];
    ListNode* l2 = lists[1];
    lists.erase(lists.begin(),lists.begin()+2);
    ListNode* dm = new ListNode(-1);
    ListNode* cur = dm;
    
    while(l1!=NULL&&l2!=NULL){
        if(l1->val>l2->val){
            cur->next=l2;
            l2=l2->next;
        }else{
            cur->next=l1;
            l1=l1->next;
        }
        cur=cur->next;
    }
    cur->next=l1==NULL?l2:l1;
    lists.push_back(dm->next);
    return mergeKLists(lists);
}

t14LRU缓存

需要用到双向链表加哈希表的算法

需要自己实现双向链表,因此自己写一个struct

struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

使用map储存值和节点,用来get和put的时候使用

初始化LRU缓存时,创建链表需要一个头结点和尾节点,不保存任何值

get时,通过map的count查找,如果不存在则返回-1,如果存在则用map找到对应节点,将节点移到头部

put时,通过map的count查找,如果不存在则创建新的节点,添加至双向链表的头部,再检查是否超出容量,若超出则从尾结点开始删除直到符合容量,如果存在则直接修改,将节点移到头部

超长超麻烦

struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    int capacity;
    int size;
    DLinkedNode*head;
    DLinkedNode*tail;
    unordered_map<int,DLinkedNode*>map;
public:
    LRUCache(int _capacity):capacity(_capacity),size(0) {
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        if(!map.count(key))return -1;
        movetohead(map[key]);
        return map[key]->value;
    }
    
    void put(int key, int value) {
        if(!map.count(key)){
            DLinkedNode* node = new DLinkedNode(key,value);
            map[key]=node;
            addtohead(node);
            size++;
            if(size>capacity){
                DLinkedNode* removed=removetail();
                map.erase(removed->key);
                delete removed;
                size--;
            }
        }else{
            map[key]->value=value;
            movetohead(map[key]);
        }
    }
    void deletenode(DLinkedNode*node){
        node->prev->next=node->next;
        node->next->prev=node->prev;
    }
    void movetohead(DLinkedNode*node){
        deletenode(node);
        addtohead(node);
    }
    DLinkedNode* removetail(){
        DLinkedNode* node = tail->prev;
        deletenode(node);
        return node;
    }
    void addtohead(DLinkedNode*node){
        head->next->prev=node;
        node->next=head->next;
        node->prev=head;
        head->next=node;
    }
};

8二叉树

t1二叉树的中序遍历

递归解决,先向答案中输入root的值,再利用递归输入左子树再输入右子树最后返回

vector<int> inorderTraversal(TreeNode* root) {
    if(root==NULL)return{};
    vector<int>ans;
    vector<int>left=inorderTraversal(root->left);
    vector<int>right=inorderTraversal(root->right);
    ans.insert(ans.end(),left.begin(),left.end());
    ans.push_back(root->val);
    ans.insert(ans.end(),right.begin(),right.end());
    return ans;
}

迭代法则是用栈来做

t2二叉树的最大深度

同样是用递归去遍历左右子树

int rtdepth(TreeNode* root,int depth){
    if(!root)return depth-1;
    return max(rtdepth(root->left,depth+1),rtdepth(root->right,depth+1));
}
int maxDepth(TreeNode* root) {
    return rtdepth(root,1);
}

t3翻转二叉树

同样使用递归解决即可,先翻转子树再翻转根

TreeNode* invertTree(TreeNode* root) {
    if(!root)return root;
    TreeNode*left=root->left;
    TreeNode*right=root->right;
    invertTree(left);
    invertTree(right);
    root->left=right;
    root->right=left;
    return root;
}

t4对称二叉树

我的想法是从根节点的左右子树开始用两个中序遍历检查

递归思路是检查两个指针节点是否相等,再检查左节点的左子树和右节点的右子树,左节点的右子树和右节点的左子树是否相等

bool checklr(TreeNode*left,TreeNode*right){
    if(!left&&!right)return true;
    else if(!left)return false;
    else if(!right)return false;

    if(left->val!=right->val)return false;
    else return checklr(left->left,right->right)&&checklr(left->right,right->left);
}
bool isSymmetric(TreeNode* root) {
    if(!root)return true;
    TreeNode* ptrl = root->left;
    TreeNode* ptrr = root->right;
    return checklr(ptrl,ptrr);
}

t5二叉树的直径

看评论的思路是,对每个节点,计算其左右子树深度,计算经过该节点的路径最大值(路径不包括其根节点)和其左右子树路径最大值,使用递归遍历所有节点,返回该节点和左右子树路径最大值的最大值

int diameterOfBinaryTree(TreeNode* root) {
    if(!root||!root->left&&!root->right)return 0;
    int leftmax = diameterOfBinaryTree(root->left);
    int rightmax = diameterOfBinaryTree(root->right);
    int cmax = rtdepth(root->left,1)+rtdepth(root->right,1);

    return max(max(leftmax,rightmax),cmax);
}
int rtdepth(TreeNode* root,int depth){
    if(!root)return depth-1;
    return max(rtdepth(root->left,depth+1),rtdepth(root->right,depth+1));
}

使用DFS进行查找,每个节点向下延伸,如果当前节点是null则返回负一,节点向下dfs时自动加一,每个节点使用max更新全局的ans,

int ans;
int dfs(TreeNode* node){
    if(node==nullptr)return -1;
    int llen = dfs(node->left)+1;
    int rlen = dfs(node->right)+1;
    ans = max(ans,llen+rlen);
    return max(llen,rlen);
}
int diameterOfBinaryTree(TreeNode* root) {
    ans = 0;
    dfs(root);
    return ans;
}

t6二叉树的层级遍历,逐层,从左向右访问

解法:正常的BFS,不过每次使用一个n来确定当前层拥有的对象数量,遍历一次之后获得一个res用来存入最终答案,再确定下一个n用来遍历

vector<vector<int>> levelOrder(TreeNode* root) {
    deque<TreeNode*> clist;
    vector<vector<int>>ans;
    if(root)clist.push_back(root);
    while(!clist.empty()){
        int n = clist.size();//!!!重要部分
        vector<int> res;
        for(int i = 0;i<n;i++){//!!!重要部分
            TreeNode* p = clist.front();
            clist.pop_front();
            res.push_back(p->val);
            if(p->left)clist.push_back(p->left);
            if(p->right)clist.push_back(p->right);
        }
        ans.push_back(res);
    }
    return ans;

t7将有序数组转化为二叉搜索树

一半给左子树一半给右子树

使用递归即可,中间值作为根节点,中间值左边做左子树,同理得到右子树

TreeNode* sortedArrayToBST(vector<int>& nums) {
    return buildTree(nums, 0, nums.size() - 1);
}

TreeNode* buildTree(vector<int>& nums, int left, int right) {
    if (left > right) return nullptr;

    int mid = left + (right - left) / 2;
    TreeNode* root = new TreeNode(nums[mid]);

    root->left = buildTree(nums, left, mid - 1);
    root->right = buildTree(nums, mid + 1, right);

    return root;
}

t8验证二叉搜索树

不是我喜欢的题,直接递归,难点在于右子树的所有节点都大于,左子树的所有节点都小于

!中序遍历升序即可!使用递归进行中序遍历的比较

long long pre = LLONG_MIN;
bool isValidBST(TreeNode* root) {
    if(root==nullptr)return true;
    if(!isValidBST(root->left))return false;//左
    if(root->val<=pre)return false;//中
    pre = root->val;
    return isValidBST(root->right);//右
}

t9二叉搜索树中第k小的元素

中序遍历然后找第k个(神人)

我的解法是用一个pre作为序号,遍历到序号为k是设置答案即可

int pre = 0;
int ans = INT_MAX;
int kthSmallest(TreeNode* root, int k) {
    checkans(root,k);
    return ans;
}
void checkans(TreeNode*root,int k){
    if(!root)return;
    checkans(root->left,k);
    pre++;
    if(pre==k)ans=root->val;
    checkans(root->right,k);
}

t10二叉树的右视图

解法应该和层级遍历类似,只不过只返回最右侧内容

vector<int> rightSideView(TreeNode* root) {
    deque<TreeNode*> nq;
    vector<int>ans;
    if(root)nq.push_back(root);
    while(!nq.empty()){
        int n =nq.size();
        for(int i = 0;i<n;i++){
            TreeNode* p = nq.front();
            nq.pop_front();
            if(p->left)nq.push_back(p->left);
            if(p->right)nq.push_back(p->right);
            if(i==n-1)ans.push_back(p->val);
        }
    }
    return ans;
}

t11二叉树展开为链表

结果要和先序遍历相同,即根左右

一直循环,方法是将根节点的左子树根,接到根节点右孩子的位置上,再将右子树根接到原左子树最右下的节点的右子树上,接下来再使用同样的方法处理根节点的右孩子,直到根节点为空,就可以完成展开了,但是这个方法需要创建指针用来查找节点,所以空间复杂度不为o1

void flatten(TreeNode* root) {
    while(root){
        if(!root->left)root=root->right;
        else{
            TreeNode* pre=root->left;
            while(pre->right){
                pre=pre->right;
            }
            pre->right=root->right;
            root->right=root->left;
            root->left=nullptr;
            root=root->right;
        }
    }
}

空间复杂度为1,需要使用递归后序遍历(右、左、中),然后维护一个pre,函数循环遍历当前节点时,pre就指向左节点了,且右子树也已经访问过,其根节点已经被左子树的最右节点指向了,这样就用非常简洁且空间复杂度低的方法解决了问题

虽然这道题需要使用的是先序遍历解题,后序遍历实际上是先序遍历倒过来,使用pre记录节点就可以使最终结果成为先序遍历

TreeNode* pre;
void flatten(TreeNode* root) {
    if(root==nullptr)return;
    flatten(root->right);
    flatten(root->left);
    root->right=pre;
    root->left=nullptr;
    pre=root;
}

t12从前序和中序遍历序列构造二叉树

先序遍历是根左右,中序遍历是左根右

这里需要用到递归和循环(确定根节点在中序序列中位置),将左子树的序列提出来构造树,再将右子树的序列提出来构造树

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    TreeNode* root=bt(preorder,inorder,0,preorder.size()-1,0,inorder.size()-1);
    return root;
}
TreeNode* bt(vector<int>& preorder, vector<int>& inorder,int pl,int pr,int il,int ir){
    if(pl>pr)return nullptr;
    TreeNode* root=new TreeNode(preorder[pl]);
    int i = il;
    while(inorder[i]!=preorder[pl])i++;
    root->left=bt(preorder,inorder,pl+1,pl+i-il,il,i-1);
    root->right=bt(preorder,inorder,pl+i-il+1,pr,i+1,ir);
    return root;
}

t13路径总和III

看评论有一个暴力法符合我的思路,将每个节点作为根进行查找,再递归其子节点,将所有结果相加

int pathSum(TreeNode* root, int targetSum) {
    if(!root)return 0;
    return dfs(root,targetSum)+pathSum(root->left,targetSum)+pathSum(root->right,targetSum);
}
int dfs(TreeNode* root,long long target){
    if(!root)return 0;
    int cnt = 0;
    if(root->val==target)cnt++;
    cnt+=dfs(root->left,target-root->val);
    cnt+=dfs(root->right,target-root->val);
    return cnt;
}

t14二叉树的最近公共祖先

通过递归对二叉树进行先序遍历,当遇到节点 p 或 q 时返回。从底至顶回溯,当节点 p,q 在节点 root 的异侧时,节点 root 即为最近公共祖先,则向上返回 root 。

所以首先递归的出口是当前节点是p或者q,或者当前节点是null,三种情况都直接返回root即可

再递归left节点和right节点,获取返回值,分为四种情况

如果left和right都有值,说明当前节点是其公共祖先,返回当前节点

如果left或right有值,说明left或right返回的值是公共祖先,返回公共祖先

如果都没值,说明当前子树没有pq节点,返回NULL

TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    if(root==p||root==q||!root)return root;
    TreeNode* l = lowestCommonAncestor(root->left,p,q);
    TreeNode* r = lowestCommonAncestor(root->right,p,q);
    if(l&&r)return root;
    else if(l)return l;
    else if(r)return r;
    else return NULL;
}

t15二叉树中的最大路径和

使用全局变量保存最大路径和,每个节点作为路径根节点,左右子树dfs,每个节点都更新一次最大路径和

过于复杂,时间复杂度和空间复杂度都超标

最优解还是采取dfs,优先递归左右子树得到含子树根的路径最大和,每次递归都更新总的最大和,并返回左右子树最大和中更大的一方和根相加的值

int maxs = INT_MIN;
int maxPathSum(TreeNode* root) {
    dfs(root);
    return maxs;
}
int dfs(TreeNode* root){
    if(!root)return 0;
    int lval=dfs(root->left);
    int rval=dfs(root->right);
    maxs=max(maxs,lval+rval+root->val);
    return max(max(lval,rval)+root->val,0);
}

9图论

t1岛屿数量

解法是使用一个燃烧递归函数,将相连的岛屿地块全部燃烧掉,主函数内遍历所有地块,如果有岛屿地块则cnt加一并启动燃烧函数消灭该岛屿

void check(vector<vector<char>>&grid,int i,int j){
    if(i<0||i>grid.size()-1||j<0||j>grid[0].size()-1||grid[i][j]!='1')return;
    grid[i][j]='2';
    check(grid,i-1,j);
    check(grid,i,j-1);
    check(grid,i+1,j);
    check(grid,i,j+1);
}
int numIslands(vector<vector<char>>& grid) {
    int cnt = 0;
    for(int i = 0;i<grid.size();i++){
        for(int j =0;j<grid[0].size();j++){
            if(grid[i][j]=='1'){
                check(grid,i,j);
                cnt++;
            } 
        }
    }
    return cnt;
}

t2腐烂的橘子,BFS

感觉类似于燃烧地块,但是有时间记录的要求

暴力解法个人思路是遍历整个网格t遍,每次让腐烂橘子进行感染,当遍历完还有新鲜橘子时,再次遍历,直到遍历完一个新鲜橘子都没有为止,但是限制条件非常多

这里使用广度优先遍历,也就是层级遍历,初始化时,找出所有腐烂的橘子作为第0层,进行BFS遍历,同时记录所有新鲜的橘子,当BFS结束后如果仍存在新鲜橘子,说明存在无法污染的橘子,返回-1,有一个特殊情况是一个橘子也没有,由于cnt初始值为-1(-1是由于最后一次虽然所有橘子都腐烂了,队列里仍存了最后一次腐烂的橘子,所以cnt会多1个要在初始值中扣除),要将cnt重置为0来符合该特殊情况。

int orangesRotting(vector<vector<int>>& grid) {
    deque<pair<int,int>>q;
    int fcnt = 0;
    for(int i = 0 ;i<grid.size();i++){
        for(int j = 0;j<grid[0].size();j++){
            if(grid[i][j]==1)fcnt++;
            else if(grid[i][j]==2)q.push_back({i,j});
        }
    }
    int cnt = -1;
    while(!q.empty()){
        cnt++;
        int n =q.size();
        for(int k =0;k<n;k++){
            int i =q.front().first;
            int j =q.front().second;
            q.pop_front();
            if(i>0&&grid[i-1][j]==1){
                grid[i-1][j]=2;
                fcnt--;
                q.push_back({i-1,j});
            }
            if(j>0&&grid[i][j-1]==1){
                grid[i][j-1]=2;
                fcnt--;
                q.push_back({i,j-1});
            }
            if(i<grid.size()-1&&grid[i+1][j]==1){
                grid[i+1][j]=2;
                fcnt--;
                q.push_back({i+1,j});
            }
            if(j<grid[0].size()-1&&grid[i][j+1]==1){
                grid[i][j+1]=2;
                fcnt--;
                q.push_back({i,j+1});
            }
        }
    }
    if(fcnt!=0)return -1;
    else if(cnt==-1)return 0;
    else return cnt;
}

t3课程表,拓扑排序题

类似BFS,使用一个队列储存入度为0的课,再将和这些课相关的课入度减1,再次储存入度为0的课,如果没有可以储存的课,但是课还没有选完则说明无法学完所有课

代码使用irate保存入度,rel作为邻接表,实现课出栈时,对以其为先修课的课程入度-1的操作

使用queue栈来保存入度为0的课程

bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
    vector<int>irate(numCourses,0);
    vector<vector<int>>rel(numCourses,vector<int>{});
    for(auto &c :prerequisites){
        irate[c[0]]++;
        rel[c[1]].push_back(c[0]);
    }        
    queue<int>q;
    for(int i =0;i<numCourses;i++){
        if(irate[i]==0)q.push(i);
    }
    int cnt = 0;
    while(!q.empty()){
        cnt++;
        int fin=q.front();
        q.pop();
        for(auto &c :rel[fin]){
            irate[c]--;
            if(irate[c]==0)q.push(c);
        }
    }
    return cnt==numCourses;
}

t4实现Trie(前缀树)

实际上是实现一个多叉树,每个节点有26个分支

所以需要的属性有isEnd,用来确认是否可以作为单词结尾判断,trie* next[26]用来指向下一个节点

插入就是从头部开始查找,如果next[c-'a']存在则可以将节点跳转到该节点并继续查找下一个,如果不存在则为该next new一个新的节点对象再跳转,直到遍历完需要插入的单词

查询和插入类似,如果next存在则跳转,不存在则返回false,如果遍历完了则返回node->isEnd,判断是否是单词结尾

是否有该前缀和查询一样,只不过遍历完了直接返回true,不用判断是否是单词结尾

class Trie {
private:
    bool isEnd;
    Trie* next[26];
public:
    Trie() {
        isEnd = false;
        memset(next,0,sizeof(next));
    }
    
    void insert(string word) {
        Trie* node = this;
        for(char c:word){
            if(node->next[c-'a']==NULL){
                node->next[c-'a']=new Trie();
            }
            node = node->next[c-'a'];
        }
        node->isEnd = true;
    }
    
    bool search(string word) {
        Trie*node = this;
        for(char c:word){
            if(node->next[c-'a']==NULL)return false;
            node=node->next[c-'a'];
        }
        return node->isEnd;
    }
    
    bool startsWith(string prefix) {
        Trie* node = this;
        for(char c :prefix){
            if(node->next[c-'a']==NULL)return false;
            node=node->next[c-'a'];
        }
        return true;
    }
};

10回溯

t1全排列,经典回溯法

使用回溯递归,第i层,将i和i之后的所有位置进行交换,交换后递归下一层,再交换回来,和下一个位置进行交换

使用一个全局变量储存答案,递归的出口在递归到第n+1层,即超出数组大小之后,将当前的数组存入答案,return寻找下一个答案;

vector<vector<int>>ans;
vector<vector<int>> permute(vector<int>& nums) {
    int n =nums.size();
    dfs(nums,n,0);
    return ans;
}
void dfs(vector<int>&nums,int n,int i){
    if(i==n){
        ans.push_back(nums);
    }else{
        for(int j = i;j<n;j++){
            swap(nums[i],nums[j]);
            dfs(nums,n,i+1);
            swap(nums[i],nums[j]);
        }
    }
}

t2子集,经典回溯法

使用回溯递归,第i层决定是否取第i个值

vector<vector<int>>ans;
vector<vector<int>> subsets(vector<int>& nums) {
    vector<int>tmp;
    dfs(nums,tmp,0);
    return ans;
}
void dfs(vector<int>&nums,vector<int>&tmp,int i){
    if(i==nums.size()){
        ans.push_back(tmp);
    }else{
        tmp.push_back(nums[i]);
        dfs(nums,tmp,i+1);
        tmp.pop_back();
        dfs(nums,tmp,i+1);
    }
}

t3电话号码的字母组合

回溯法的经典模板,求全排列的类似解法,主要还是每个电话号码能够对应多个字母,需要建表来提取对应数字对应的字母

image-20250730110913975

vector<string>ans;
string mp[8]={"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> letterCombinations(string digits) {
    if(digits.length()==0)return ans;
    string tmp;
    dfs(digits,0,tmp);
    return ans;
}
void dfs(string digits,int i,string tmp){
    if(i==digits.length()){
        ans.push_back(tmp);
        return;
    }else{
        for(char c :mp[digits[i]-'2']){
            tmp.push_back(c);
            dfs(digits,i+1,tmp);
            tmp.pop_back();
        }
    }
}

t4组合总和

数组中的数字每个都可以取任意数量,同样使用经典回溯法模板进行递归

重点在于加上当前层值之后dfs中给的i仍然是当前层,因为如果dfs返回,要么是i到上限,要么是已经把后续全部为i层的值遍历完了,这时候pop掉这个值再向下层走即可

vector<vector<int>>ans;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<int>tmp;
    dfs(candidates,target,0,0,tmp);
    return ans;
}
void dfs(vector<int>&candidates,int target,int sum,int i,vector<int>&tmp){
    if(i==candidates.size()||sum>=target){
        if(sum==target)ans.push_back(tmp);
        return;
    }else{
        tmp.push_back(candidates[i]);
        sum+=candidates[i];
        dfs(candidates,target,sum,i,tmp);
        //dfs(candidates,target,sum,i+1,tmp);
        tmp.pop_back();
        sum-=candidates[i];
        dfs(candidates,target,sum,i+1,tmp);
    }
}

t5括号生成

也是使用经典的回溯法模板,生成左括号和右括号,使用两个int来标记左右括号的数量如果生成的字符串长度为2*n说明符合条件并且已经结束,存入答案,i小于n时可以生成左括号,i大于j时可以生成右括号

vector<string>ans;
vector<string> generateParenthesis(int n) {
    string tmp;
    dfs(n,0,0,tmp);
    return ans;
}
void dfs(int n,int i,int j,string tmp){
    if(tmp.length()==2*n){
        ans.push_back(tmp);
        return;
    }else{
        if(i<n){
            tmp.push_back('(');
            dfs(n,i+1,j,tmp);
            tmp.pop_back();
        }
        if(i>j){
            tmp.push_back(')');
            dfs(n,i,j+1,tmp);
            tmp.pop_back();
        }
    }
}

t6单词搜索

单纯折磨人这题,时间复杂度不管怎么样都可能变得很高,因为要检索整个网格

不过做法还是单纯的dfs加回溯法,感觉回溯和dfs纯纯绑定

int rows;
int cols;
bool exist(vector<vector<char>>& board, string word) {
    rows = board.size();
    cols = board[0].size();
    //从任意位置出发
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if (dfs(board, word, i, j, 0)) return true;
        }
    }
    return false;
}
bool dfs(vector<vector<char>>&board,string word,int i,int j,int k){
    //使用word[k]作为当前位置字符的暂存,如果取了当前字符,就将其改成\0防止重复访问,这样就不用维护一个vis表
    if (i >= rows || i < 0 || j >= cols || j < 0 || board[i][j] != word[k]) return false;
    if(k==word.size()-1)return true;
    board[i][j]='\0';
    bool res = dfs(board,word,i+1,j,k+1)||dfs(board,word,i,j+1,k+1)||dfs(board,word,i-1,j,k+1)||dfs(board,word,i,j-1,k+1);
    board[i][j]=word[k];
    return res;
}

t7分割回文串

给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回溯法暴力枚举,记得tmp一定要传入地址,string s 也可以传入地址,这样会极大降低时间和空间复杂度因为不需要复制

vector<vector<string>>ans;
bool check(string s,int l ,int r ){
    while(r>l&&s[l]==s[r]){
        l++;
        r--;
    }
    return l>=r;
}
void dfs(string s,vector<string>&tmp,int l){
    if(l==s.length()){
        ans.push_back(tmp);
        return;
    }
    for(int r = l;r<s.length();r++){
        if(check(s,l,r)){
            tmp.push_back(s.substr(l,r-l+1));
            dfs(s,tmp,r+1);
            tmp.pop_back();
        }
    }
}
vector<vector<string>> partition(string s) {
    vector<string>tmp;
    dfs(s,tmp,0);
    return ans;
}

官方题解给出的方法是使用动态规划计算某一区间内的字符串是否是回文串,再通过回溯法进行枚举和检查,检查后发现时间和空间复杂度和传入地址版的暴力枚举一样

t8N皇后

依稀记得可以使用n位二进制来控制位置,这里使用c++的解法是基于一个定理,rc的左对角线上所有点,其r-c相等,右对角线上所有点,其r+c相等,由于r-c可能为负数,因此需要使用r-c+n-1进行控制,传入col数组记录当前列是否被使用,传入diag1记录右对角线,diag2记录左对角线,使用回溯法进行枚举即可

vector<vector<string>>ans;

vector<vector<string>> solveNQueens(int n) {
    vector<int>tmp(n,0);
    vector<uint8_t>col(n,false);
    vector<uint8_t>diag1(n*2-1,false);
    vector<uint8_t>diag2(n*2-1,false);
    dfs(n,0,tmp,col,diag1,diag2);
    return ans;

}
void dfs(int n,int r,vector<int>&tmp,vector<uint8_t>&col,vector<uint8_t>&diag1,vector<uint8_t>&diag2){
    if(r==n){
        vector<string>board(n);
        for(int i =0;i<n;i++){
            board[i]=string(tmp[i],'.')+'Q'+string(n-1-tmp[i],'.');
        }
        ans.push_back(board);
        return;
    }else{
        for(int c = 0;c<n;c++){
            int rc=r-c+n-1;//左对角线
            if(!col[c]&&!diag1[r+c]&&!diag2[rc]){
                tmp[r]=c;
                col[c]=diag1[r+c]=diag2[rc]=true;
                dfs(n,r+1,tmp,col,diag1,diag2);
                col[c]=diag1[r+c]=diag2[rc]=false;
            }
        }
    }
}

11二分查找

t1搜索插入位置

二分查找一般是使用lr指针和mid中间值进行比较查找,前提是给出的数组是有序的

返回l的逻辑:因为如果上面的没有返回mid,说明最后一定是left>right从而跳出循环的,如果最后是right-1导致的left>right,说明原来的right位置是大于target的,所以返回原来的right位置即left位置;如果最后是left+1导致的left>right,说明是原来的的left=right这个位置小于target,而right能移动到这个位置,说明此位置右侧是大于target的,left现在加1就移动到了这样的位置,返回left即可

int searchInsert(vector<int>& nums, int target) {
    int l = 0,r=nums.size()-1;
    while(l<=r){
        int mid = (r+l)/2;
        if(nums[mid]==target)return mid;
        else if(nums[mid]>target)r=mid-1;
        else l=mid+1;
    }
    return l;
}

t2搜索二维矩阵

之前使用二叉树的解法是从右上角开始将整个矩阵视为二叉树,进行查找,

这里需要使用二分,而且每行的第一个整数一定大于前一行的最后一个整数,可以先二分找到对应行,再二分找到对应列

也可以将整个矩阵看做一个一维数组进行二分,效果是一样的

bool searchMatrix(vector<vector<int>>& matrix, int target) {
    int rl=0,rr=matrix.size()-1;//行
    int cl=0,cr=matrix[0].size()-1;//列
    while(rl<=rr){
        int midr=(rl+rr)/2;
        if(matrix[midr][cr]==target)return true;
        else if(matrix[midr][cr	]>target)rr=midr-1;
        else rl = midr+1;
    }
    if(rl==matrix.size())return false;
    while(cl<=cr){
        int midc = (cl+cr)/2;
        if(matrix[rl][midc]==target)return true;
        else if(matrix[rl][midc]>target)cr=midc-1;
        else cl = midc+1;
    }
    return false; 
}

t3在排序数组中查找元素的第一个和最后一个位置

初步想法还是二分两次,一次查找第一个位置,一次查找最后一个位置

实现如下,还要特判一下数组为空的情况;

vector<int> searchRange(vector<int>& nums, int target) {
    if(nums.empty())return{-1,-1};
    int l=0,r=nums.size()-1;
    while(l<=r){
        int mid = (l+r)/2;
        if(nums[mid]>target)r=mid-1;
        else l=mid+1;
    }
    //这里确定r是最右侧符合条件的,因此也有可能小于0
    if(r<0||nums[r]!=target)return{-1,-1};
    vector<int>ans={0,r};
    l=0;
    while(l<=r){
        int mid=(l+r)/2;
        if(nums[mid]>=target)r=mid-1;
        else l=mid+1;
    }
    ans[0]=l;
    return ans;
}

t4 搜索旋转排序数组

旋转排序数组,是由一个有序数组,以第k位作为开头,前k-1位移动到末尾组成

将其作为正常数组二分,会得到一个有序区间和一个非有序区间,只需要检查mid和left和right的比较结果就可以知道有序区间的位置,在有序区间内,检查target值是否存在,只需要检查这个区间左右的边界是否包含target即可,然后移动指针

int search(vector<int>& nums, int target) {
    int left = 0,right = nums.size()-1;
    while(left<=right){
        int mid = (left+right)/2;
        if(target==nums[mid])return mid;
        else{
            if(nums[left]<=nums[mid]){
                if(nums[left]<=target&&nums[mid]>target)right=mid-1;
                else left=mid+1;
            }else{
                if(nums[mid]<target&&nums[right]>=target)left=mid+1;
                else right=mid-1;
            }
        }
    }
    return -1;
}

t5寻找旋转排序数组中的最小值

寻找方法应该是找到原来的0位

取mid后,先将mid和ans比较,更新最小值

检查有序区间

如果lmid,midr是有序的,则最小值就是l,比较ans和l,更新最小值

如果lmid有序,midr部分有序,则最小值在midr内,将l向右移动

如果lmid无序,则最小值在lmid内,将r向左移动

int findMin(vector<int>& nums) {
    int l=0,r=nums.size()-1;
    int ans = INT_MAX;
    while(l<=r){
        int mid = (l+r)/2;
        if(nums[mid]<ans)ans=nums[mid];
        if(nums[l]<=nums[mid]){
            if(nums[mid]<=nums[r]){
                return min(ans,nums[l]);
            }else{
                l=mid+1;
            }
        }
        else r=mid-1;
    }
    return ans;
}

t6寻找两个正序数组的中位数

给定两个正序数组,找出其中位数

使用数学方法解题,给定的两个数组设为AB,将A在i处分隔,B在j处分隔(j是更长的数组中对应的位置,为了确保i不越界时j也不越界)

将i的左半和j的左半合成AB左半,i的右半和j的右半合成AB右半

当AB总长度是偶数时,要保证:

  • 左半部分长度等于右半部分,j = ( m + n ) / 2 - i,
  • 和左半部分最大值等于右半部分最小值, max ( A [ i - 1 ] , B [ j - 1 ]) <= min ( A [ i ] , B [ j ])

此时中位数是左半最大加右半最小再除以二,(max ( A [ i - 1 ] , B [ j - 1 ])+ min ( A [ i ] , B [ j ])) / 2

当AB总长为奇数时,要保证

  • 左半部分长度比右半部分大1,j = ( m + n + 1) / 2 - i
  • 左半部分最大值小于等于右半部分最小值,max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ]))

此时中位数是左半部分最大值,即左半比右半多出的一个数,max ( A [ i - 1 ] , B [ j - 1 ])

如何确定i,当取到一个i时,我们需要确定max ( A [ i - 1 ] , B [ j - 1 ]) <= min ( A [ i ] , B [ j ])是否成立,由于AB天然有序,所以只需要确认A[i]>=B[j-1]和A[i-1]<=B[j]

因此可以分为两种情况讨论

1A[i]<B[j-1]要保证j!=0,i!=m

此时需要增加i

2A[i-1]>B[j]要保证j!=n,i!=0

此时需要减少i

上边两种情况排除了边界值,需要单独考虑

1i=0或者j=0

j=0时,左半的最大值是i-1;i=0时,左半的最大值是j-1

2i=m或者j=n

i=m时,右半的最小值是j

j=n时,右半的最小值是i

最后,我们使用二分来增加i,保证num1的长度要小于num2即可

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int m=nums1.size();
    int n=nums2.size();
    if(m>n)return findMedianSortedArrays(nums2,nums1);

    int imin = 0,imax=m;
    while(imin<=imax){
        int i =(imin+imax)/2;
        int j =(m+n+1)/2-i;
        if(j!=0&&i!=m&&nums1[i]<nums2[j-1])imin=i+1;
        else if(i!=0&&j!=n&&nums1[i-1]>nums2[j])imax=i-1;
        else{
            int leftmax;
            if(j==0)leftmax=nums1[i-1];
            else if(i==0)leftmax=nums2[j-1];
            else leftmax = max(nums1[i-1],nums2[j-1]);

            if((m+n)%2==1)return leftmax;

            int rightmin;
            if(j==n)rightmin=nums1[i];
            else if(i==m)rightmin=nums2[j];
            else rightmin=min(nums1[i],nums2[j]);
            return (leftmax+rightmin)/2.0;
        }
    }
    return 0.0;
}

12栈

t1有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

使用栈来完成非常简单

bool check(char c){
    if(c=='('||c=='['||c=='{')return true;
    return false;
}
char rtc(char c){
    if(c==')')return'(';
    if(c==']')return'[';
    if(c=='}')return'{';
    return c;
}
bool isValid(string s) {
    stack<char>st;
    for(int i = 0;i<s.length();i++){
        if(check(s[i]))st.push(s[i]);
        else{
            if(!st.empty()&&rtc(s[i])==st.top())st.pop();
            else return false;
        }
    }
    return st.empty();
}

t2最小栈

设计一个栈,能够自动储存最小值,有一个额外要求,即除了给出的push数以外,额外辅助空间为O1

储存偏移量,输出top时检查top值是否为负数,负数说明当前值是最小值,直接输出,偏移量为整数输出偏移量+最小值,

push时入栈偏移量,当栈为空时需要push0进去,偏移量小于0时需要更新最小值

class MinStack {
private:
    long long minv;
    stack<long long>s;
public:
    MinStack() {
       minv=0;
    }
    
    void push(int val) {
        if(s.empty()){
            s.push(0LL);
            minv=val;
        }else{
            long long diff = val-minv;
            s.push(diff);
            if(diff<0)minv=val;
        }
    }
    
    void pop() {
        if(s.top()<0)minv-=s.top();
        s.pop();
    }
    
    int top() {
        if(s.top()<0)return minv;
        return s.top()+minv;
    }
    
    int getMin() {
        return minv;
    }
};

t3字符串解码

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

输入:s = "3[a]2[bc]"
输出:"aaabcbc"

思路是,当遇到数字时,更新乘数,遇到字母时,向res中加入字母,如果遇到[,则将乘数和res入栈,乘数是给后续的res使用的,而现在入栈的res将作为后续res的前缀

当遇到]时,开始出栈乘数和res(这个res改名为last_res),res=lastres+乘数*res即可

最后返回res

string decodeString(string s) {
    string res;
    int k;
    stack<string>ss;
    stack<int>mul;
    for(int i = 0;i<s.length();i++){
        if(s[i]>='0'&&s[i]<='9')k=k*10+s[i]-'0';
        else if(s[i]>='a'&&s[i]<='z')res+=s[i];
        else if(s[i]=='['){
            ss.push(res);
            mul.push(k);
            res.clear();
            k=0;
        }else{
            int ck=mul.top();
            mul.pop();
            string lr=ss.top();
            ss.pop();
            for(int j = 0;j<ck;j++){
                lr+=res;
            }
            res = lr;
        }
    }
    return res;
}

t4每日温度

给定一个tem温度数组,需要返回ans代表第i天时,下一个温度更高的数组出现在几天后

使用一个栈保存未记录时间的天数索引,当当前温度大于栈顶时,给栈顶索引记录天数并出栈

vector<int> dailyTemperatures(vector<int>& temperatures) {
    int n = temperatures.size();
    vector<int>ans(n,0);
    stack<int>s;
    for(int i = 0;i<n;i++){
        while(!s.empty()&&temperatures[i]>temperatures[s.top()]){
            ans[s.top()]=i-s.top();
            s.pop();
        }
        s.push(i);
    }
    return ans;
}

t5矩形图中的最大矩形

在一个非顺序的矩形图中,找到最大的矩形

同样是使用单调栈来解,使用单调栈储存下标,单调栈的储存逻辑是高度递增,当遇到h[i]高度小于单调栈顶时,开始出栈,计算栈内的最大矩形,计算方法是取栈顶高度,将栈顶出栈,再取宽度为栈顶到次栈顶距离(如果没有次栈顶,那就是栈顶位置到开头的距离),更新最大矩形大小,重复上述步骤直到hi大于栈顶高度,入栈hi

为了防止给出的例子单调递增,将数组尾部加上0;

int largestRectangleArea(vector<int>& heights) {
    int n =heights.size(),ans=0;
    heights.push_back(0);
    stack<int>st;
    for(int i = 0;i<n+1;i++){
        while(!st.empty()&&heights[i]<=heights[st.top()]){
            int ht=heights[st.top()];
            int weight=i;
            st.pop();
            if(!st.empty())weight-=st.top()+1; 

            ans=max(ans,ht*weight);

        }
        st.push(i);
    }
    return ans;
}

13堆

t1数组中的第K大个元素

题目放这里可能是想让我们用堆排序做,但是本身这道题有要求,时间必须为on,堆排序的时间复杂度是nlogn并不符合要求,这里采取题解的答案,使用哨兵划分和递归进行快排

(这里的递归其实相当于剪枝,并不会将划分后的所有元素进行排序,而是只取需要的部分,比如说,划分后,左侧有m个,右边有n个,m小于k,则在右边找第k-m,反之则在左边找第k)

使用随机数确定哨兵

这道题是第k大,就是倒序排序,大的在左边

int findKthLargest(vector<int>& nums, int k) {
    int pivot = nums[rand()%nums.size()];
    vector<int>small,big,equal;
    for(int i = 0;i<nums.size();i++){
        if(nums[i]<pivot)small.push_back(nums[i]);
        else if(nums[i]==pivot)equal.push_back(nums[i]);
        else big.push_back(nums[i]);
    }
    if(k<=big.size())return findKthLargest(big,k);
    else if(k<=big.size()+equal.size())return equal[0];
    else return findKthLargest(small,k-big.size()-equal.size());
}

t2前k个高频元素

返回一个数组中,出现频率前k高的元素

  • 求前 k 大,用小根堆,求前 k 小,用大根堆。

因此这题用小根堆,维护一个大小为k的小根堆,将数字用频率压入,如果频率小于堆顶则不入堆,否则压入堆并检查堆大小,如果超过k则丢掉堆顶

使用哈希表建立数字和频率的映射,使用优先队列和cmp函数建立小根堆,遍历哈希表,将所有的数字和对应频率取出,当小根堆没满时向内存入,存满时按照上方的压入规则进行压入

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>f;
    for(int num:nums){
        f[num]++;
    }
    priority_queue<pair<int,int>,vector<pair<int,int>>,decltype(&cmp)>q(cmp);
    for(auto& [num,count]:f){
        if(q.size()==k){
            if(q.top().second<count){
                q.pop();
                q.emplace(num,count);
            }
        }else{
            q.emplace(num,count);
        }
    }
    vector<int>ret;
    while(!q.empty()){
        ret.emplace_back(q.top().first);
        q.pop();
    }
    return ret;
}

t3数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。
  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

题解:

可以将数据流保存在一个列表中,并在添加元素时 保持数组有序

建立一个 小顶堆 A大顶堆 B ,各保存列表的一半元素,A保存较大的一半(m=N/2或N+1/2),B保存较小的一半(n=N/2或N-1/2)

中位数可仅根据 A,B 的堆顶元素计算得到

a1为A堆顶,b1为B堆顶,若m!=n则a1是中位数,否则中位数是(a1+b1)/2

addnum:m=n时向A中插入元素(新元素插入B,将B的堆顶插入A),m!=n时向B中插入元素(新元素插入A,将A的堆顶插入B)

findmedian:m=n时返回A堆顶+B堆顶除2,否则返回A堆顶

C++ 中 greater 为小顶堆, less 为大顶堆

private:
    priority_queue<int,vector<int>,greater<int>>A;
    priority_queue<int,vector<int>,less<int>>B;
public:
    MedianFinder() {
        
    }
    
    void addNum(int num) {
        if(A.size()!=B.size()){
            A.push(num);
            B.push(A.top());
            A.pop();
        }else{
            B.push(num);
            A.push(B.top());
            B.pop();
        }
    }
    
    double findMedian() {
        if(A.size()!=B.size()){
            return A.top();
        }else{
            return(A.top()+B.top())/2.0;
        }
    }

14贪心算法

t1买卖股票的最佳时机

使用profit记录最大利润,cost记录到目前为止的股票价格最小值,遍历时,对每个对象,更新cost(取cost和price[i]的较小值),更新profit(取price[i]-cost和profit的较大值),最后得到的profit为最大值

int maxProfit(vector<int>& prices) {
    int profit = 0;
    int cost = INT_MAX;
    for(int price : prices){
        cost = min(price,cost);
        profit=max(price-cost,profit);
    }
    return profit;
}

t2跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

题解:将每个起跳点和它的最大长度结合,记录最远距离,如果最远距离小于当前起跳点,那么永远无法达到,反之遍历完所有起跳点,证明可以达到终点

bool canJump(vector<int>& nums) {
    int dis = 0;
    for(int i = 0;i<nums.size();i++){
        if(i>dis)return false;
        dis = max(i+nums[i],dis);
    }
    return true;
}

t3跳跃游戏2

给定一个长度为 n0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

题解:每次都记录dis跳跃最远距离和一个end上次跳跃的最远距离,dis每个i都更新,而end只有当i到达end之后才更新,并且取dis的值

int jump(vector<int>& nums) {
    int dis=0;
    int end=0;
    int steps = 0;
    for(int i = 0;i<nums.size()-1;i++){
        dis=max(dis,nums[i]+i);
        if(end==i){
            end=dis;
            steps++;
        }
    }
    return steps;
}

题解2:也可以使用dp,

int jump(vector<int>& nums) {
    int dis=0;
    vector<int>times(nums.size(),0);
    for(int i = 0;i<nums.size();i++){
        if(i+nums[i]>dis){
            for(int j = dis+1;j<=i+nums[i]&&j<nums.size();j++){
                times[j]=times[i]+1;
            }
            dis=i+nums[i];
        }
    }
    return times[nums.size()-1];
}

t4划分字母区间

题解:贪心解法,使用一个表储存每个出现过的字符最后一次出现的位置,使用两个指针,一个指向字段开始,一个指向字段结束,遍历这个字段区间,如果出现某个字符的最后位置大于当前字段结束位置,则更新字段结束位置

每次遍历完一个字段区间,将其长度存入ans中

vector<int> partitionLabels(string s) {
    vector<int>ans;
    int lastp[26];
    for(int i = 0;i<s.length();i++){
        lastp[s[i]-'a']=i;
    }
    int start=0;
    int end;
    while(start<s.length()){
        end=lastp[s[start]-'a'];
        int p =start;
        while(p<=end){
            end=max(lastp[s[p]-'a'],end);
            p++;
        }
        ans.push_back(end-start+1);
        start=end+1;

    }
    return ans;
}

15动态规划

t1爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

题解:直接动规,dp[i]=dp[i-1]+dp[i-2],设定初始dp1=1,dp2=2即可

int climbStairs(int n) {
    vector<int> dp(n+1,0);
    dp[0]=1;
    dp[1]=2;
    for(int i = 2;i<n;i++){
        dp[i]=dp[i-1]+dp[i-2];
    }
    return dp[n-1];
}

t2杨辉三角

给定一个非负整数 numRows生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

第i行的第k个由第i-1行的第k个和k-1个构成,每行的开头和末尾是1

因此可知

dpik=dpi-1k-1+dpi-1k

vector<vector<int>> generate(int numRows) {
    vector<vector<int>>ans;
    int i = 0;
    while(i<numRows){
        i++;
        vector<int>tmp(i);
        tmp[0]=1;
        tmp[i-1]=1;
        for(int j=1;j<i-1;j++){
            tmp[j]=ans[i-2][j-1]+ans[i-2][j];
        }
        ans.push_back(tmp);
    }
    return ans;
}

t3打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

题解:dpi是到第i间房为止能偷到的最多金额,可得动规如下

dp0=nums0,dp1=max(nums0,nums1)

dp2=max(dp1,dp0+nums2)--------->dpi=max(dpi-1,dpi-2+numsi)

int rob(vector<int>& nums) {
    int n = nums.size();
    if(n==1)return nums[0];
    if(n==2)return max(nums[0],nums[1]);
    vector<int>dp(n);
    dp[0]=nums[0];
    dp[1]=max(nums[0],nums[1]);
    for(int i =2;i<n;i++){
        dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
    }
    return dp[n-1];
}

t4完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

题解:

数学解

  1. 任何正整数都可以拆分成不超过4个数的平方和 ---> 答案只可能是1,2,3,4
  2. 如果一个数最少可以拆成4个数的平方和,则这个数还满足 n = (4^a)*(8b+7) ---> 因此可以先看这个数是否满足上述公式,如果不满足,答案就是1,2,3了
  3. 如果这个数本来就是某个数的平方,那么答案就是1,否则答案就只剩2,3了
  4. 如果答案是2,即n=a2+b2,那么我们可以枚举a,来验证,如果验证通过则答案是2
  5. 只能是3
public int numSquares(int n) {
    while(n % 4 == 0) n /= 4;  //判4
    if(n % 8 == 7) return 4;

    for(int i = 0; i * i <= n; ++i) {  //判1
        if(n - i * i == 0) return 1;
    }

    for(int i = 0; i * i < n; ++i) {   //判2
        for(int j = 0; j * j < n; ++j) {
            if(n - i * i - j * j == 0) return 2;
        }
    }
    return 3;   //4、1、2,都不是,直接返回3
}

动规解:

dp0=0,dp1=1,dp2=2,dp3=3,dp4=1,dp5=1+1=2,dp6=1+2=3,dp7=1+3=4,dp8=2

假如一个数是完全平方数,则dpi=1

否则dp[i] =min(dp[i], dp[i-j*j] + 1),(j取1到j^2<i)

int numSquares(int n) {
    vector<int>dp(n+1,0);
    for(int i =1;i<=n;i++){
        int minn=INT_MAX;
        for(int j = 1;j*j<=i;j++){
            minn=min(minn,dp[i-j*j]);
        }
        dp[i]=minn+1;
    }
    return dp[n];
}

t5零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

dpi=min(dpi,dpi-j+1)(j取任意硬币面额,但i>=j)

由于这里有可能会出现无法凑齐硬币的情况,需要用一个固定的值进行判断,将dpi的初始值设为amount+1,

每次取min时,取dpi和dpi-j的小值,等到取值结束再根据dpi的值选择是否加1,这样可以确保如果没有解,dpi的值仍旧是这个固定值,最后返回时也采用该固定值进行判断,如果变了则说明有解,否则输出-1

int coinChange(vector<int>& coins, int amount) {
    vector<int>dp(amount+1,amount+1);
    dp[0]=0;
    for(int i = 1;i<amount+1;i++){
        for(int j = 0;j<coins.size();j++){
            if(i>=coins[j])dp[i]=min(dp[i],dp[i-coins[j]]);
        }
        if(dp[i]!=amount+1)dp[i]++;
    }
    return dp[amount]==amount+1?-1:dp[amount];
}

t6单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

题解:抽象为爬楼梯,遍历整个s,用worddict中的元素作为步数,同时需要满足word要和s的子串相同

bool wordBreak(string s, vector<string>& wordDict) {
    vector<bool>dp(s.length()+1,false);
    dp[0]=true;

    for(int i =1;i<s.length()+1;i++){
        for(int j = 0;j<wordDict.size();j++){
            if(i>=wordDict[j].length()&&!dp[i]){
                if(s.substr(i-wordDict[j].length(),wordDict[j].size())==wordDict[j])dp[i]=dp[i-wordDict[j].length()];
            }
        }
    }
    return dp[s.length()];
}

t7最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

题解:on2的时间题解,遍历到某个数时,从当前数开始向前遍历,直到numsj小于numsi,取dpi=dpj+1,若没有则设dpi为1,dp[i] 的值代表 numsnums[i] 结尾的最长子序列长度

int lengthOfLIS(vector<int>& nums) {
    vector<int>dp(nums.size(),1);

    int ans = 1;

    for(int i =1;i<nums.size();i++){
        for(int j = i-1;j>=0;j--){
            if(nums[j]<nums[i]){
                dp[i]=max(dp[j]+1,dp[i]);
                ans=max(ans,dp[i]);
            }
        }

    }
    return ans;
}

onlogn的时间题解,遍历到某个数时,采取二分查找的方式找j即可,维护一个列表 tail**s,其中每个元素 tail**s[k] 的值代表 长度为 *k*+1 的子序列尾部元素的值

int lengthOfLIS(vector<int>& nums) {
    vector<int>tails(nums.size());
    int res = 0;
    for(int num:nums){
        int l=0,r=res;
        //取l的原因,退出循环时的tails[l]刚好大于或者等于num,可知tails[l-1]刚好小于num,因此取l,将tails[l]改为num,使num作为长度为l+1的子序列的末尾值
        while(l<r){
            int m = (l+r)/2;
            if(tails[m]<num)l=m+1;
            else r=m;
        }
        tails[l]=num;
        if(l==res)res++;
    }
    return res;
}

t8乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

题解:

当numsi为正数时,当前的最大乘积=numsi*前面的最大乘积,或numsi本身

当numsi为负数时,当前的最大乘积=numsi *前面的最小乘积,或numsi本身

而当前的最小乘积和上面刚好相反

用maxi,mini和res来分别保存最大乘积,最小乘积和结果,用max和min无视numsi的正负

得到的代码如下

int maxProduct(vector<int>& nums) {
    int maxi=1,mini=1,res=-INT_MAX;
    for(int i = 0;i<nums.size();i++){
        int tmp = maxi;
        maxi=max(nums[i],max(maxi*nums[i],mini*nums[i]));
        mini=min(nums[i],min(tmp*nums[i],mini*nums[i]));
        res = max(res,maxi);
    }
    return res;
}

t9分割等和子集

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

「恰好装满」型 0-1 背包

dpij表示在0-i中选取一个子序列,满足占用空间为j的条件

递归方程式如下

f[i+1][j]=f[i][j−nums[i]]∨f[i][j]

由于可以优化掉i的部分

也就是采用fj,j是当前占用背包空间,fj即背包空间为j能否成立

我们遍历每一个物品x,采用s2储存到目前为止子序列的最大占用空间,这个空间不超过s/2,然后j遍历s2到x(防止溢出),fj成立当fj-x成立

每次遍历完都检查fs/2是否true,如果true则直接返回压缩时间

bool canPartition(vector<int>& nums) {
    int s =reduce(nums.begin(),nums.end());
    if(s%2)return false;
    s/=2;
    vector<int>f(s+1);
    f[0]=true;
    int s2 = 0;
    for(int x :nums){
        s2=min(s2+x,s);
        for(int j =s2;j>=x;j--){
            f[j]|=f[j-x];
        }
        if(f[s])return true;
    }
    return false;
}

t10最长有效括号

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

题解:

dpi是以si为结尾的最长有效括号子串的长度

当读到si时,有两种情况:

si是'(',si无法和前面组成有效的字符对,dpi=0

si是')',si可以和前面组成有效字符对,这里还可以分成两种情况:

1si-1是'(',则dpi=dpi-2+2;

2si-1是')',则需要向前寻找,如果dpi-1不为0,向前寻找dpi-1个位置,即i-dpi-1-1

如果si-dpi-1-1是'(',则配对, dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2

int longestValidParentheses(string s) {
    vector<int>dp(s.length(),0);
    int ans = 0;
    for(int i = 1;i<s.length();i++){
        if(s[i]==')'){
            if(s[i-1]=='('){
                dp[i]=2;
                if(i-2>=0)dp[i]+=dp[i-2];
            }else if(dp[i-1]>0){
                if(i-1-dp[i-1]>=0&&s[i-1-dp[i-1]]=='('){
                    dp[i]=dp[i-1]+2;
                    if(i-1-dp[i-1]-1>=0)dp[i]+=dp[i-1-dp[i-1]-1];
                }
            }
            ans = max(ans,dp[i]);
        }
    }
    return ans;
}

16多维动态规划

t1不同路径

需要的式子是dp[i][j]=dp[i][j-1]+dp[i-1][j]

int uniquePaths(int m, int n) {
    vector<vector<int>>dp(m,vector<int>(n));
    for(int i =0;i<m;i++){
        for(int j = 0;j<n;j++){
            if(i==0&&j==0)dp[i][j]=1;
            else if(i==0)dp[i][j]=dp[i][j-1];
            else if(j==0)dp[i][j]=dp[i-1][j];
            else dp[i][j]=dp[i][j-1]+dp[i-1][j];
        }
    }
    return dp[m-1][n-1];
}

t2最小路径和

给定一个包含非负整数的 *m* x *n* 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

题解:和t1一样,采取dp记录权值,移动的时候选择左边或上面的路径中权值小的那一条

int minPathSum(vector<vector<int>>& grid) {
    int m = grid.size();
    int n = grid[0].size();
    vector<vector<int>>dp(m,vector<int>(n));
    for(int i = 0;i<m;i++){
        for(int j = 0;j<n;j++){
            if(i==0&&j==0)dp[i][j]=grid[i][j];
            else if(i==0)dp[i][j]=dp[i][j-1]+grid[i][j];
            else if(j==0)dp[i][j]=dp[i-1][j]+grid[i][j];
            else dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j];
        }
    }
    return dp[m-1][n-1];
}

t3最长回文子串

给你一个字符串 s,找到 s 中最长的 回文 子串。

题解:dpij表示i到j是否是回文串,由于j是一定大于i的,因此动规矩阵只需要遍历上三角,时间复杂度是On^2

需要注意的是遍历顺序,要从i=n-1,i>=0,j=i,j<n开始遍历确保使用的dp[i+1][j-1]是已经遍历过的

还需要注意返回的是子串,所以需要保存left和right,以及一个用来判断是否更新的maxlenth

string longestPalindrome(string s) {
    int n = s.length();
    vector<vector<bool>>dp(n,vector<bool>(n,false));
    int maxlength = 0;
    int left = 0;
    int right = 0;
    for(int i = n-1;i>=0;i--){
        for(int j =i;j<n;j++){
            if(s[i]==s[j]){
                if(j-i<=1)dp[i][j]=true;
                else if(dp[i+1][j-1])dp[i][j]=true;
            }
            if(dp[i][j]&&j-i+1>maxlength){
                maxlength=j-i+1;
                left=i;
                right=j;
            }
        }
    }
    return s.substr(left,right-left+1);
}

t4最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

dpij是到t1的i-1和t2的j-1为止的最长子序列长度,当t1的i-1和t2的j-1相等时,dpij=dpi-1j-1+1,这是一定最长的结果

int longestCommonSubsequence(string text1, string text2) {
    int m = text1.length();
    int n = text2.length();
    vector<vector<int>>dp(m+1,vector<int>(n+1));
    for(int i =0;i<m;i++){
        for(int j = 0;j<n;j++){
            dp[i+1][j+1]=text1[i]==text2[j]?dp[i][j]+1:max(dp[i+1][j],dp[i][j+1]);
        }
    }
    return dp[m][n];
}

t5编辑距离

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

题解:dpij是w1前i个字符变为w2前j个字符所需的最少操作数

三种状态转移

增:dpij=dpij-1 +1

删:dpij=dpi-1j +1

改:dpij=dpi-1j-1 +1

如果w1i-1和w2j-1相同,那么不需要加一

不同的话,就需要从上面三种之中取最小值

int minDistance(string word1, string word2) {
    int m = word1.length();
    int n = word2.length();
    vector<vector<int>>dp(m+1,vector<int>(n+1));
    for(int i =0;i<=m;i++){
        dp[i][0]=i;
    }
    for(int j = 0;j<=n;j++){
        dp[0][j]=j;
    }
    for(int i = 1;i<=m;i++){
        for(int j = 1;j<=n;j++){
            dp[i][j]=min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]))+1;
            if(word1[i-1]==word2[j-1])dp[i][j]=min(dp[i][j],dp[i-1][j-1]);
        }
    }
    return dp[m][n];
}

17技巧

t1只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

题解:位运算,a(异或)a=0,0a=a,所以aab=b,将整个数组异或处理得到的结果就是只出现了一次的元素

int singleNumber(vector<int>& nums) {
    int sum = 0;
    for(int num : nums){
        sum^=num;
    }
    return sum;
}

t2多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

题解:

暴力方法是建表遍历,然后返回出现次数最多的元素

也可以排序然后返回n/2的对象,只需要排序一半即可

还有算法:

boyer-moore算法最简单理解方法:
假设你在投票选人 如果你和候选人(利益)相同,你就会给他投一票(count+1),如果不同,你就会踩他一下(count-1)当候选人票数为0(count=0)时,就换一个候选人,但因为和你利益一样的人占比超过了一半 不论换多少次 ,最后留下来的都一定是个和你(利益)相同的人。

这里使用该算法解题

int majorityElement(vector<int>& nums) {
    int count =0;
    int max = -1;
    for(int i = 0;i<nums.size();i++){
        if(count==0)max=nums[i];
        count+=max==nums[i]?1:-1;
    }
    return max;
}

t3颜色分类

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

题解:使用2个指针表示0和1的有序末端,读到一个数字时,先记录其值,再将其修改为2,如果它的值小于等于1,就需要将p1的末端修改为1,增加p1,如果它的值等于0,就需要将p0的末端修改为0,增加p0

image-20250810121844928

void sortColors(vector<int>& nums) {
    int p0 = 0, p1 = 0;
    for (int i = 0; i < nums.size(); i++) {
        int x = nums[i];
        nums[i] = 2;
        if (x <= 1) {
            nums[p1++] = 1;
        }
        if (x == 0) {
            nums[p0++] = 0;
        }
    }
}

t4下一个排列

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

题解:如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:

​ 我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
​ 我们还希望下一个数 增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:
在 尽可能靠右的低位 进行交换,需要 从后向前 查找
​ 将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
​ 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列

算法流程:

1从后向前 查找第一个 相邻升序 的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序
2在 [j,end) 从后向前 查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是上文所说的「小数」、「大数」
3将 A[i] 与 A[k] 交换
4可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序
5如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4

void nextPermutation(vector<int>& nums) {
    int j=nums.size()-1;
    int i=j-1;
    while(i>-1&&nums[i]>=nums[j]){
        j--;
        i--;
    }
    if(i!=-1){
        int k = nums.size()-1;
        while(k>=j&&nums[k]<=nums[i]){
            k--;
        }
        swap(nums[i],nums[k]);

    }
    
    int l=j,r=nums.size()-1;
    while(l<r){
        swap(nums[l],nums[r]);
        l++;
        r--;
    }
}

t5寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

题解:有环链表,快慢指针

以数组 [1,3,4,2,2] 为例,我们将数组下标 n 和数 nums[n] 建立一个映射关系 f(n),
其映射关系 n->f(n) 为:
0->1
1->3
2->4
3->2
4->2

image-20250810125825351

从理论上讲,数组中如果有重复的数,那么就会产生多对一的映射,这样,形成的链表就一定会有环路了,

使用p=num[p]来进行链表跳转即可

当s==f时,再出一个指针从链表头部开始向后和s一起向后推进,当两个指针相同时,两者处于环的入点

int findDuplicate(vector<int>& nums) {
    int s = 0,f=0,n=nums.size();;
    s=nums[s];
    f=nums[nums[f]];
    while(s!=f){
        s=nums[s];
        f=nums[nums[f]];
    }
    int pre1 = 0;
    int pre2 = s;
    while(pre1!=pre2){
        pre1=nums[pre1];
        pre2=nums[pre2];
    }
    return pre1;
}
posted on 2025-07-29 11:46  chycal  阅读(10)  评论(1)    收藏  举报