top100题-前50题

0 说明

约定
*: 表示这道题没有吃透或者一开始想不到思路或者没有用推荐解法需要改进的
r: 表示这道题在2022.7.1之前做过一次
?:表示拿到这道题没有任何思路,包括暴力思路

1 两数之和

考查: 数组
思路1: 双循环
思路2: 哈希表(推荐)
尽管可能出现重复数字,但是采用先问后存的方式避免重复使用同一位置上的数字问题
难度: easy

2 两数相加

题意: 给你两个用链表表示的100位数字,逆序存储, 问这两个数相加是多少?
考查: 链表 尾插法
思路1: 由于链表首存放低位数字,故低位数字已经对齐,两个链表相同位置数字可以直接相加。所以只需遍历
两个链表即可。
难度: easy
扩展1: 假设数字用链表表示,正序存储,那么这两个数字如何相加? 
答1: 使用栈,使得低位数字对齐。

3 不含重复字符的最长子串*

考察: 字符串 最值 滑动窗口
思路1: 双循环,从左到右扫描这个字符串,对于任意位置i,找到以第i个字符为首的不含重复字符的最长子
串。取取其中最大值即可。
思路2:  使用变形的滑动窗口,窗口维持一个性质: 不含重复字符.
思路3优化: 在思路2的基础上进一步优化(推荐)
难度: hard 
个人认为难点在于不好想到滑动窗口优化
为什么可以用滑动窗口呢? 因为不含重复字符的字符串其后缀也是不含重复字符的,所以我们可以通过判重的
方式在尾部添加字符找到最长子串,首部删除字符的方式找到每个字符的最长子串。
// 思路1实现
int lengthOfLongestSubstring(string s) {
    if(s=="") return 0;
    int ans=0;
    for(int i=0;i<s.size();++i){
        unordered_set<char> st;
        for(int j=i;j<s.size();++j){
            if(st.empty()||st.find(s[j])==st.end()){
                st.insert(s[j]);
            }else break;
        }
        ans=max(ans, (int)st.size());
    }
    return ans;
}
// 思路2实现(queue+set)
 int lengthOfLongestSubstring(string s) {
     if(s=="") return 0;
     queue<char> q;
     int ans=0;
     q.push(s[0]);
     int curId=1;
     unordered_set<int> st; // 判断重复
     st.insert(s[0]);
     while(curId<s.size()||!q.empty()){
         while(curId<s.size()&&st.find(s[curId])==st.end()){
             st.insert(s[curId]);
             q.push(s[curId]);
             ++curId;
         }
         char ch = q.front();
         q.pop();
         st.erase(ch);
         ans = max(ans, (int)q.size()+1);
     }
     return ans;
 }
 // 思路2优化(使用hh+tt双指针 set)
 int lengthOfLongestSubstring(string s) {
    if(s=="") return 0;
    int hh=0, tt=-1;
    int ans=0;
    unordered_set<char> st;
    for(hh=0;hh<s.size();++hh){
        while((tt+1)<s.size()&&!st.count(s[tt+1])){
                st.insert(s[tt+1]);
                ++tt;
        }
        ans =max(ans, (tt+1-hh));
        if(tt==s.size()-1) break; //优化1
        st.erase(s[hh]);
    }
    return ans;
}
 

4 寻找两个正序数组的中位数*r

考察: 数组 中位数
思路1: 采用双指针, 根据两个元素大小移动指针
思路2: 由于思路1存在大量重复代码,用函数简化了一波
思路3: (推荐) 
       和二分有区别,是二分的思想吧。大致思路就是从两个数组中选择两个数,根据这两个数的大小,删除
一部分数。这个选择肯定有讲究的。假设找第k个数,那么两个数组各自派出k/2个数,比较两个数组派出的末尾
数字的大小,因为较小的那个数至多是第k-1个数,所以这个较小数及其左侧数组都可以删去。
	这里还有三种特殊情况。
	1)两个数组有一个为空,好理解, 因为指针在前进。 
	2) k==1, 并且必须要特判。否则会出现nums1[-1]这种情况
	3)  k/2作为数组下标,发生越界
	
难度: hard,体现要求时间复杂度在log(M+N), 不然就是easy
// 思路2实现
 double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
     int n1=nums1.size();
     int n2 = nums2.size();
     bool flag=true; // true表示奇数
     if((n1+n2)%2==0) flag=false;
     int ans1=-1;
     int ans2=-1;
     int cnt=0;
     int pos1=(n1+n2+1)/2;
     int pos2=(n1+n2+1)/2+1;
     int p1=0,p2=0;
     while(p1<n1&&p2<n2){
         if(nums1[p1]<=nums2[p2]){
             helper(nums1, pos1, pos2, ans1, ans2, p1, cnt, flag); 
         }else{
             helper(nums2, pos1, pos2, ans1, ans2, p2, cnt, flag);
         }
     }
     if(flag&&ans1!=-1) return ans1*1.0;
     if(!flag&&ans1!=-1&&ans2!=-1) return (ans1+ans2)*1.0/2;
     while(p1<n1){
             helper(nums1, pos1, pos2, ans1, ans2, p1, cnt, flag);           
     }
     while(p2<n2){
             helper(nums2, pos1, pos2, ans1, ans2, p2, cnt, flag);           
     }
     if(flag) return ans1*1.0;
     return (ans1+ans2)*1.0/2;
 }
 void helper(vector<int> &nums, int &pos1, int &pos2, int &ans1, int &ans2, int  &p, 
 int &cnt, bool &flag){
     ++cnt;
     if(flag){
         if(cnt==pos1){
             ans1=nums[p];return;
         }
     }else{
         if(cnt==pos1){
             ans1=nums[p];
         }else if(cnt==pos2){
             ans2=nums[p];return;
         }
     }
     ++p;
 }
思路2分析
时间复杂度: O(M+N)
空间复杂度: O(1)
// 思路三实现
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int k1,k2;
    bool flag=false;
    double ans=0;
    int n=nums1.size()+nums2.size();
    if(n%2==1){
        flag=true;
        k1=(n+1)/2;
    }else{
        k1=(n+1)/2;
        k2=k1+1;
    }
    if(flag){
        ans = findKth(nums1, nums2, k1)*1.0;
    }else{
        ans = (findKth(nums1, nums2, k1)+findKth(nums1, nums2, k2))*1.0/2;
    }
    return ans;
    
}
int findKth(vector<int> &nums1, vector<int> &nums2, int k){
    int n1=nums1.size(), n2=nums2.size();
    int index1=0, index2=0;  // k>=1
    while(true){
        if(index1==nums1.size()){
            return nums2[index2+k-1];
        }
        if(index2==nums2.size()){
            return nums1[index1+k-1];
        }
        if(k==1){
            return (nums1[index1]<=nums2[index2]?nums1[index1]:nums2[index2]);
        }
        int leftNum1=min(k/2, n1-index1);
        int leftNum2=min(k/2, n2-index2);
        if(nums1[index1+leftNum1-1]<=nums2[index2+leftNum2-1]){
            index1=index1+leftNum1;
            k=k-leftNum1;
        }else{
            index2=index2+leftNum2;
            k=k-leftNum2;

        }
    }
    return -1;
}  

5 求回文的最长子串*

考察: 字符串
思路1: 暴力枚举,居然过了,
如果这类题实在想不到思路,就用这个,几乎可以解决这类问题,但是可能超时
思路2: dp  (推荐)
LeetCode题解上讲了4种方法,思路2对应解法3
string longestPalindrome(string s) {
    int len=s.size();
    vector<vector<bool> >dp(len+1, vector(len+1, false));
    string ans;
    int _max=-1;
    // dp[i][j]=dp[i-2][j+1]&&s[j]==s[j+i-1]  i: 表示长度 j:表示起始位置
    // 特殊情况: 1) 越界  2)空串  3)长度为1的串
    for(int i=0;i<len;++i){
        dp[0][i]=true;
        dp[1][i]=true;
    }
    for(int i=2;i<=len;++i){
        for(int j=0;j<len;++j){
            if(j+i>len) continue; 
            dp[i][j]=dp[i-2][j+1]&&s[j]==s[j+i-1];
        }
    }
    bool flag=false;
    for(int i=len;i>=1;--i){
        for(int j=0;j<len;++j){
            if(i+j>len) continue;
            if(dp[i][j]){
                ans=s.substr(j, i);
                flag=true;break;
            }
        }
        if(flag) break;
    }
    return ans;
}
分析:
时间复杂度: O(N^2)
空间复杂度: O(N^2)

6 正则表达式匹配r

考察: 二维dp  字符串
思路1: dp, 定义好dp数组, 写出状态转移方程,写代码(推荐)
思路2: 递归
难度: hard 想到dp不容易的
扩展1: 没有.和*, 只有.,只有*,那又怎么写呢?

7 盛最多水的容器*r

题意: 求面积最大的二元组
考察: 数组 最值
思路1: 暴力枚举每种组合,无脑求面积,取其中最大值(可能超时), 54/60
思路2: 暴力枚举枚举每种组合,但是加上优化判断(不仅超时,变得更差) 53/60
思路3: 双指针,将时间复杂度从O(N^2) ----->  O(N)  (推荐)

难度: 第一次做很难想到,或者越想越复杂。 想到的话easy,因为代码实现简单。
// 思路2实现
int maxArea(vector<int>& height) {
    int max=-1;
    for(int i=0;i<height.size();++i){
        int cur_max=-1, maxId=-1;
        for(int j=height.size()-1;j>i;--j){
            if(j==height.size()-1){
                cur_max=(j-i)*min(height[i], height[j]);
                maxId=j;
            }else{
                if(height[j]>height[maxId]){  // 如果j又窄又矮,不可能成为答案,所以判断它的补集
                    if(height[i]>height[maxId]){
                        int tmp=(j-i)*min(height[i], height[j]);
                        if(tmp>cur_max){
                            cur_max=tmp;
                            maxId=j;
                        }
                    }
                }
            }
            if(cur_max>max){
                max=cur_max;
            }
        }
    }
    return max;
}
// 思路三实现
int maxArea(vector<int>& height) {
    int max=0;
    int i=0, j=height.size()-1;
    int tmp;
    while(i<j){
        if(height[i]<=height[j]){
            tmp=height[i]*(j-i);
            if(tmp>=max){
                max=tmp;
            }
            ++i;
        }else{
            tmp=height[j]*(j-i);
            if(tmp>=max){
                max=tmp;
            }
            --j;
        }
    }
    return max;
}

8 三数之和*r

考察: 数组
思路1: 暴力循环每种组合,判断是否等于target, 时间复杂度在O(N^3)
思路2: 排序+双指针, 难点在于去重
难度: medium
vector<vector<int>> threeSum(vector<int>& nums) {
    vector<vector<int> > ans;
    if(nums.size()<3) return ans;
    sort(nums.begin(), nums.end());
    int target=0; 
    for(int i=0;i<nums.size();++i){
        if(nums[i]>0) break; // 最小的数都大于0, 那么这三个数比不可能为答案
        if(i==0||nums[i]!=nums[i-1]){ // 去重方式1
            int findV=target-nums[i];
            int p1=i+1, p2=nums.size()-1;
            while(p1<p2){
                if(p1>i+1&&nums[p1]==nums[p1-1]){
                    ++p1;
                    continue; // 去重方式2
                }        
                if(nums[p1]+nums[p2]==findV){
                    ans.push_back(vector<int>{nums[i], nums[p1], nums[p2]});
                    ++p1;
                }else if(nums[p1]+nums[p2]>findV){
                    --p2;
                }else{
                    ++p1;
                }   
            }
        }
    }
    return ans;
}

9 电话号码的字母组合r

考察: 回溯
思路1: 回溯(推荐)
难度: easy

10 删除链表倒数第N个结点

题意: 面试官希望你只遍历一次链表就完成相应操作
考察: 链表 删除操作 头指针 头结点 首元结点
思路1: 快慢指针 只遍历一次链表,做到删除操作(推荐)
难度: easy
ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode *pre=NULL, *fast=head, *slow=head;
    while(n--){
        fast=fast->next;
    }
    while(fast){
        pre=slow;
        slow=slow->next;
        fast=fast->next;
    }
    // 如果没有头结点的话,链表删除要分类讨论
    if(pre!=NULL) pre->next=slow->next;   // 删除非首元结点
    else{ // 删除首元结点
        head=slow->next;
    }
    delete slow;
    return head;
}

11 有效的括号

考察: 字符串 栈
思路1: 使用栈(推荐)
难度: easy
// 思路1实现
bool isValid(string s) {
    stack<char> stk;
    bool flag=true;
    for(int i=0;i<s.size();++i){
        if(s[i]=='('||s[i]=='['||s[i]=='{'){
            stk.push(s[i]);
        }else if(s[i]==')'||s[i]==']'||s[i]=='}'){
            if(stk.empty()){     
                flag=false;break;
            }else{
                if(stk.top()=='('&&s[i]==')'||stk.top()=='['&&s[i]==']'||stk.top()=='{'&&s[i]=='}'){
                    stk.pop();
                }else{
                    flag=false; break;
                }
            }
        }
    }
    if(!stk.empty()) flag=false;
    return flag;
}

12 合并两个有序链表

考查: 链表
思路1: 双指针+尾插法(推荐)
思路2: 递归(可以看看)
难度: easy

13 括号生成*r

考查: 字符串  回溯
思路1: 回溯+剪枝  (推荐)
难度:medium, 难点在于剪枝
vector<string> generateParenthesis(int n) {
    string cur;
    dfs(cur,0,0,n);
    return ans;
}
void dfs(string cur, int left, int right, int &n){
    if(left==n&&right==n){
        ans.push_back(cur);
        return;
    }
    if(left<right) return; // 剪枝1, 当前字符串右括号多,不可能成为有效字符串
    else if(left==right) dfs(cur+'(', left+1, right, n);
    else{
        if(left<n) dfs(cur+'(', left+1, right, n);
        if(right<n) dfs(cur+')', left, right+1, n);
    }
}

14 合并K个有序链表*//

考查: 链表
思路1: k指针
思路2: 链表的归并排序
思路3: 队列, 队列+哈夫曼
思路4: ?推荐
难度: ?
// 思路1实现
ListNode* mergeKLists(vector<ListNode*>& lists) {
    if(lists.empty()) return NULL;
    ListNode* head = new ListNode();
    ListNode *tail=head;
    vector<ListNode*> ptrs(lists.size(), NULL);
    // 初始化指针
    for(int i=0;i<lists.size();++i){
        ptrs[i]=lists[i];
    }
    while(true){
        int minId=-1;
        int minV= 0x3f3f3f3f;
        bool flag=false;
        for(int i=0;i<lists.size();++i){
            if(ptrs[i]!=NULL){
                if(ptrs[i]->val<minV){
                    minV=ptrs[i]->val;
                    minId=i;
                    flag=true;
                }
            }
        }
        if(!flag) break;
        tail->next=ptrs[minId];
        tail=ptrs[minId];
        ptrs[minId]=ptrs[minId]->next;
    }
    return head->next;
}

15 下一个排列*

考查: 数组 数学 找规律  排列
错误思路:  找到一个正序序偶,要求尽可能靠后,其次第二位值尽可能小,这种思路是错的
反例,如[1,3,2], 按照上述思路找到(1,2)变为[2,3,1], 但是存在更优解,如[2,1,3]。所以上述思路错的
思路1:但错误思路可以得到启发
假设.数组为...a...b...。// 替代为...a(2)b(3)
(a,b)是从后往前的第一对正序序偶。
所以(2)b(3)必然逆序,
(3)<b&&(3)<a, 否则(a,b)不是第一对正序序偶
(2)>b
所以说如果(a,b)交换, (2)a(3)也是逆序的,故逆置即可。
还有重复的情况,序偶尽可能靠后即可。
思路2: ?推荐
// 思路1实现
void nextPermutation(vector<int>& nums) {
    if(nums.empty()||nums.size()==1) return;
    int p1=-1, p2=-1;
    for(int i=nums.size()-2;i>=0;--i){
        int minV=-1;
        int minId=-1;
        for(int j=i+1;j<nums.size();++j){
            if(nums[i]<nums[j]){
                if(minV==-1){
                    minV=nums[j];
                    minId=j;
                    p1=i;p2=j;
                }else{
                    if(nums[j]<=minV){
                        minV=nums[j];
                        minId=j;
                        p1=i;p2=j;
                    }
                }
            }
        }
        if(minV!=-1) break;
    }
    if(p1==-1){
        reverse(nums.begin(), nums.end());
    }else{
        swap(nums[p1], nums[p2]);
        reverse(nums.begin()+p1+1, nums.end());
    }
    return;
}

16 最长有效括号*?

题意: 求括号有效的最长子串
考查: 字符串
思路1: 暴力枚举每种组合+括号有效判断(大概率超时)
思路2: ?推荐
难度: hard, 找不到思路

17 搜索旋转排序数组*?

题意: 给你一个旋转数组, target。让你返回数组中等于target的下标,否则返回-1。 
LeetCode上有一个求最小值,比这更简单。
考查: 二分查找
思路1: 二分查找
	写的不够优雅
难度: hard, 很容易写成死循环,尤其是在不知道测试样例的情况下。
// 思路1实现
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left=0, right=nums.size()-1;
        while(left<=right){
            int mid = (left+right)>>1;
            printf("[%d, %d] mid=%d\n", left, right, mid);
            if(left==right){  // 加上这句,防止出现死循环
                if(target==nums[left]) return left;
                else return -1;
            }
            if(target==nums[mid]){
                return mid;
            }
            if(target>nums[right]){
                if(nums[mid]>=target){
                    right=mid;
                }else if(nums[mid]>nums[right]){
                    left=mid+1;
                }else{
                    right=mid-1;
                }

            }else{
                if(nums[mid]>nums[right]){
                    left=mid+1;
                }else if(nums[mid]>=target){
                    right=mid;
                }else{
                    left=mid+1;
                }
            }
        }
        return -1;
    }
};

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

考查: 二分查找
思路1: 定义loweBound函数,找到第一个>=target的数字位置。最后一个位置可以复用lowerBound函数,
或者定义个upperBound函数。(推荐)
难度: medium 
笔者之前思考过这种题,所以一遍过

19 组合总和*r?

20 接雨水?

考查: 数组 最值
思路1: 自己想的思路和实现的代码。
从左到右扫描这个数组,如果当前元素i右边存在一个大于等于它的元素j,那么累加这两个元素之间的雨水。
指针跳到位置j, 进行下一轮迭代。  
否则,找到右边小于当前元素的元素j, 对这两个元素之间的雨水进行累加。指针跳到j, 进行下一轮迭代。
因为j>i, 所以循环必须结束。
第一种情况,我用的单调递减栈,求得大于等于当前元素的右边第一个元素。
第二种情况, 求小于当前元素的右边最大元素。我用的是循环。

思路2: 左右扫描法。看了官方思路,我这个想的复杂了。
#define N 20010
class Solution {
public:
    int trap(vector<int>& height) {
        int n=height.size();
        if(n<=2) return 0;
        fill(nge, nge+N, -1);
        for(int i=0;i<n;++i){
            while(top!=-1&&height[stk[top]]<=height[i]){
                int x = stk[top];
                --top;
                nge[x]=i;
            }
            stk[++top]=i;
        }

        cout<<endl;
        int ans=0;
        for(int i=0;i<n-1;){
            if(nge[i]==-1){
                int maxId=-1;
                int _max = -1;
                for(int j=i+1;j<n;++j){
                    if(height[j]>_max){
                        _max=height[j];
                        maxId=j;
                    }
                }
          //      if(maxId==-1) break;  //  这种情况必不存在,因为i必然存在后继
                if(maxId==i+1){
                    ++i;
                }else{
                    int min_height = height[maxId];
                    for(int j=i+1;j<maxId;++j){
                        ans+=(min_height-height[j]);
                    }
                    i=maxId;

                }
            }
            else{
                int next = nge[i];
                if(next==i+1){
                    i=next; continue;
                }
                int min_height = min(height[i], height[next]);
                for(int j=i+1;j<next;++j){
                    ans+=(min_height-height[j]);
                }
                i=next;
            }
        }
       return ans;
    }
    
private:
    int nge[N]; // next greater element
    int stk[N];
    int top=-1;
};
执行结果:通过
执行用时:292 ms, 在所有 C++ 提交中击败了6.29%的用户
内存消耗:19.3 MB, 在所有 C++ 提交中击败了33.56%的用户
通过测试用例:322 / 322

21 全排列

考查: 回溯 排列 
思路1: 回溯
难度: easy
扩展: 如果含重复数字的数组呢?怎么做到去重?不重不漏

22 旋转图像

考查: 二维数组遍历
思路1: 由剑指offer得到启发,顺时针遍历数组,将当前位置上的数字保存到队列上,之后放上正确的数字。(推荐)
思路2: 看看题解
难度: medium

23 字母异位词分组

题意: 对n个字符串进行分组 (推荐)
考查: map
思路1: 遍历这个数组,对于每个字符串,确定一个分类标准,将它放到属于这个类的容器中。
比如,根据这个字母异位词,可以对字符串排序,如“ate”, 它的分类标准就是"aet"。

24 最大子数组和

题意: 求和最大的子数组
考查: dp
思路1: 一维dp, 定义dp[i]表示以第i(i从0开始)个数字结尾的最大子数组的长度。(推荐)
思路2: 分治。 LeetCode原话,尝试更精妙的分治算法。

25 跳跃游戏

考查: 暴力搜索+贪心优化, 从一颗多分支树变成一个单支树。
思路1: 从当前状态可以有n种选择到达下一个状态,那么从这n种选择中挑出一个最优的选择。(推荐)

26 合并区间

考查: 排序
思路1: 对于两个序偶,排序的第一优先级是左端点,第二优先级是右端点。之后就是合并。(推荐)
想一想为什么先对左端点进行排序?
补充知识: 做了一个小实验, 对[1,1,1]这样的数组排序 bool cmp(int &a, int &b) { return a<b};
发现数组不变。 我原先一直以为返回true,两个数字保持原位置; 返回false,两个数交换位置。
按照这种观点,当两个数相等时,返回的是false。 但实际上没有变化。后来我画蛇添足加了=, 
变成return a<=b; 结果就是程序死循环,报错。
   结论: 当两个数不一样时, true表示保持原位置。 两个数一样时,false表示保持原位置。
// 思路1实现
vector<vector<int>> merge(vector<vector<int>>& intervals) {
    vector<vector<int> > ans;
    sort(intervals.begin(), intervals.end(), cmp);
    vector<int> cur=intervals[0];
    for(int i=1;i<intervals.size();++i){
        vector<int> next;
        if(cur[1]>=intervals[i][0]){
            next.push_back(cur[0]);
            next.push_back(intervals[i][1]>cur[1]?intervals[i][1]:cur[1]);
        }else{
            ans.push_back(cur);
            next=intervals[i];
        }
        cur=next;
    }
    ans.push_back(cur);
    return ans;
}
static bool cmp(vector<int> &a, vector<int> &b){
    if(a[0]!=b[0]) return a[0]<b[0];
    return a[1]<b[1];
}

2022/7/11


27 不同路径

考查: 地图的DFS dp
思路1: 地图类的DFS 超时
思路2: dp   (推荐)
思路3: 组合
很容易看到dp[i][j]可以从另外两个状态转移过来。并且这两种状态不相交。
难度: easy
// 思路1
int uniquePaths(int m, int n) {
    int ans=0;
    dfs(0,0,m,n,ans);
    return ans;
}
void dfs(int x, int y, int &m, int &n, int &ans){
    if(x==m-1&&y==n-1){
        ++ans;
        return;
    }
    if(x<=m-1&&y+1<=n-1) dfs(x,y+1, m,n, ans);
    if(x+1<=m-1&&y<=n-1) dfs(x+1, y, m,n , ans);
}
//思路2
int uniquePaths(int m, int n) {
    vector<vector<int> > dp(m+1, vector<int>(n+1, 0));
    dp[0][0]=1;
    // dp[i][j] = dp[i-1][j]+dp[i][j-1]
    for(int i=0;i<m;++i){
        for(int j=0;j<n;++j){
            if(i==0&&j==0) continue;
            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-1][j]+dp[i][j-1];
            }
        }
    }
    return dp[m-1][n-1];
}

28 最小路径和

考查: 地图的DFS  dp
思路1: 地图的DFS  超时
思路2: dp, 和27一类的题目, 注意到只能向下或者向右 (推荐)
扩展: 如果是4个方向都可以走呢? 还能用dp吗?
难度: easy
int minPathSum(vector<vector<int>>& grid) {
    int m=grid.size(), n=grid[0].size();
    vector<vector<int> > dp(m+1, vector<int>(n+1, 0));
    dp[0][0]=grid[0][0];
    for(int i=0;i<m;++i){
        for(int j=0;j<n;++j){
            if(i==0&&j==0) continue;
            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];
}

29 爬楼梯

考查: dp
难度: easy

30 编辑距离*

考查: 字符串
难度: hard
思路1: 
判断这是一道什么题? dp。
定义状态,借助之前做过的题。写状态转移方程。处理特殊情况
思考: 为什么每次都是对字符串A尾部进行操作得到B,可以是最小操作次数呢?

扩展: 编辑距离,又称为莱温斯坦距离, 算法是由俄罗斯科学家莱温斯坦在1965年提出。编辑距离应用非常广
泛,可以用来表示两个字符串的差异化程度,适用于短字符串。长字符串比较耗时。
class Solution {
public:
    int minDistance(string word1, string word2) {
        int len1=word1.size(), len2=word2.size();
        if(!len1||!len2) return max(len1, len2);
        vector<vector<int> > dp(len1+1, vector<int>(len2+1, 0));
        // dp[i][j] 表示word1中前i个字符和word2中前j个字符的编辑距离
        for(int i=0;i<=len1;++i){
            for(int j=0;j<=len2;++j){
                if(i==0||j==0){
                    dp[i][j]=max(i, j); continue;
                } 
                // 添加
                int v1 = dp[i][j-1]+1;
                // 删除
                int v2 = dp[i-1][j]+1;
                // 替换
                int v3 = dp[i-1][j-1]+(word1[i-1]!=word2[j-1]);  // 这里必须加括号, 优先级: +大于!=
                dp[i][j]=min(v1, min(v2,v3));
            }
        }
        return dp[len1][len2];
    }
};

31 颜色分类

考查: 排序  
思路1: 快排
思路2: 指针  (推荐)
难度: easy
// 思路1实现
void sortColors(vector<int>& nums) {
    int n = nums.size();
    if(n==0||n==1) return;
    quickSort(nums, 0, n-1);
    return;
}
void quickSort(vector<int>& nums, int left, int right){
    if(left>=right) return;
    int pivot=nums[left];
    int i=left, j=right;
    while(i<j){
        while(i<j&&nums[j]>=pivot) --j;
        nums[i]=nums[j];
        while(i<j&&nums[i]<pivot) ++i;
        nums[j]=nums[i];
    }
    nums[i]=pivot;
    quickSort(nums, left, i-1);
    quickSort(nums, i+1, right);
}

31 最小覆盖子串

考查: 字符串  滑动窗口
难度: hard

思路1: 自己实现代码, 代码存在重复
思路2: 对思路1,进行简化。原则是简单情况先判断,复杂情况拿到后面
思路3: 看官方题解, 描述和视频讲解都很不错。 
// 自己实现的代码1
struct node{
    int need;
    int cur;
    int far_id;
    node(): need(1), cur(0),far_id(-1){}
};
class Solution {
public:
    string minWindow(string s, string t) {
        int n=s.size();
        vector<int> q(n+10, 0);
        int hh=0, tt=-1;
        map<char, node*> _map;
        for(int i=0;i<t.size();++i){
            char ch = t[i];
            if(_map.find(ch)!=_map.end()){
                _map[ch]->need+=1;
            }else{
                node* _node = new node;
                _map[ch]=_node;
            }
        }

        // cur_total  cur, far_id
        string ans;
        int cur_total=0;
        int _min=0x3f3f3f3f;
        for(int i=0;i<n;++i){
            char ch=s[i];
            if(hh<=tt){
                if(t.find(ch)==string::npos){
                   
                    q[++tt]=i; continue;
                }
                q[++tt]=i;
                if(_map[ch]->cur<_map[ch]->need){
          
                    _map[ch]->cur+=1;
                    if(_map[ch]->far_id==-1){
                        _map[ch]->far_id=i;
                    }
                    
                    cur_total+=1;
                }else{
                    for(int i=_map[ch]->far_id+1;i<=q[tt];++i){
                        if(s[i]==ch){
                     
                            _map[ch]->far_id=i; break;
                        }
                    }
                }
                while(hh<=tt&& (t.find(s[q[hh]])==string::npos||q[hh]<_map[s[q[hh]]]->far_id) ){  
                     // 队列头部去掉
                    ++hh;
                }
               
                if(cur_total==t.size()){           
                    if(tt+1-hh<_min){
                        ans=s.substr(q[hh], tt+1-hh);
                        _min = tt+1-hh;
             
                    }
                }

            }else{
                if(t.find(ch)==string::npos) continue;
                q[++tt]=i;
                if(_map[ch]->cur<_map[ch]->need){
              
                      _map[ch]->cur+=1;
                      if(_map[ch]->far_id==-1){
                         _map[ch]->far_id=i;
                        }
                    cur_total+=1;                   
                 }
                if(cur_total==t.size()){
                    if(tt+1-hh<_min){
                        ans=s.substr(q[hh], tt+1-hh);
                        _min = tt+1-hh;
                   
                    }
                }
            }
        }
        return ans;
    }

};
// 思路2实现
struct node{
    int need;
    int cur;
    int far_id;
    node(): need(1), cur(0),far_id(-1){}
};
class Solution {
public:
    string minWindow(string s, string t) {
        int n=s.size();
        vector<int> q(n+10, 0);
        int hh=0, tt=-1;
        map<char, node*> _map;
        for(int i=0;i<t.size();++i){
            char ch = t[i];
            if(_map.find(ch)!=_map.end()){
                _map[ch]->need+=1;
            }else{
                node* _node = new node;
                _map[ch]=_node;
            }
        }
        // cur_total  cur, far_id
        string ans;
        int cur_total=0;
        int _min=0x3f3f3f3f;
        for(int i=0;i<n;++i){
            char ch=s[i];
            if(hh>tt&&t.find(ch)==string::npos) continue;
			q[++tt]=i;
			if(hh<=tt&&t.find(ch)==string::npos) continue;        
            if(_map[ch]->cur<_map[ch]->need){              
                      _map[ch]->cur+=1;
                      if(_map[ch]->far_id==-1){
                         _map[ch]->far_id=i;
                        }
                    cur_total+=1;                   
             }else{
                   for(int i=_map[ch]->far_id+1;i<=q[tt];++i){
                        if(s[i]==ch){                 
                            _map[ch]->far_id=i; break;
                        }
                    }
			 }			       
            while(hh<=tt&& (t.find(s[q[hh]])==string::npos||q[hh]<_map[s[q[hh]]]->far_id) ){  
                     // 队列头部去掉
                    ++hh;
            }
            			
            if(cur_total==t.size()){
                    if(tt+1-hh<_min){
                        ans=s.substr(q[hh], tt+1-hh);
                        _min = tt+1-hh;        
                    }
				}
        }
        return ans;
    }
};

32 子集

考查: 回溯
难度: easy

33 单词搜索

考查: 回溯
难度: medium

34 柱状图中最大的矩形*

考查: 单调递增栈
难度: hard
第一次看没思路。
2022/7/18, 又看了一次,想了90min还是没思路。
思考历程:我想了可能会是dp。 猜想dp[i]表示以第i个柱子结尾的最大矩形面积,或者dp[i]是前i个柱
子的最大矩形面积。会不会不是dp? 我过度猜想了?尼玛,BBQ了。 学渣QWQ

为什么这道题不好想呢? 假设我手上有一个木板,高度是h。 如果我遇到一个一个连续递增的木板,我很开心。
矩形面积会变大。 但是呢? 如果遇到一个矮于h的木板呢? 如果我要这个木板,那么矩形面积会变小。
如果我不要呢? 如果接连遇到这种短木板。由于宽度在不断增加,最后可能又超过了这个原来的最大面积。
所以,要或者不要都不可以。那这个时候大脑就觉得没办法解决,会越想越复杂。

  官方题解的方法就是高度你先固定起来。思维变换一下。
  
这道题和接雨水那道题类似。


class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        // dp[i]
        int n=heights.size();
        if(n==0) return 0;
        if(n==1) return heights[0];
        vector<int> left_min(n+1, -1);  // 左侧离当前数最近并且小于这个数的柱子。
        vector<int> right_min(n+1,n);
        vector<int> stk(n+10, 0);
        int hh=0, tt=-1;
        for(int i=0;i<n;++i){
            while(hh<=tt&&heights[stk[tt]]>heights[i]){
                int _top=stk[tt];
                --tt;
                right_min[_top]=i;
            }
            if(hh<=tt) left_min[i]=stk[tt];
            stk[++tt]=i;
        }
        int ans=0;
        int left, right, tmp;
        for(int i=0;i<n;++i){
            // 左边界
            left = left_min[i]+1;
            // 右边界
            right=right_min[i]-1;
            // 当前面积
            tmp = heights[i]*(right-left+1);
            ans=max(ans, tmp);
        }
        return ans;
    }

};

35 最大矩形

难度: hard

36 二叉树的中序遍历

考查: 树  遍历
 难度:  easy

37 不同的二叉搜索树

难度: hard
无从入手

考查: dp 卡特兰数
思路1: 手动求出前面几个数,看到 1 1 2 5 14 这种, 立马想到卡特兰数,求第n个卡特兰数即可。
思路2: 证明,
假设有n个结点的二叉搜索树的种类记为G(n)。
给n个节点进行编号。我们选取其中编号为i的结点作为BST的根结点。
假设,编号为i作为根节点的BST有F(i)种。 那么根据BST树的性质,i根结点的左子树有i-1个结点, 右子树
有n-i个结点。
并且左子树上的结点编号为1....i-1。 所以左子树的种类数为G(i-1), 同理右子树种类数为G(n-i)。
所以G(n) = F(1)+F(2)+F(3)+...+F(n) = G(0)*G(n-1)+G(1)*G(n-2)+....+G(n-1)*G(0)。
由于G(0)=G(1)=1。所以这个数列就是卡特兰数。

扩展: 编程中有两个数列要记忆, 斐波拉契和卡特兰数。

class Solution {
public:
    int numTrees(int n) {
        if(n==0||n==1) return 1;
        vector<int> dp(n+1, 0);
        dp[0]=1, dp[1]=1;
        for(int i=2;i<=n;++i){  // dp[0]*
            for(int j=0;j<i;++j){
                dp[i]+=(dp[j]*dp[i-1-j]);
            }
        }
        return dp[n];
    }
};

38 验证一颗BST

考查: 树  遍历
思路1: 中序遍历这颗树,对于每个结点,大于它的左孩子,小于它的右孩子即可。
这种想法对吗?
思路2: 利用性质,中序遍历BST的序是有序。

39 判断一棵树是对称二叉树

考查: 树
思路1: 用两个指针, 一个根左右, 另一个根右左这样

40 二叉树的层次遍历

考查: 树   bfs  遍历
思路1: 树的层次遍历+map
思路2: 树的层次遍历+迭代   (推荐, 这个技巧一定要掌握)
	思路2就是在树进行BFS遍历的时候,队列要么为空,要么存放的是同一层的所有节点。 如果队列不为空,
那么访问这些同一层的节点,同时将下一层的节点放到队列中,如此进行迭代, 直到队列为空。
	我觉得迭代就是下一个状态只依赖于当前状态。如这里我们期望得到第2层的结点,那么首先我们要知道第1
层结点, 根据孩子关系可以得到第2层结点。
// 思路1实现
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int> > ans;
        if(!root) return ans;
        layerOrder(root, ans);
        return ans;
    }
    void layerOrder(TreeNode* root, vector<vector<int> > &ans){
        queue<TreeNode*> q;
        q.push(root);
        _map[root]=1;
        while(!q.empty()){
            TreeNode* cur = q.front();
            q.pop();
            if(_map[cur]>max_height) max_height=_map[cur];
            if(cur->left){
                _map[cur->left]=_map[cur]+1;
                q.push(cur->left);
            }
            if(cur->right){
                _map[cur->right]=_map[cur]+1;
                q.push(cur->right);
            }
        }
        ans.resize(max_height);
        for(map<TreeNode*, int>::iterator it=_map.begin();it!=_map.end();++it){
            int cur_height = it->second;
            TreeNode* cur_node = it->first;
            ans[cur_height-1].push_back(cur_node->val);
        }
    }
private:
    map<TreeNode*, int> _map;
    int max_height=0;
};
// 思路2实现,和思路1相比,有写的优雅的地方,比如1)不用max_height, 2)不用
//  ans[ans.size()-1].push_back(value)
vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int> > ans;
    if(!root) return ans;
    bfs(root, ans);
    return ans;
}
void bfs(TreeNode* root, vector<vector<int> > &ans){
    queue<TreeNode*> q;
    q.push(root);
    while(!q.empty()){
        int n = q.size();
        ans.push_back(vector<int>());
        for(int i=0;i<n;++i){  // for循环是关键
            TreeNode* cur = q.front();
            q.pop();
            ans.back().push_back(cur->val);  
            if(cur->left) q.push(cur->left);
            if(cur->right) q.push(cur->right);
        }
    }
    return;
}

41 二叉树的最大深度

考查: 树 遍历
难度: easy

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

考查: 树  建树  递归
思路1: 这种题PAT考过3次, 前序遍历告诉你根是谁,根据这个根,可以将中序遍历序列分成两半。
重新更新前序遍历,中序遍历序列。
难度: medium

43 二叉树展开为链表

考查: 树  遍历  链表
思路1: 递归式前序遍历+list
因为之前做过一道类似题,给的启发。我这里一开始定义pre, 直接更新每个结点的指针关系,不借助list, 
可惜的是破坏了二叉树的结构,丢失了一些尚未访问的节点。
思路2:迭代式前序遍历+list
思路3: 迭代式前序遍历+同时展开
思路4: 寻找按前序遍历序列的每个结点前驱  (推荐)
难度: medium

44 买卖股票的最佳时机

45 二叉树的最大路径和

难度: hard

46 求连续的最长子序列*

考查: 反向索引的思想
题意: 给你一个数组,无序, 返回连续的最长子序列。要求时间复杂度在O(N)
思路1: 如果不考虑复杂度,排序之后还是简单的。 
思路2: 使用set<int> 对数组隐式排序
思路3:  不对数组排序。使用unordered_set<int>   
  原数组中每个元素会访问两次。所以时间复杂度在O(N)。 但是实际上运行时间比思路1要长多了。
字节二面   没有找到一个比较好的解法。
// 思路1实现
class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        int n=nums.size();
        if(n<=1) return n;
        sort(nums.begin(), nums.end());
        int ans=0;
        int cnt=1;
        for(int i=1;i<n;++i){
            if(nums[i]==nums[i-1]) continue;
            if(nums[i]==nums[i-1]+1){
                ++cnt;
            }else{
                ans=max(ans, cnt);
                cnt=1;
            }
        }
        ans=max(ans, cnt);
        return ans;
    }
};
// 思路2实现
class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        int n=nums.size();
        if(n<=1) return n;
        set<int> st;
        for(auto &_v: nums){
            st.insert(_v);
        }
        int lastId=INT_MIN;
        int ans=0;
        int cnt=0;
        for(auto  &_v: st){
            int vv = _v;
            if(lastId==INT_MIN||vv==lastId+1){
                ++cnt;
            }else{
                ans=max(ans, cnt);
                cnt=1;
            }
            lastId=vv;        
        }
        ans = max(ans, cnt);
        return ans;
    }
};
// 思路3实现
class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        int n=nums.size();
        if(n<=1) return n;
        unordered_set<int> st;
        for(int &_v: nums){
            st.insert(_v);
        }
        int ans=0;
        for(int &_v: nums){
            if(!st.count(_v-1)){
                int cnt=1;
                int vv = _v;
                while(st.count(vv+1)){
                    ++cnt;
                    vv=vv+1;
                }
                ans=max(ans, cnt);
            }
        }
        return ans;
    }
};

/*
虽然这里有双循环,但是不可简单认为是O(N^2)
对于数组中每个数字, 首先判断这个数字有没有前驱,所以会访问一次。

其次对于每个数字所在的连续序列, 由于序列第一个数字没有前驱,会进入内循环,所以这个数字还会访问
一次。 

所以每个数字至多访问两次。 故时间复杂度O(2*N) =  O(N)

数学推导, 假设n个数字有 n1个节点没前驱, n2个节点有前驱。
对于n1个没前驱的节点, 它的连续序列长度分别为l1, l2,....ln1.
f(n1)表示访问这n1个节点的耗时。
整个访问次数=  n2 + f(n1) = n2 + n1 + (l1+l2+...+ln1) = n2 +n1 + n = 2n。
所以时间复杂度是O(N)

*/

47 只出现一次的数

考查: 位运算

48 单词拆分

难度: hard
思路1: 回溯, 可惜超时
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        for(int i=0;i<wordDict.size();++i){
            char ch = wordDict[i][0];
            _map[ch].push_back(wordDict[i]);
        }
        dfs(s, 0, wordDict);
        return flag;

    }
    void dfs(string s, int pos, vector<string>& wordDict){
        if(flag) return;
        if(pos==s.size()){
            flag=true;
            return;
        }
        char ch = s[pos];
        if(_map.find(ch)==_map.end()) return;
        vector<string> strs = _map[ch];
        for(int i=strs.size()-1;i>=0;--i){
            string word = strs[i];
            int len = word.size();
            if(pos+len<=s.size()&&s.substr(pos,len)==word){
                dfs(s, pos+len, wordDict);
            }
        }
    }
private:
    bool flag=false;
    map<char, vector<string> > _map;
};
// 字典树
class Trie{
    class TreeNode{
    public:
        bool isLeaf;
        TreeNode* children[26];
    };
private:
    TreeNode* root;
public:
    Trie(){
        root = new TreeNode();
    }
    void insert(string word){
        TreeNode* p =root;
        for(auto &ch: word){
            int i=ch-'a';
            if(p->children[i]==nullptr){
                p->children[i]=new TreeNode();
            }
            p=p->children[i];
        }
        p->isLeaf=true;
        return;
    }
    vector<string> search(string word){
        TreeNode* p=root;
        vector<string> ans;
        int len=0;
        for(auto &ch: word){
            int i=ch-'a';
            if(p->children[i]==nullptr) return ans;
            ++len;
            p=p->children[i];
            if(p->isLeaf){
                ans.push_back(word.substr(0, len));
            }
        }
        return ans;
    }


};
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        for(string &word: wordDict){
            trie.insert(word);
        }
        dfs(s, 0);
        return flag;

    }
    void dfs(string s, int pos){
        if(flag) return;
        if(pos==s.size()){
            flag=true;
            return;
        }
        vector<string> strs=trie.search(s.substr(pos));
        if(strs.empty()) return;
        for(int i=strs.size()-1;i>=0;--i){
            string word = strs[i];
            int len = word.size();
            dfs(s, pos+len);
            
        }
    }
private:
    bool flag=false;
    Trie trie;
};
// dp+字典树
class Trie{
    class TreeNode{
    public:
        bool isLeaf;
        TreeNode* children[26];
    };
private:
    TreeNode* root;
public:
    Trie(){
        root = new TreeNode();
    }
    void insert(string word){
        TreeNode* p =root;
        for(auto &ch: word){
            int i=ch-'a';
            if(p->children[i]==nullptr){
                p->children[i]=new TreeNode();
            }
            p=p->children[i];
        }
        p->isLeaf=true;
        return;
    }
    bool search(string word){
        TreeNode* p=root;
        for(auto &ch: word){
            int i=ch-'a';
            if(p->children[i]==nullptr) return false;         
            p=p->children[i];
        }
        return p->isLeaf;
    }


};
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        int len=s.size();
        if(len==0) return true;
        for(string &word: wordDict){
            trie.insert(word);
        }
        vector<bool> dp(len+1, false); // dp[i]表示长度为i的字符串前缀能否break成字典中单词
        dp[0]=true;
        for(int i=1;i<=len;++i){   // i表示长度
            // 0...i-1
            for(int j=0;j<=i-1;++j){ // 枚举分割点
                if(dp[j]&&trie.search(s.substr(j,i-j))){
                    dp[i]=true;
                } 
            }
        }
        return dp[len];

    }
private:
    bool flag=false;
    Trie trie;
};

49 环形链表

考查: 链表
思路1: 快慢指针
难度: easy

50 环形链表II

题意: 49是问你一个链表是否含有环,50是要求你返回环形链表的入口。
思路1: 快慢指针
first那个地方写的不够优雅
难度: medium
// 思路1实现
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head==nullptr) return nullptr;
        ListNode *fast=head, *slow=head;
        bool first=true;
        while(first||!(fast==NULL||fast==slow)){
            if(fast->next) fast=fast->next->next;
            else{
                fast=NULL; break;
            }
            slow=slow->next;
            if(first) first=false;
        }
        if(fast==NULL) return NULL;
        ListNode* p=fast;
        int cnt=0;
        while(cnt==0||p!=fast){
            ++cnt;
            p=p->next; 
        }
        int k=cnt;
        fast=head, slow=head;
        while(k--){
            fast=fast->next;
        }
        while(fast!=slow){
            fast=fast->next;
            slow=slow->next;
        }
        return fast;
    }
};
系列题目:
	买卖股票
	打家劫舍
	跳跃游戏
没做出来的题目:
	32. 最长有效括号
	33. 搜索旋转排序数组
	42. 接雨水
	72. 编辑距离
	76. 最小覆盖子串
	84. 柱状图中最大的矩形
	85. 最大矩形
	96. 不同的二叉搜索树
	128. 最长连续序列
	139. 单词拆分
11种dp:

1) 线性dp

用过的状态:
dp[i]表示长度为i的字符串或者数组前缀的最优值    i表示长度,从0开始  (dp[i]表示前i个数字或者字符的最优值)
dp[i]表示以第i个字符或者数字结尾的最优值   i表示位置,从0开始
dp[i[表示从0到i上字符串或者数组前缀的最优值
dp[i][j]表示数组或者字符串上区间的最优值
dp[i][j]表示长度为i的字符串前缀和另一个长度为j个字符串前缀的最优值。

单串问题:
	
双串问题:
   10. 正则表达式匹配
   11. 最小覆盖子串
 
多串问题:
	单词拆分
木板系列:
	11. 盛最多水的容器
	42. 接雨水
	84. 柱状图中最大的矩形
	85. 最大矩形
区间系列问题
	56. 合并区间
	731. 我的日程安排表 II
    253. 会议室 II
假设有两个区间  [s1,  e1]和 [s2,  e2]
判断两个区间相交  :  s1<=e2&&s2<=e1
两个区间的交集: ( max(s1,s2), min(e1, e2) )
两个区间的并集:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值