通关算法题之 ⌈数组⌋ 上

滑动窗口

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

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

输入: s = "abcabcbb"
输出: 3 

这就是变简单了,连 needvalid 都不需要,而且更新窗口内数据也只需要简单的更新计数器 window 即可。当 window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了。

唯一需要注意的是,在哪里更新结果 res 呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?

这里和之前不一样,要在收缩窗口完成后更新 res,因为窗口收缩的 while 条件是存在重复元素的,换句话说收缩完成后一定保证窗口中没有重复。

class Solution {
    
    
public:
    int lengthOfLongestSubstring(string s) {
    
    
        unordered_map<char, int> window;
        int left = 0, right = 0, res = 0;
        while(right < s.size()){
    
    
            char c = s[right];
            right++;
            window[c]++;
            while(window[c] > 1){
    
    
                char d = s[left];
                left++;
                window[d]--;
            }
            res = max(res, right - left);
        }
        return res;
    }
};

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target ,找出该数组中满足其和 ≥ target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr],并返回其长度。如果不存在符合条件的子数组,返回 0 。

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2

不过需要强调的是,题目说了 nums 数组中的元素都是正数,有了这个前提才能使用滑动窗口算法,因为窗口扩大时窗口内元素之和必然增大,窗口缩小时窗口内元素之和必然减小。

如果 nums 数组中包含负数,则窗口扩大时元素和不见得就增大,窗口缩小时元素和不见得就减小,这种情况就不能单纯使用滑动窗口技巧了,可能需要混合动态规划和单调队列来做。

class Solution {
    
    
public:
    int minSubArrayLen(int target, vector<int>& nums) {
    
    
        int left = 0, right = 0, sum = 0, res = INT_MAX;
        while(right < nums.size()){
    
    
            sum += nums[right];
            right++;
            while(sum >= target && left < right){
    
    
                res = min(res, right - left);
                sum -= nums[left];
                left++;
            }
        }
        return res == INT_MAX ? 0 : res;

    }
};

76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t ,返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串""

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

滑动窗口的思路:

1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。

2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。

初始状态:

image-20220520115217917

增加 right,直到窗口 [left, right) 包含了 T 中所有字符:

image-20220520115349776

现在开始增加 left,缩小窗口 [left, right)

image-20220520115450943

直到窗口中的字符串不再符合要求,left 不再继续移动:

image-20220520115526871

之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。

首先,初始化 windowneed 两个哈希表,记录窗口中的字符和需要凑齐的字符:

unordered_map<char, int> need, window;
for (char c : t) need[c]++;

然后,使用 leftright 变量初始化窗口的两端,不要忘了,区间 [left, right)左闭右开的,所以初始情况下窗口没有包含任何元素:

int left = 0, right = 0;
int valid = 0; 
while (right < s.size()) {
    
    
    // 开始滑动
}

其中 valid 变量表示窗口中满足 need 条件的字符个数,如果 validneed.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T

如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

string minWindow(string s, string t) {
    
    
    unordered_map<char, int> need, window;
    for (char c : t){
    
    
        need[c]++;
    }
    int left = 0, right = 0, valid = 0; // valid表示窗口中满足 need 条件的字符个数
    // 记录最小覆盖子串的起始索引及长度
    int start = 0, len = INT_MAX;
    while (right < s.size()) {
    
    
        // c 是将移入窗口的字符
        char c = s[right];
        // 扩大窗口
        right++;
        // 进行窗口内数据的一系列更新
        if (need.count(c)) {
    
    
            window[c]++;
            if (window[c] == need[c]) valid++;
        }

        // 判断左侧窗口是否要收缩
        while (valid == need.size()) {
    
    
            // 在这里更新最小覆盖子串
            if (right - left < len) {
    
    
                start = left;
                len = right - left;
            }
            // d 是将移出窗口的字符
            char d = s[left];
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            if (need.count(d)) {
    
    
                if (window[d] == need[d]) valid--;
                window[d]--;
            }                    
        }
    }
    // 返回最小覆盖子串
    return len == INT_MAX ? "" : s.substr(start, len);
}

需要注意的是,当我们发现某个字符在 window 的数量满足了 need 的需要,就要更新 valid,表示有一个字符已经满足要求,而且两次对窗口内数据的更新操作是完全对称的。

valid == need.size() 时,说明 T 中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。

移动 left 收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。

567. 字符串的排列

给你两个字符串 s1s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false

输入:s1 = "ab" s2 = "eidbaooo"
输出:true
输入:s1= "ab" s2 = "eidboaoo"
输出:false

注意哦,输入的 s1 是可以包含重复字符的,所以这个题难度不小。

这种题目,是明显的滑动窗口算法,相当给你一个 S 和一个 T,请问你 S 中是否存在一个子串,包含 T 中所有字符且不包含其他字符

对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:

1、本题移动 left 缩小窗口的时机是窗口大小大于 t.size() 时,应为排列嘛,显然长度应该是一样的。

2、当发现 valid == need.size() 时,就说明窗口中就是一个合法的排列,所以立即返回 true

至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。

class Solution {
    
    
public:
    // 判断 s2 中是否存在 s1 的排列
    bool checkInclusion(string s1, string s2) {
    
    
        unordered_map<char, int> need, window;
        for(char c : s1){
    
    
            need[c]++;
        }
        int left = 0, right = 0, valid = 0;
        while(right < s2.size()){
    
    
            char c = s2[right];
            right++;
            if(need.count(c)){
    
    
                window[c]++;
                if(window[c] == need[c]) valid++;
            }
            // 判断左侧窗口是否要收缩
            while(valid == need.size()){
    
    
                // 判断是否找到了合法的子串
                if(right - left == s1.size()) return true;
                char d = s2[left];
                left++;
                if(need.count(d)){
    
    
                    if(window[d] == need[d]) valid--;
                    window[d]--;
                }
            }
        }
        // 未找到符合条件的子串
        return false;
    }
};

438. 找到字符串中所有字母异位词

给定两个字符串 sp,找到 s 中所有 p的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]

所谓的字母异位词就是排列,相当于输入一个串 S,一个串 T,找到 S 中所有 T 的排列,返回它们的起始索引。跟寻找字符串的排列一样,只是找到一个合法异位词(排列)之后将起始索引加入 res 即可。

class Solution {
    
    
public:
    vector<int> findAnagrams(string s, string p) {
    
    
        unordered_map<char, int> need, window;
        for(char c : p) need[c]++;
        int left = 0, right = 0, valid = 0;
        vector<int> res;
        while(right < s.size()){
    
    
            char c = s[right];
            right++;
            if(need.count(c)){
    
    
                window[c]++;
                if(window[c] == need[c]) valid++;
            }
            // 判断左侧窗口是否要收缩
            while(valid == need.size()){
    
    
                // 当窗口符合条件时,把起始索引加入 res
                if(right - left == p.size()) res.push_back(left);
                char d = s[left];
                left++;
                if(need.count(d)){
    
    
                    if(need[d] == window[d]) valid--;
                    window[d]--;
                }
            }
        }
        return res;
    }
};

剑指 Offer 57 - II. 和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

输入:target = 9
输出:[[2,3,4],[4,5]]
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

双指针,滑动窗口。

class Solution {
    
    
public:
    vector<vector<int>> findContinuousSequence(int target) {
    
    
        int left = 1, right = 1, sum = 0;
        vector<vector<int>> res;
        deque<int> add;
        while(left <= target / 2){
    
    
            sum += right;
            add.push_back(right);
            right++;
            while(sum >= target){
    
    
                if(sum == target){
    
    
                    res.push_back(vector<int>(add.begin(), add.end()));
                }
                sum -= left;
                add.pop_front();
                left++;
            }
            
        }
        return res;      
    }
};

数组遍历/旋转

BM97 旋转数组

一个数组A中存有 n 个整数,在不允许使用另外数组的前提下,将每个整数循环向右移 M( M >=0)个位置,即将A中的数据由(A0 A1 ……AN-1 )变换为(AN-M …… AN-1 A0 A1 ……AN-M-1 )(最后 M 个数循环移至最前面的 M 个位置)。如果需要考虑程序移动数据的次数尽量少,要如何设计移动的方法?

输入:6,2,[1,2,3,4,5,6]
输出:[5,6,1,2,3,4]

使用三次翻转,思路:

  • step 1:因为m可能大于n,因此需要对n取余,因为每次长度为n的旋转数组相当于没有变化;
  • step 2:第一次将整个数组翻转,得到数组的逆序,它已经满足了右移的整体出现在了左边;
  • step 3:第二次就将左边的m个元素单独翻转,因为它虽然移到了左边,但是逆序了;
  • step 4:第三次就将右边的n−m个元素单独翻转,因此这部分也逆序了;
class Solution {
    
    
public:
    vector<int> solve(int n, int m, vector<int>& a) {
    
    
        //取余,因为每次长度为n的旋转数组相当于没有变化
        m = m % n; 
        //第一次逆转全部数组元素
        reverse(a.begin(), a.end()); 
        //第二次只逆转开头m个
        reverse(a.begin(), a.begin() + m); 
        //第三次只逆转结尾m个
        reverse(a.begin() + m, a.end()); 
        return a;
    }
};

48. 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

img
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

旋转二维矩阵的难点在于将「行」变成「列」,将「列」变成「行」,而只有按照对角线的对称操作是可以轻松完成这一点的,对称操作之后就很容易发现规律了。

我们可以先将 n x n 矩阵 matrix 按照左上到右下的对角线进行镜像对称:

image-20220521115152062

然后再对矩阵的每一行进行反转:

image-20220521115345798

发现结果就是 matrix 顺时针旋转 90 度的结果:

image-20220521115523512
class Solution {
    
    
public:
    void rotate(vector<vector<int>>& matrix) {
    
    
        int n = matrix.size();
        for(int i = 0; i < n; i++){
    
    
            for(int j = 0; j < i; j++){
    
    
                swap(matrix[i][j], matrix[j][i]);
            }
        }
        for(int i = 0; i < n; i++){
    
    
            reverse(matrix[i].begin(), matrix[i].end());
        }
    }
};

那如何将矩阵逆时针旋转 90 度呢

思路是类似的,只要通过另一条对角线镜像对称矩阵,然后再反转每一行,就得到了逆时针旋转矩阵的结果:

image-20220521115657715
class Solution {
    
    
public:
    void rotate(vector<vector<int>>& matrix) {
    
    
        int n = matrix.size();
        for(int i = 0; i < n; i++){
    
    
            for(int j = 0; j < n - i; j++){
    
    
                swap(matrix[i][j], matrix[n - j - 1][n - i - 1]);
            }
        }
        for(int i = 0; i < n; i++){
    
    
            reverse(matrix[i].begin(), matrix[i].end());
        }
    }
};

54. 螺旋矩阵

给你一个 m n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

Oj35qK.jpg
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

解题的核心思路是按照右、下、左、上的顺序遍历数组,并使用四个变量圈定未遍历元素的边界。

随着螺旋遍历,相应的边界会收缩,直到螺旋遍历完整个数组:

class Solution {
    
    
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
    
    
        int m = matrix.size(), n = matrix[0].size();
        int upper = 0, lower = m - 1, left = 0, right = n - 1;
        vector<int> res;
        // res.size() == m * n 则遍历完整个数组
        while(res.size() < m * n){
    
    
            // 在顶部从左向右遍历
            if(upper <= lower){
    
    
                for(int j = left; j <= right; j++){
    
    
                    res.push_back(matrix[upper][j]);
                }
                // 上边界下移
                upper++;
            }
            if(left <= right){
    
    
                // 在右侧从上向下遍历
                for(int i = upper; i <= lower; i++){
    
    
                    res.push_back(matrix[i][right]);
                }
                // 右边界左移
                right--;
            }
            if(upper <= lower){
    
    
                // 在底部从右向左遍历
                for(int j = right; j >= left; j--){
    
    
                    res.push_back(matrix[lower][j]);
                }
                // 下边界上移
                lower--;
            }
            if(left <= right){
    
    
                // 在左侧从下向上遍历
                for(int i = lower; i >= upper; i--){
    
    
                    res.push_back(matrix[i][left]);
                }
                // 左边界右移
                left++;
            }
        }
        return res;
    }
};

59. 螺旋矩阵II

给你一个正整数 n ,生成一个包含 1n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

Oj35qK.jpg
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

和上一道题类似的思路:

class Solution {
    
    
public:
    vector<vector<int>> generateMatrix(int n) {
    
    
        vector<vector<int>> res(n, vector<int>(n));
        int upper = 0, lower = n - 1, left = 0, right = n - 1;
        int num = 1;
        while(num <= n * n){
    
    
            if(upper <= lower){
    
    
                for(int j = left; j <= right; j++){
    
    
                    res[upper][j] = num++;
                }
                upper++;
            }
            if(left <= right){
    
    
                for(int i = upper; i <= lower; i++){
    
    
                    res[i][right] = num++;
                }
                right--;
            }
            if(upper <= lower){
    
    
                for(int j = right; j >= left; j--){
    
    
                    res[lower][j] = num++;
                }
                lower--;
            }
            if(left <= right){
    
    
                for(int i = lower; i >= upper; i--){
    
    
                    res[i][left] = num++;
                }
                left++;
            }
        }
        return res;
    }
};

179. 最大数

给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。**注意:**输出结果可能非常大,所以你需要返回一个字符串而不是整数。

输入:nums = [10,2]
输出:"210"

此题求拼接起来的最小数字,本质上是一个排序问题。设数组nums中任意两数字的字符串为x和y,则规定排序判断规则为:若拼接字符串x + y > y + x,则x“大于”y;反之,若x + y < y + x,则x"小于”y。x“小于”y代表:排序完成后,数组中x应在y左边;“大于”则反之。根据以上规则,套用任何排序方法对nums执行排序即可。

class Solution {
    
    
public:
    string largestNumber(vector<int>& nums) {
    
    
        vector<string> strs;
        string res;
        for(int num : nums){
    
    
            strs.push_back(to_string(num));
        }
        sort(strs.begin(), strs.end(), [&](string a, string b){
    
    
            return a + b > b + a;
        });
        for(string str : strs){
    
    
            res += str;
        }
        //避免全部为0的情形
        return res[0] == '0' ? "0" : res;
    }
};

剑指 Offer 66. 构建乘积数组

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积,即B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]

分别迭代计算下三角和上三角两部分的乘积,再相乘,即可 不使用除法 就获得结果。

image-20220628162754163
class Solution{
    
    
public:
    vector<int> constructArr(vector<int> &a){
    
    
        vector<int> res(a.size(), 1);
        int left = 1;	// 左侧乘积
        int right = 1;	// 右侧乘积
        for (int i = 0; i < a.size(); ++i){
    
    
            //左侧的乘积
            res[i] *= left; 
            left *= a[i];
            //右侧的乘积
            res[a.size() - 1 - i] *= right; 
            right *= a[a.size() - 1 - i];
        }
        return res;
    }
};

66. 加一

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外,这个整数不会以零开头。

输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123

其实就是一个数的各位储存在数组里,然后要按照整数加法的法则来计算,然后同样返回一个数组:

class Solution {
    
    
public:
    vector<int> plusOne(vector<int>& digits) {
    
    
        int len = digits.size();
        for(int i = len - 1; i >= 0; i --){
    
    
            digits[i] ++;
            digits[i] %= 10;
            if(digits[i] != 0){
    
    
                return digits;
            }
        }
        digits = vector<int>(len + 1);
        digits[0] = 1;
        return digits;
    }
};

杨辉三角

118. 杨辉三角

给定一个非负整数 *numRows,*生成「杨辉三角」的前 numRows 行,在「杨辉三角」中,每个数是它左上方和右上方的数的和。

image-20220703141313905
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
class Solution {
    
    
public:
    vector<vector<int>> generate(int numRows) {
    
    
        vector<vector<int>> res(numRows);
        for (int i = 0; i < numRows; ++i) {
    
    
            res[i].resize(i + 1);
            res[i][0] = res[i][i] = 1;
            for (int j = 1; j < i; ++j) {
    
    
                res[i][j] = res[i - 1][j] + res[i - 1][j - 1];
            }
        }
        return res;
    }
};

119. 杨辉三角 II

给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。在「杨辉三角」中,每个数是它左上方和右上方的数的和。

输入: rowIndex = 3
输出: [1,3,3,1]

由于只要输出指定的某一行,故只需保存当前行的前一行结果即可。

class Solution {
    
    
public:
    vector<int> getRow(int rowIndex) {
    
    
        if(rowIndex == 0) return {
    
    1};
        if(rowIndex == 1) return {
    
    1, 1};
        vector<int> res(2, 1);
        for(int i = 2; i <= rowIndex; i++){
    
    
            vector<int> tmp(i + 1, 1);
            for(int j = 1; j < i; j++){
    
    
                tmp[j] = res[j - 1] + res[j];
            }
            swap(res, tmp);
        }
        return res;
    }
};

870. 优势洗牌(田忌赛马)

给定两个大小相等的数组 nums1nums2nums1 相对于 nums2 的优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。返回 nums1任意排列,使其相对于 nums2 的优势最大化。

输入:nums1 = [2,7,11,15], nums2 = [1,10,4,11]
输出:[2,11,7,15]

田忌赛马算法实现。比得过最弱的,则正常放置;比不过,则用来消耗最强的。

class Solution {
    
    
public:
    vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
    
    
        int n = nums1.size();
        vector<int> idx(n), res(n);
        // 给数组递增赋值
        iota(idx.begin(), idx.end(), 0);
        // 按照nums2的顺序给idx排序
        sort(idx.begin(), idx.end(), [&](int i, int j){
    
    
            return nums2[i] < nums2[j];
        });
        sort(nums1.begin(), nums1.end());
        int left = 0, right = n - 1;
        for(int i = 0; i < n; i++){
    
    
            if(nums1[i] > nums2[idx[left]]){
    
    
                // 比得过最小的,则正常放置
                res[idx[left]] = nums1[i];
                left++;
            }else{
    
    
                // 比不过最小的,则用来消耗最强的
                res[idx[right]] = nums1[i];
                right--;
            }
        }
        return res;
    }
};

索引和数组元素对应

287. 寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

输入:nums = [1,3,4,2,2]
输出:2
输入:nums = [3,1,3,4,2]
输出:3

由于数组中的元素都是正数,且都在1到n 的范围内,故可以使用数组的下标作为辅助。

将和数组元素值相同的数组下标中对应的值置为其对应的负数,这样,当两个相同的元素映射到同一个位置,会发现元素值已经为负数了,既该值为重复的值。由于有映射负数的步骤,故使用数组元素时,需要使用其绝对值。

class Solution {
    
    
public:
    int findDuplicate(vector<int>& nums) {
    
    
        for(int num : nums){
    
    
            if(nums[abs(num)] < 0){
    
    
                return abs(num);
            }else{
    
    
                nums[abs(num)] *= -1;
            }
        }
        return 0;
    }
};

442. 数组中重复的数据

给你一个长度为 n 的整数数组 nums ,其中 nums 的所有整数都在范围 [1, n] 内,且每个整数出现 一次两次 。请你找出所有出现 两次 的整数,并以数组形式返回。你必须设计并实现一个时间复杂度为 O(n) 且仅使用常量额外空间的算法解决此问题。

输入:nums = [4,3,2,7,8,2,3,1]
输出:[2,3]

和上一题一样的思路,但是需要注意索引的取值范围。

class Solution {
    
    
public:
    vector<int> findDuplicates(vector<int>& nums) {
    
    
        vector<int> res;
        for(int num : nums){
    
    
            if(nums[abs(num) - 1] < 0){
    
    
                res.push_back(abs(num));
            }else{
    
    
                nums[abs(num) - 1] *= -1;
            }
        }
        return res;
    }
};

448. 找到所有数组中消失的数字

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

输入:nums = [4,3,2,7,8,2,3,1]
输出:[5,6]

nums 中的元素取值都在 [1, n] 之间,即元素本身就和索引成一一映射关系,所以可以用 nums 本身模拟哈希集合的效果。那么最终缺失的那些元素对应的索引就不会被标记,我们也就可以把缺失的那些元素找出来。

注意:用乘 -1 的方式给索引做标记的前提是,nums 中的元素都为正数,不能包含 0,否则无法区分该索引是否被标记。

class Solution {
    
    
public:
    vector<int> findDisappearedNumbers(vector<int>& nums) {
    
    
        for(int num : nums){
    
    
            // 注意索引,元素大小从 1 开始,有一位索引偏移
            if(nums[abs(num) - 1] < 0) continue; // 索引重复出现
            nums[abs(num) - 1] *= -1; // 把索引 num - 1 置为负数
        }
        vector<int> res;
        for(int i = 0; i < nums.size(); i++){
    
    
            if(nums[i] > 0){
    
    
                // 说明没有元素和这个索引对应,即找到一个缺失元素
                res.push_back(i + 1);
            }
        }
        return res;
    }
};

334. 递增的三元子序列

给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。

输入:nums = [1,2,3,4,5]
输出:true
解释:任何 i < j < k 的三元组都满足题意
输入:nums = [5,4,3,2,1]
输出:false
解释:不存在满足题意的三元组

把所有的数分成了三部分,一部分小于等于 first,中间部分小于等于 second,以及最后的一部分大于second。只要出现了第三部分,就满足条件,返回true;否则,跳出循环后,返回false。

class Solution {
    
    
public:
    bool increasingTriplet(vector<int>& nums) {
    
    
        int first = INT_MAX;
        int second = INT_MAX;
        for(int& num : nums){
    
    
            //不停的将后面的数据分成三部分
            if(num <= first){
    
    
                //先更新first
                first = num;
            }else if(num <= second){
    
    
                //然后才能更新second
                second = num;
            }else if(num > second){
    
    
                //则出现大于second的数据时,直接返回true
                return true;
            }
        }
        return false;
    }
};

剑指 Offer 51. 数组中的逆序对(难)

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

输入: [7,5,6,4]
输出: 5

归并排序过程中会将数组划分成最小为1个元素的子数组,然后依次比较子数组的每个元素的大小,依次取出较小的一个合并成大的子数组。这里我们也可以用相同的方法划分,划分之后相邻一个元素的子数组就可以根据大小统计逆序对,而不断往上合并的时候,因为已经排好序了,我们逆序对可以往上累计。我们主要有以下三个阶段。

具体做法:

  • step 1: 划分阶段:将待划分区间从中点划分成两部分,两部分进入递归继续划分,直到子数组长度为1.
  • step 2: 排序阶段:使用归并排序递归地处理子序列,同时统计逆序对,因为在归并排序中,我们会依次比较相邻两组子数组各个元素的大小,并累计遇到的逆序情况。而对排好序的两组,右边大于左边时,它大于了左边的所有子序列,基于这个性质我们可以不用每次加1来统计,减少运算次数。
  • step 3: 合并阶段:将排好序的子序列合并,同时累加逆序对。
class Solution {
    
    
public:
    int reversePairs(vector<int>& nums) {
    
    
        int n = nums.size();
        vector<int> res(n);
        return mergeSort(0, n - 1, nums, res);
    }

    int mergeSort(int left, int right, vector<int>& nums, vector<int>& tmp){
    
    
        // 停止划分
        if(left >= right) return 0;
        // 取中间
        int mid = (left + right) / 2;
        // 左右划分合并
        int res = mergeSort(left, mid, nums, tmp) + mergeSort(mid + 1, right, nums, tmp);
        int i = left, j = mid + 1;
        for(int k = left; k <= right; k++){
    
    
            tmp[k] = nums[k];
        }
        for(int k = left; k <= right; k++){
    
    
            if(i == mid + 1){
    
    
                nums[k] = tmp[j++];
            }else if(j == right + 1 || tmp[i] <= tmp[j]){
    
    
                nums[k] = tmp[i++];
            }else{
    
     // 左边比右边大,答案增加
                nums[k] = tmp[j++];
                // 统计逆序对
                res += mid - i + 1;
            }
        }
        return res;
    }
};

猜你喜欢

转载自blog.csdn.net/weixin_42461320/article/details/127986116