LeetCode究极刷题班

1. 两数之和

用的暴力做法——时间复杂度是O(n^2)

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        for(int i = 0; i < nums.size(); i++){
            int l = nums[i];
            for(int j = i + 1; j < nums.size(); j++){
                if(nums[j] == target - l){
                    return {i,j};
                }
            }
        }
        return {-1,-1};
    }
};

y总思路:
怎么实现O(n)的时间复杂度?
在这里插入图片描述
思路
对于给定的数组区间上,我们从头开始遍历数组,假设我们走到了箭头的位置,我们就去查看该位置前面是否有一个数等于 target - nums[i],这个查看过程我们就可以使用hash表来完成。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        //声明一个hash表<数值,索引>
        unordered_map<int, int> heap;
        for(int i = 0; i < nums.size(); i++){
            int r = target - nums[i];
            //查看 i 前面是否存在满足条件的数
            //count()是unordered_map的一个方法判断 Key 中是否存在目标值
            if(heap.count(r)) return {heap[r], i};
            //不存在则放入
            heap[nums[i]] = i;
        }
        return {};
    } 
};

在这里插入图片描述


2. 两数相加

在这里插入图片描述
思想
模拟了手动相加列竖式的过程,分别从两个链表头开始相加,直到两个链表结束并且没有进位。
技巧
可以创建一个虚拟头节点,方便最后结果输出

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        //创建一个node节点,head作为虚拟头节点存放地址
        ListNode* head = new ListNode(0);
        //cur也指向第一个头节点,但是用于更新后面的节点
        ListNode* cur = head;
        int t = 0;
        //链表不为空,或者t不为0就一直循环
        while(l1 || l2 || t) {
            if(l1) t += l1->val, l1 = l1->next;
            if(l2) t += l2->val, l2 = l2->next;
            cur = cur->next = new ListNode(t % 10);
            t /= 10;
        }
        //虚拟头节点派上用处
        return head->next;
    }
};
//技巧:
ListNode* head = new ListNode(0);
ListNode* cur = head;
//等价于
auto head = new ListNode(0), cur = head;

3. 无重复字符的最长子串

在这里插入图片描述
思路
这道题目是使用双指针算法。首先是第一个指针 i 我们枚举表示这个字串的末尾,另一个指针 j 则是从 i 开始不断往前枚举,直到最远位置。[j,i]就表示以 i 结尾的最长字串,然后枚举 i 找出所有 i 中的最长字串长度就是 res。
为什么 i 枚举出来的就是最长字串?
假设我们 i 往后移一位,j’ 新的位置一定位于原来 j 或者 j 的右边。我们可以通过反证法说明,因为[j,i]就表示以 i 结尾的最长字串,如果 j’ 位于原来 j 的左边,说明原来的 j 不是最远的位置,就矛盾了。
在这里插入图片描述
所以正是这样的单调性才能得到正确的结果。并且我们通过维护一个hash表来存放[j,i]中的所有元素,当加入下一个元素的时候,如果有重复就将 j 移到重复元素的位置开始。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> heap;
        int res = 0;
        for(int i = 0, j = 0; i < s.size(); i++){
            //把i字符加入到hash表中
            heap[s[i]]++;
            //判断是都存在重复,存在重复就移动j指针的位置
            while(heap[s[i]] > 1) heap[s[j++]]--;
            res = max(res, i - j + 1);
        }
        return res;
    }
};

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

在这里插入图片描述
思路
由于时间复杂度是log(m+n),就要思考一种递归的做法来实现。对于给定的两个数组nums1,nums2,我们从中找出第 k 个数是多少,在这里要找的k = (m+n)/2。我们去判断 nums1[k/2−1],nums2[k/2−1]大小关系:

  • nums1[k/2−1] > nums2[k/2−1] 就表明中nums2中的 起始点到 k/2−1范围的数都不是第 k 个数,可以删除
    在这里插入图片描述
    所以剩下的数 k - k/2 个数我们就从剩下的区间中去递归找
  • nums2[k/2−1] > nums1[k/2−1] 这种情况则是和上面分析的相反,过程分析一致
  • nums1[k/2−1] = nums2[k/2−1] 当相同的时候就是表示找到了第 k 个数,取其中之一就是答案
    剩下是代码实现的细节和注意事项
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        //首先计算两个数组的长度之和
        int tot = nums1.size() + nums2.size();
        //对于tot要分奇偶两种情况讨论
        if(tot % 2 == 0) {
            int left = findkthnumber(nums1, 0, nums2, 0, tot / 2);
            int right = findkthnumber(nums1, 0, nums2, 0, tot / 2 + 1);
            return (left + right) / 2.0;
        } else {
            return findkthnumber(nums1, 0, nums2, 0, tot / 2 + 1);
        }
    }
    int findkthnumber(vector<int> nums1, int i, vector<int> nums2, int j, int k) {
        //先判断nums1和nums2的大小关系,保证第一个数组是小的
        if(nums1.size() - i > nums2.size() - j) return findkthnumber(nums2, j, nums1, i, k);
        //判断小数组是否为空
        if(nums1.size() == i) return nums2[j + k - 1];
        //当k == 1时候选择最小的起始点就是答案
        if(k == 1) return min(nums1[i], nums2[j]);
        int si = min(i + k / 2, int(nums1.size())), sj = j + k / 2;
        if(nums1[si - 1] > nums2[sj - 1]) {
            return findkthnumber(nums1, i, nums2, sj, k - (sj - j));
        } else {
            return findkthnumber(nums1, si, nums2, j, k - (si - i));
        }
    }
};

这题难点一个是怎么相处log(m+n)的解法,另一个难点就是递归过程中完整的边界情况的考虑


5. 最长回文子串

在这里插入图片描述
思想
这道题目解法很多,我们这里使用一个比较容易想的方法
对于给定的字符串,我们可以枚举每一个可能的回文串的中心位置,由于回文串长度可能是奇数,也可能是偶数所以我们都需要考虑,从回文串的中心位置开始,如果左右两边相等,我们就使两个指针分别左右移动。
在这里插入图片描述

class Solution {
public:
    string longestPalindrome(string s) {
        string res;
        for(int i = 0; i < s.size(); i++){
            //如果是奇数长
            int l = i - 1, r = i + 1;
            while(l >= 0 && r < s.size() && s[l] == s[r]) l--,r++;
            if(res.size() < r - l - 1)res = s.substr(l + 1, r - l - 1);
            //如果是偶数长
            l = i, r = l + 1;
            while(l >= 0 && r < s.size() && s[l] == s[r]) l--,r++;
            if(res.size() < r - l - 1)res = s.substr(l + 1, r - l - 1);
        }
        return res;
    }
};

6. Z 字形变换

在这里插入图片描述
思想
在这里插入图片描述
我们可以假设数字来模拟,可以发现第一行和最后一行都是以第一个数字为起点的等差数列,公差 = 2n - 2;
对于中间的行,我们可以进行分组,每一行的第一组是再竖线上的数列,公差同样满足 2n - 2;第二组数列是在斜线上的,同样满足2n - 2

class Solution {
public:
    string convert(string s, int numRows) {
        string res;
        //对于1行的情况需要特殊判断
        if(numRows == 1) return s;
        for(int i = 0; i < numRows; i++){
            //对于第一行或者最后一行情况
            if(i == 0 || i == numRows - 1) {
                for(int j = i; j < s.size(); j += 2 * numRows - 2) {
                    res += s[j];
                }
            //对于中间行的处理
            } else {
                for(int k = i, j = 2 * numRows - 2 - k; k < s.size() || j < s.size(); k += 2 * numRows - 2, j += 2 * numRows - 2) {
                    //只有k ,j都没有超出边界的时候才能加入结果
                    if(k < s.size()) res += s[k];
                    if(j < s.size()) res += s[j];
                }
            }
        }
        return res;
    }
};

7. 整数反转

在这里插入图片描述
思想
对于正数例如1234,我们就是分别取出各个数字,算法就是通过循环 1234 % 10 = 4;1234 / 10;
对于负数例如-1234,C++中取模运算有所不同,-1234 % 10 = -4;-1234 / 10;
当我们获得各个数字的以后我们可以每位 * 10 相加恢复成一个整数
注意点:题目要求了不能使用 long long 就需要特殊判断一下溢出的情况

class Solution {
public:
    int reverse(int x) {
        int r = 0;
        while(x) {
            if(x > 0 && r > (INT_MAX - x % 10) / 10) return 0;
            if(x < 0 && r < (INT_MIN - x % 10) / 10) return 0;
            //以上两个if就是用来判断r可能溢出INT的情况
            r = r * 10 + x % 10;
            x /= 10;
        }
        return r;
    }
};

8. 字符串转换整数 (atoi)

在这里插入图片描述
思想
我们一步一步考虑可能出现情况,并将他处理了即可

  • 可能会出现前面很多空格的情况,我们就声明一个变量循环取出字符串中的每一个字符,和’ '比较
  • 空格后可能出现符号 - 和 + 我们同样需要进行判断,并用变量存储起来
  • 符号后面就是数字,数字部分我们需要注意额外的范围判断,并且注意正负范围不是对称的情况
class Solution {
public:
    int myAtoi(string s) {
        //首先需要判断一下' ',把前面的空格全部删除
        int k = 0;
        while(s[k] == ' ') k++;
        if(k == s.size()) return 0;
        //再判断可能遇到的 - 和 +
        int f = 1;
        if(s[k] == '-') f = -1, k++;
        else if(s[k] == '+') k++;
        //取出每一个数字
        int res = 0;
        while(k < s.size() && s[k] >= '0' && s[k] <= '9') {
            int x = s[k++] - '0';
            //可能存在溢出情况需要特殊判断
            if(f > 0 && res > (INT_MAX - x) / 10) return INT_MAX;
            if(f < 0 && -res < (INT_MIN + x) / 10) return INT_MIN;
            //由于正负区间不是对称的,所以需要特殊判断一下范围更大的负数边界
            if(-res * 10 - x == INT_MIN) return INT_MIN;
            res = res * 10 + x;
        }
        return res * f;
    }
};

9. 回文数

在这里插入图片描述
思想
这题目的方法很多也比较简单,这里主要介绍翻转法和数值法

  1. 翻转法
    rbegin()表示翻转后的第一个元素,rend()同理。
class Solution {
public:
    bool isPalindrome(int x) {
        if(x < 0) return false;
        string s = to_string(x);
        return s == string(s.rbegin(), s.rend()); 
    }
};
  1. 数值法
    数值法就是和前面整数翻转思想一模一样,并且这道题目没有long long的限制
class Solution {
public:
    bool isPalindrome(int x) {
        if(x < 0) return false;
        long long res = 0, k = x;
        while(x) {
            res = res * 10 + x % 10;
            x /= 10;
        }
        return k == res;
    }
};

11. 盛最多水的容器

在这里插入图片描述
思想
使用双指针算法,i从数组的起点开始,j从数组的结尾开始,每次都先计算一下[i, j]范围内的结果,和上一次计算结果比较取最大值。随后比较i,j两个位置的大小,小的一侧移动一位。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int res = 0;
        for(int i = 0, j = height.size() - 1; i < j;) {
            res = max(res, min(height[i], height[j]) * (j - i));
            if(height[i] < height[j]) i++;
            else j--;
        }
        return res;
    }
};

12. 整数转罗马数字

思想
我们根据题干和数据得内容判断,这个题目应该可以枚举找出其中的规律
在这里插入图片描述
[图片]
其中每一位的情况中有几个特殊的情况比如,900、500、400、90、50、40、9、5、4
给定的整数我们从高位开始从大到小判断整数属于哪个区间,减去对应区间的数值,这样描述比较抽象,我按照2469距离:
在这里插入图片描述

class Solution {
public:
    string intToRoman(int num) {
        int a[] = {
            1000,
            900, 500, 400, 100,
            90, 50, 40, 10,
            9, 5, 4, 1
        };
        string b[] = {
            "M",
            "CM", "D", "CD", "C",
            "XC", "L", "XL", "X",
            "IX", "V", "IV", "I"
        };
        string res;
        for(int i = 0; i < 13; i++) {
            while(num >= a[i]) {
                num -= a[i];
                res += b[i];
            }
        }
        return res;
    }
};

13. 罗马数字转整数

这道题目就是上一题目的逆运算,需要特殊判断的情况是:前一位小于后一位,例如4、40…

class Solution {
public:
    int romanToInt(string s) {
        unordered_map<char, int> heap;
        heap['I'] = 1, heap['V'] = 5,
        heap['X'] = 10, heap['L'] = 50,
        heap['C'] = 100, heap['D'] = 500,
        heap['M'] = 1000;
        int res = 0;
        for(int i = 0; i < s.size(); i++) {
            if(i + 1 < s.size() && heap[s[i]] < heap[s[i + 1]]) res -= heap[s[i]];
            else res += heap[s[i]];
        }
        return res;
    }
};

14. 最长公共前缀

思想
通过暴力做法:第一层循环遍历每一位,第二层循环遍历i位置的每一个字符串是否相等

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        string res;
        if(strs.empty()) return res;
        for(int i = 0; i < strs[0].size(); i++) {
            char c = strs[0][i];
            for(auto stri : strs) {
                if(i > stri.size() || c != stri[i]) return res;
            }
            res += c;
        }
        return res;
    }
};

启发:增强for循环这里第二层循环就很好用,可以快速取出strs中的每一个字符串


15. 三数之和

思想
这题目和后面一些题目都是类似的方法,都会用到双指针算法,而双指针算法需要明确的第一件事——数组是有序的。因为这个题目里面一共有三个变量,定义 i,j,k三个指针,没有三指针算法,所以需要先固定一个指针i,默认三个指针大小顺序是 i < j < k,可以减少一部分重复,固定了i指针,j从i后面开始遍历,k从尾部倒序遍历,目标是找出满足 nums[i] + nums[j] + nums[k] >= 0 的最小的k ,这样能够简化时间复杂度的运用是排完序以后,如果j增加,那么k一定只能往左走,因为nums[i]是固定的。
重复的问题怎么解决?从i入手,每一次先判断i位置的数值和上个是否相同,相同就跳过。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        //第一个指针i遍历整个集合
        for(int i = 0; i < nums.size(); i++) {
            //i 不等于0 并且 第i个数和i - 1相同的时候是重复的情况
            if(i && nums[i] == nums[i - 1]) continue;
            //双指针
            for(int j = i + 1, k = nums.size() - 1; j < k; j++) {
                //去重
                if(j > i + 1 && nums[j] == nums[j - 1]) continue;
                //试探性查看k - 1是不是最小能取到的值
                while(j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= 0) k--;
                if(nums[i] + nums[j] + nums[k] == 0) 
                    res.push_back({nums[i], nums[j], nums[k]});
            }
        }
        return res;
    }
};

16. 最接近的三数之和

思想
这题和上面的题目解法是类似的,并且这个问题最后只需要返回结果数值,所以可以省去去重的过程,但是需要注意的一个问题是最终结果可能是nums[i] + nums[j] + nums[k] (大于target)或者nums[i] + nums[j] + nums[k - 1](小于target)

class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        //第一步先排序
        sort(nums.begin(), nums.end());
        //pair数组first存放和target的插值,second存放实际数值
        pair<int, int> res(INT_MAX, INT_MAX);
        for(int i = 0; i < nums.size(); i++) {
            for(int j = i + 1, k = nums.size() - 1; j < k; j++) {
                while(j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= target) k--;
                int s = nums[i] + nums[j] + nums[k];
                res = min(res, make_pair(abs(s - target), s));
                //这个特判不可少
                if(j < k - 1) {
                    int n = nums[i] + nums[j] + nums[k - 1];
                    res = min(res, make_pair(target - n, n));
                }
            }
        }
        return res.second;
    }
};

17. 电话号码的字母组合

思想
需要找出所有的排序方式,这个问题是典型的DFS暴搜问题,这里我们先来回顾一下DFS知识点。DFS中比较重要的两个概念是回溯和剪枝。
例题:给定一个整数n,将数字1~n排成一排,请你按照字典序将所有的排列方法输出。这就是一个最经典的DFS问题,思考DFS问题我们首先需要画出搜索树,这样能够帮助我们理解DFS过程,DFS就是从第一个位置开始一直往下搜索知道没有路可以走,再回溯。

//给定一个整数n,将数字1~n排成一排,将会有很多种排列方法。
//现在,请你按照字典序将所有的排列方法输出。
//经典的DFS问题
#include <iostream>

using namespace std;
const int N = 20;
int n, path[N];//path数组用来存放搜索的路径结果
bool st[N];//st数组用来保存状态,表示是否已经使用过

void dfs(int u) {
    //当 u == n 表示搜索到最后+1位,这个路径搜索结束可以输出结果了
    if (u == n) {
        for (int i = 0; i < n; ++i)
            printf("%d", path[i]);
        puts("");
        return;
    }
    //没有走到最后一位+1时,就递归搜索
    for (int i = 1; i <= n; ++i) {
        if (!st[i]) {
            path[u] = i;
            st[i] = true;
            dfs(u + 1);
            //递归搜索结束以后需要恢复状态
            st[i] = false;
        }
    }
}

int main() {
    scanf("%d", &n);
    dfs(0);//从第一个位置开始搜索
    return 0;
}

有了这个基础我们再来看电话号码的问题:

  • 首先我们需要定义一个数组用来存放电话数字对应的字符
  • DFS搜索出全部的结果
class Solution {
public:
    vector<string> res;
    string path;//path作为某一次路径的记录变量
    string phone[10] = {
        "", "", "abc", "def",
        "ghi", "jkl", "mno",
        "pqrs", "tuv", "wxyz"
    };

    vector<string> letterCombinations(string digits) {
        //特判是否为空的情况
        if (digits.empty()) return res;
        dfs(digits, 0);
        return res;
    }

    void dfs(string& digits, int u) {
        //判断u是否已经走到最后的位置
        if (u == digits.size()) {
            res.push_back(path);
        } else {
        //else 就是没有搜末尾的情况,需要递归dfs处理
            for (auto c : phone[digits[u] - '0']) {
                path.push_back(c);
                dfs (digits, u + 1);
                //搜索完回溯
                path.pop_back();
            }
        }
    }
};

在精简,path作为参数传入

class Solution {
public:
    vector<string> res;
    string phone[10] = {
        "", "", "abc", "def",
        "ghi", "jkl", "mno",
        "pqrs", "tuv", "wxyz"
    };

    vector<string> letterCombinations(string digits) {
        if (digits.empty()) return res;
        dfs(digits, 0, "");
        return res;
    }

    void dfs(string& digits, int u, string path) {
        if (u == digits.size()) res.push_back(path);
        else {
            for (auto str : phone[digits[u] - '0'])
                //包含了回溯的过程
                dfs (digits, u + 1, path + str);
        }
    }
};

18. 四数之和

和上面做的三数之和完全类似

class Solution {
public:
    vector<vector<int>> res;
    //暴力做法n^4,可以通过双指针做法实现n^3
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        //双指针问题时有序为前提的,所以排序操作不能忘记
        sort(nums.begin(), nums.end());
        //处理一下超范围情况
        if(target == -294967296 || target == 294967296)return {};
        for(int i = 0; i < nums.size(); i++) {
            if(i && nums[i] == nums[i - 1]) continue;
            for(int j = i + 1; j < nums.size(); j ++) {
                if(j > i + 1 && nums[j] == nums[j - 1]) continue;
                for(int k = j + 1, l = nums.size() - 1; k < l; k++) {
                    if(k > j + 1 && nums[k] == nums[k - 1]) continue;
                    while(k < l - 1 && nums[i] + nums[j] >= target - nums[k] - nums[l - 1]) l--;
                    if(nums[i] + nums[j] == target - nums[k] - nums[l]) res.push_back({nums[i], nums[j], nums[k], nums[l]});
                }
            }
        }
        return res;
    }
};

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

思想
过程就是先遍历一遍链表得到链表的总长度,删除倒数第k个结点,需要知道倒数第k+1个结点是什么。
注意点:遇到头节点可能变化的题目需要使用到一个技巧——使用虚拟头节点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        //创建一个虚拟结点-1,dummy指针指向这个虚拟节点
        //头指针dummy指向虚拟头节点,虚拟头节点的next是原始头节点
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;
        //统计链表的长度
        int cnt = 0;
        for(ListNode* i = dummy; i; i = i->next) cnt++;
        //找出倒数 k + 1 个结点
        ListNode* p = dummy;
        for(int i = 0; i < cnt - n - 1; i++) p = p->next;
        p->next = p->next->next;
        return dummy->next;
    }
};

20. 有效的括号

思想
分析题目首先想到这个和栈数据结构非常相似,如果是左括号的一种情况那么就选择放入栈中,如果是右括号中的一种情况,就去分析能否和栈顶的元素配对,能配对就弹出原来的栈顶元素,不能配对就说明不满足条件return false

class Solution {
public:
    bool isValid(string s) {
        //可以先通过奇偶判断
        int n = s.size();
        if(n % 2 == 1) return false;
        unordered_map<char, char> k = {
            {')', '('},
            {']', '['},
            {'}', '{'}
        };
        stack<char> stack;
        for(auto str: s) {
            //判断是左括号还是右括号;左括号考虑加入栈,右括号考虑是否配对
            if(k.count(str)) {
                if(stack.empty() || stack.top() != k[str]) return false;
                else stack.pop();
            } else {
                stack.push(str);
            }
        }
        return stack.empty();
    }
};

y总的简化代码

class Solution {
public:
    bool isValid(string s) {
        stack<char> stack;
        for(auto ca: s) {
            if(ca == '(' || ca == '[' || ca == '{') stack.push(ca);
            else {
                //通过ascaii表发现配对的括号差小于2
                if(!stack.empty() && abs(stack.top() - ca) <= 2) stack.pop();
                else return false;
            }
        }
        return stack.empty();
    }
};

21. 合并两个有序链表

思想
这道题目就是归并排序中的核心思想,只不过是在链表的背景之下。同时,这里再次重申一下题目的小技巧,凡是涉及到头节点需要特判或者头节点会变化的情况,我们就创建一个虚拟头节点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        //创建一个虚拟头节点dummy,同时为了方便我们更新下一个结点,使用tail节点
        auto dummy = new ListNode(-1), tail = dummy;
        //等价于 ListNode* dummy = new ListNode(-1);
        //      ListNode* tail = dummy;
        //l1 和 l2 都不为空的时候判断大小
        while(list1 && list2) {
            if(list1->val < list2->val) {
                //tail和list1都需要更新
                tail = tail->next = list1;
                list1 = list1->next;
            } else {
                tail = tail->next = list2;
                list2 = list2->next;
            }
        }
        //当一个指针到达末尾时,只需要把tail下一个节点接到不为空的那个链表后面
        if(list1) tail->next = list1;
        if(list2) tail->next = list2;
        return dummy->next;
    }
};

22. 括号生成

思想
对于括号配对问题需要了解两个常用到的结论(前提是只有一种类型括号):

  1. 左括号的数量小于等于右括号的数量时,才能配对
  2. 最终左右括号数量相等
    对于这个问题需要给出所有的排列方式的题目,我们应该条件反射的想到dfs来解决。
    代码中并没有显式的写出回溯的过程,起始回溯隐藏在了if判断中,if的本质含义起始就是可以实现的几种方案,当一种方案可行并且结束以后,返回到if判断,就会考虑下一个if;即通过多个if考虑了所有可能的情况
class Solution {
public:
    vector<string> res;
    vector<string> generateParenthesis(int n) {
        dfs(n, 0, 0, "");
        return res;
    }
    void dfs(int& n, int ln, int rn, string path) {
        //当左括号数目和右括号数目都等于n的时候这条搜索路径结束
        if(ln == n && rn == n) res.push_back(path);
        else {
            //if含义,就是dfs时当前可能走的分支情况,本题就只有两种情况
            if(ln < n) dfs(n, ln + 1, rn, path + "(");
            if(ln > rn && rn < n) dfs(n, ln, rn + 1, path + ")");
        }
    }
};

24. 两两交换链表中的节点

思想
整个过程如图所示,涉及到头节点变化的题目都可以通过创建虚拟头节点提供便利。交换过程需要考虑先后问题
链表题注意事项:
以下错误原因,链表的下一个节点是否为空都需要有一个判断,否则就会出现这个错误

runtime error: member access within null pointer of type 'ListNode'

在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        auto dummy = new ListNode(-1), tail = dummy;
        dummy->next = head;
        while(tail->next && tail->next->next) {
            auto h1 = tail->next, h2 = h1->next;
            h1->next = h2->next;
            h2->next = h1;
            tail->next = h2;
            tail = h1;
        }
        return dummy->next;
    }
};

25. K 个一组翻转链表

思想
本题和上一题思路非常相似,变成了翻转k个数。

  1. 第一步我们还是创建虚拟头节点
  2. 通过遍历的方式判断后续是否含有k个节点,不足则不进行翻转操作
  3. 整个翻转的过程我们可以分为三步
    a. 第一步完成k个节点内部指针的反向指针(注意翻转变化的顺序)
    b. 第二步虚拟头节点下一个指针改变
    c. 第三步k个节点的最后一个节点指针改变
  4. 指针改变需要三个指针,通过下图帮助理解
    在这里插入图片描述
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        //注意提前保存后续要使用到的节点
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;
        for(ListNode* h0 = dummy;;) {
            //遍历查看是否含有k个节点
            ListNode* h00 = h0;
            for(int i = 0; i < k && h00; i++) h00 = h00->next;
            if(!h00) break;
            //含有k个元素,需要通过赋值的两个指针进行链表内部的翻转
            auto h1 = h0->next, h11 = h0->next, h2 = h1->next;
            //一共k个元素需要进行k - 1次翻转操作
            for(int i = 0; i < k - 1; i++) {
                auto h3 = h2->next;
                h2->next = h1;
                h1 = h2, h2 = h3;
            }
            h0->next = h1;
            h11->next = h2;
            h0 = h11;
        }
        return dummy->next;
    }
};

26. 删除有序数组中的重复项

思想
这其实就是一个unique函数,我们就去判断一个数和它前一个数是否重复,比较简单直接上代码

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int cnt = 0;
        for(int i = 0; i < nums.size(); i++) {
            if(!i || nums[i] != nums[i - 1]) nums[cnt++] = nums[i];
        }
        return cnt;
    }
};

27. 移除元素

思想
和上面题目思想高度类似。通过cnt变量记录应该放置在数组的什么位置,比较简单代码如下:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int cnt = 0;
        for(int i = 0; i < nums.size(); i++) {
            if(nums[i] != val)
                nums[cnt++] = nums[i];
        }
        return cnt;
    }
};

28. 找出字符串中第一个匹配项的下标

这道题目就是KMP算法的内容,需要回顾一下KMP的知识。

class Solution {
public:
    int strStr(string haystack, string needle) {
        if(needle.empty()) return 0;
        int n = haystack.size(), m = needle.size();
        int ne[m];
        //预处理needle
        ne[0] = -1;
        for(int i = 1, j = -1; i < m; i++) {
            //如果新加入的字符不能匹配,j一直回退
            while(j != -1 && needle[i] != needle[j + 1]) j = ne[j];
            //如果新加入的字符能够匹配,最长字串长度增加,继续匹配
            if(needle[i] == needle[j + 1]) j++;
            ne[i] = j;
        }
        //KMP处理,和上面处理思想一致
        for(int i = 0, j = -1; i < n; i++) {
            while(j != -1 && haystack[i] != needle[j + 1]) j = ne[j];
            if(haystack[i] == needle[j + 1]) j++;
            if(j == m - 1) {
                return i - j;
            }
        }
        return -1;
    }
};

29. 两数相除

思想
我们以147 / 3 = 49为例,49的二进制表示 = 110001
由此我们可以借助二进制表示,提前存储好所有2^k * divisor的情况,题目条件限制比较多,只能使用int类型,并且考虑到正数范围比负数小,所以我们在处理的时候统一使用负数来处理。在处理过程中需要及时考虑边界条件溢出的情况。

class Solution {
public:
    int divide(int x, int y) {
        bool flag = (x > 0) ^ (y > 0);
        if (x > 0) x = -x;
        if (y > 0) y = -y;
        vector<pair<int, int>> tmpadd;
        for(int i = y, j = -1; i >= x; i += i, j += j) {
            tmpadd.emplace_back(i, j);
            if(i < (INT_MIN >> 1)) break;
        }
        int res = 0;
        for(int i = tmpadd.size() - 1; i >= 0; i--) {
            if(tmpadd[i].first >= x) {
                res += tmpadd[i].second;
                x -= tmpadd[i].first;
            }
        }
        if(!flag) {
            if(res == INT_MIN) return INT_MAX;
            res = -res;
        }
        return res;
    }
};


⭐这个专栏将会持续更新LeetCode题解和思路分享,欢迎留言交流!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值