C++ : 力扣_Top(22-41)

C++ : 力扣_Top(22-41)

文章目录

  • C++ : 力扣_Top(22-41)
      • 22、括号生成(中等)
      • 23、合并K个排序链表(困难)
      • 26、删除排序数组中的重复项(简单)
      • 28、实现 strStr()(简单)
      • 29、两数相除(中等)
      • 33、搜索旋转排序数组(中等)
      • 34、在排序数组中查找元素的第一个和最后一个位置(中等)
      • 36、有效的数独(中等)
      • 38、外观数列(简单)
      • 41、缺失的第一个正数(困难)


22、括号生成(中等)

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。例如,给出 n = 3,生成结果为:
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”

// 暴力递归法
class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<string> result;
        if(n==0) return result;
        string s = "";
        generateCore(s, 0, 2*n, result);
        return result;
    }
    void generateCore(string s, int sum, int left, vector<string>& result){
        if( sum!=0 && left==0 ){  // 不匹配的情况
            return;
        }
        if( sum<0 ){ // 该排序路线错误
            return;
        }
        if( sum==0 && left==0 ){ // 排序到了最后
            result.push_back(s);
            return;
        }
        generateCore(s+"(", sum+1, left-1, result);
        generateCore(s+")", sum-1, left-1, result);
    }
};

思路: 按照每次添加一个括号的思路进行递归搜索,该方法不难但速度较慢;


// 动态规划速度法:
class Solution {
    bool invalid_input = false;
public:
    vector<string> generateParenthesis(int n) {
        // dp[n] 存放输入 n 时的所有排列组合string
        vector<vector<string> > dp(n+1); // 0~n
        dp[0] = {""};
        dp[1] = {"()"};
        if(n<0){
            invalid_input = true;
            return dp[0];
        }
        if(n==0) return dp[0];
        if(n==1) return dp[1];
        for(int i=2; i<=n; ++i){ // 从输入i为2时开始遍历
            for(int j=0; j<i; ++j){ // 输入为0~i-1的所有组合
                // 该循环和下面的循环用来遍历在当前()内侧和右侧的两组排列组合pq
                for(string p : dp[j]){ 
                    for(string q : dp[i-1-j]){
                        string s = "(" + p + ")" + q;
                        dp[i].push_back(s);
                    }
                }
            }
        }
        return dp[n];
    }
};

思路:一种非常巧妙的动态规划方法:主要思想是利用一个vector存放输入为n时的所有排列组合到一个元素里(vector),然后当输入为n+1时,相当于多了一对括号,这对括号可以是 “(” + p “)” + q 的形式,其中p和q分别表示输入为i和(n-i)时的所有括号排列组合。代码比较难想,但该方法的效率非常高;


23、合并K个排序链表(困难)

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6

// 递归分治法
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        if(lists.size()==0) return nullptr;
        return Split(lists, 0, lists.size()-1);
    }
    ListNode* Split(vector<ListNode*>& lists, int left, int right){
        if(left==right) return lists[left];  // 如果只有一个链表,返回其头节点指针
        int mid = (left + right) / 2;
        ListNode* l1 = Split(lists, left, mid);  // 递归到最底层,返回一个链表指针
        ListNode* l2 = Split(lists, mid+1, right); // 递归到最底层,返回一个链表指针
        return MergeTwoLists(l1, l2);
    }
    ListNode* MergeTwoLists(ListNode* l1, ListNode* l2){
        if(l1==nullptr) return l2;  // 遍历到最后有一个链表为空时停止迭代
        if(l2==nullptr) return l1;  // 以及不排除一开始就有的链表头节点为null的情况
        if(l1->val<l2->val){
            l1->next = MergeTwoLists(l1->next, l2);
            return l1;
        }
        else{
            l2->next = MergeTwoLists(l1, l2->next);
            return l2;
        }
    }
};

思路:该题有很多种解法,包括暴力链接,分解为两个链表两两合并的分治法等等,思路不难想,但代码难写,上述是一种比较简单的写法,利用到了两两分开然后归并的递归方法以及递归合并两个链表的方法,是一种双重递归写法。


26、删除排序数组中的重复项(简单)

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

程序返回一个不重复数字的个数len,原数组的前len个数变成不重复的排序数列,后面的数字不用处理。

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        if(nums.size()==0) return 0;
        int val = nums[0], len = 1; // val 保存上一个值、len表示当前长度
        for(int i=1; i<nums.size(); ++i){ // 从第二个数开始遍历
            if(nums[i]==val) continue; // 如果相等,直接完后遍历
            else{
                nums[len] = nums[i]; // 不相等,直接赋值
                val = nums[len];
                ++len;
            }
        }
        return len;
    }
};

思路:比较简单,双指针法遍历一遍完成。


28、实现 strStr()(简单)

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

class Solution {
public:
    int strStr(string haystack, string needle) {
        int len1 = haystack.size();
        int len2 = needle.size();
        if(len2==0) return 0;  // 若needle是空串
        int j = 0;  // needle 字符串的索引
        for(int i=0; i<len1; ++i){
            if(haystack[i]==needle[j]){  // 当前字符相等
                if(j==len2-1) return i-j;  // 返回needle第一个字符出现的位置
                ++j;  // needle索引也前进一位
            }
            else{  // 当前字符不相等(特别注意,needle已经遍历过的部分也要重新判断)
                if(j!=0){
                    i = i - j;  // 将i往前重置已经遍历过的j个位置,以防之前遍历的部分也有符合的部分。
                    j = 0;
                }
            }
        }
        return -1;
    }
};

思路:双指针法,一次遍历。这里需要特别注意如果当前判断子字符串的过程中发现不一致时,需要将父字符串的索引往前退回已经已经遍历过的j个位置,以防漏找;此种属于暴力解决问题,最差情况每次移动父字符串n都要遍历所有子字符串m,故计算复杂度为:
O((n-m)×m);

引入高级的字符串匹配算法:MKP算法

特别注意:在字符串中找到对应子串的问题看似简单,但优化却非常复杂;该题有计算复杂度更低的算法,即KMP字符串匹配算法:该算法借助一个与子字符串等长的F[i]数组,其表示子字符串中从头算起当前长度为i的子串的最大的相等前后缀长度,如abaafabaa的F矩阵为[0,0,1,1,0,1,2,3,4],然后对父串子串进行双指针遍历,如果在下标j遇到了不同字符,则查找F矩阵中j-1的数值,就可以知道再从子串的哪个下标处继续遍历了(不用像双指针法一样不匹配时只能将父串下标往前移动一个再继续从头匹配子串),KMP算法计算复杂度为:
O(n+m) (其中m表示建立F矩阵的复杂度,n表示最后遍历查找时的复杂度)

注意:这里建立矩阵 F[i] 的思路是求解字符串相同的前缀后缀的好方法!!!

一个比较典型的模拟例子:
A=“abaabaabbabaaabaabbabaab”
B=“abaabbabaab”


29、两数相除(中等)

给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod(取余) 运算符。返回被除数 dividend 除以除数 divisor 得到的商。

注意:
被除数和除数均为 32 位有符号整数。
除数不为 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31, 2^31 − 1]。本题中,如果除法结果溢出,则返回 2^31 − 1。

class Solution {
public: // 记得考虑: -2147483648 / -1或1
    int divide(int dividend, int divisor) {
        int a = dividend, b = divisor;
        if(a==0) return 0; // 被除数为0
        if(a==INT_MIN && b==-1) return INT_MAX; // 考虑越界情况
        int sign = 1;
        if((a>0&&b<0)||(a<0&&b>0)) sign = -1; // 两数一正一负
        if(a>0){  // 避免越界,都转换为负数进行计算
            a = -a;
        }
        if(b>0){
            b = -b;
        } 
        int count = div(a,b); // 递归除法,为避免越界,返回真实值的负数
        if(sign==1){ // 两数同号
            return count * -1;
        }
        else{ // 两数异号
            return count;
        }
    }
    int div(int a,int b){ // 两个负数的递归除法,这里面的数可都作为正数理解
        if(a>b) return 0; // 如 a = -1, b = -3;
        int b_sum = b;  // 定义当前的除数之和
        int count = -1; // 当前除法结果的负值(为避免越界)
        while(1){
            if(a-b_sum>b_sum) break; // 再加就大于被除数了
            count = count + count; // 除法结果翻倍
            b_sum = b_sum + b_sum; // 除数翻倍
        }
        return count + div(a-b_sum, b); // 返回的是结果的负数
    }
};

思路:这道题的关键在于必须认识到除法的越界问题,即当-2147483648/-1时,会造成越界。而且还需要注意在计算过程中统计结果变量的越界情况,各种越界可能下的预防和处理非常关键。另外,采取一种类似二分的思想加速运算,如11/3时,首先11比3大,结果至少是1,然后我让3翻倍,就是6,发现11比3翻倍后还要大,那么结果就至少是1×2了,那我让这个6再翻倍,得12,11不比12大。我让11减去刚才最后一次的结果6,剩下5,我们计算5是3的几倍,也就是除法,递归出现了。


33、搜索旋转排序数组(中等)

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )

搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

你可以假设数组中不存在重复的元素。你的算法时间复杂度必须是 O(log n) 级别。

// 好理解但复杂的二分法
class Solution {
public: // 数组中不存在重复的元素
    int search(vector<int>& nums, int target) {
        if(nums.empty()) return -1;
        // 先二分查找最小值的位置,在根据最小值的位置查找目标值的位置
        int start_pos = Find_Start_Pos(nums,0,nums.size()-1);
        if(target<=nums.back()){  // 目标值在最小值和数组右侧之间
            return Find_Target(nums,start_pos,nums.size()-1,target);
        }
        else{ // 目标值在数组左侧和最大值之间
            return Find_Target(nums,0,start_pos-1,target);
        }
    } 
    // 二分查找最小值的位置
    int Find_Start_Pos(vector<int>& nums, int left, int right){
        // 需要牢记:旋转数组的左侧值和右侧值是紧紧相邻的两个变量,且左侧大于右侧
        // 二分一个旋转数组时,分为大于等于左侧 和 小于右侧 两种情况;
        while(nums[left]>nums[right]){  // 如果当前是个旋转数组
            int mid = (left + right) / 2;
            if(nums[mid]>=nums[left]){ // 如果中间值大于等于左侧值
                left = mid + 1;  // 最小值肯定在mid右侧
            }
            else if(nums[mid]<nums[right]){ // 如果中间值小于右侧值
                right = mid; // 最小值肯定在左侧
            }
        } // 循环完毕,锁定当前的非旋转片段最左侧;
        return left;
    } 
    // 二分查找target的位置
    int Find_Target(vector<int>& nums, int left, int right, int target){
        while(left<=right){
            int mid = (left + right) / 2; 
            if(nums[mid]==target) return mid; // 如果相等则直接返回坐标
            if(nums[mid]<target){ // 如果mid小于目标值
                left = mid + 1; // 则目标值在mid右侧
            }
            if(nums[mid]>target){ // 如果mid大于等于目标值
                right = mid - 1; // 目标值在mid左侧
            }
        }
        return -1;
    }   
};

// 简单二分法
class Solution {
public: // 数组中不存在重复的元素
    int search(vector<int>& nums, int target) {
        if(nums.empty()) return -1;
        int left = 0;
        int right = nums.size()-1;
        while(left<=right){
            int mid = (left+right)/2;
            if(nums[mid]==target) return mid;
            if(nums[mid]>=nums[left]){ // mid左侧是个递增序列
                if(target>=nums[left] && target<nums[mid]){ // target在该递增序列里
                    right = mid - 1;
                }
                else{ // 不在该递增序列
                    left = mid + 1;
                }
            }
            else{ // mid左侧不是递增序列,说明mid右侧是递增的
                if(target>nums[mid] && target<=nums[right]){ // target在该递增序列里
                    left = mid + 1;
                }
                else{ // 不在该递增序列
                    right = mid - 1;
                }
            }
        }
        return -1; // 退出循环,没找到
    }
};

思路:在旋转数组中查找目标节点,是典型的二分法的应用练习(必须多练并熟悉),不同于在旋转数组中查找最小值,该题的复杂性提升到了查找任意值;首先第一种代码方法是先按在旋转数组中查找最小值的位置,然后把数组分为左侧的递增序列和右侧的递增序列,然后判断target在左右哪一侧,然后再利用一次二分查找找到。该方法容易想到,但需要写两个二分,其边界关系也十分复杂,不推荐;第二种方法比较难想,但写起来容易,它也是利用了两个递增数列的思想,但不用找到最小值;首先计算mid,如果相等就返回;如果mid大于了最左侧值,说明左侧是一个递增序列,如果target在该范围内,就继续二分查找,如不在该范围内则继续查找mid右侧;若mid小于最右侧值,则说明右侧是一个递增,再判断target是否在该范围内,,balabala.


34、在排序数组中查找元素的第一个和最后一个位置(中等)

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。如果数组中不存在目标值,返回 [-1, -1]。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> result = {-1, -1};
        if(nums.empty()) return result; // 数组为空
        result[0] = getleft(nums,0,nums.size()-1,target); // 获取左边的值
        result[1] = getright(nums,0,nums.size()-1,target); // 获取右边的值
        return result;
    }
    int getleft(vector<int>& nums, int left, int right, int target){
        while(left<=right){
            int mid = (left + right) / 2;
            if(nums[mid]==target){
                if((mid>0&&nums[mid-1]<target)||mid==0){ // 符合最左目标值
                    return mid;
                }
                else{
                    right = mid - 1;
                }
            }
            else if(nums[mid]>target){  // 当前大于目标值
                right = mid - 1;
            }
            else{ // 当前小于目标值
                left = mid + 1;
            }
        }
        return -1;
    }
    int getright(vector<int>& nums, int left, int right, int target){
        while(left<=right){
            int mid = (left + right) / 2;
            if(nums[mid]==target){
                if((mid<nums.size()-1&&nums[mid+1]>target)||mid==nums.size()-1){ // 符合最左目标值
                    return mid;
                }
                else{
                    left = mid + 1;
                }
            }
            else if(nums[mid]>target){  // 当前大于目标值
                right = mid - 1;
            }
            else{ // 当前小于目标值
                left = mid + 1;
            }
        }
        return -1;
    }
};

思路:分别利用二分查找最左点和最右点即可;


36、有效的数独(中等)

判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aahbFUSQ-1585506196091)(evernotecid://1C69F8F2-E27B-445E-9329-9AF067309442/appyinxiangcom/23736103/ENResource/p325)]
上图是一个部分填充的有效的数独。数独部分空格内已填入了数字,空白格用 ‘.’ 表示。

class Solution {
public:
    bool isValidSudoku(vector<vector<char>>& board) {
        if(board.size()!=9||(!board.empty()&&board[0].size()!=9)) return false;
        int r[9][9] = {0}; // 第几行的哈希表
        int c[9][9] = {0}; // 第几列的哈希表
        int b[9][9] = {0}; // 第几个子数独的哈希表
        int val = 0;
        for(int i = 0; i < board.size(); ++i){
            for(int j = 0; j < board[0].size(); ++j){
                if(board[i][j]!='.'){
                    val = board[i][j] - '1';
                    if(r[i][val]) return false;
                    if(c[j][val]) return false;
                    if(b[j/3+i/3*3][val]) return false;
                    // 如果都没在哈希表中出现过,则对应位置变为1
                    r[i][val] = 1;
                    c[j][val] = 1;
                    b[j/3+i/3*3][val] = 1;
                }
            }
        }
        return true;
    }
};

思路:两种方法一种是遍历数组三遍,然后每一遍时检查三个条件中点的一个是否满足;但三个条件不冲突,可以在一次遍历中检查完毕;需要建立三个同等大小的哈希表,这里哈希表的建立比较抽象,比较难想;其中第三个条件下需要找到每个坐标对应的子数独块的序号;


38、外观数列(简单)

「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。前五项如下:

  1. 1
    
  2. 11
    
  3. 21
    
  4. 1211
    
  5. 111221
    

1 被读作 “one 1” (“一个一”) , 即 11。
11 被读作 “two 1s” (“两个一”), 即 21。
21 被读作 “one 2”, “one 1” (“一个二” , “一个一”) , 即 1211。

给定一个正整数 n(1 ≤ n ≤ 30),输出外观数列的第 n 项。

注意:整数序列中的每一项将表示为一个字符串。

class Solution {
public:
    string countAndSay(int n) {
        if(n<1||n>30) return "";
        if(n==1) return "1";
        int num = 1; 
        string tmp1 = "1"; // 存放当前n的结果
        string tmp2 = ""; // 存放n+1结果
        auto iter = tmp1.begin();
        for(int i=2; i<=n; ++i){ // 按次数循环
            // 注意!!!tmp1被赋值后所有迭代器失效,需要重新赋值
            iter = tmp1.begin();  
            while(iter!=tmp1.end()){ // 遍历当前string
                while(iter+1!=tmp1.end() && *iter==*(iter+1)){
                    ++num; // 统计当前字符的重复个数
                    ++iter; 
                }
                tmp2.append(to_string(num)); // 先放置该字符数字的个数
                tmp2.push_back(*iter); // 再放置该字符数字
                num = 1;   // 计数重置为1
                ++iter; // 位移一位
            }
            // 遍历完当前string,交换string
            tmp1 = tmp2; 
            tmp2 = "";  // tmp2置为空,以放置下一个结果string
        }
        return tmp1;
    }
};

思路:这道题不太好用巧办法,只得从头开始一个一个计算,由n-1计算得到n,每个相邻连续的相同数字都会被读作个数+数值,代码中需要注意的地方:每次递推一个string,都把tmp2赋给了tmp1,于是之前tmp1的迭代器会失效!需要重新赋值一次;另外数字转string要活用to_string(),如果必须要转成char则可以用:char c[2]; sprintf(c,"%c",num);


41、缺失的第一个正数(困难)

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。

示例 1:
输入: [1,2,0]
输出: 3

示例 2:
输入: [3,4,-1,1]
输出: 2

示例 3:
输入: [7,8,9,11,12]
输出: 1

提示:你的算法的时间复杂度应为O(n),并且只能使用常数级别的额外空间。

// 修改数组本身当做哈希表的做法
class Solution {
public: 
    int firstMissingPositive(vector<int>& nums) {
        vector<int>::size_type n = nums.size();
        if(n==0) return 1;
        for(size_t i=0; i<n; ++i){ // 遍历数组,将每个下标处的值送到这个值对应的下标处;
            while( nums[i]>0 && nums[i]<n+1 && nums[i]!=i+1 && nums[i]!=nums[nums[i]-1] ){
                swap(nums[i], nums[nums[i]-1]);
            }
        }
        for(size_t i=0; i<n; ++i){ // 再次遍历并查找,如果当前数组的值不等于下标+1,则说明该处缺失
            if(nums[i]!=i+1){
                return i+1;
            }
        }
        return n+1; // 如果都排序整齐如1234,则缺失5
    }
};

思路:该题的时间空间限制比较严格,首先确定不能使用排序的方法,再然后也不能建立长度为n的哈希表,所以最好的办法是在原数组上建立哈希表的思想,需要改变数组,也就是在遍历的过程中不断地将当前值为val的数放到下标为val-1的地方,如果遇到值为非正数或者值大于长度n的数也不需要处理,直接遍历下一个数即可;时间复杂度为O(n),利用的是均摊复杂度的思想;

另外需要注意一点的是:定义容器长度的时候尽量使用size_type,for下标循环容器时尽量使用size_t类型,避免越界问题;显得严谨;


你可能感兴趣的:(C++ : 力扣_Top(22-41))