LeetBook数组和字符串专题题解

数组和字符串

文章目录

数组入门

寻找数组的中心索引

(暴力解法(不能AC))
class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        int size = nums.size();
        // 遍历中心点
        for (int center = 0; center < size; center ++)
        {
            int left = 0;
            int right = 0;
            // 算出中心点左边的总和
            for (int l = 0; l < center; l ++)
                left += nums[l];
            // 算出中心点右边的总和
            for (int r = center + 1; r < size; r ++)
                right += nums[r];
            // 判断左边的总和和右边的总和是否相等
            if (left == right)
                return center;
        }
        // 不存在就返回-1
        return -1;
    }
};
(前缀和)
class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        int size = nums.size();
        // 求出从左到右的前缀和
        vector<int> left(size, 0);
        vector<int> right(size, 0);
        // 初始化第一个数
        left[0] = nums[0];
        right[size - 1] = nums[size - 1];
        // 处理左边的前缀和
        for (int i = 1; i < size; i ++)
            left[i] = nums[i] + left[i - 1];
        // 处理右边的前缀和
        for (int i = size - 2; i >= 0; i --)
            right[i] = nums[i] + right[i + 1];
        // 枚举中心点
        for (int center = 0; center < size; center ++)
        {
            int l = center == 0 ? 0 : left[center - 1];
            int r = center == size - 1 ? 0 : right[center + 1];
            if (l == r)
                return center;
        }
        return -1;
    }
};
(左右和)
class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        if (nums.empty())
            return -1;
        int rightSum = 0, leftSum = 0;;
        for (int num : nums)
            rightSum += num;
        int size = nums.size();
        for (int i = 0; i < size; i ++)
        {
            // 左边数的总和
            leftSum += nums[i];
            // 右边数的总和
            if (i > 0)
                rightSum -= nums[i - 1];
            // 如果左边的总和和右边的总和相等就返回下标
            if (rightSum == leftSum)
                return i;
        }
        return -1;
    }
};
(前缀和不用数组)
class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        int sum = 0;
        for (auto num : nums)
            sum += num;
        // 记录前缀和
        int preSum = 0;
        for (int i = 0; i < nums.size(); i ++)
        {   
            // 右边数组中的和
            int rightSum = sum - preSum - nums[i];
            if (preSum == rightSum)
                return i;
            preSum += nums[i];
        }
        return -1;
    }
};

搜索插入位置

(二分)

(判断括号里的东西)

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = 0, r = nums.size();
        while (l < r) 
        {
            int mid = l + r >> 1;
            if (nums[mid] >= target)
                r = mid;
            else 
                l = mid + 1;
        }
        return l;
    }
};
合并区间
class Solution {
public:
    typedef pair<int, int> PII;
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> ans;
        int n = intervals.size(), m = intervals[0].size();
        PII seg[n];
        for (int i = 0; i < n; i ++)
        {
            int l = intervals[i][0], r = intervals[i][1];
            seg[i] = {l, r};
        }
        sort(seg, seg + n);// 将区间按左端点排序
        int left = seg[0].first, right = seg[0].second;
        for (int i = 1; i < n; i ++) 
        {
            int curL = seg[i].first, curR = seg[i].second;
            if (curL <= right)
                right = max(curR, right);
            else 
            {
                ans.push_back(vector<int>({left, right}));// 将一个区间放入答案中
                left = curL;
                right = curR;
            }    
        }
        ans.push_back({left, right});
        return ans;
    }
};

二维数组

》数组中数据的操作

*旋转矩阵

学到了二维矩阵的对称方式:

1)正对角线的对称反转

for (int i = 0; i < n; i ++)
{
    for (int j = 0; j <= i; j ++)
    {
        int tmp = matrix[i][j];
        matrix[i][j] = matrix[j][i];;
        matrix[j][i] = tmp;
    }
}

2)负对角线的对称反转

for (int i = 0; i < n; i ++)
{
    for (int j = 0; j < n - i; j ++)
    {
        int tmp = matrix[i][j];
        matrix[i][j] = matrix[n - 1 - j][n - 1 - i];;
        matrix[n - 1 - j][n - 1 - i] = tmp;
    }
}

3)上下对称

for (int i = 0; i < n / 2; i ++)
{
    for (int j = 0; j < n; j ++)
    {
        int tmp = matrix[i][j];
        matrix[i][j] = matrix[n - 1 - i][j];
        matrix[n - 1 - i][j] = tmp;
    }
}

因为二维矩阵的元素是一维矩阵,所以在上下对称的时候可以用整行整行换

for (int i = 0; i < n / 2; i ++)
    swap(matrix[i], matrix[n - 1 - i]);

4)左右对称

for (int i = 0; i < n; i ++)
{
    for (int j = 0; j < n / 2; j ++)
    {
        int tmp = matrix[i][j];
        matrix[i][j] = matrix[i][n - 1 - j];
        matrix[i][n - 1 - j] = tmp;
    }
}
(对称旋转法1)

思路:先负对角线对称再上下对称

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        // 顺时针旋转270度
        // 负对角线对称
        for (int i = 0; i < n; i ++)
        {
            for (int j = 0; j < n - i; j ++)
            {
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[n - 1 - j][n - 1 - i];;
                matrix[n - 1 - j][n - 1 - i] = tmp;
            }
        }
        // 顺时针旋转180度
        for (int i = 0; i < n / 2; i ++)
        {
            for (int j = 0; j < n; j ++)
            {
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[n - 1 - i][j];
                matrix[n - 1 - i][j] = tmp;
            }
        }
    }
};

这个方法比较好理解一些,通过正对角线的对称,先逆时针旋转90度,然后在根据中间的对称轴对称。就是顺时针旋转90度主要要理解n - 1 - in - 1- j

思路:先正对角线对称再左右对称

(对称旋转法2)
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        // 正对角线对称
        // 逆时针旋转90度
        for (int i = 0; i < n; i ++)
        {
            for (int j = 0; j <= i; j ++)
            {
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[j][i];;
                matrix[j][i] = tmp;
            }
        }
        // 顺时针旋转180度
        for (int i = 0; i < n; i ++)
        {
            for (int j = 0; j < n / 2; j ++)
            {
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[i][n - 1 - j];
                matrix[i][n - 1 - j] = tmp;
            }
        }
    }
};

(旋转对称法3)

思路:先上下对称再正对角线对称

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

零矩阵

(额外开空间记录点坐标)
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        unordered_set<int> x, y;// 用set记录值为0的点的坐标并且可以防止重复的行或者列
        int n = matrix.size(), m = matrix[0].size();
        // 记录一下出现0的坐标
        for (int i = 0; i < n; i ++)
            for (int j = 0; j < m; j ++)
                if (matrix[i][j] == 0)
                {
                    x.insert(i);
                    y.insert(j);
                }
        // 将标记过的行,整行清零
        for (int i = 0; i < n; i ++)
            if (x.count(i))
            {
                for (int j = 0; j < m; j ++)
                    matrix[i][j] = 0;
            }
        // 将标记过的列,整列清零
        for (int j = 0; j < m; j ++)
            if (y.count(j))
            {
                for (int i = 0; i < n; i ++)
                    matrix[i][j] = 0;
            }
        
        //下面这样也可以
        
        // // 将标记过的行,整行清零
        // for (auto i : x)
        //     {
        //         for (int j = 0; j < m; j ++)
        //             matrix[i][j] = 0;
        //     }
        // // 将标记过的列,整列清零
        // for (auto j : y)
        //     {
        //         for (int i = 0; i < n; i ++)
        //             matrix[i][j] = 0;
        //     }
    }
};

优化一下,可以将行和列的清空放在一起写,只要遇到行被标记过或者列被标记过,就清零

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        unordered_set<int> x, y;// 用set记录值为0的点的坐标并且可以防止重复的行或者列
        int n = matrix.size(), m = matrix[0].size();
        // 记录一下出现0的坐标
        for (int i = 0; i < n; i ++)
            for (int j = 0; j < m; j ++)
                if (matrix[i][j] == 0)
                {
                    x.insert(i);
                    y.insert(j);
                }
        // 将标记过的行,整行清零
        for (int i = 0; i < n; i ++)
            for (int j = 0; j < m; j ++)
                if (x.count(i) || y.count(j))
                    matrix[i][j] = 0;
    }
};
(不开额外空间,原地修改)

要先记录一下第一行和第一列是否有0,然后再判断中间的矩阵(除了第一行和第一列)是否有0,将有0的那个坐标的行头和列头标记一下。

为什么要用第一行和第一列做标记呢?,为什么不一遍判断一遍清零?

因为如果第一行有0然后将第一行全部清零的话,就会影响下面的清零,下面的所有矩阵中的点会因为第一行清零,而要全部清零。

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        bool firstRow = false, firstCol = false;
        int n = matrix.size(), m = matrix[0].size();
        // 判断第一列是否有0
        for (int i = 0; i < n; i ++)
            if (matrix[i][0] == 0)
            {
                firstCol = true;
                break;
            }
        // 判断第一行是否有0
        for (int j = 0; j < m; j ++)
            if (matrix[0][j] == 0)
            {
                firstRow = true;
                break;
            }
        // 标记一下中间矩阵是否有0
        for (int i = 1; i < n; i ++)
            for (int j = 1; j < m; j ++)
                if (matrix[i][j] == 0)
                {
                    matrix[i][0] = 0;
                    matrix[0][j] = 0;
                }
        // 将标记的行或者列全部清零
        // for (int i = 1; i < n; i ++)
        //     if (matrix[i][0] == 0)
        //         for (int j = 1; j < m; j ++)
        //             matrix[i][j] = 0;
        // for (int j = 1; j < m; j ++)
        //     if (matrix[0][j] == 0)
        //         for (int i = 1; i < n; i ++)
        //             matrix[i][j] = 0;

        // 将上面当两段代码整合一下
        for (int i = 1; i < n; i ++)
            for (int j = 1; j < m; j ++)
                if (matrix[i][0] == 0 || matrix[0][j] == 0)
                    matrix[i][j] = 0;


        if (firstCol)
            for (int i = 0; i < n; i ++)
                matrix[i][0] = 0;
        if (firstRow)
            for (int j = 0; j < m; j ++)
                matrix[0][j] = 0;
    }
};

》二维数组的不同的遍历方式

*对角线遍历

(正负对角线)

i是m+n(每一个对角线上的点的坐标之和是不变的)

class Solution {
public:
    vector<int> findDiagonalOrder(vector<vector<int>>& mat) {
        int n = mat.size(), m = mat[0].size();
        vector<int> ans;
        int i = 0;// i = m + n的总和在同一个对角线是不变的,一开始的总和为0
        while (i < m + n - 1)
        {
            int x1 = i < n ? i : n - 1;// 判断x1是否越界
            int y1 = i - x1;
            while (x1 >= 0 && y1 < m)// 完成一次对角线的遍历
            {
                ans.push_back(mat[x1][y1]);
                x1 --;
                y1 ++;
            }
            i ++;

            int y2 = i < m ? i : m - 1;// 判断y2是否越界
            int x2 = i - y2;
            while (x2 < n && y2 >= 0)// 完成一次对角线的遍历
            {
                ans.push_back(mat[x2][y2]);
                x2 ++;
                y2 --;
            }
            i ++;
        } 
        return ans;
    }
};
(整合优化版)
class Solution {
public:
    vector<int> findDiagonalOrder(vector<vector<int>>& mat) {
        int n = mat.size(), m = mat[0].size();
        bool flag = true;
        vector<int> ans;
        for (int i = 0; i < n + m - 1; i ++)
        {
            // 如果flag是true,就是正对角线遍历,所以正常判断整个矩阵
            // 如果flag是false,就是负对角线遍历,就要将矩阵行和列交换一下
            int tn = flag ? n : m;
            int tm = flag ? m : n;

            int x = i < tn ? i : tn - 1;
            int y = i - x;

            while (x >= 0 && y < tm)
            {
                ans.push_back(flag ? mat[x][y] : mat[y][x]);
                x --;
                y ++;
            }

            flag = !flag;// 正负对角线交替
        }
        return ans;
    }
};

螺旋矩阵

(方向打表)
class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0};// 右下左上的遍历顺序
        int n = matrix.size(), m = matrix[0].size();
        vector<int> ans;
        set<pair<int, int>> vis;// 记录已经遍历过的点的坐标
        // 判断矩阵遍历的方向
        int d = 0;
        // 标记起始位置
        int x = 0, y = 0;
        // i控制循环的次数,(x,y)控制点的位置
        for (int i = 0; i < m * n; i ++)
        {
            // 将当前坐标上的点放入答案中
            ans.push_back(matrix[x][y]);
            vis.insert({x, y});
            // 判断矩阵是否访问越界
            int a = x + dx[d], b = y + dy[d];
            if (a < 0 || a >= n || b < 0 || b >= m || vis.count({a, b}))
            {
                // 如果访问越界或者已经访问过了,就改变访问的方向
                d = (d + 1) % 4;
                a = x + dx[d], b = y + dy[d];
            }
            // 将改变过后的方向传递给下一个坐标
            x = a, y = b;
        }
        return ans;
    }
};

字符串

最长公共前缀

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        int n = strs.size();
        // 找出单词的最少长度
        int m = strs[0].size();
        for (int i = 0; i < n; i ++)
            if (strs[i].size() < m)
                m = strs[i].size();
        string ans;
        // 比较每个单词的字符是否相等
        for (int j = 0; j < m; j ++)// 外层循环遍历单词的字符
        {
            for (int i = 1; i < n; i ++)// 遍历所有的单词是否当前位置上的字符都相等
            {
                if (strs[i][j] != strs[i - 1][j])
                    return ans;// 如果发现第一个不相等的单词就直接退出
            }
            // 如果没有不相等的单词,就将字符加到ans中
            ans += strs[0][j];
        }
        return ans;
    }
};

最长回文子串

(暴力枚举)

枚举回文子串的开头和结尾需要O(N2),判断是否为回文子串需要O(N),所以一共需要O(N3)的时间复杂度,正常是通过不了的但是通过j - i - 1 < ans.size()就直接跳过的优化后,就可以直接通过了,但是解法实在是太暴力了。

class Solution {
public:
    bool palind(int left, int right, string& str)
    {
        // 判断是否为回文子串
        while (left < right)
        {
            if (str[left] != str[right])
                return false;
            left ++;
            right --;
        }
        return true;
    }
    string longestPalindrome(string s) {
        int n = s.size();
        string ans;
        for (int i = 0; i < n; i ++)
            for (int j = i; j < n; j ++)
            {
                // 如果当前的子串的长度要比答案小可以直接跳过,如果没有这个判断就会超时
                if (j - i + 1 < ans.size())
                    continue;
                if (palind(i, j, s))// 如果是回文子串就更新答案
                    if (j - i + 1 > ans.size())// 比较回文子串的大小
                        ans = s.substr(i, j - i + 1);                
            }

        return ans;
    }
};
(中心扩展法)
class Solution {
public:
    int expandCenter(string& str, int left, int right)
    {
        
        int size = str.size();
        // 在不越界的情况下
        while (left >= 0 && right < size && str[left] == str[right])
        {
            // 中心往外扩展,如果能得到回文子串的情况下就一直扩展
            left --, right ++;  
        }
        // 当跳出循环的时候,说明已经不满足了,这是回文子串的左边界是left+1,右边界时right-1
        // ***这时的回文串的长度是 (right - 1) - (left + 1) + 1 == right - left - 1
        return right - left - 1;
    }
    string longestPalindrome(string s) {
        int n = s.size();
        if (n < 2)
            return s;
        int start = 0, end = 0;
        for (int i = 0; i < n - 1; i ++)
        {
            int len1 = expandCenter(s, i, i);// 数组的个数为奇数
            int len2 = expandCenter(s, i, i + 1);// 数组的个数为偶数
            int maxLen = max(len1, len2);
            // 如果当前两个扩展的回文串的最大长度大于当前回文子串的长度
            // 就更新一下回文子串的开始位置的末尾位置
            if (maxLen > end - start + 1)
            {
                start = i - (maxLen - 1) / 2;
                end = i + maxLen / 2;
            }
        }
        return s.substr(start, end - start + 1);

    }
};
(动态规划)

反转字符串中的单词

先反转整个句子,然后在一个一个反转单词。去掉前导空格,然后用双指针抠出所有的单词,然后反转所有的单词。

(双指针)
class Solution {
public:

    string reverseWords(string s) {
        string ans;
        reverse(s.begin(), s.end());
        int n = s.size();
        int i = 0;
        // 去掉前导空格
        while (s[i] == ' ')
            i ++;
        
        while (i < n)
        {
            // 获取单词
            int j = i;
            while (j < n && s[j] != ' ')
                j ++;
            reverse(s.begin() + i, s.begin() + j);// 反转单词
            ans += s.substr(i, j - i) + ' ';// 将单词加到答案中
            
            // 跳过单词之间的空格
            i = j;
            while (i < n && s[i] == ' ')
                i ++;
        }
        ans.pop_back();// 最后要将最后一个空格去掉
        return ans;
    }

};
(容器存储)
class Solution {
public:
    string reverseWords(string s) {
        int left = 0, right = s.size() - 1;
        // 去掉前导空格
        while (left <= right && s[left] == ' ') left ++;
        // 去掉末尾空格
        while (left <= right && s[right] == ' ') right --;
        string ans,word;// word记录单词,ans记录答案
        while (left <= right)
        {
            // 如果word不为空,并且遇到了空格,就将将word放在答案的前面
            if (word.size() > 0 && s[left] == ' ')
            {
                ans = word + ' ' + ans;
                word.clear();
            }
            // 如果字符不为空就将字符加到word中
            else if (s[left] != ' ')
            {
                word += s[left];
            }
            left ++;
        }
        // 最后一个单词后面没有单词了,所以最后要把word放到ans最前面
        ans = word + ' ' + ans;
        // 去掉ans中最后一个单词后面的空格
        ans.pop_back();
        return ans;
    }
};
(*原地删除法)
class Solution {
public:
    string reverseWords(string s) {
        int idx = 0;
        int n = s.size();
        // 反转整个句子
        reverse(s.begin(), s.end());
        for (int start = 0; start < n; start ++)
        {
            // 如果是字符就处理,如果是空格就直接跳过
            if (s[start] != ' ')
            {
                // 如果不是第一个单词就在后面加上一个空格
                if (idx != 0) s[idx ++] = ' ';
                // 获取单词
                int end = start;
                while (end < n && s[end] != ' ')
                    s[idx ++] = s[end ++];
                // 反转单词
                // 注意这里要用idx来控制,end是单词的下一个位置,(end - start)是单词的长度,idx是单词的末尾位置
                reverse(s.begin() + idx - (end - start), s.begin() + idx);
                // 更新单词的起点(跳过当前反转过的单词)
                start = end;
            }
        }
        // 只取句子的前idx(处理过的部分)
        s = s.substr(0, idx);
        return s;
    }
};

Strstr()(*KMP算法)

求next数组的方法和匹配两个字符串的方法是相似的,因为在求next数组的时候,就是在求一段字符串中最长的公共前后缀的长度,也就是再求一段字符串中的相同子串。而匹配两个字符串的时候是在两个字符串中找出相同的字符串,所以求解的方法是类似的。

(暴力解法)
class Solution {
public:
    int strStr(string haystack, string needle) {
        int n = haystack.size(), m = needle.size();
        // 枚举test串中的开头位置
        for (int i = 0; i + m <= n; i ++)
        {
            bool flag = true;
            // 遍历模式串和test中是否有完全一样的子串
            for (int j = 0; j < m; j ++)
            {
                // 如果有一个不一样的就直接跳出
                if (haystack[i + j] != needle[j])
                {
                    flag = false;
                    break;
                }
            }
            // 如果flag为ture说明模式串和test完全匹配,返回开头位置
            if (flag)
                return i;
        }
        return -1;
    }
};
(KMP算法)

求出next数组

// 不做修改的next数组
int* GetNext(const string& s)
{
	int* next = new int[s.size()];
	// j是前缀的末尾,i是后缀的末尾
	// next[0]回退回到0的位置
	int j = 0;
	next[0] = j;
	// 从第二个字符开始匹配
	for (int i = 1; i < s.size(); i++)
	{
		// 如果不匹配,就回到前一个数所指向的位置
		// 或者找到s[i] == s[j]
		while (j > 0 && s[i] != s[j])
			j = next[j - 1];

		if (s[i] == s[j])// 如果相等,长度就+1
			j++;

		// 两种情况:
		// 1.j回到0的位置已经不能在回退了
		// 2.j是它应该的长度
		next[i] = j;
	}
	return next;
}

j表示的不仅是前缀字符串的长度(公共字符串的长度),也是字符串不匹配后前一个相同位置的字符的下一个位置(字符串的下标从0开始)

class Solution {
public:
    // 求出next数组
    int* GetNext(const string& s)
    {
        int n = s.size();
        int* next = new int[n];
        // j指向前缀的末尾,i指向后缀的末尾
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < n; i ++)
        {
            // 1.字符串不匹配 2.找到s[i]==s[j]
            while (j > 0 && s[i] != s[j]) j = next[j - 1];
            // 如果找到s[i]==s[j]前缀的字符串的长度+1
            if (s[i] == s[j]) j ++;
            // 将当前位置上的公共前后缀记录在next数组中
            next[i] = j;
        }
        return next;
    }
    int strStr(string haystack, string needle) {
        // 当模式串是空字符串的时候要特判一下,这样就可以不用求出next数组
        if (needle == "")
            return 0;
        // 求出next数组
        int* next = GetNext(needle); 
        int j = 0;
        // 用求next数组的方法,来匹配两个字符串
        for (int i = 0; i < haystack.size(); i ++)
        {
            while (j > 0 && haystack[i] != needle[j])
                j = next[j - 1];
            if (haystack[i] == needle[j])
                j ++;
            // 当后缀(公共字符串)的长度和模式串的长度相同的时候
            if (j == needle.size())
                return  i - needle.size() + 1;
        }
        return -1;
    }
};

**为什么要找模式中的前后缀:**前后缀因为是相同的,所以如果后缀不匹配的话,就找从前缀开始的字符串比较,这样就可以节省了前缀长度这么多的时间,而且这里的前缀可能还要它自己的前缀,所以如果当前的前缀不匹配,就再找前缀的前缀做匹配

(KMP无注释版)

假设源字符串长度为n,模式串的长度为m,则时间复杂度为O(m + n),空间复杂度为O(m)。因为求出next数组需要遍历一遍模式串O(m),对比源字符串和模式串需要O(n),此外其中还需要用O(m)的空间保留next数组。

class Solution {
public:
    int* GetNext(const string& s)
    {
        int n = s.size();
        int* next = new int[n];
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < n; i ++)
        {
            while (j > 0 && s[i] != s[j]) j = next[j - 1];
            if (s[i] == s[j]) j ++;
            next[i] = j;
        }
        return next;
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0)
            return 0;
        int* next = GetNext(needle);
        int j = 0;
        for (int i = 0; i < haystack.size(); i ++)
        {
            while (j > 0 && haystack[i] != needle[j]) j = next[j - 1];
            if (haystack[i] == needle[j]) j ++;
            if (j == needle.size()) return i - needle.size() + 1;
        }
        return -1;
    }
};
(多种求next数组的版本* * * *)
// 将原来的next数组往右移动一位,然后用-1填补第一个位置
int* buildNext(const char* P) { // 构造模式串 P 的 next 表
	size_t m = strlen(P), j = 0; // “主”串指针
	int* N = new int[m]; // next 表
	int  t = N[0] = -1; // 模式串指针
	while (j < m - 1)
		if (0 > t || P[j] == P[t]) { // 匹配
			j++; t++;
			N[j] = t; // 此句可改进为 N[j] = (P[j] != P[t] ? t : N[t]);
		}
		else // 失配
			t = N[t];
	return N;

}

int* GetNext1(const char T[])
{
	int* next = new int[strlen(T)];
	int j = 0, k = -1;
	next[j] = k;
	while (T[j] != '\0')
	{
		if (k == -1 || T[j] == T[k])
		{
			j++;
			k++;
			next[j] = k;
		}
		else k = next[k];
	}
	return next;
}

// 不做修改的next数组
int* GetNext2(const string& s)
{
	int* next = new int[s.size()];
	// j是前缀的末尾,i是后缀的末尾
	// next[0]回退回到0的位置
	int j = 0;
	next[0] = j;
	// 从第二个字符开始匹配
	for (int i = 1; i < s.size(); i++)
	{
		// 如果不匹配,就回到前一个数所指向的位置
		// 或者找到s[i] == s[j]
		while (j > 0 && s[i] != s[j])
			j = next[j - 1];

		if (s[i] == s[j])// 如果相等,长度就+1
			j++;

		// 两种情况:
		// 1.j回到0的位置已经不能在回退了
		// 2.j是它应该的长度
		next[i] = j;
	}
	return next;
}

// 不做修改的next数组
int* GetNext3(const char* s)
{
	int n = strlen(s);
	int* next = new int[n];
	int i = 1, len = 0;
	next[0] = 0;
	while (i < n)
	{
		if (s[i] == s[len])
		{
			len++;
			next[i] = len;
			i++;
		}
		else
		{
			if (len > 0)
				len = next[len - 1];
			else 
			{
				next[i] = 0;
				i++;
			}
		}
	}
	return next;
}

双指针

反转字符串

(双指针)
class Solution {
public:
    void reverseString(vector<char>& s) {
        int left = 0, right = s.size() - 1;
        while (left < right)
        {
            char tmp = s[left];
            s[left] = s[right];
            s[right] = tmp;
            left ++;
            right --;
        }
    }
};

数组拆分Ⅰ

(排序)(贪心)
class Solution {
public:
    int arrayPairSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int ans = 0;
        for (int i = 0; i < nums.size(); i ++)
        {
            if (i % 2 == 0)
                ans += nums[i];
        }
        return ans;
    }
};

两数之和Ⅱ-输入有序数组

(同向双指针)

因为数组有单调性,所以可以想到用双指针

两个指针智能相向而行,不能同向而行,因为相向而行指针才可以具有单调性的走(只有right指针往左走才可以使得sum变小,只有left指针往右走才可以使得sum变大),而同向而行不能使得指针具有单调性。

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int left = 0, right = numbers.size() - 1;
        while (left < right)
        {
            int sum = numbers[left] + numbers[right];
            // 因为数组具有单调性,而且right已经在最右端,left已经在最左端
            // 如果sum>target,right--才可以让sum变小
            if (sum > target)
                right --;
            // 如果sum<target,left++才可以让sum变大
            else if (sum < target)
                left ++;
            else 
                return vector<int>({left + 1, right + 1});
        }
        return vector<int>();
    }
};
(简洁版)
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int left = 0, right = numbers.size() - 1;
        while (numbers[left] + numbers[right] != target)
            if (numbers[left] + numbers[right] > target)
                right --;
            else
                left ++;
        return vector<int>({left + 1, right + 1});
    }
};
(map)
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        map<int, int> nums;
        for (int i = 0; i < numbers.size(); i ++)
            nums[numbers[i]] = i;// 记录每一个数字对应的下标
        for (int i = 0; i < numbers.size(); i ++)
        {
            int t = target - numbers[i];
            // 如果找到可以和当前数相加等于target的数t并且t不是numbers[i]
            if (nums.count(t) && nums[t] != i)
                return vector<int>({i + 1, nums[t] + 1});
        }
        return vector<int>();
    }
};

移除元素

(快慢指针)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slow = 0;
        for (int fast = 0; fast < nums.size(); fast ++)// 如果快指针的位置等于val,fast指针就直接跳过
            if (nums[fast] != val)// 如果快指针指向的位置不等于val,就将值赋值给slow
            {
                nums[slow] = nums[fast];
                slow ++;
            }
        return slow;
    }
};

最大连续1的个数

(暴力遍历)
class Solution {
public:
    int findMaxConsecutiveOnes(vector<int>& nums) {
        int ans = 0, cnt = 0;
        for (int i = 0; i < nums.size(); i ++)
        {
            if (nums[i] == 1)// 统计连续1的个数
                cnt ++;
            else // 如果遇到0就结算一下ans,然后将cnt清零,让cnt继续统计下一端连续1的个数
            {
                ans = max(ans, cnt);
                cnt = 0;
            }
        }
        // 因为只有遇到0才会结算ans,如果数组最后没有0就不可以结算
        // 所以最后出来循环还要自己再结算一遍
        ans = max(ans, cnt);
        return ans;

    }
};
(同向双指针)

利用双指针(同向双指针)可以将一段需要的字符串或者一段数字抠出来,在这里就可以用双指针将连续的1抠出来,也就是统计个数。

class Solution {
public:
    int findMaxConsecutiveOnes(vector<int>& nums) {
        int n = nums.size();
        int ans = 0;
        for (int i = 0; i < n; i ++)
        {
            // 利用双指针,如果是1就让j指针一直往后走
            int j = i;
            while (j < n && nums[j] == 1) j ++;
            ans = max(ans, j - i);// j指针的位置已经不是1了,所以用j-i,而不是j-i+1
            i = j;// 让i指针跳过连续1的一段,用i=j,因为for循环中还有i++
        }
        return ans;
    }
};

双指针总结:利用双指针(同向双指针)可以将一段需要的字符串或者一段数字抠出来,一般用双指针抠出来的一段区间都是连续的。

*长度最小的子数组

(伪双指针,暴力枚举)

这里的双指针是O(N2)的,因为并没有将遍历过得数字直接跳过,而是枚举每一段区间的开头,然后统计sum的和。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size(), ans = n;
        bool flag = false;// 是否更新过答案
        for (int i = 0; i < n; i ++)
        {
            int j = i;
            int sum = 0;
            while (j < n && sum < target) sum += nums[j ++];// 找到sum大于target
            if (sum >= target)// 如果一段区间和大于target就更新一下答案
            {
                ans = min(ans, j - i);
                flag = true;
            }
        }
        return flag ? ans : 0;
    }
};
(前缀和+二分)

O(NlogN)

前缀和注意点:求出[L, R]之间的和

1)前缀和数组下标从1开始

2)[L, R]区间中的和尾ans = sum[R] - sum[L - 1];(因为求出闭区间中的和一定要有下标-1的操作,所以前缀和数组一定要从下标为1开始)

因为是求出和比较一段连续区间的和,所以可以用前缀和来算,但是单独用前缀和还得遍历找sum>=target的位置。所以还要配合二分查找找到大于target的第一个位置。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size();
        // 处理前缀和数组
        vector<int> sum(n + 1, 0);
        for (int i = 1; i <= n; i ++)
            sum[i] = nums[i - 1] + sum[i - 1];
        
        int ans = n + 1;
        for (int i = 1; i <= n; i ++)
        {
            // 二分查找,找>=target的第一个数
            int left = 1, right = n;
            while(left < right)
            {
                int mid = left + right >> 1;
                int tSum = sum[mid] - sum[i - 1];// 区间[i,mid]之间的和
                if (tSum >= target) right = mid;
                else left = mid + 1;
            }
            // 如果区间中的和>=target才更新答案,因为可能left到后面区间中的总和不能达到target
            if (sum[left] - sum[i - 1] >= target)
                ans = min(ans, left - i + 1);
        }
        // 如果ans没有被更新过就返回0,否则返回ans
        return ans == n + 1 ? 0 : ans;
    }
};
(可变滑动窗口解法)

不是经典的利用单调队列求出滑动窗口中的最值,而是利用可变的滑动窗口,求出窗口中的总和,如果没有到target就扩展滑动窗口,如果sum>=target就缩小滑动窗口中使得sum变小,并且在缩小滑动窗口的同时就可以更新连续的子数组的长度。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size();
        int left = 0, right = 0;// 可变的滑动窗口的左右两端
        int sum = 0;// 滑动窗口中数的总和大小
        int ans = INT_MAX;
        while (right < n)// 当窗口的右端没有出界就继续
        {
            sum += nums[right];// 将窗口的右端点的值相加
            while (sum >= target)// 当窗口内总和比target大的时候,就结算ans
            {
                ans = min(ans, right - left + 1);
                sum -= nums[left ++];// 缩小滑动窗口的大小
            }
            right ++;// 扩展滑动窗口的右端
        }
        return ans == INT_MAX ? 0 : ans;
    }
};

滑动窗口中的最大值

用双端队列来存储是因为:可以从开头删除元素也可以从末尾删除元素,

1)在保证窗口中只有k个数的时候,必须要删掉已经是窗口外的并且下标已经不再窗口中的数,所以要从前面删除元素,这样才可以保证删除的元素是最早进入窗口的元素。

2)在保证队列的单调性的时候需要从队列的末尾开始删除。

另外在队列中需要存储对应项的下标而不是数值本身是因为可能前后两个数的数值是一样的,为了区分数组中相同的两个数,所以要存储下标来区分

(经典单调队列)

解题的顺序:先保证窗口中只有k个数再形成单调队列最后填入答案

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        int n = nums.size();
        deque<int> q;
        for (int i = 0; i < n; i ++)
        {
            // 保证窗口中只有k个数
            if (i > k - 1 && q.front() == i - k)
                q.pop_front();
            // 形成单调队列
            while (!q.empty() && nums[i] >= nums[q.back()])
                q.pop_back();
            q.push_back(i);
            // 插入元素
            if (i >= k - 1)
                ans.push_back(nums[q.front()]);
        }
        return ans;
    }
};

总结:如果涉及连续子数组的问题,通常由三种思路:滑动窗口,前缀和,二分查找。这其中前缀和需要额外的O(N)空间,二分查找时间复杂度常为O(NlogN),滑动窗口时间复杂度为O(N),优先采用滑动窗口。

数组和字符串小结

杨辉三角形

(模拟)
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        if (numRows == 1) return {{1}};
        if (numRows == 2) return {{1}, {1, 1}};
        vector<vector<int>> matrix(numRows, vector<int>(numRows, 0));// 初始化长和宽都是numRows的矩阵
        // 将二维矩阵的对角线和第一列都赋值为1
        for (int i = 0; i < numRows; i ++)
        {
            matrix[i][i] = 1;
            matrix[i][0] = 1;
        }
        // 进行加法
        for (int i = 2; i < numRows; i ++)
            for (int j = 1; j < i; j ++)
                matrix[i][j] = matrix[i - 1][j - 1] + matrix[i - 1][j];
        // 缩小每一行数组的大小
        for (int i = 0; i < numRows; i ++)
            matrix[i].resize(i + 1);
        return matrix;
    }
};
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> ans;
        if (numRows == 0) return ans;
        ans.push_back({1});// 初始化第一行
        for (int i = 1; i < numRows; i ++)
        {
            vector<int> t(i + 1);// 定义一个一维数组
            for (int j = 0; j <= i; j ++) 
            {
                if (j == 0 || j == i) t[j] = 1;// 如果是第一列或者是对角线就初始化为1
                else t[j] = ans[i - 1][j] + ans[i - 1][j - 1];// 对应的左上和正上方
            }
            ans.push_back(t);
        }
        return ans;
    }
};

杨辉三角形Ⅱ

(模拟1)
class Solution {
public:
    vector<int> getRow(int rowIndex) {
        vector<int> ans{1};
        for (int i = 1; i <= rowIndex; i ++)
        {
            vector<int> t (i + 1);// 初始化一维数组大小为i+1
            t[0] = t[i] = 1;// 第一个和最后一个位置都是1
            // 临时数组的i位置上数=ans相同位置上的数和前一位数之和
            for (int j = 1; j < i; j ++)
                t[j] = ans[j] + ans[j - 1];
            ans = t;// 将临时数组赋给ans
        }
        return ans;
    }
};
(*模拟2)
class Solution {
public:
    vector<int> getRow(int rowIndex) {
        vector<int> ans(rowIndex + 1, 1);// 初始化rowIndex+1个1
        for (int i = 1; i < rowIndex; i ++)// 控制迭代次数同时也再为下一层循环枚举开头
            for (int j = i; j > 0; j --)
                ans[j] += ans[j - 1];
        return ans;
    }
};

反转字符串中的单词Ⅲ

(双指针)

典型的利用双指针算法抠出字符串中的一段子串

class Solution {
public:
    string reverseWords(string s) {
        int size = s.size();
        for (int i = 0; i < size; i ++)
        {
            int j = i;
            while (j < size && s[j] != ' ') j ++; // 找到第一个不是单词的位置
            reverse(s.begin() + i, s.begin() + j);// 反转单词
            i = j;
        }
        return s;
    }
};

寻找旋转排序数组中的最小值

(二分)

找到比最后一个数小的第一个数

class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size();
        // 二分,寻找第一个比最后一个数要小的数
        int l = 0, r = n - 1;
        while (l < r)
        {
            int mid = l + r >> 1;
            if (nums[mid] < nums[r]) r = mid;
            else l = mid + 1;
        }
        return nums[l];
    }
};

找到比第一个数小的第一个数

class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size();
        if (nums[n - 1] > nums[0]) return nums[0];// 如果是没有旋转过的数组最后一个书一定比第一个数大
        int l = 0, r = n - 1;
        // 二分在数组中出比第一个数大的第一个数
        while (l < r)
        {
            int mid = l + r >> 1;
            if (nums[mid] < nums[0]) r = mid;
            else l = mid + 1;
        }
        return nums[l];
    }
};

👆两种方式没有本质区别,只是如果和nums[0]相比寻找数值的话,要特判一下有序数组的特殊情况,这只是根据这题而言需要加一个特殊判断。

删除排序数组中的重复项

(双指针1)
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int i = 0, j = 0;
        int n = nums.size();
        // 双指针算法
        while (j < n)
        {
            nums[i ++] = nums[j];// 将不同数字赋值给num[i],并让i往后移动
            while (j < n && nums[j] == nums[i - 1]) j ++;// 找到不为前一个数的数字
        }
        return i;
    }
};
(双指针2)
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) return 0;
        int i = 0, j = 1;// 默认第一个数已经在非重复项的数组中
        while (j < n)
        {
            if (nums[i] != nums[j])
                nums[++ i] = nums[j];
            j ++;
        }
        return i + 1;
    }
};

移动零

(双指针1)
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; i ++)// 枚举慢指针
        {
            int j = i;
            while (j < n && nums[j] == 0) j ++;// 找到第一个不为0的数,然后交换
            if (j < n)
                swap(nums[i], nums[j]);
        }
    }
};
(双指针2)
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size();
        int slow = 0;
        for (int fast = 0; fast < n; fast ++)// 枚举快指针,如果fast不为0,就和slow交换位置
            if (nums[fast] != 0)
                swap(nums[slow++], nums[fast]);
    }
};

一般情况下,枚举快指针要比枚举慢指针要好,因为slow是为了填补空缺位置的,fast指针是找符合条件的位置,如果是枚举慢指针的话,如果fast已经找完所有的符合条件的位置后,slow就没有用来,但是枚举fast的话,fast一定会走完整个数组。

(原地移除元素的隐晦版)
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size();
        int i = 0, j = 0;
        // 将不是零的数覆盖掉前面的位置
        while (j < n)
        {
            if (nums[j] != 0)
                nums[i ++] = nums[j];
            j ++; 
        }
        // 将i后面的位置全部置零
        while (i < n)
            nums[i++] = 0;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值