力扣Hot100

哈希

两数之和

有点困扰可能就是数据的返回形式,在 C++11 之后,C++支持列表初始化(list initialization),也叫 统一初始化

C++ 会自动根据函数的返回类型(这里是 vector<int>)把 {i, j} 当作构造这个 vector 的初始化列表。

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

字母异位词分组

思路就是将排序后的字符串作为分类依据,因为异位词的话,排序得到的结果是一样的。

通过创建unordered_map,即键值对(key-value) 的集合。

map = 有序(按key自动从小到大排序) + 红黑树unordered_map = 无序 + 哈希表 + 快

key 唯一,每个 key 对应一个 value。

查找、插入、删除的平均时间复杂度都是 O(1)

unordered_map<string, vector<string>> mp 具体含义:

  • mp 是一个哈希表。
  • key 类型string,也就是说你可以通过字符串去查找。
  • value 类型vector<string>,也就是 key 对应的是一个字符串数组

然后就是emplace_back,它是vectordequelist等容器的方法,用来在容器末尾直接构造元素

它和push_back()的区别在于:

push_back:把一个已存在的对象 拷贝或移动 到容器末尾

emplace_back直接构造对象在容器末尾,无需临时对象

最长连续序列(set)

很容易想到这道题就是排序,但是题目要求是时间复杂度为O(n)O(n)的时间复杂度。

用到了set结构。set是自动排序(自动升序)。但是用到了一个count函数来模拟排序,因此用unordered_set,其不会自动排序,但是查找更快。

set:有序、不重复,适合需要排序的场景。

unordered_set:无序、不重复,查找速度更快,适合只关心存在性而不关心顺序的场景。

set常用函数如下:

函数作用
insert(x)插入元素
erase(x)删除元素(值/迭代器)
find(x)查找元素,返回迭代器
count(x)判断元素是否存在(返回 0 或 1)
begin() / end()迭代器遍历(升序)
rbegin() / rend()迭代器遍历(降序)
size()集合大小
empty()是否为空
clear()清空集合
lower_bound(x)返回 ≥ x 的第一个迭代器
upper_bound(x)返回 > x 的第一个迭代器

双指针

移动零

题目要求,必须在不复制数组的情况下原地对数组进行操作

思路是这样,右指针每次不是0就和左指针所指元素交换,每次交换后两指针++,不然只有右指针++

因为要把0一到数组的最后,因此要移动的数应该是右指针不为0

c++
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int left = 0,right = 0;
        int n = nums.size();
        while(right < n)
        {
            if(nums[right] )
            {
                swap(nums[left],nums[right]);
                left++;
            }
            right++;
        }
    }
};

盛最多水的容器

c++
[1, 8, 6, 2, 5, 4, 8, 3, 7]
 ^                       ^

在初始时,左右指针分别指向数组的左右两端,它们可以容纳的水量为 min(1,7)∗8=8。

此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针(即此时的左指针)。这是因为,由于容纳的水量是由两个指针指向的数字中较小值指针之间的距离

如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动数字较小的那个指针。

c++
class Solution {
public:
    int maxArea(vector<int>& height) {
        int l = 0,r = height.size() - 1;
        int ans = 0;
        while(l < r){
            int area = min(height[l],height[r]) * (r - l);
            ans = max(ans,area);
            if(height[l] < height[r]) l++;
            else r--;
        }
        return ans;
    }
};

三数之和

我们枚举的三元组(a,b,c)(a,b,c) 满足 abca≤b≤c,保证了只有(a,b,c)(a,b,c) 这个顺序会被枚举到,而 (b,a,c)(b,a,c)(c,b,a)(c,b,a) 等等这些不会,这样就减少了重复。

先对数组进行排序,方便去重,并使用双指针。固定一个数 nums[i],问题就变成: i 后面的数组里,找两个数 nums[j]nums[k],使得 nums[j] + nums[k] = -nums[i] 注意去重,避免结果重复

接雨水

这道题用动态规划做比较好理解

创建两个长度为 n 的数组 leftMaxleftMaxrightMaxrightMax。对于 0i<n0≤i<nleftMax[i]leftMax[i] 表示下标ii 及其左边的位置中,heightheight 的最大高度,rightMax[i]rightMax[i] 表示下标 i 及其右边的位置中,heightheight 的最大高度。

显然,leftMax[0]=height[0]leftMax[0]=height[0]rightMax[n1]=height[n1]rightMax[n−1]=height[n−1]。两个数组的其余元素的计算如下:

1in11≤i≤n−1 时,leftMax[i]=max(leftMax[i1],height[i])leftMax[i]=max(leftMax[i−1],height[i])

0in20≤i≤n−2 时,rightMax[i]=max(rightMax[i+1],height[i])rightMax[i]=max(rightMax[i+1],height[i])

因此可以正向遍历数组 heightheight 得到数组 leftMaxleftMax 的每个元素值,反向遍历数组 height 得到数组 rightMaxrightMax 的每个元素值。

在得到数组 leftMaxleftMax rightMaxrightMax 的每个元素值之后,对于 0i<n0≤i<n,下标ii处能接的雨水量等于 min(leftMax[i],rightMax[i])height[i]min(leftMax[i],rightMax[i])−height[i]。遍历每个下标位置即可得到能接的雨水总量。

双指针的做法

注意到下标 ii 处能接的雨水量由 leftMax[i]\textit{leftMax}[i]rightMax[i]\textit{rightMax}[i] 中的最小值决定。由于数组 leftMax\textit{leftMax} 是从左往右计算,数组 rightMax\textit{rightMax} 是从右往左计算,因此可以使用双指针和两个变量代替两个数组。

维护两个指针 left\textit{left}right\textit{right},以及两个变量 leftMax\textit{leftMax}rightMax\textit{rightMax},初始时 left=0\textit{left} = 0right=n1\textit{right} = n - 1leftMax=0\textit{leftMax} = 0rightMax=0\textit{rightMax} = 0。指针 left\textit{left} 只会向右移动,指针 right\textit{right} 只会向左移动,在移动指针的过程中维护两个变量 leftMax\textit{leftMax}rightMax\textit{rightMax} 的值。

当两个指针没有相遇时,进行如下操作:

  • 使用 height[left]\textit{height}[\textit{left}]height[right]\textit{height}[\textit{right}] 的值更新 leftMax\textit{leftMax}rightMax\textit{rightMax} 的值;
  • 如果 height[left]<height[right]\textit{height}[\textit{left}] < \textit{height}[\textit{right}],则必有 leftMax<rightMax\textit{leftMax} < \textit{rightMax},下标 left\textit{left} 处能接的雨水量等于 leftMaxheight[left]\textit{leftMax} - \textit{height}[\textit{left}],将下标 left\textit{left} 处能接的雨水量加到能接的雨水总量,然后将 left\textit{left} 加 1(即向右移动一位);
  • 如果 height[left]height[right]\textit{height}[\textit{left}] \geq \textit{height}[\textit{right}],则必有 leftMaxrightMax\textit{leftMax} \geq \textit{rightMax},下标 right\textit{right} 处能接的雨水量等于 rightMaxheight[right]\textit{rightMax} - \textit{height}[\textit{right}],将下标 right\textit{right} 处能接的雨水量加到能接的雨水总量,然后将 right\textit{right} 减 1(即向左移动一位)。

当两个指针相遇时,即可得到能接的雨水总量

就是每次判断在两个柱子中,选择哪个柱子接水。思路还是和动态规划一样,每次选择柱子两侧最大值的最低点来算。正如第一种情况的话,iji、j两个柱子的话,因为leftleftrightright小了,那么rightMaxrightMax一定会比leftMaxleftMax大,又因为左边柱子的leftMaxleftMax一定会比右边柱子的rightMaxrightMax大,所以minmin的值就是leftMaxleftMax,这里省略掉了只是。

滑动窗口

无重复字符的最长子串

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

子串

和为 K 的子数组(map)

为什么先放mp0 = 1?表示“在任何数之前”有一个“前缀和为 0” 的情况。这样如果一开始累计到的前缀和本身就等于 k(即 pre == k),那么 pre - k == 0,就能正确计数到这个从 0 开始的子数组。

类比:你在走路(遍历数组),手里累加步数(前缀和 pre)。你想知道有没有一段路程长度为 k。只要你当前的总步数 pre 与过去某次的总步数 old 之差是 k,即 old = pre - k,就说明那段之间的路长是 k。于是你每走一步就问:之前有多少次总步数等于 pre - k?有几次就有几个合法子数组。

unordered_map<K, V>:键值对容器,K 类型,V 类型

mp.find(key):在 unordered_map 里查找是否存在键为 key 的元素。

  • 如果存在:返回指向该元素的迭代器(不是值)
  • 如果不存在:返回 mp.end()(尾后迭代器)
  • key唯一

mapunordered_map区别

  1. 底层结构
    map:平衡二叉搜索树(C++ 标准里通常是红黑树)
    unordered_map:哈希表(数组 + 链表/桶,或拉链法,具体实现取决于库)
  2. 键的顺序
    map:迭代时按 key 从小到大有序
    unordered_map:无序(遍历顺序和插入、rehash 等相关,不可依赖)

mp[pre]

当你用 下标运算符 [] 访问一个键 pre 时:

  • 如果这个键存在,就返回它对应的 value 的引用。
  • 如果这个键不存在,就会 自动插入 一个新的键值对:
c++
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int,int> mp;
        //mp[sum]=cnt表示前缀和为sum的情况出现过cnt次
        mp[0] = 1;
        int count = 0,pre = 0;
        for(auto &x : nums)
        {
            pre += x;
            if(mp.find(pre-k) != mp.end()) count += mp[pre-k];
            mp[pre]++;
        }
        return count;
    }
};

滑动窗口最大值

队列的性质是先进先出

deque(双端队列)

  • 全称 double-ended queue,存储结构是一个 动态数组块链表(不是连续的大数组,扩展时比 vector 高效)。
  • 特点:
    • 两端都能高效插入和删除push_front / pop_front / push_back / pop_back)。
    • 随机访问([] 运算符)和迭代器可用,类似 vector

queue(队列)

  • 容器适配器,默认基于 deque 实现。
  • 特点: 只能从一端进,另一端出(FIFO,先进先出)。 接口比 deque 少很多,只允许:push():入队(尾部),pop():出队(头部),front():访问队头,back():访问队尾
  • 不能随便访问中间元素。

我们用 双端队列 deque 保存索引,保证:

  1. 队头 q.front() 总是窗口的最大值索引。
  2. 队列从前到后递减(对应 nums 的值递减)。

维护一个单调递减的队列,元素是下标,队首是窗口里最大元素的下标,遍历数组,首先判断最大元素是否还在窗口里,然后将队列里小于当前新加入元素的老元素去掉(因为只要新元素在窗口,在老元素右边,它们就不可能是滑动窗口的最大值),然后加入这个新元素,遍历时记录队首的最大值即可。

nums[q[0]] >= nums[q[1]] >= nums[q[2]] ...,队列中只存当前窗口范围内的下标。

如何维持这个结构:

  • 插入新元素 i 之前:把队尾那些值 <= 新值 的下标全部弹掉,因为它们永远不可能再成为最大值(被更靠右且不更小的值遮蔽)。
  • 把 i 放到队尾。
  • 再把队头如果已经滑出窗口(下标 <= i - k)就弹掉。

最小覆盖子串

思路:滑动窗口

在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的 r 指针,和一个用于「收缩」窗口的 l 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 s 上滑动窗口,通过移动 r 指针不断扩张窗口。当窗口包含 t 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。

普通数组

最大子数组和

c++
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int pre = 0,maxAns = nums[0];
        for(auto x : nums){
            pre = max(pre+x,x);//pre记录以当前元素结尾的连续子数组的最大和
            maxAns = max(maxAns,pre);
        }
        return maxAns;
    }
};

合并区间

默认 sortvector<int> 是按 字典序 排序。

vector<vector<int>> 就是 先比较第一列,再比较第二列...

vector::back() 的作用

在 C++ STL 里,back()vector 的一个成员函数,功能是:

返回容器中最后一个元素的引用

首先,我们将列表中的区间按照左端点升序排序。然后我们将第一个区间加入 merged 数组中,并按顺序依次考虑之后的每个区间: 如果当前区间的左端点在数组 merged 中最后一个区间的右端点之后,那么它们不会重合,我们可以直接将这个区间加入数组 merged 的末尾; 否则,它们重合,我们需要用当前区间的右端点更新数组 merged 中最后一个区间的右端点,将其置为二者的较大值。

c++
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        if(intervals.size() == 0) return {};

        sort(intervals.begin(),intervals.end());
        vector<vector<int>> merged;
        for(int i = 0; i < intervals.size();i++)
        {
            int L = intervals[i][0],R = intervals[i][1];
            if(!merged.size() || merged.back()[1] < L) merged.push_back({L,R});
            else merged.back()[1] = max(merged.back()[1],R);
        }
        return merged;
    }
};

轮转数组

🔹 vector::assign 的作用

assignstd::vector 的一个成员函数,用来把容器里的内容替换成新的内容

assign替换元素、修改 size

它不直接关心原来的容量,但如果原容量不够,会自动扩容。

c++
class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> newArr(n);
        for(int i = 0; i < n; i ++)
        {
            newArr[(i+k)%n] = nums[i];
        }
        nums.assign(newArr.begin(),newArr.end());
        //nums = newArr;
    }
};

除自身以外数组的乘积

缺失的第一个正数

我们的思路是首先,正数的范围为[1,N+1][1,N+1],为N+1N+1[1,N][1,N]都出现,则N+1N+1是缺少的数,其余则是[1,N][1,N]然后,将数组中出现的正数进行标记,对于负数和0则不标记,那么,从小到大,没标记的第一个正整数就是缺失的数。

具体来说,对于遍历到的数xx ,如果它在 [1,N][1,N] 的范围内,那么就将数组中的第x1x−1 个位置打上标记

也就是说,数组下标ii对应着正数i+1i+1,第一个没被标记的位置,其就是答案,对应的数为i+1i+1

矩阵

矩阵置零(set)

这里一开始我想用find()count()但是想到只有setmap这两种容器有(并且都是不允许重复的)。set 只存元素map键值对

序列容器(vector/deque/list)

  • 支持 push_back / push_front / insert
  • 支持就地构造 emplace_back / emplace_front / emplace(iterator, ...)

注意std::vector 没有 push_front,因为在开头插入元素效率低(需要移动整个数组)。

关联容器(set/multiset/map/multimap)

  • 没有“末尾/前端”的概念 → 没有 push_back / push_front
  • 插入用 insert()emplace(),支持就地构造

下面是我的做法,通过利用2个set来存储出现0的位置,如果出现就将其变为0,否则不做变化。

螺旋矩阵

当路径超出界限或者进入之前访问过的位置时,顺时针旋转,进入下一个方向

旋转图像

关键是在对于矩阵中第i i 行的第jj个元素,在旋转后,它出现在倒数第ii列的第jj个位置。

我们将其翻译成代码。由于矩阵中的行列从00开始计数,因此对于矩阵中的元素matrix[row][col] matrix[row][col],在旋转后,它的新位置为matrixnew[col][n1row] matrix_{new}[col][n-1-row]

参考题解:旋转图像

关键是在推出每个点的旋转涉及到四个点时,应该旋转哪些点

偶数如图所示,只需枚举四个块中一个即可,为了方便起见,选择蓝色的块。而当奇数时,要考虑下,

同样四种颜色块选一个,这里我们将图中垂直纸面向左外翻转,枚举青色的这块,正好和偶数的能够对应上了。

搜索二维矩阵 II

直接暴力枚举,因为数据较小,不会超时

c++
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int n = matrix.size(),m = matrix[0].size();
        for(int i = 0; i < n;i++)
        for(int j = 0; j < m;j++)
        {
            if(matrix[i][j] == target) return true;
        }
        return false;
    }
};

但是这种并不高效

每行二分查找,复杂度 O(mlogn)O(m log n)mmn n 列。

核心点:lower_bound 找到第一个 target≥ target 的位置,然后判断是否等于target target

介绍下lower_bound函数:

功能:在 [first, last) 范围内查找 第一个不小于 value 的元素位置

要求:区间必须 已排序(升序)才能正确工作。

返回值:一个迭代器,指向 第一个 >= value 的元素

  • 如果所有元素都小于 value,返回 last(即末尾迭代器)。
c++
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        for(const auto& row : matrix)
        {
            auto it = lower_bound(row.begin(),row.end(),target);
            if(it != row.end() && *it == target) return true;
        }
        return false;
    }
};

划分字母区间

要求片段数尽可能的多,同时一个字母只能出现在一个片段中

链表

相交链表

反转链表

回文链表

指针存的是地址.

通过快慢指针,得到中点位置,然后对两段进行比较。

环形链表

环形链表 II

合并两个有序链表

回溯

全排列

N 皇后

clear()作用是清空里面的内容。

assignSTL 容器的方法(如 vector, string, deque, …),作用是把容器重新赋值(替换内容)。 它和 = 类似,但更灵活

贪心算法

买卖股票的最佳时机

c++
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int ans = 0,minprice = prices[0];
        for(auto i : prices)
        {
            minprice = min(minprice,i);
            ans = max(ans,i - minprice);
        }
        return ans;
    }
};

跳跃游戏

只需每次考虑最远能跳到哪。维护一个当前能够跳到最远的变量rightmostrightmost,遍历数组,先看当前位置是否在当前能跳到的位置上,如果可以,更新rightmostrightmost。然后判断当前的rightmostrightmost是否>=n1>=n-1

c++
class Solution {
public:
    bool canJump(vector<int>& nums) {
        int n = nums.size();
        int rightmost = 0;//当前最远能够到达的位置
        for(int i = 0; i < n;i++)
        {
            if(i <= rightmost) 
            {
                rightmost = max(rightmost,i + nums[i]);
                if(rightmost >= n - 1) return true;
            }
        }
        return false;
    }
};

跳跃游戏 II

每次找到可到达最远位置

在遍历数组时,我们不访问最后一个元素,这是因为在访问最后一个元素之前,我们的边界一定大于等于最后一个位置,否则就无法跳到最后一个位置了。如果访问最后一个元素,在边界正好为最后一个位置的情况下,我们会增加一次「不必要的跳跃次数」,因此我们不必访问最后一个元素。

关于这里,详细解释下:如果访问最后一个元素,这种情况很好理解,之前已经能用更少的步数到达,不用再加次数,这里的依据是在访问最后一个元素之前,我们的边界一定大于等于最后一个位置

那么我们假设访问最后一个元素之前,边界小于最后一个位置。也就是说当访问i=n2i=n-2时,maxPos<n1maxPos<n-1。当到达n2n-2时,maxPos=max(maxPos,n2+nums[n2])maxPos = max(maxPos,n-2+nums[n-2]),那么如果maxPosmaxPos要小于n1n-1的话,就得确保maxPos<=n2maxPos<=n-2,那么可得

nums[n2]<=0nums[n-2]<=0,题目中说0<=nums[i]<=10000 <= nums[i] <= 1000,即nums[n2]=0nums[n-2]=0,那么在此刻maxPosmaxPos是无法通过更新来比n2n-2大的,即无法到达n1n-1,而题目中说了,保证可以到达n1n-1,因此假设错误,即访问最后一个元素之前,我们的边界一定大于等于最后一个位置

动态规划

爬楼梯

c++
class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(n+1,0);
        dp[1] = 1;
        dp[0] = 1;
        for(int i = 2; i <= n; i ++)
            dp[i] = dp[i-1] + dp[i-2];
        return dp[n];
    }
};

杨辉三角

c++
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> ret(numRows);
        for(int i = 0; i < numRows;i++)
        {
            ret[i].resize(i+1);
            ret[i][0] = ret[i][i] = 1;
            for(int j = 1; j < i;j++)
            ret[i][j] = ret[i-1][j-1] + ret[i-1][j];
        }
        return ret;
    }
};

打家劫舍

完全平方数

f[i]f[i] 表示最少需要多少个数的平方来表示整数ii

这道题的关键在于深刻理解f[i]f[i]是由前面的几个平方数加上最后一个平方数

举例子 (n = 13)

  • f[1] = f[0] + 1 = 1 → 1
  • f[2] = f[1] + 1 = 2 → 1+1
  • f[3] = f[2] + 1 = 3 → 1+1+1
  • f[4] = min(f[3], f[0]) + 1 = 1 → 4
  • f[12] = min(f[11], f[8], f[3]) + 1 = 3 → 4+4+4
  • f[13] = min(f[12], f[9], f[4]) + 1 = 2 → 4+9
c++
class Solution {
public:
    int numSquares(int n) {
        vector<int> f(n+1);
        for(int i = 1;i <= n;  i++)
        {
            int minn = INT_MAX;
            //枚举最后一个平方数之前的最少平方数是多少
            for(int j = 1;j * j <= i;j++) minn = min(minn,f[i-j*j]);

            f[i] = minn + 1;
        }
        return f[n];
    }
};

零钱兑换

C++面向对象与YOLO目标检测
复习——数学篇

评论区

评论加载中...