LeetCode Top100特训


两数相加

链接

  • 个人思路:将链表1转换为num1,链表2转换为num2,两数相加后再转换回链表。存在的问题:链表长度超过long long数字范围,无法做加法

  • 题解:此题需要模拟加法

  • 需要注意:
    1. 进位的问题,最后一位进位的问题
    2. 链表的遍历,新建链表,返回链表头结点

    ListNode* head = nullptr;
    ListNode* node = nullptr;
    if(head == nullptr){
         
        head = node = new ListNode(cur);
    }
    else{
         
        node->next = new ListNode(cur);
        node = node->next;
    }
    
    return head
    
  • 反思:
    1. 需要考虑数字的可表示范围
    4. 注意链表的构造,并返回头结点的方法

盛水最多的容器

链接

  • 个人思路:想到了接雨水,想到了按行、按列遍历,但还是没有明确思路

  • 题解:双指针。
    1. 使用双指针卡出容器的两个边界, 计算当前的盛水量(当前盛水量 = 容器两侧的最小值 * 宽度)
    2. 移动较小的指针,重复步骤1,直到指针重合

    为什么是移动较小的指针,因为移动指针会让宽度减小,移动较大的指针只会让盛水量减小,因此只有移动较小的指针才可能使盛水量增加
    (二周目卡到正确性这里)

  • 注意:无

  • 反思:
    1. 编码没有难度
    2. 思路比较巧妙,还需要多做题

电话号码的字母组合

链接

  • 个人思路:hash+回溯。遍历字符串,对每个数字对应的字母进行排列组合
  • 题解:
    1. 递归参数:题目字符串的索引
    2. 递归出口:索引超出字符串,return
    3. 递归体:遍历字符对应的3~4个字母,进行排列组合
  • 注意:
    1. 要明确递归是在遍历什么的
    2. 递归体是在遍历什么的
    3. 代码就会比较好写
  • 反思:
    1. 代码写的还是比较顺利的,对回溯有了更明确的认识

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

链接

  • 个人思路:只想到了两次遍历
  • 题解:快慢指针,快指针比慢指针先行n步,即快慢指针之间相差n,当快指针为null时,慢指针指向待删节点
  • 注意:删除节点为头结点时的特殊情况
  • 反思:
    1. 思路还是不够打开
    2. 双指针(左右指针、快慢指针)对于自己比较难想到’

字母异位词分组

链接

  • 个人思路:观察到相同字母的单词,排序后的结果是相同的,因此对排序后结果相同的单词放在一个子集中即可

  • 题解:与个人思路一致

  • 注意:
    1. 二维vecotr在未声明行数时,不能通过索引插入。要么提前声明行数,要么用一个空vector插入
    2. 可以用map<string, vector >避免上述情况

    	vector<vector<string> > ans;
    	vector<string> temp;
    	ans.push_back(temp);
    	mp[word] = index;
    	ans[index].push_back(strs[i]);
    	index++
    
  • 反思:
    1. string同样可以使用sort()
    2. 二维vector的遍历要格外注意是否已经存在这一行
    3. 做题时要牢记,写注释 map中key-value含义,
    4. 多尝试不同的STL容器解决该问题

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

链接

  • 个人思路:是利用归并排序,合并过程中找到中位数,时间复杂度为O(m + n)。要注意序列长度为奇数和偶数,对应的中位数不同
  • 题解:要求时间复杂度为O(log(m+n)),采用找第k小元素的思路。中位数索引k = (m + n)/2,在两个有序数组中寻找k/2的数字,每次将较小元素前k/2个数字排除掉
  • 注意:
    1. 序列为奇数,中位数是第 len/2 + 1个数字,对应的索引(从0开始): len/2,例:[2,4,5],第2个数字为中位数,下标为1
    2. 序列为偶数,中位数是第 len/2 和 len/2 + 1两个数字,对应的索引(从0开始):len/2 - 1 和 len/2,例[4,5,6,7],第2个和第3个数字为中位数,下标为1和2
    3. 其实还是索引和对应的第x个数字之间相差1,写代码时建议提前明确,写着写着容易犯晕
  • 反思:
    1. 看了题解思路,未复现题解代码

合并区间

链接

  • 个人思路:个人思路没有注意到排序(经常被乱序坑),暴力解法。用map的key表示范围内的数字,value表示当前数字对应的范围,如:map[2] = [1,6],即2对应的范围区间是[1,6]
    如果旧区间和新区间的范围重叠,start取二者的最小值,end取二者的最大值,将范围内所有数字更新为合并后的区间
    遍历map,将所有范围存入set去重
    set的元素丢入二维vector中
    (随即发现,当前和并的区间会和已经合并后的区间重合,需要提前排序)
  • 题解:提前排序,用当前范围的左边界与ans中最后一个范围的右边界作比较,如果当前的左边界要比之前的最远右边界还大,说明区间不重合,否则说明重合
  • 注意:
    1. set的key值是pair时,没办法用unordered_set,因为C++中没有给pair做Hash的函数
    2. 格外需要多想想提前排序
  • 反思:
    1. 提前排序没有想到,尤其是对大小关系比较依赖的题目
    2. 暴力思想还是太重了,容器无脑堆叠

不同路径(与最小路径和类似)

链接

  • 个人思路:回溯法,到达右下角为递归出口,此时为一条完整路径,计数加一。递归体:向下和向右两个方向行进。剪枝:超出边界,已经访问过
 int cnt;
 void dfs(int m, int n, int x, int y){
   
     if(x == m - 1 && y == n - 1){
   
         cnt++;
         return;
     }
     for(int i = 0; i < 2; ++i){
   
         int tx = x + dir[i][0];
         int ty = y + dir[i][1];
         if(tx >= 0 && tx <= m - 1 && ty >= 0 && ty <= n - 1 && vis[tx][ty] == false){
   
             vis[tx][ty] = true;
             cnt++;
             dfs(m, n, tx, ty);
             cnt--;
             vis[tx][ty] = false;
         }
     }
 }
  • 题解:因为本题m,n取值为100,回溯会超时,因此本题采用动态规划。dp[i][j]表示到达坐标(i,j)的不同路径个数为dp[i][j];初始化第一行和第一列全为1(表示只有一种路径可以到达);其他的点(i,j),肯定是由前一个点向下、向右行进过来的,因此dp[i][j] = dp[i-1][j] + dp[i][j-1]
for(int i = 0; i < m; ++i){
   
    dp[i][0] = 1;
}
for(int j = 0; j < n; ++j){
   
    dp[0][j] = 1;
}
for(int i = 1; i < m; ++i){
   
    for(int j = 1;  j < n; ++j){
   
        dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    }
}
return dp[m -1][n - 1];
  • 注意:注意对dp数组的初始化,和初始化的含义,为什么要把这些初始化为1
  • 反思:
    1. 注意m,n取值为100,回溯一定会超时

正则表达式匹配

链接
参考

  • 个人思路:模拟,对p的取值分类讨论,过了75%
    1. p取字母
    2. p取 .
    3. p取 *
  • 题解:题解居然是动态规划。dp[i][j]表示字符串s前i位与字符串p前j位能否匹配上,从p[j]的取值来考虑,p[j]可取字母、‘.’、'* '。当p[j]==s[i]时,dp[i][j] = dp[i-1][j-1],再从p取.和考虑:p[j] == “.” : dp[i][j] = dp[i-1][j-1];当p取时,分为两种情况,* 前的字母取0个(和s[j]没有匹配上),或取任意个(和s[j]匹配上了,或者*前的字母为万能符.)
  • 注意:模拟时没有考虑到的特殊场景
    1. *表示任意个,可以是0个
    2. *前为.的特例
    3. s没匹配完,但p已经匹配完了,匹配失败
  • 反思:
    1. 压根没有往动态规划上考虑
    2. 一些特殊场景处理不到位

颜色分类

链接

  • 个人思路:直接对数组快速排序,没有利用数组中只有三种元素0,1,2的条件
  • 题解:双指针p0和p1,p0表示待插入0元素的末尾索引,p1表示待插入1元素的末尾索引。遍历数组,遇到0插入到p0位置,遇到1插入到p1位置。
  • 注意:在遇到0时,如果已经排序了一部分的0和1,则此时数组中若干个0后面已经有了若干个1,一定会发生将1交换出去的情况,此时要多进行一次交换,把交换出去的1再交换回来
int ptr0 = 0;//指向待插入元素0的位置
int ptr1 = 0;//指向待插入元素1的位置
for(int i = 0; i < nums.size(); ++i){
   
    if(nums[i] == 0){
   
        swap(nums[i], nums[ptr0]);
        if(ptr1 > ptr0){
   //说明0后面已经有排好序的1元素,经过上面的交换后,会把已经排好序的1元素交换出去
            swap(nums[i], nums[ptr1]);//因此需要再交换一次1元素
        }
        //交换0后,0指针和1指针均增加
        ptr0++;
        ptr1++;
    }
    else if(nums[i] == 1){
   
        swap(nums[i], nums[ptr1]);
        ptr1++;//1指针后移
    }
}

二周目双指针,再次明确指针含义和指针移动的操作,操作有些复杂需要提前想好指针移动的思路

  • 反思:
    1. 对题目条件利用不充分
    2. 在处理0后面有若干个1的情况,即二次交换,处理的不够到位,逻辑不够清晰

单词搜索

链接

  • 个人思路:典型回溯法,个人思路是逐步完善的过程。
    主要思路是搜索出来所有单词情况,和模式单词比对,判断是否能匹配上
    • 最初个人思路是认为只能向下和向右走,只枚举了两个方向
    • 后来发现可以向上向左走,修改为4个方向
    • 又发现并不要求从左上角出发,可以从任意点开始,因此又增加了遍历board的双重for循环
    • 依照自己的思路,把现在组合出来的单词记为cur,当cur长度大于word时做剪枝,当cur和word一样时,标记为找到了。这样的操作仍然超时,而且也想不出能做的剪枝策略了
  • 题解:不依靠当前组合出来的单词cur做判断,而是在board中每走一步,就和word[index]做比对,只有匹配上的才能继续搜索,否则就剪枝,这个回溯思路效率大大提高
  • 注意:
    1. 回溯过程中可以走的方向是两个还是四个
    2. 回溯的起始位置是固定的还是不固定的
    3. 对回溯中的所有参数都思考一下是不是可以剪枝
  • 反思:
    1. 回溯的角度有很多,不同的回溯角度效率不同。还是需要多思考多练习
    比如此题,个人的回溯思路是先组合出所有的单词,再看是否和目标词匹配上;题解的思路是,只有当前字符和目标词的字符匹配上,才可以进行下一步搜索

只出现一次的数字

题目联想到了剑指offer中,出现半数以上的元素,PK的方法(摩尔投票)解决
链接

  • 个人思路:先对数组排序,线性遍历数组,从0开始,步长为2,使用当前元素与下一个元素进行比对,不同(或是数组最后一个元素)则当前元素即为只出现一次的数字。
  • 题解:要求线性时间和常数空间,利用异或运算的特性:
  1. 任何数和 00 做异或运算,结果仍然是原来的数,即 a ⊕ 0 = a a \oplus 0=a a0=a
  2. 任何数和其自身做异或运算,结果是 00,即 a ⊕ a = 0 a \oplus a=0 aa=0
  3. 异或运算满足交换律和结合律,即 a ⊕ b ⊕ a = b ⊕ a ⊕ a = b ⊕ ( a ⊕ a ) = b ⊕ 0 = b a \oplus b \oplus a=b \oplus a \oplus a=b \oplus (a \oplus a)=b \oplus0=b aba=baa=b(aa)=b0=b
  • 注意:异或运算是将数字转换为二进制,逐位进行异或运算,并不是对十进制数字直接进行异或的
  • 反思:个人只想着线性遍历数组,忘记前面的排序O(nlogn),

合并K个升序链表

参考
链接

  • 个人思路:
    1. 顺序合并,先合并前两个链表,合并结果再与第三个合并……以此类推
    2. 看到优先队列的提示,思考的是:先把k个链表的头结点放入优先队列中,取出堆顶元素,把堆顶元素对应的链表指针后移一位。自己纠结的问题是,怎么确定堆顶元素和链表之间的映射关系,这是错误的思考方向
  • 题解:
    1. 优先队列,遍历所有链表,把每个元素丢到优先队列中,每次取出堆顶元素构建新链表(主要思想是先排序再建表,也可以把所有元素丢到数组中,sort排序后再建表)
    2. 分治法
    分:把k个链表divide,直到分到最细粒度,也就是只有自己一个链表为止
    合:把k个链表两两向上合并,直到合成一个链表
    过程:每次合并调用“合并两个有序链表”的代码
ListNode* divide(vector<ListNode*>& lists, int left, int right){
   
   if(left == right){
   
       return lists[left];
   }
   if(left > right){
   
       return nullptr;
   }
   int mid = (left + right) / 2;
   return mergeTwoLists(divide(lists, left, mid), divide(lists, mid + 1, right));
}
  • 注意:
    1. 构建链表,返回头结点的代码,head不动,不断让node后移
    ListNode* head = nullptr;
    ListNode* node = nullptr;
    if(head == nullptr){
         
        head = node = new ListNode(cur);
    }
    else{
         
        node->next = new ListNode(cur);
        node = node->next;
    }
    return head
    
  1. 分治思想,先分到最细粒度,再合并,大规模的问题简单化
  • 反思:
    1. 优先队列的错误思路,是自己想得过于复杂,陷入了归并排序的思路中

接雨水

链接
参考

  • 个人思路:个人无思路。。。
  • 题解:
    1. 按列求,遍历所有位置,找到当前位置左右两侧的最高的高墙,只有两侧的高墙都比当前位置高,才能盛雨水
    2. 动态规划。根据思路1(按列),每次都要往左右两侧遍历找出最高的高墙。考虑使用动态规划的状态转移方程优化

    对于 max_left我们其实可以这样求。
    max_left [i] = Max(max_left [i-1],height[i-1])。它前边的墙的左边的最高高度和它前边的墙的高度选一个较大的,就是当前列左边最高的墙了。

二周目,想到了暴力解法,按列遍历,找到当前位置左右两侧最高的墙。动规提前找出每个位置左右两侧最高的墙

int dpMaxLeft[20005];//dpMaxLeft[i]表示i位置左侧的高墙高度为dpMaxLeft[i]
int dpMaxRight[20005];//dpMaxRight[i]表示i位置右侧的高墙高度为dpMaxRight[i]
//初始化dp数组
dpMaxLeft[0] = height[0];
dpMaxRight[n] = height[n];
for(int i = 1; i < n; ++i){
   //状态转移左侧高墙
   dpMaxLeft[i] = max(dpMaxLeft[i - 1], height[i - 1]);
}
for(int i = n - 1; i >= 0; --i){
   //状态转移右侧高墙
   dpMaxRight[i] = max(dpMaxRight[i + 1], height[i + 1]);
}
  • 注意:
    1. 动态规划思路是对按列方法的优化。按列方法会超时
    2. 只有两侧的高墙都比当前位置高,才能盛雨水
  • 反思:
    1. 要学会思考状态转移的过程,思考当前状态是从前面的哪个转移过来的

移除元素

链接
题目要求时间复杂度O(N),空间复杂度O(1),即不开辟额外空间

  • 题解:快慢双指针,双指针初始化为下标0,快指针的作用:遍历整个数组;慢指针的作用:确定满足条件的数据位置,实现O(1)空间复杂度
class Solution {
   
public:
    int removeElement(vector<int>& nums, int val) {
   
        if(nums.size() == 0){
   
            return 0;
        }
        int slow = 0;
        for(int quick = 0; quick < nums.size(); ++quick){
   
            if(nums[quick] != val){
   
                nums[slow] = nums[quick];
                slow++;
            }
        }
        return slow;
    }
};
  • 二周目个人思路:左右指针指向数组首尾,如果左指针元素== val,与右指针交换。如果右指针元素==val,移除尾部元素,右指针重新指向数组尾部
int removeElement(vector<int>& nums, int val) {
   
    int left = 0;
    int right = nums.size() - 1;
    while(left <= right){
   
        if(nums[left] == val){
   //左指针元素==val,与右指针元素相等
            swap(nums[left], nums[right]);//交换
        }
        else{
   
            left++;
        }
        if(nums[right] == val){
   //右指针元素==val,pop
            nums.pop_back();
            right = nums.size() - 1;
        }
    }
    return nums.size(); //返回的是移除后,数组遍历的长度,可以不pop_back()
}
  • 注意:
    1. 该快慢指针高效复制数组的方法可以抽象为:将原数组中满足条件的数抽取出来,并构成新数组
    2. 返回的int含义:遍历数组的长度,可以不删除数组元素
    3. 这里提到条件不限于数组中元素与目标元素相等,还可以是下标是质数or非质数……
  • 反思:
    1. 这个快慢指针的高效复制数组的方法值得琢磨

最长有效括号

链接
参考

for(int i = 0; i < s.size(); ++i){
   
 if(s[i] == '('){
   //左括号
     st.push(s[i]);
 }
 else{
   //右括号
     if(st.empty()){
   //栈为空
         st.push(s[i]);
     }
     else{
   //栈不空
         if(st.top() == '('){
   //栈顶元素是左括号,说明匹配上了
             st.pop();//左括号出栈
             tempans += 2;
             ismatch = true;
         }
         else{
   //没有匹配上
             st.push(s[i]);
         }
 	}
 }
  • 题解:栈中存储的是索引,通过计算当前索引-出栈后的栈顶元素索引,即可求得连续最长有效括号。栈中参照元素“-1”的作用,处理出栈后栈为空的情况
stack<int> st;
st.push(-1);
int ans = 0, temp = 0;
for(int i = 0; i < s.size(); ++i){
   
    int topindex = st.top();
    if(topindex == -1){
   
        st.push(i);
    }
    else{
   
        if(s[i] == ')' && s[st.top()] == '('){
   //左右括号匹配上了
            st.pop();
            temp 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值