刷题笔记C++

目录


元戎启行 面试 编程

1、木棒切割问题

// 元戎启行 木棒切割问题:
/*
    给你n根木棍,长度为li,使得这些木棍能够砍出k块最长的长度x,得到至少K段长度相等的木棒
    ==============即问所有木棒最少能切k段,得到的长度相等的木棒的最长长度x是多少==========

    木棒切割问题,给出N根木棒,长度已知,现在希望通过分割他们来得到至少K段长度相等的木棒
    (长度必须为整数),问这些长度相等的木棒最长能有多长
*/

// 计算每段木棒切割长度为x时对应的总的木棒段数
int woodNum(const vector<int>& woods, int x)
{
   
    int count = 0;
    for(int i = 0; i < woods.size(); ++i)
    {
   
        // woods[i] / x 计算每一根木棒切割为x长度时的段数
        count = count + woods[i] / x;
    }
    return count;
}


// k为要求木棒的段数
int bSearch(const vector<int>& woods, int k)
{
   
    // 二分法
    int left = 0, right = woods.size() - 1;
    int mid = 0;
    while(left < right)
    {
   
        mid = left + (right - left) / 2;
        // 以长度为mid切割每一根木棒,总的木棒段数小于k,说明只要x够小还能切
        // 长度越短,木棒数越多
        if(woodNum(woods, mid) < k)
        {
   
            right = mid;// 缩小right,使得mid更小,因为我切割出的木棒总段数未达到k数量
        }
        // 总的木棒段数大于k,试试x更大一些能否切割出更多的木棒
        // 木棒数太多,试试扩大x的大小,相当于缩小x的右边界
        else
        {
   
            // 移动left,使得mid增大,因为木棒总段数足够,需要找到符合k的最大长度mid
            left = mid + 1;
        }
    }
    return right - 1;
}

int main()
{
   
    // 木棒切割问题,主函数中需要参数 n:木棒数量 li:木棒长度 k:至少切k段
    int n = 5;
    int li = 10;
    int k = 2;

    vector<int> woods(n, li);// 创建数组,每个元素为木棒对应的长度
    int ret = bSearch(woods, k);
    cout << ret << endl;
	return 0;
}

1、数组 (二分)


C++中,二维数组在地址空间上是连续的。

void test_arr() {
    int array[2][3] = {
		{0, 1, 2},
		{3, 4, 5}
    };
    cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
    cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}

int main() {
    test_arr();
}

704.二分查找

注意:二分查找尽量不用else,只用else if代替。

需要注意的是区间的范围,如果最初确定好是左闭右闭区间,那就需要在while循环的判断条件添加=,因为left == right是有意义的。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        // 给定有序数组 搜索目标值 使用二分法
		// 时间复杂度O(logn) 空间复杂度O(1)

        // 注意左闭右闭区间
        // 声明双指针
        int left = 0, right = nums.size() - 1;

        while(left <= right)
        {
            // 中间下标位置
            int mid = left + (right - left) / 2;
            if(target > nums[mid])
            {
                left = mid + 1;
            }
            else if(target < nums[mid])
            {
                right = mid - 1;
            }
            else if(target == nums[mid])
                return mid;
        }
        return -1;
    }
};

35.搜索插入位置

class Solution {
   
public:
    int searchInsert(vector<int>& nums, int target) {
   
        // 二分法在排序数组内寻找目标值
        // 时间复杂度O(logn) 空间复杂度O(1)
        int left = 0, right = nums.size() - 1;

        // 左闭右闭区间
        while(left <= right)
        {
   
            int mid = left + (right - left) / 2;
            if(target > nums[mid])
            {
   
                left = mid + 1;
            }
            else if(target < nums[mid])
            {
   
                right = mid - 1;
            }
            else if(target == nums[mid])
                return mid;
        }
        // 跳出循环,target不存在数组中,指针left指向合适的插入位置
        return left;
    }
};

题解如下:

class Solution {
   
private:
    // 寻找左侧边界(左闭右闭区间)
    int getLeftBorder(vector<int>& nums, int target)
    {
   
        // 寻找左侧边界-左闭右闭区间
        int left = 0, right = nums.size() - 1;
        // 搜索区间为[left, right]
        while(left <= right)
        {
   
            int mid = left + (right - left) / 2;
            if(target > nums[mid])
            {
   
                // 搜索区间变为[mid + 1, right]
                left = mid + 1;
            }
            else if(target < nums[mid])
            {
   
                // 搜索区间变为[left, mid - 1]
                right = mid - 1;
            }
            else if(target == nums[mid])
            {
   
                // 别返回,收缩左侧边界
                right = mid - 1;
            }
        }
        // 循环结束还没有返回值,还需要判断索引是否越界(这里left == right + 1会结束循环)
        // 检查出界情况 || (不出界判断是否是target)
        if(left >= nums.size() || nums[left] != target)
        {
   
            return -1;
        }
        return left;
    }
    // 寻找右侧边界
    int getRightBorder(vector<int>& nums, int target)
    {
   
        // 寻找右侧边界-左闭右闭区间
        int left = 0, right = nums.size() - 1;
        while(left <= right)
        {
   
            int mid = left + (right - left) / 2;
            if(target > nums[mid])
            {
   
                // 搜索区间变为[mid + 1, right]
                left = mid + 1;
            }
            else if(target < nums[mid])
            {
   
                // 搜索区间变为[left, mid - 1]
                right = mid - 1;
            }
            else if(target == nums[mid])
            {
   
                // 别返回,收缩右侧边界
                left = mid + 1;
            }
        }

        // 循环结束,判断出界情况或者是否是target
        if(right < 0 || nums[right] != target)
        {
   
            return -1;
        }
        return right;
    }
public:
    vector<int> searchRange(vector<int>& nums, int target) {
   
        int left = 0, right = nums.size() - 1;
        vector<int> ret(2, -1);
        int leftRet = getLeftBorder(nums, target);
        int rightRet = getRightBorder(nums, target);

        if(leftRet == -1 || rightRet == -1)
            return ret;
        
        // 找到了左右边界
        ret[0] = leftRet;
        ret[1] = rightRet;
        return ret;
    }
};

34.在排序数组中查找元素的第一个和最后一个位置 (二分查找左右边界)

(在排序数组内搜索左右边界)

思路:labuladong

① 搜索一个元素时,搜索区间两端闭;while条件带等号,if相等就返回;mid必须加减一,因为区间两端闭;while结束就凉了,凄凄惨惨返-1。

② 搜索左右边界时,搜索区间要阐明;左闭右开最常见,其余逻辑便自明;while要用小于号,这样才能不漏掉;if相等别返回,利用mid锁边界;mid加一或减一?要看区间开或闭;while结束不算完,因为你还没返回;索引可能出边界,if检查保平安。

将right初始化为nums.size() - 1, while的终止条件为left == right + 1,也就是left > right时循环结束,那么while的循环条件就应该为 <= 。这样就将搜索区间统一成左闭右闭区间。

// 寻找左侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
// 搜索区间为[left, right]
while(left <= right)
{
    int mid = left + (right - left) / 2;
    if(target > nums[mid])
    {
        // 搜索区间变为[mid + 1, right]
        left = mid + 1;
    }
    else if(target < nums[mid])
    {
        // 搜索区间变为[left, mid - 1]
        right = mid - 1;
    }
    else if(target == nums[mid])
    {
        // 别返回,收缩左侧边界
        right = mid - 1;
    }
}
// 循环结束还没有返回值,还需要判断索引是否越界(这里left == right + 1会结束循环)
// 检查出界情况 || (不出界判断是否是target)
if(left >= nums.size() || nums[left] != target)
{
    return -1;
}
return left;


// 寻找右侧边界-左闭右闭区间
int left = 0, right = nums.size() - 1;
while(left <= right)
{
    int mid = left + (right - left) / 2;
    if(target > nums[mid])
    {
        // 搜索区间变为[mid + 1, right]
        left = mid + 1;
    }
    else if(target < nums[mid])
    {
        // 搜索区间变为[left, mid - 1]
        right = mid - 1;
    }
    else if(target == nums[mid])
    {
        // 别返回,收缩右侧边界
        left = mid + 1;
    }
}

// 循环结束,判断出界情况或者是否是target
if(right < 0 || nums[right] != target)
{
    return -1;
}
return right;
class Solution {
   
private:
    // 寻找左侧边界(左闭右闭区间)
    int getLeftBorder(vector<int>& nums, int target)
    {
   
        // 寻找左侧边界-左闭右闭区间
        int left = 0, right = nums.size() - 1;
        // 搜索区间为[left, right]
        while(left <= right)
        {
   
            int mid = left + (right - left) / 2;
            if(target > nums[mid])
            {
   
                // 搜索区间变为[mid + 1, right]
                left = mid + 1;
            }
            else if(target < nums[mid])
            {
   
                // 搜索区间变为[left, mid - 1]
                right = mid - 1;
            }
            else if(target == nums[mid])
            {
   
                // 别返回,收缩左侧边界
                right = mid - 1;
            }
        }
        // 循环结束还没有返回值,还需要判断索引是否越界(这里left == right + 1会结束循环)
        // 检查出界情况 || (不出界判断是否是target)
        if(left >= nums.size() || nums[left] != target)
        {
   
            return -1;
        }
        return left;
    }
    // 寻找右侧边界
    int getRightBorder(vector<int>& nums, int target)
    {
   
        // 寻找右侧边界-左闭右闭区间
        int left = 0, right = nums.size() - 1;
        while(left <= right)
        {
   
            int mid = left + (right - left) / 2;
            if(target > nums[mid])
            {
   
                // 搜索区间变为[mid + 1, right]
                left = mid + 1;
            }
            else if(target < nums[mid])
            {
   
                // 搜索区间变为[left, mid - 1]
                right = mid - 1;
            }
            else if(target == nums[mid])
            {
   
                // 别返回,收缩右侧边界
                left = mid + 1;
            }
        }

        // 循环结束,判断出界情况或者是否是target
        if(right < 0 || nums[right] != target)
        {
   
            return -1;
        }
        return right;
    }
public:
    vector<int> searchRange(vector<int>& nums, int target) {
   
        int left = 0, right = nums.size() - 1;
        vector<int> ret(2, -1);
        int leftRet = getLeftBorder(nums, target);
        int rightRet = getRightBorder(nums, target);

        if(leftRet == -1 || rightRet == -1)
            return ret;
        
        // 找到了左右边界
        ret[0] = leftRet;
        ret[1] = rightRet;
        return ret;
    }
};

27.移除元素

移除元素思想:遇到与val值不相等的元素,交换到数组前面位置,相等则不做处理,最后数组前面元素都是与val值不相等的元素,由慢指针控制其范围,快指针只负责遍历。

// 快慢指针同向解法
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        // 使用快慢指针移除数组内指定元素

        int slowIndex = 0;
        // 快指针先移动,只要找到不等于val的元素,就进行快慢指针的元素交换,然后慢指针移动一步
        // 时间复杂度O(n) 空间复杂度O(1)
        for(int fastIndex = 0; fastIndex < nums.size(); ++fastIndex)
        {
            if(nums[fastIndex] != val)
            {
                swap(nums[slowIndex], nums[fastIndex]);
                ++slowIndex;
            }
        }
        return slowIndex;
    }
};

// 快慢指针双向解法:确保了移动最少元素
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int leftIndex = 0;
        int rightIndex = nums.size() - 1;
        while (leftIndex <= rightIndex) {
            // 找左边等于val的元素
            while (leftIndex <= rightIndex && nums[leftIndex] != val)
            {
                ++leftIndex;
            }
            // 找右边不等于val的元素
            while (leftIndex <= rightIndex && nums[rightIndex] == val)			  {
                -- rightIndex;
            }
            // 将右边不等于val的元素覆盖左边等于val的元素
            if (leftIndex < rightIndex) {
                nums[leftIndex++] = nums[rightIndex--];
            }
        }
        return leftIndex;   // leftIndex一定指向了最终数组末尾的下一个元素
    }
};

977.有序数组的平方

时间复杂度O(n) 不考虑结果集所占用空间,空间复杂度O(1)

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        // 双指针解法
        // 因为非递减且数组内存在正负元素,负数的平方也可能比正数的平方更大(数组两端元素的平方会最大),所以需要双指针从数组的首尾位置开始向中间进行遍历,将指定位置元素平方的大者存储在数组末尾,最终结果也为非递减顺序

        // 创建新数组
        vector<int> result(nums.size(), 0);
        int index = nums.size() - 1;// 新数组索引,从后向前存储元素

        // 前后双指针
        int beginIndex = 0, endIndex = nums.size() - 1;

        while(beginIndex <= endIndex)
        {
            if(nums[beginIndex] * nums[beginIndex] <= nums[endIndex] * nums[endIndex])
            {
                // 将双指针指向的元素的平方中的大值,赋值到新数组中
                result[index] = nums[endIndex] * nums[endIndex];
                // 更新所指元素平方最大的指针
                --endIndex;
            }
            else if(nums[beginIndex] * nums[beginIndex] > nums[endIndex] * nums[endIndex])
            {
                // 同理
                result[index] = nums[beginIndex] * nums[beginIndex];
                ++beginIndex;
            }

            --index;// 结果集上每循环一次,需要更新一下索引,指向下一个待赋值的位置
        }
        return result;
    }
};

59.螺旋矩阵II <二维数组>

双指针固定始末位置,然后根据旋转矩阵元素的性质,利用start与end向矩阵进行赋值操作. 时间复杂度O(n^2) 空间O(1)

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {

        int num = 1;
        // 创建 n x n 正方形矩阵
        vector<vector<int>> matrix(n, vector<int>(n, 0));

        // 确定边界 初始位置和终止位置下标
        int start = 0, end = n - 1;

        while(start < end)
        {
            // 注意旋转矩阵赋值的时候为 左闭右开 区间或者 左开右闭 区间,即 不能同时取到边界值
            // 以n = 3为例,第一次界限下标为[0, 2],但是赋值的时候按照旋转矩阵的性质只能赋值[0, 1],索引2交由下一个循环赋值,以此类推
            // [1, 2][3]
            // [8][ ][4]
            // [7][6, 5]
            for(int i = start; i < end; ++i)
            {
                matrix[start][i] = num++;
            }
            for(int i = start; i < end; ++i)
            {
                matrix[i][end] = num++;
            }

            for(int i = end; i > start; --i)
            {
                matrix[end][i] = num++;
            }
            for(int i = end; i > start; --i)
            {
                matrix[i][start] = num++;
            }
            // 外圈赋值完毕,边界同时向内圈缩进
            ++start;
            --end;
        }
        // n是奇数,需要对中间位置进行赋值操作
        if(n % 2 != 0)
        {
            matrix[start][end] = num;
        }
        // for(int i = 0; i < matrix.size(); ++i)
        // {
        //     for(int j = 0; j < matrix[0].size(); ++j)
        //     {
        //         cout<<matrix[i][j]<<" ";
        //     }
        //     cout<<endl;
        // }

        return matrix;
    }
};

54.螺旋矩阵

矩阵的顺时针遍历,注意不是n x n的矩阵,所以要设置四个变量来对要遍历的矩阵边界进行限定. 时间复杂度O(mn) 空间复杂度O(1)

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        // 确定矩阵的行与列
        int rows = matrix.size();
        int cols = matrix[0].size();

        // 创建结果集
        vector<int> result(rows * cols, 0);
        int index = 0;// 结果集中的索引

        // 因为是矩阵,所以需要4个变量确定矩阵的索引边界
        int up = 0, down = rows - 1;
        int left = 0, right = cols - 1;

        // 顺时针 遍历"矩阵": 注意一定要按照顺时针进行遍历
        // 按照顺时针顺序,每遍历完某个边界,对应边界要向内圈缩进更新
        while(true)
        {
            // 先遍历第up行
            for(int i = left; i <= right; ++i)
            {
                result[index++] = matrix[up][i];
            }
            // 第up行遍历完,更新初始up,即up边界下移,并判断是否符合规范,是否越界
            if(++up > down)
                break;
            
            // 接着遍历right列
            for(int i = up; i <= down; ++i)
            {
                result[index++] = matrix[i][right];
            }
            // 更新right,right边界左移
            if(--right < left)
                break;
            
            // 接着遍历down行
            for(int i = right; i >= left; --i)
            {
                result[index++] = matrix[down][i];
            }
            // 更新down,down边界上移
            if(--down < up)
                break;
            
            // 接着遍历left列
            for(int i = down; i >= up; --i)
            {
                result[index++] = matrix[i][left];
            }
            // 更新left列,left边界右移
            if(++left > right)
                break;
        }
        return result;
    }
};

剑指 Offer 03. 数组中重复的数字

优化解法,利用该题元素的性质
时间复杂度为O(n) 空间复杂度优化为O(1)

class Solution {
   
public:
    int findRepeatNumber(vector<int>& nums) {
   
        // 找出数组中任意一个重复的数字

        // 如果这个数组中没有重复数字,那么对数组进行排序之后,数字i将出现在下标为i的位置;如果数组中有重复数字,有些位置可能存在多个数字,有些位置可能没有数字

        // 对未排序的数组进行遍历
        for(int i = 0; i < nums.size(); ++i)
        {
   
            // i元素应该出现在i位置(无重复的情况)
            // 例:2应该在下标为2的位置

            // 若当前位置的元素不是第i个位置应该存在的元素
            // 比较当前遍历到的元素是否等于i
            while(nums[i] != i)
            {
   
                // 不相等,进入while循环
                // 比较nums[i]和索引nums[i]处的元素值是否相等
                if(nums[i] == nums[nums[i]]) return nums[i];// 相等,则找到第一个重复的数字

                // 不相等,则交换,将nums[i]放到属于它的地方(排序)
                swap(nums[i], nums[nums[i]]);// [0, 1, 2, 3, 2, 5, 3]
            }
        }
        return -1;
    }
};

剑指 Offer 04. 二维数组中的查找

方法2:以右上角(左下角)位置元素为根节点不断搜索目标元素

取右上角或者左下角的目的在于,每次比较后进行移动的时候,可以保证只能移动一个方向,另一个方向严格不能移动,每一次都在数组的查找范围中剔除一行或者一列,这样每一步都可以缩小查找的范围,直到找到要查找的数字。
如果取左上角或者右下角为起始搜索位置,则无法缩小查找的范围。
算法时间复杂度为O(n + m) 空间复杂度为O(1)

class Solution {
   
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
   
        // 方法1:逐列遍历,在每一列上都使用二分查找 时间复杂度O(m * logn)
        // 方法2:以右上角或者左下角位置元素为根节点,进行搜索,这里的搜索每次只能原则一个方向前进(取其它元素不能确定搜索的方向)

        // 方法2:以右上角位置元素为根节点不断搜索目标元素

        if(matrix.size() == 0) return false;

        int n = matrix.size();
        int m = matrix[0].size();

        // 记录根节点的位置
        int row = 0, col = m - 1;
        // 根节点的位置 
        // 行可以++操作,所以需要设定上界;列进行--操作移动,所以需要设定上界
        while(row < n && col >= 0)
        {
   
            // 目标值比根节点值要小,因为当前列的元素是升序,所以不能向同列的下一行进行移动;当前行从前向后为升序,从后向前为降序,所以需要移动列,行不变
            if(target < matrix[row][col])
            {
   
                --col;
            }
            else if(target > matrix[row][col])
            {
   
                ++row;
            }
            else
            {
   
                return true;
            }
        }
        return false;
    }
};

方法1:<二分查找>虽然使用剪枝操作,但是平均的时间复杂度还是为O(mlogn) 空间复杂度为O(1)

class Solution {
   
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
   
        // 方法1:逐列遍历,在每一列上都使用二分查找 时间复杂度O(m * logn)
        // 方法2:以右上角或者左下角位置元素为根节点,进行搜索,这里的搜索每次只能原则一个方向前进(取其它元素不能确定搜索的方向)

        // 方法1:时间复杂度O(mlogn)

        // 特例:矩阵为空
        if(matrix.size() == 0) return false;

        int n = matrix.size();// 行数
        // 遍历矩阵的每一列
        int m = matrix[0].size();// 列数
        for(int j = 0; j < m; ++j)
        {
   
            // 剪枝
            if(target < matrix[0][0]) break;

            // 因为每一行都是升序,每一列都是升序
            // 对在每一列中使用二分查找搜索target 左闭右闭区间
            int left = 0, right = n - 1;
            int mid = 0;
            while(left <= right)
            {
   
                mid = left + (right - left) / 2;
                if(target < matrix[mid][j])
                {
   
                    right = mid - 1;
                }
                else if(target > matrix[mid][j])
                {
   
                    left = mid + 1;
                }
                // 在第j列中找到了目标值
                else
                {
   
                    // target == matrix[mid][j]
                    return true;
                }
            }
            // 跳出循环,说明未找到target,则继续遍历列
        }
        // 执行到这里说明在每一列都没有找到目标值,返回false
        return false;
    }
};

15.三数之和

三数之和:一层for循环 + 双指针 时间复杂度O(n^2) 结果集所占用的空间不记录的话,空间复杂度为O(1)
四数之和:两层for循环 + 双指针操作 时间复杂度O(n^3) 空间复杂度O(1)

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // target为0
		// 先对数组内元素进行排序操作,方便后续使用下标进行去重
        sort(nums.begin(), nums.end());
        
        vector<vector<int>> result;
        
        for(int i = 0; i < nums.size(); i++)
        {
            // 对排序后的数组进行剪枝操作
            if(nums[i] > 0)
                break;
            // 去重
            if(i > 0 && nums[i] == nums[i - 1])
                continue;
            
            // 这里使用双指针对数组剩余元素进行操作
            int left = i + 1;
            int right = nums.size() - 1;
            int sum = 0;// 存储三个数之和
            
            // 注意这里的while循环只能是<,不能加=
            // 因为三个元素的下标各不相同,所以这里的双指针 不能取<=
            while(left < right)
            {
                sum = nums[i] + nums[left] + nums[right];
                if(sum > 0) --right;
                else if(sum < 0) ++left;
                else
                {
                    // sum符合条件,添加到结果集中
                    result.push_back(vector<int>{nums[i], nums[left], nums[right]});

                    // 对双指针进行去重操作,判断双指针之间是否还存在符合条件的元素
                    while(left < right && nums[left] == nums[left + 1])
                        ++left;
                    while(left < right && nums[right] == nums[right - 1])
                        --right;
                    
                    // 若双指针存在重复元素,则两个指针都需要再进行一次移动才能走出重复区间
                    // 若不存在重复元素,则两个指针也需要移动一次开始下一次sum判断
                    ++left;
                    --right;
                }
            }
        }
        return result;
    }
};

16.四数之和

四数之和:两个for循环+一对双指针前后去重(时间复杂度O(n^3))

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {

        // 创建结果集
        vector<vector<int>> result;

        // 对数组内元素进行排序,方便后续使用下标进行去重
        sort(nums.begin(), nums.end());// O(nlogn)

        // 双重for循环 + 双指针
        for(int i = 0; i < nums.size(); ++i)
        {
            // 去重
            if(i > 0 && nums[i] == nums[i - 1])
                continue;
           	// 剪枝处理
            if (nums[i] > target && nums[i] >= 0) break;
            
            for(int j = i + 1; j < nums.size(); ++j)
            {
                // 剪枝
                // 当target为负数不能这样剪枝
                if(nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;

                // 去重
                if(j > i + 1 && nums[j] == nums[j - 1])
                    continue;

                // 声明双指针,对剩下的元素进行判断
                // 注意这里可能会int overflow,所以使用long类型的变量
                long sum = 0;

                int left = j + 1;
                int right = nums.size() - 1;

                while(left < right)
                {
                    sum = (long)nums[i] + nums[j] + nums[left] + nums[right];

                    if(sum > target) --right;
                    else if(sum < target) ++left;
                    else
                    {
                        // sum符合条件,添加结果集
                        result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});

                        // 判断双指针之间的区间内是否还存在符合条件的元素
                        while(left < right && nums[left] == nums[left + 1]) ++left;
                        while(left < right && nums[right] == nums[right - 1]) --right;

                        ++left;
                        --right;
                    }
                }
            }
        }
        return result;
    }
};

X.N数之和

class Solution {
public:
    vector<vector<int>> nSum(vector<int>& nums, int target, int n, int startIndex) {

        // 创建结果集
        vector<vector<int>> result;
        int size = nums.size();
        
        // 至少是2Sum,且数组大小不能小于n
        if(n < 2 || size < n) return result;

        // 对数组内元素进行排序,方便后续使用下标进行去重
        sort(nums.begin(), nums.end());// O(nlogn)

        if(n == 2)
        {
            // 在数组中找到和为target的两个整数
        
            // 创建哈希表 key为元素,value为下标
            unordered_map<int, int> hash;
            // 遍历数组
            for(int i = 0; i < nums.size(); ++i)
            {
                // 找到了,返回两个元素对应下标 => 数组当前元素之前存在x, x + nums[i] == target
                if(hash.find(target - nums[i]) != hash.end())
                {
                    result.push_back(vector<int>{hash[target - nums[i]], i});
                }
                // 未找到,将当前元素添加到哈希表中
                else
                {
                    hash[nums[i]] = i;
                }
            }
        }
        else
        {
            // n > 2时,递归计算 (n - 1)Sum 的结果
            // nSum(nums, target - nums[i], n - 1, i + 1);
        }
        
        return result;
    }
};

offer4.只出现一次的数字

137.只出现一次的数字II

算法思路:

如果数组中所有数字的 第i个数位相加之和 能 被3整除,那么只出现一次的数字的 第i个数位一定是0;如果数组中所有数字的 第i个数位相加之和 被3除余1,那么只出现一次的数字的 第i个数位一定是1.

知道一个整数任意一位是0还是1之后,最终就演变成了使用二进制还原十进制。

以此类推,如果其它元素都出现k次,找只出现一次的元素,也是一样的道理

class Solution {
   
public:
    int singleNumber(vector<int>& nums) {
   

        // 使用额外空间 那么时间复杂度O(n) 空间复杂度O(n)
        // 要求不使用额外空间来实现 那么就应该想到使用常数空间

        // int类型固定为32位,使用长度为32的数组,记录所有数值二进制的每一位共出现了多少次1
        vector<int> cnt(32, 0);

        for(int num : nums)
        {
   
            // 记录每个数字二进制的每一位共出现了多少次1
            for(int i = 0; i < 32; ++i)
            {
   
                // 得到整数num的二进制形式中从右起第i个数位
                if((num >> i) & 1 == 1) ++cnt[i];
            }
        }

        // 对cnt数组的每一位进行 mod 3 的操作,重新拼凑出只出现一次的数值
        int ans = 0;
        for(int i = 0; i < 32; ++i)
        {
   
            // 如果当前位对3取余后值为1,那么说明 只出现一次的那个数字的二进制在cnt[i]的位置值也为1
            // 例如(cnt[2] % 3) & 1 == 1 那么对应的二进制位为 1 0 0 ,即 1 << 2
            // 又cnt数组中有多个 (cnt[i] % 3) & 1 == 1 存在,所以这里使用累加和求出最终的结果
            if((cnt[i] % 3) & 1 == 1) ans += (1 << i);
        }
        return ans;
    }
};

offer70.排序数组中只出现一次的数字

给定一个只包含整数的有序数组 nums ,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字

二分法,针对排序数组,时间复杂度为O(log n)

如果是不排序的数组,搜索只出现一次的数字,可以进行位操作,同offer4.只出现一次的数字

class Solution {
   
public:
    int singleNonDuplicate(vector<int>& nums) {
   

        if(nums.size() == 1) return nums[0];

        // 每个元素都会出现两次,只有一个数只会出现一次
        int left = 0, right = nums.size() - 1;
        // 如果mid为奇数索引,那么如果mid和mid-1指向的元素相等,说明只出现一次的元素在当前mid的右侧;如果mid为偶数索引,判断mid和mid+1是否相等,相等,则唯一出现的元素在mid+1的右侧

        int mid = 0;
        while(left <= right)
        {
   
            if(left == right) break;
            
            mid = left + (right - left) / 2;

            if(mid % 2 == 1)
            {
   
                if(nums[mid] == nums[mid - 1]) left = mid + 1;
                // 不相等,说明唯一出现的元素在mid的左侧
                else right = mid;
            }
            else
            {
   
                if(nums[mid] == nums[mid + 1]) left = mid + 1;
                else right = mid;
            }
        }
        return nums[left];
    }
};

offer5.单词长度的最大乘积

时间复杂度O(n^2) 空间复杂度O(n)

可进行优化:该解法是用一个长度为26的bool型数组记录字符串中出现的字符,bool型要么为true,要么为false,在二进制中数字的每个数位要么是0要么是1,因此可以将长度为26的bool型数组用26个二进制的数位代替,二进制的0对应false,1对应true

class Solution {
   
public:
    int maxProduct(vector<string>& words) {
   
        // 判断两个字符串中是否出现相同的字符,只需要从'a'到'z'判断某个字符是否在两个字符串对应的哈希表中都出现了

        // 记录某个字符串中对应字符出现的位置
        vector<vector<bool>> flags(words.size(), vector<bool>(26, false));

        for(int i = 0; i < words.size(); ++i)
        {
   
            for(char ch : words[i])
            {
   
                flags[i][ch - 'a'] = true;
            }
        }

        int result = 0;

        // 比较两个字符串是否包含相同字符
        for(int i = 0; i < words.size(); ++i)
        {
   
            for(int j = i + 1; j < words.size(); ++j)
            {
   
                int k = 0;
                for( ; k < 26; ++k)
                {
   
                    // 说明两个字符串出现了相同的字符
                    if(flags[i][k] == true && flags[j][k] == true) break;
                }

                // 说明两个字符串不包含相同字符
                if(k == 26)
                {
   
                    int prod = words[i].size() * words[j].size();
                    result = max(result, prod);
                }
                
            }
        }
        return result;
    }
};

offer6.排序数组中两个数字之和

对于排序数组,首先要想到双指针实现,这里的双指针实现时间复杂度为O(n) 空间复杂度O(1)

class Solution {
   
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
   
        // 两数之和 + 排序数组 
        // 双指针实现:双指针,不是语言中的指针,而是一个能定位数据容器中某个数据的手段

        int left = 0, right = numbers.size() - 1;
        while(left < right)
        {
   
            if(numbers[left] + numbers[right] > target) --right;
            else if(numbers[left] + numbers[right] < target) ++left;
            else return vector<int>{
   left, right};
        }
        return vector<int>{
   -1, -1};
        
    }
};

1.1 前缀和

offer10.和为k的子数组

利用前缀和求和为k的连续子数组的个数
时间复杂度O(n) 空间复杂度O(n)

class Solution {
   
public:
    int subarraySum(vector<int>& nums, int k) {
   
        // 定义pre[i]为[0..i]所有数之和
        // 那么[j..i]连续子数组和为k 可以转换为 pre[i] - pre[j - 1] == k
        // 即pre[j - 1] == pre[i] - k
        // 相当于只要在哈希表中找到了pre[j - 1],那么就找到了连续子数组[j..i]和为k,统计对应连续子数组出现的个数

        // 创建哈希表,key为[0..i]连续子数组之和;value为连续子数组之和 出现的次数
        unordered_map<int, int> hash;
        // 这里一定要初始化哈希表,当preSum和为k时,会在哈希表中寻找pre[j-1],也即是preSum-k == 0,如果哈希表未初始化,就会错过这个符合条件的子数组
        hash[0] = 1;

        int count = 0; // 符合条件的子数组的个数
        int preSum = 0;// 定义preSum为[0..i]所有数之和
        for(int i = 0; i < nums.size(); ++i)
        {
   
            preSum += nums[i];

            // 找到了符合条件的连续子数组
            if(hash.find(preSum - k) != hash.end())
            {
   
                // 因为连续子数组和相同的子数组在此之前不一定只出现一次,所以当符合条件的时候,不能使用++count,而应累加key对应的value
                count += hash[preSum - k];
            }
            
            // 将当前连续子数组和添加到哈希表中
            ++hash[preSum];
        }

        return count;
    }
};

offer11.0和1个数相同的子数组

关键思路同和为k的子数组,但是本题是求最长连续子数组的长度,所以还需要注意很多细节方面的问题,如:哈希表的初始化问题,最长连续子数组如何使得其长度最长问题等
时间复杂度O(n) 空间复杂度O(n)

class Solution {
   
public:
    int findMaxLength(vector<int>& nums) {
   
        // 求0和1个数相同的最长连续子数组的长度,难点在于连续子数组0和1的个数统计
        // 将0转换为-1,那么题目就转换为 和为0的最长连续子数组的长度
        // 问题就转换为前缀和问题

        // 本题 key为"第一次出现"连续子数组之和preSum;value为"第一次出现"连续子数组之和为preSum的最后一个元素对应的下标(这样才能保证最长连续子数组)
        unordered_map<int, int> hash;
        // 同上一题求和为k的连续子数组,上一题求符合条件的连续子数组出现的次数,所以初始化hash[0] = 1,本题是求长度,需要将hash[0] = -1,初始化为-1
        // 例如[0, 1] 遍历到1,符合条件,那么连续子数组的长度为1 - (-1) = 2
        hash[0] = -1;

        int maxLength = 0;
        int preSum = 0;
        for(int i = 0; i < nums.size(); ++i)
        {
   
            preSum += nums[i] == 0 ? -1 : 1;

            // 找到了和为k的连续子数组 k == 0
            if(hash.find(preSum - 0) != hash.end())
                maxLength = max(maxLength, i - hash[preSum - 0]);
            // hash.find(preSum) == hash.end() 未找到的时候,才添加键值对
            else 
                hash[preSum] = i;
        }
        return maxLength;
    }
};

offer12.左右两边子数组的和相等

利用前缀和思路求解子数组和类面试题

时间复杂度O(n) 空间复杂度O(1)

class Solution {
   
public:
    int pivotIndex(vector<int>& nums) {
   
        // 求数组元素总和
        int sum = 0;
        for(int num : nums) sum += num;

        // [0..i-1]所有数之和 等于 [i+1..nums.size()-1]所有数之和
        // pre[i-1]为[0..i-1]所有元素之和 [i..nums.size()-1]所有元素和为sum-pre[i-1]
        // [i+1..nums.size()-1]所有元素和为sum-pre[i-1]-nums[i]

        // 遍历数组
        int preSum = 0;// preSum表示[0..i-1]数组元素之和(不能包含中心下标元素)
        // sum - preSum 则为[i..nums.size()-1]数组元素之和
        // 因为要求中心下标左右两边元素和相等,这里不能包含中心下标
        for(int i = 0; i < nums.size(); ++i)
        {
   
            // 注意中心下标右侧所有元素相加之和为sum - preSum - nums[i]
            if(preSum == sum - preSum - nums[i]) return i;

            preSum += nums[i];
        }
        // 不存在中心下标,返回-1
        return -1;
    }
};

offer50.向下的路径节点之和

同offer10求和为k的子数组的个数原理类似,前缀和,只不过这里是二叉树的某个路径符合条件的数目,同时这里需要注意的是,向哈希表中添加键值对的时候,在递归回溯的时候,对相应的键值对也需要进行回溯撤销处理

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
   
private:
    // 将哈希表创建为全局变量,因为要进行递归
    // 创建哈希表,key为[0..i]连续子数组之和;value为连续子数组之和 出现的次数
    unordered_map<long long, int> hash;
    int dfs(TreeNode* root, int targetSum, long long preSum)
    {
   
        int ret = 0;
        if(root == nullptr) return ret;

        // 根节点不为空
        preSum += root->val;
        // 找到符合条件的连续子数组,更新路径数目
        if(hash.find(preSum - targetSum) != hash.end())
        {
   
            ret += hash[preSum - targetSum];
        }
        // 将前缀和添加到哈希表中
        ++hash[preSum];

        ret += dfs(root->left, targetSum, preSum);
        ret += dfs(root->right, targetSum, preSum);

        // 注意回溯的时候需要将对应的前缀和从哈希表中去除
        --hash[preSum];

        return ret;
    }
public:
    int pathSum(TreeNode* root, int targetSum) {
   
        // 转换为求和为k的子数组的个数
        // 定义pre[i]为[0..i]所有数之和
        // 那么[j..i]连续子数组和为k,可以转换为 pre[i] - pre[j - 1] == k
        // 即pre[j - 1] == pre[i] - k
        // 相当于只要找到了pre[j - 1],即pre[i] - k,就找到了连续子数组[j..i]和为k,统计相应子数组出现的次数即可


        // 这里一定要初始化哈希表,当preSum和为k时,会在哈希表中寻找pre[j-1],也即是preSum-k == 0,如果哈希表未初始化,就会错过这个符合条件的子数组
        hash[0] = 1;

        return dfs(root, targetSum, 0);
    }
};

offer13.二维子矩阵的和

求二维子矩阵和,思路同利用前缀和求一维子数组和;在初始化一次NumMatrix对象后,后续不断调用函数,不需要再重新对矩阵内元素进行搜索,每次调用是需要做几次加减法,调用操作的时间复杂度为O(1)
时间复杂度O(mn) 空间复杂度O(mn)

class NumMatrix {
   
public:
    // 利用前缀和思路求解二维子矩阵的和

    /*
        f(i, j)表示以[i,j]为右下角元素的矩阵之和:
        f(i, j) = f(i-1, j) + f(i, j-1) - f(i-1, j-1) + matrix[i][j];

        sumRegion(int r1, int c1, int r2, int c2)求给定子矩阵元素之和:
        = f(r2, c2) - f(r1-1, c2) - f(r2, c1-1) + f(r1-1, c1-1)
		如果坐标值(r1, c1)值为0,f(r1-1, c1-1) = f(-1, -1)下标会失效,那么可以在矩阵的最上面增加一行,最左面增加一列
        // 将前缀和矩阵统一向右下移动一个单位(横纵坐标统一 + 1)

        注:为什么后面要加上f(r1-1, c1-1),因为减去两个子矩阵和的时候,减了两次f(r1-1, c1-1),所以要加上一个f(r1-1, c1-1)
    */


    // sum[i][j]表示以[i, j]为右下角元素的矩阵元素总和
    vector<vector<int>> sum;
    NumMatrix(vector<vector<int>>& matrix) {
   
        int row = matrix.size();
        // 行不符合条件
        if(row <= 0) return;

        // 行符合条件
        int col = matrix[0].size();
        // 这里初始化二维矩阵大小分别为原数组的行列数 + 1,为了避免求f(0,0)等出现负数下标
        sum.resize(row + 1, vector<int>(col + 1, 0));

        for(int i = 0; i < matrix.size(); ++i)
        {
   
            for(int j = 0; j < matrix[0].size(); ++j)
            {
   
                sum[i + 1][j + 1] = sum[i][j + 1] + sum[i + 1][j] - sum[i][j] + matrix[i][j];
            }
        }

        // for(int i = 0; i < sum.size(); ++i)
        // {
   
        //     for(int j = 0; j < sum[0].size(); ++j)
        //     {
   
        //         cout << sum[i][j] << " ";
        //     }
        //     cout << endl;
        // }
    }
    
    int sumRegion(int row1, int col1, int row2, int col2) {
   
        // 求f(i, j)对应sum下标分别+1
        return sum[row2 + 1][col2 + 1] - sum[row1][col2 + 1] - sum[row2 + 1][col1] + sum[row1][col1];
    }
};

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix* obj = new NumMatrix(matrix);
 * int param_1 = obj->sumRegion(row1,col1,row2,col2);
 */

1.2 质数

204.计数质数

给定整数 n ,返回 所有小于非负整数 n 的质数的数量

class Solution {
   
public:
    int countPrimes(int n) {
   
        // 厄拉多塞筛法
        // 表示第i个数是否是质数
        vector<int> isPrime(n, 1);
        int ans = 0;
        for (int i = 2; i < n; ++i) 
        {
   
            if (isPrime[i]) 
            {
   
                ans += 1;
                // 如果x是质数,那么大于x的x的倍数 2x, 3x, 4x ... ( < n)一定不是质数
                if ((long long)i * i < n) 
                {
   
                    for (int j = i * i; j < n; j += i) 
                    {
   
                        isPrime[j] = 0;
                    }
                }
            }
        }
        return ans;
    }
};

2761.和等于目标值的质数对

先声明大小为n的数组将所有的质数都标记出来,然后接下来利用条件x + y == n && 都是质数 && x <= y 求出所有的质数对
计算质数的时间复杂度O(nloglogn),空间复杂度O(n)
求质数对的时间复杂度为O(n)

class Solution {
   
public:
    vector<vector<int>> findPrimePairs(int n) {
   
        // 创建结果集
        vector<vector<int>> result;
        if(n <= 2) return result;

        // 厄拉多塞筛法
        // 表示第i个数是否是质数 : 质数是大于 1 的自然数,并且只有两个因子,即它本身和 1
        vector<int> isPrime(n, 1);
        isPrime[0] = 0;
        isPrime[1] = 0;
        for (int i = 2; i < n; ++i) 
        {
   
            if (isPrime[i]) 
            {
   
                // 如果x是质数,那么大于x的x的倍数 2x, 3x, 4x ... ( < n)一定不是质数
                if ((long long)i * i < n) 
                {
   
                    for (int j = i * i; j < n; j += i) 
                    {
   
                        isPrime[j] = 0;
                    }
                }
            }
        }
        
        // x + y == n && 都是质数
        for(int i = 1; i <= n; ++i)
        {
   
            int temp = n - i;

            if(i > temp) break;// 1 <= x <= y <= n

            if(isPrime[i] == 1 && isPrime[temp] == 1)
            {
   
                result.push_back(vector<int>{
   i, temp});
            }
            
        }
        return result;
    }
};

1.3 重叠区间

6929.数组的最大美丽值(重叠区间个数)

转换为计算所有区间内的最大重叠区间个数问题(优先队列小顶堆实现),同类问题可都使用该解法

时间复杂度O(nlogn) 空间复杂度O(n)

如果使用暴力解法,双循环计算重叠区间个数,会超时,时间复杂度O(n^2)

class MyComp
{
   
public:
    // 将所有区间按照从小到大的顺序排序
    bool operator()(const vector<int>& v1, const vector<int>& v2)
    {
   
        if(v1[0] == v2[0]) return v1[1] < v2[1];
        return v1[0] < v2[0];
    }
};
class Comp
{
   
public:
    // 实现小顶堆
    bool operator()(const int& n1, const int& n2)
    {
   
        return n1 > n2;
    }
};
class Solution {
   
public:
    int maximumBeauty(vector<int>& nums, int k) {
   
        // 区间重叠问题
        // ranges中存储的是每一个区间,转换为判断最多有多少个重复区间问题
        vector<vector<int>> ranges(nums.size(), vector<int>(2, 0));
        
        vector<int> range(2, 0);
        for(int i = 0; i < nums.size(); ++i)
        {
   
            range[0] = nums[i] - k;
            range[1] = nums[i] + k;
            ranges[i] = range;
        }
        if(ranges.size() == 1) return 1;
        
        // 首先对每个区间进行从小到大排序
        // 这里如果不进行排序的话,那么后续对区间进行遍历,在维持队列内区间重叠的时候,可能导致某个区间end边界pop,但是后续仍有区间与之重叠这种情况发生
        // 进行排序,如果当前遍历到的区间与堆顶对应的区间不重叠,那么与之前的所有区间一定不重叠
        sort(ranges.begin(), ranges.end(), MyComp());
        
        // 创建优先队列 小顶堆
        priority_queue<int, vector<int>, Comp> pri;

        //利用小顶堆解决重叠区间个数的问题:
        /*
            目的:***始终维持优先队列内的区间是重叠的***
            如何保证:借助小顶堆 小顶堆内元素为区间的结束end边界
            1. 如果待添加区间的开始位置 小于等于 优先队列堆顶元素(前面某个区间的最小end边界),说明队列内所有的区间都重叠 ,即start <= pri.top(),则将当前区间end边界添加到队列中,不断更新ans为队列的最大size()
            2. 如果待添加区间的开始位置 大于 优先队列堆顶元素,说明两个区间不重叠,则while循环不断的弹出堆顶元素,直到满足条件1

            问:为什么是小顶堆实现?
            如果是大顶堆,如果当前区间与堆顶元素对应的区间不重叠的情况下,我们不能保证与队列内所有的区间都不重叠,同理,重叠也一样;只有小顶堆才能保证,队列内的区间始终是重叠的
        */

        pri.push(ranges[0][1]);//结束时间

        int ans = 0;// 记录重叠区间的个数

        for(int i = 1; i < ranges.size(); i++)
        {
   
            int start = ranges[i][0]; //当前区间的开始边界

            // 两个区间不重叠,不满足队列条件,弹出堆顶元素,始终维持队列内区间是重叠的
            while(!pri.empty() && start > pri.top())
                pri.pop();
            
            pri.push(ranges[i][1]);

            int count = pri.size();
            ans = max(ans, count);
        }
        return ans;
        
    }
};

offer58.日程表 (判断是否可以插入某个区间类问题)

判断是否可以插入某个区间类问题

使用map集合实现添加日程,map与set的底层实现都是红黑树,增删查的时间复杂度为O(logn)
使用map将key作为日程开始时间,则底层会默认对开始时间进行排序,可以优化查找的效率O(logn)
总的时间复杂度为O(logn) 空间复杂度O(n)

upper_bound()函数返回指向第一个大于给定值的元素的迭代器,也就是说,它返回的是大于目标值的最小元素的位置

lower_bound()函数返回指向第一个大于或等于给定值的元素的迭代器,也就是说,它返回的是大于等于目标值的最小元素的位置

class MyCalendar {
   
public:
    // 使用map存储键值对
    map<int, int> mp;
    MyCalendar() {
   

    }
    
    bool book(int start, int end) {
   
        // 如果待插入的时间段与集合中某个时间段重叠,则直接返回false
        // 若未重叠,则将对应的时间段添加到集合中,并返回true

        // 待插入的区间为[start, end) 需要在集合中找到:
        // 开始时间小于start的所有事项中最晚的一个
        // 开始时间大于start的所有事项中最早的一个

        // 当前区间与后一个时间段重叠
        auto it = mp.lower_bound(start);
        if(it != mp.end() && it->first < end) return false;

        // 移动迭代器,找到当前迭代器指向时间的前一个时间
        if(it != mp.begin() && (--it)->second > start) return false;

        // 未出现重叠
        mp[start] = end;

        return true;
    }
};

/**
 * Your MyCalendar object will be instantiated and called as such:
 * MyCalendar* obj = new MyCalendar();
 * bool param_1 = obj->book(start,end);
 */

offer57.值和下标之差都在给定的范围内 (220.存在重复元素 III)

需要使用一个大小为k的滑动窗口,在窗口内判断值之差是否符合条件

判断滑动窗口中是否存在某个数 y 落在区间 [x−t,x+t]中,只需要判断滑动窗口中所有 大于等于x−t 的元素中的 最小元素 是否 小于等于 x+t即可

// upper_bound()函数返回指向第一个大于给定值的元素的迭代器,也就是说,它返回的是大于目标值的最小元素的位置

// lower_bound()函数返回指向第一个大于或等于给定值的元素的迭代器,也就是说,它返回的是大于等于目标值的最小元素的位置

class Solution {
   
public:
    bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
   

        // 下标之差为k
        // 值之差为t

        // abs(nums[i] - nums[j]) <= t 可以推导出 nums[j] - t <= nums[i] <= nums[j] + t 并且i,j下标的绝对值小于等于k
        // 那么就需要使用一个大小为k的滑动窗口,在窗口内判断值之差是否符合条件
        // 判断滑动窗口中是否存在某个数 y 落在区间 [x−t,x+t]中,只需要判断滑动窗口中所有 大于等于x−t 的元素中的 最小元素 是否 小于等于 x+t即可

        set<long> st;
        for(int i = 0; i < nums.size(); ++i)
        {
   
            // 在集合中找到 >= x-t 的最小元素 y
            auto it = st.lower_bound((long)nums[i] - t);
            if(it != st.end() && *it <= (long)nums[i] + t)
                return true;

            // 若是集合中不存在这个元素y,则将当前遍历到的元素添加到集合中
            st.insert(nums[i]);

            // 始终保证滑动窗口的大小为k
            if(i >= k)
                st.erase(nums[i - k]);// 删除的是指定窗口外的元素

            // set底层是红黑树,默认排序,但是这里没有使用到其排序的功能,只是按照元素值进行增删查操作
            // 时间复杂度O(nlogk) 红黑树增删查的时间复杂度为O(logk) k为红黑树节点数量 空间复杂度O(k)
        }
        return false;
    }
};

2、滑动窗口

tips:

使用双指针解决滑动窗口类问题,需要注意一个细节,就是使用双指针的解法基于如下假设:

  • 向右移动快指针相当于在子数组中添加一个新的数字,从而得到更大的子数组的数字之和。
    • 如果新添加的数字是正数,那么这个假设是成立的。
    • 如果题目的数组没有明确的说明数组是由正整数组成的,因此不能保证在子数组中添加新的数字就能得到和更大的子数组,同样也不能保证删除子数组最左边的数字就能得到和更小的子数组。

209.长度最小的子数组

使用滑动窗口方法 (不断调节子序列的起始位置和终止位置) 求得长度最小的子数组。

每个元素在滑动窗口进来操作一次,出去操作一次,每个元素被操作2次,所以时间复杂度是O(2n) = 时间复杂度O(n) 空间复杂度O(1)

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        // 滑动窗口
        int minLength = INT_MAX;
        // 声明两个变量,分别表示滑动窗口的左右侧
        int slow = 0;
        int fast = 0;
        
        int sum = 0;// 窗口内元素之和
        while(fast < nums.size())
        {
            sum += nums[fast];

            // 当前窗口符合条件
            // 这里使用while循环,是为了多次判断缩小左侧窗口时出现sum符合条件的情况
            while(sum >= target)
            {
                // 添加结果
                minLength = min(minLength, fast - slow + 1);

                // 缩小左侧窗口,同时更新窗口内元素之和
                sum -= nums[slow];
                ++slow;
            }
            ++fast;// 更新快指针,即窗口右侧右移
        }
        // 根据初始值判断是否存在符合条件的子数组
        return minLength == INT_MAX ? 0 : minLength;
    }
};

904.水果成篮(☆☆☆)

通过滑动窗口限定窗口内水果种类,通过哈希表查找指定元素是否在集合中出现过以及出现的次数,两者相结合。时间复杂度O(n) 空间复杂度O(1)

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        // 滑动窗口解法
        int maxFruits = 0;
        // 双指针控制窗口的大小
        int slow = 0;
        int fast = 0;

        // 创建哈希表,查询一个元素是否在集合中出现过
        unordered_map<int, int> hash;

        while(fast < fruits.size())
        {
            ++hash[fruits[fast]];

            // 窗口大小符合收缩条件,因为需要收集水果的最大数目,所以按照上限3确定窗口界限(篮子只能装2种类型的水果)
            while(hash.size() > 2)
            {
                // 缩小左侧窗口
                --hash[fruits[slow]];

                if(hash[fruits[slow]] == 0)
                {
                    hash.erase(fruits[slow]);
                }
                // 应该先判断当前慢指针指向元素对应哈希表中的数据,然后慢指针再进行移动
                ++slow;
            }
            // 窗口大小 <= 2,添加结果
            maxFruits = max(maxFruits, fast - slow + 1);
            // 窗口大小不符合收缩条件,移动窗口右侧,扩大窗口
            ++fast;
            
        }
        return maxFruits;
    }
};

76.最小覆盖子串(☆☆☆)

时间复杂度O(n) 空间复杂度O(m + n) 两个字符串都存储到哈希表中 可以声明128大小的数组将空间复杂度优化为O(1)
slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度,在进行滑动窗口时,向哈希表中添加字符为减,删除字符为加。

降低空间复杂度可以用26大小的数组,代替哈希表

class Solution {
public:
    string minWindow(string s, string t) {
        // 滑动窗口解法
        // 求字符串s中涵盖t所有字符的最小子串

        // 子串的最小长度
        int length = INT_MAX;
        // 最小长度子串的起始索引
        int startIndex = 0;

        // 创建哈希表,将t所有字符存储到哈希表中 key为t中字符,value为字符出现频率
        unordered_map<char, int> hash;
        for(char ch : t)
        {
            ++hash[ch];
        }

        int needCount = t.size();// 窗口内子字符串所需要指定字符的个数

        // 声明快慢指针,限定窗口大小
        // slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度
        int slow = 0;
        for(int fast = 0; fast < s.size(); ++fast)
        {
            // 步骤1:不断增加fast使得窗口增大,使得窗口内包含t中所有字符

            // 遍历到的是t中的字符
            if(hash[s[fast]] > 0)
                --needCount;
            
            --hash[s[fast]];// 向哈希表中添加字符,扩大窗口

            // 2、不断增加慢指针,使得窗口缩小,直到遇到一个必须包含的元素
            if(needCount == 0)
            {
                while(true)
                {
                    // 这里必须找到t中的字符并且判断其在哈希表中的value,若不为0,则说明其在窗口内出现多次,还可以进行缩小窗口操作;只有为0,则窗口为最小窗口
                    if(hash.find(s[slow]) != hash.end() && hash[s[slow]] == 0)
                        break;
                    
                    // 不是必须包含的元素,缩小窗口,++
                    ++hash[s[slow]];
                    ++slow;
                }

                // 添加结果
                if(fast - slow + 1 < length)
                {
                    length = fast - slow + 1;
                    startIndex = slow;// 更新起始位置
                }

                // 步骤3、让slow慢指针在增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤
                // 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
                ++hash[s[slow]];
                ++needCount;
                ++slow;
            }
        }
        return length == INT_MAX ? "" : s.substr(startIndex, length);
    }
};

offer17.含有所有字符的最短字符串

同76题最小覆盖子串 求字符串s中包含t的所有字符的最短子字符串,返回符合条件的子字符串
时间复杂度O(n) 空间复杂度O(m + n)
因为这里的两个字符串都由英文字母(大小写字母)组成 26 + 6 + 26 = 58 可通过创建大小为58的数组优化空间复杂度O(1)

class Solution {
   
public:
    string minWindow(string s, string t) {
   
        // 同76题最小覆盖子串
        // 求字符串s中包含t的所有字符的最短子字符串,返回符合条件的子字符串

        // 记录符合条件的最短子字符串的长度
        int length = INT_MAX;
        // 记录符合条件的最短子字符串对应的起始下标,方便后续返回最短子字符串
        int startIndex = 0;

        // 创建哈希表,记录字符串t中字符出现的频率
        unordered_map<char, int> hash;
        for(char ch : t) ++hash[ch];

        int needCount = t.size();// 窗口中必须包含的t的字符的个数

        // 创建双指针,限制窗口大小,进行滑动窗口
        int slow = 0;
        for(int fast = 0; fast < s.size(); ++fast)
        {
   
            // 步骤1:移动快指针,扩大窗口大小,直到窗口内包含t中所有字符
            if(hash[s[fast]] > 0) --needCount;
            // 扩大窗口,则向哈希表中添加窗口内元素(扩大窗口--,缩小++)
            --hash[s[fast]];

            // 当needCount为0,则表示窗口内包含了t中所有字符,开始执行步骤2
            // 步骤2:移动慢指针,缩小窗口,直到慢指针指向窗口内必须包含的元素
            if(needCount == 0)
            {
   
                while(true)
                {
   
                    // 当前慢指针指向窗口内必须包含的字符,若其哈希值不为0,则说明该字符在窗口中多次出现,不是窗口内必须包含的字符,还需要进行缩小窗口;若为0,则当前窗口不能再缩小,跳出循环
                    if(hash.find(s[slow]) != hash.end() && hash[s[slow]] == 0)
                        break;

                    // 不是窗口必须包含的字符,还需要进行缩小窗口
                    ++hash[s[slow]];
                    ++slow;
                }

                // 添加结果
                if(fast - slow + 1 < length)
                {
   
                    length = fast - slow + 1;
                    startIndex = slow;
                }

                // 步骤3:破坏当前窗口满足的条件,移动一步慢指针,则继续重复三个步骤
                // 因为慢指针指向的元素是窗口内必须包含的元素,所以移动慢指针进行缩小一个单位窗口时,需要先更新哈希表中对应的值,然后再更新needCount,然后再移动慢指针
                ++hash[s[slow]];
                ++needCount;
                ++slow;
            }
        }
        // if(length == INT_MAX) return ""; 未找到符合条件的子字符串
        return length == INT_MAX ? "" : s.substr(startIndex, length);
    }
};

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

同76求最小覆盖子串类问题,相同的解法,这里只需要将长度为p.size()的最小子串的初始位置添加到结果集中就可以解决此类问题。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> result;
        // 特判
        if(s.size() < p.size())
            return result;

        // 创建哈希表 key为字符串p字符,value为p中字符出现频率
        unordered_map<char, int> hash;
        for(char ch : p)
        {
            ++hash[ch];
        }

        int needCount = p.size();// 窗口内子字符串所需要指定字符的个数

        // 声明快慢指针,限定窗口大小
        // slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度
        int slow = 0;
        for(int fast = 0; fast < s.size(); ++fast)
        {
            // 1.fast指针移动,增大窗口,使得窗口内包含t中所有字符
            if(hash[s[fast]] > 0)
            {
                --needCount;
            }
            --hash[s[fast]];

            // 2.慢指针移动,缩小窗口,直到遇到一个必须包含的元素
            if(needCount == 0)
            {
                while(true)
                {   
                    // 这里必须找到t中的字符并且判断其在哈希表中的value,若不为0,则说明其在窗口内出现多次,还可以进行缩小窗口操作;只有为0,则窗口为最小窗口
                    if(hash.find(s[slow]) != hash.end() && hash[s[slow]] == 0)
                        break;
                    
                    // 不是必须包含的字符,++
                    ++hash[s[slow]];
                    ++slow;
                }
                // 添加结果
                if(fast - slow + 1 == p.size())
                {
                    result.push_back(slow);
                }

                // 3、让slow慢指针在增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤
                // 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
                ++hash[s[slow]];
                ++needCount;
                ++slow;
            }
        }
        return result;
    }
};

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

滑动窗口解法,同76覆盖最小子串,时间复杂度O(m) 最坏情况下为O(mn),即两个字符串相同。

空间复杂度O(n) 可进行优化.

虽然不是最佳解法,但是滑动窗口可以解决这一类问题,即统一解法。

class Solution {
private:
    // 判断闭区间内的子串和needle字符串是否相同,注意needle字符串要从头开始
    bool isValid(string& haystack, int slow, int fast, string& needle)
    {
        int index = 0;
        while(slow <= fast)
        {
            if(haystack[slow] != needle[index]) return false;

            ++slow;
            ++index;
        }
        return true;
    }   
public:
    int strStr(string haystack, string needle) {
        // 同76最小覆盖子串类问题

        // 创建哈希表,key为needle字符,value为字符出现频率
        unordered_map<int, int> hash;
        for(char ch : needle)
        {
            ++hash[ch];
        }
        // 所需覆盖字符串长度
        int needCount = needle.size();

        int start = -1;// 记录起始位置
        
        int slow = 0;
        for(int fast = 0; fast < haystack.size(); ++fast)
        {
            // 1、不断扩大窗口,直到包含needle字符串中所有字符
            if(hash[haystack[fast]] > 0)
            {
                --needCount;
            }
            --hash[haystack[fast]];

            // 包含了needle字符串中所有字符,开始缩小窗口
            if(needCount == 0)
            {
                // 2、开始缩小左侧窗口,直到左侧窗口遍历到的字符是窗口内必须包含的元素,即needle中的字符
                while(true)
                {
                    // 必须包含的元素:是needle中的字符,并且在哈希表中的频率为0,说明此时不能再缩小左侧窗口了
                    if(hash.find(haystack[slow]) != hash.end() && hash[haystack[slow]] == 0)
                    {
                        break;
                    }

                    // 否则,还能缩小窗口 扩大窗口--,缩小就++
                    ++hash[haystack[slow]];
                    ++slow;
                }
				
                // 添加结果这一步需要具体问题具体分析***
                // 跳出while循环,判断是否符合条件,若符合,添加到结果集中
                // 该题的符合条件是指找到子串为needle字符串
                // 该题为了避免 ip 与 pi 两种结果的出现,再添加一个判断条件
                if(fast - slow + 1 == needle.size() && isValid(haystack, slow, fast, needle) == true)
                {
                    // 记录符合条件的子字符串起始位置
                    start = slow;
                    break;
                }

                // 3、让slow慢指针再增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤(***第三步注意移动先后顺序,很重要)
                // 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
                ++hash[haystack[slow]];
                ++needCount;
                ++slow;
            }
        }
        return start;
    }
};

优化空间复杂度O(1)

class Solution {
private:
    // 判断闭区间内的子串和needle字符串是否相同,注意needle字符串要从头开始
    bool isValid(string& haystack, int slow, int fast, string& needle)
    {
        int index = 0;
        while(slow <= fast)
        {
            if(haystack[slow] != needle[index]) return false;

            ++slow;
            ++index;
        }
        return true;
    }   
public:
    int strStr(string haystack, string needle) {
        // 同76最小覆盖子串类问题

        // 创建哈希表,key为needle字符,value为字符出现频率
        // unordered_map<int, int> hash;
        vector<int> hash(26, 0);
        for(char ch : needle)
        {
            ++hash[ch - 'a'];
        }
        // 所需覆盖字符串长度
        int needCount = needle.size();

        int start = -1;// 记录起始位置
        
        int slow = 0;
        for(int fast = 0; fast < haystack.size(); ++fast)
        {
            // 1、不断扩大窗口,直到包含needle字符串中所有字符
            if(hash[haystack[fast] - 'a'] > 0)
            {
                --needCount;
            }
            --hash[haystack[fast] - 'a'];

            // 包含了needle字符串中所有字符,开始缩小窗口
            if(needCount == 0)
            {
                // 2、开始缩小左侧窗口,直到左侧窗口遍历到的字符是窗口内必须包含的元素,即needle中的字符
                while(true)
                {
                    // 必须包含的元素:是needle中的字符,并且在哈希表中的频率为0,说明此时不能再缩小左侧窗口了
                    if(hash[haystack[slow] - 'a'] == 0)
                    {
                        break;
                    }

                    // 否则,还能缩小窗口 扩大窗口--,缩小就++
                    ++hash[haystack[slow] - 'a'];
                    ++slow;
                }

                // 跳出while循环,判断是否符合条件,若符合,添加到结果集中
                // 该题的符合条件是指找到子串为needle字符串
                // 该题为了避免 ip 与 pi 两种结果的出现,再添加一个判断条件
                if(fast - slow + 1 == needle.size() && isValid(haystack, slow, fast, needle) == true)
                {
                    // 记录符合条件的子字符串起始位置
                    start = slow;
                    break;
                }

                // 3、让slow慢指针再增加一个位置,此时当前窗口一定不满足条件,继续重复3个步骤
                // 当前slow索引指向必须包含的元素,所以先将该位置的元素添加到哈希表中,更新needCount,再进行移动指针操作
                ++hash[haystack[slow] - 'a'];
                ++needCount;
                ++slow;
            }
        }
        return start;
    }
};

offer14.字符串中的变位词

同76 最小覆盖子串类问题,统一解法

class Solution {
   
public:
    bool checkInclusion(string s1, string s2) {
   
        // 与76最小覆盖子串同解法
        // 转换为求在字符串s2中涵盖s1所有字符的最小子串问题 ==> 滑动窗口

        // 记录最小子串的长度
        int length = INT_MAX;

        // 创建哈希表,记录字符串s1中字符出现的频率
        unordered_map<char, int> hash;

        for(char ch : s1) ++hash[ch];
        
        // 窗口的大小,即为s1中字符个数
        int needCount = s1.size();

        // 创建双指针,限定窗口大小,进行滑动窗口
        // slow, fast表示滑动窗口的左右边界,当窗口内元素满足条件,即包含字符串t中所有字符,则记录滑动窗口长度,那么最终的结果即为最小的那个窗口长度
        int slow = 0;
        for(int fast = 0; fast < s2.size(); ++fast)
        {
   
            // 步骤1:不断增加fast使得窗口增大,使得窗口内包含t中所有字符

            // 如果当前遍历到的s2中的字符在哈希表中存在
            if(hash[s2[fast]] > 0) --needCount;

            // 向哈希表中添加字符,扩大窗口(添加字符是--,减少字符是++)
            --hash[s2[fast]];

            // 步骤2:不断增加慢指针,使得窗口缩小,直到遇到一个必须包含的元素
            if(needCount == 0)
            {
   
                while(true)
                {
   
                    // 在窗口内找到字符串s1中的字符,若其哈希值不为0,则说明该字符在窗口中多次出现,不是窗口内必须包含的元素,还需要进行缩小窗口;若为0,则窗口为最小窗口
                    if(hash.find(s2[slow]) != hash.end() && hash[s2[slow]] == 0)
                        break;
                    
                    // 不是窗口必须包含的元素,缩小窗口,++
                    ++hash[s2[slow]];
                    ++slow;
                }

                // 添加结果
                if(fast - slow + 1 < length)
                {
   
                    length = fast - slow + 1;
                    if(length == s1.size()) return true;
                }

                // 步骤3:破坏当前窗口满足的条件,让slow指针再增加一个位置,此时当前窗口一定不满足条件,继续重复三个步骤
                // 当前slow指针指向必须包含的元素,所以缩小窗口时需要将该指针指向的元素添加到哈希表中,同时更新needCount,然后再移动慢指针
                ++hash[s2[slow]];
                ++needCount;
                ++slow;
            }
        }
        return false;
    }
};

offer15.字符串中的所有变位词

同76题最小覆盖子串统一解法,时间复杂度O(n) 空间复杂度O(mn),可对空间复杂度进行优化

class Solution {
   
public:
    vector<int> findAnagrams(string s, string p) {
   
        // 同76题最小覆盖子串统一解法
        // 转换为求字符串s中涵盖p中所有字符的最小子串问题 ==> 滑动窗口

        vector<int> result;

        // 记录最小子串的长度
        int length = INT_MAX;

        // 创建哈希表,记录字符串p中字符出现的频率(因为是求s中涵盖p)
        unordered_map<char, int> hash;
        for(char ch : p) ++hash[ch];

        // 窗口大小为p中字符个数
        int needCount = p.size();

        // 创建双指针,限定窗口大小,进行滑动窗口
        int slow = 0;
        for(int fast = 0; fast < s.size(); ++fast)
        {
   
            // 步骤1:移动快指针,扩大窗口,使得窗口内包含p中所有字符
            if(hash[s[fast]] > 0) --needCount;
            --hash[s[fast]];

            // 步骤2:缩小滑动窗口,使得窗口内包含p中必须包含的字符
            if(needCount == 0)
            {
   
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值