线性枚举
线性枚举(Linear Enumeration)是一种暴力枚举的方法,它逐一检查每个可能的解,适用于搜索和枚举问题。
其核心思路是:对问题的所有可能情况逐一进行遍历,并针对每种情况判断是否满足条件,从而得到解答。
线性枚举是一种顺序搜索算法,从线性表的第一个元素开始,根据特定的判断条件,做出相应的行为。变种比较多,最常见的就是求最值、求和等等。比如:
-
求最大值,伪代码
最大值 = 非常小的数 for 当前元素 in 给定的线性表: if 当前元素 > 最大值: 最大值 = 当前元素 返回 最大值 的值
-
求和,伪代码
求和 = 0 for 当前元素 in 给定的线性表: 求和 = 求和 + 当前元素 返回 求和 的值
时间复杂度
线性枚举需要遍历列表中的每个元素。
线性枚举的时间复杂度为 O (nm),其中 n 是线性表的长度,m 是每次操作的量级。
对于求最大值和求和来说,因为操作比较简单,所以 m 为 1,则整体的时间复杂度是 O (n) 的。
线性枚举是一种简单而有效的算法思想,它可以用于解决许多基本的算法问题。虽然它的时间复杂度较高,但在处理小型数据集时仍然是一种常用的算法。
线性枚举的优化
对于线性枚举,有很多优化算法:
- 二分查找:如果线性表已经排序,可以使用二分搜索来提高搜索效率。
- 哈希表:可以使用哈希表来存储已经搜索过的元素,避免重复搜索。
- 前缀和:可以存储前 i 个元素的和,避免重复计算。
- 双指针:可以从两头开始搜索,提升搜索效率。
- ...
实战
存在连续三个奇数的数组
给你一个整数数组 arr
,请你判断数组中是否存在连续三个元素都是奇数的情况:如果存在,请返回 true
;否则,返回 false
。
示例 1:
输入:arr = [2,6,4,1]
输出:false
解释:不存在连续三个元素都是奇数的情况。
示例 2:
输入:arr = [1,2,34,3,4,5,7,23,12]
输出:true
解释:存在连续三个元素都是奇数的情况,即 [5,7,23] 。
提示:
1 <= arr.length <= 1000
1 <= arr[i] <= 1000
题解
cpp
class Solution {
public:
bool threeConsecutiveOdds(vector<int>& arr) {
int count = 0; // 计数器:记录连续的奇数个数
for (int i = 0; i < arr.size(); ++i){
if (arr[i] % 2 != 0){ // 取模判断是否为奇数
count ++;
if (count == 3){
return true; // 当计数器为3时直接返回
}
}else{
count = 0; // 遇到偶数重置计数器
}
}
return false;
}
};
官方题解
cpp
class Solution {
public:
bool threeConsecutiveOdds(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i <= n - 3; ++i) {
if ((arr[i] & 1) && (arr[i + 1] & 1) && (arr[i + 2] & 1)) {
return true;
}
}
return false;
}
};
Note
- 判断奇数可以用按位与运算符
&
,arr[i] & 1
:如果arr[i]
是奇数,则其最低位是1
,因此arr[i] & 1
结果为1
(真);如果是偶数,则结果为0
(假)。
最大连续 1 的个数
给定一个二进制数组 nums
,计算其中最大连续 1
的个数。
示例 1:
输入:nums = [1,1,0,1,1,1]
输出:3
解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3.
示例 2:
输入:nums = [1,0,1,1,0,1]
输出:2
提示:
1 <= nums.length <= 105
nums[i]
不是0
就是1
.
官方题解
cpp
class Solution {
public:
int findMaxConsecutiveOnes(vector<int>& nums) {
int maxCount = 0, count = 0;
int n = nums.size();
for (int i = 0; i < n; i++) {
if (nums[i] == 1) {
count++;
} else {
maxCount = max(maxCount, count); // 刷新最大计数
count = 0;
}
}
// 如果数组末位为1不会触发刷新最大计数,需要手动刷新一次最大计数
maxCount = max(maxCount, count);
return maxCount;
}
};
有序数组中的单一元素
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n)
时间复杂度和 O(1)
空间复杂度。
示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10
题解
暴力求解很简单,但时间复杂度不满足要求,没想出标准答案。数组有序,复杂度要求为 O (logn),就是在暗示用二分法。
cpp
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
// 遍历前n-1个元素
for (int i = 0; i < nums.size()-1; i += 2) {
if (nums[i] != nums[i+1]){
return nums[i];
}
}
// 默认返回数组最后一个元素
return nums.back();
}
};
官方题解
cpp
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int low = 0, high = nums.size() - 1;
while (low < high) {
int mid = (high - low) / 2 + low;
if (nums[mid] == nums[mid ^ 1]) {
low = mid + 1;
} else {
high = mid;
}
}
return nums[low];
}
};
先找规律:
c++
(0,1),2,(3,4),(5,6),(7,8) // 索引
(1,1),2,(3,3),(4,4),(8,8) // 数组
数组有序,且两两为一对。可以观察到,在单身狗 2 的左侧,每一对相同元素的前一个元素的索引都是偶数(规律 1), 比如 0;直到单身狗打破规律,因为它是单独的一个,没有人和它一对,所以从这个单身狗以后的右侧,每一对相同元素的前一个元素的索引就变成了奇数(规律 2),比如 3,5,7。
假设要找的数的索引为 x,我们把符合规律 1 的称为"正常的",这些数全在 x 的左边,那么"不正常的"就是符合规律 2 的,这些数全在 x 的右边。
使用二分查找,目标是找到 x:
-
如果 mid 是偶数,就比较
nums[mid]
和nums[mid+1]
,如果相等,说明是正常的符合规律 1,x 在 mid 的右边,此时更新 low=mid;如果不相等,说明规律 1 被打破,那么 x 就在 mid 的左边,此时更新 high=mid。 -
如果 mid 是奇数,就比较
nums[mid-1]
和nums[mid]
,如果相等,说明是正常的符合规律 1,x 在 mid 的右边,此时更新 low=mid;如果不相等,说明规律 1 被打破,那么 x 就在 mid 的左边,此时更新 high=mid。
这样,不断缩小区间,直到 low=high,就是 x 的索引。
细节
官方题解中没有分奇偶数考虑比较的数,而是使用了 nums[mid] == nums[mid ^ 1]
。按位异或运算符 ^
是一个二元运算符,它对两个操作数的每一位进行比较。如果两个对应的位不同,则结果为 1
;如果相同,则结果为 0
。
-
当
mid
是偶数时,mid + 1 = mid ^ 1
-
如果
mid
是偶数,它的二进制表示的最低位是0
。 -
mid ^ 1
会将最低位从0
变成1
,而其他位保持不变。 -
这等价于将
mid
加上1
。
-
-
当
mid
是奇数时,mid - 1 = mid ^ 1
- 如果
mid
是奇数,它的二进制表示的最低位是1
。 mid ^ 1
会将最低位从1
变成0
,而其他位保持不变。- 这等价于将
mid
减去1
。
- 如果
模拟
前言
模拟算法是一类通过模仿自然现象或物理过程来解决复杂问题的计算方法。
模拟算法其实就是根据题目做,题目要求什么,就做什么。一些复杂的模拟题其实还是把一些简单的操作组合了一下,所以模拟题是最锻炼耐心的,也是训练编码能力的最好的暴力算法。
数据结构
对于模拟题而言,最关键的其实是数据结构,看到一个问题,选择合适的数据结构,然后根据问题来实现对应的功能。模拟题的常见数据结构主要就是:数组、字符串、矩阵、链表、二叉树 等等。
1、基于数组
利用数组的数据结构,根据题目要求,去实现算法,如:1920.基于排列构建数组、1389.按既定顺序创建目标数组、1603.设计停车系统、2149.按符号重排数组、2221.数组的三角和
2、基于字符串
利用字符串的数据结构,根据题目要求,去实现算法,如:2011.执行操作后的变量值、2744.最大字符串配对数目、LCP 17.速算机器人、537.复数乘法
3、基于链表
利用链表的数据结构,根据题目要求,去实现算法,如:2181.合并零之间的节点、1823.找出游戏的获胜者
4、基于矩阵
利用矩阵的数据结构,根据题目要求,去实现算法,如:2120.执行所有后缀指令、1252.奇数值单元格的数目、832.翻转图像、657.机器人能否返回原点、289.生命游戏、59.螺旋矩阵 II、885.螺旋矩阵 III
5、基于栈
利用栈的数据结构,如:1441.用栈操作构建数组
6、基于队列
利用队列的数据结构,如:1700.无法吃午餐的学生数量
算法技巧
模拟时一般会用到一些算法技巧,或者说混合算法,比如 排序、递归、迭代 等等。
1、排序
排序后,干一件事情,如:950.按递增顺序显示卡牌
2、递归
需要借助递归来实现,如:1688.比赛中的配对次数 、2169.得到 0 的操作数、258.各位相加
3、迭代
不断迭代求解,其实就是利用 while 循环来实现功能,如:1860.增长的内存泄露、258.各位相加
实战
交换数字
编写一个函数,不用临时变量,直接交换numbers = [a, b]
中a
与b
的值。
示例:
输入: numbers = [1,2]
输出: [2,1]
提示:
numbers.length == 2
-2147483647 <= numbers[i] <= 2147483647
题解
不正确的解法:
cpp
class Solution {
public:
vector<int> swapNumbers(vector<int>& numbers) {
numbers[0] = numbers[1] - numbers[0];
numbers[1] = numbers[1] - numbers[0];
numbers[0] = numbers[1] + numbers[0];
return numbers;
}
};
最直接的想法就是使用加减法,通过记录两个数的和或者差来消除中间变量。可以通过绝大多数测试用例,但是有溢出风险,如果:
numbers = [-2147483647,2147483647]
对这两个数进行加减就会溢出,需要强转类型。
实际上这个题的考点在于按位异或^
的巧用,异或即:相同为0,不同为1,那么一个数异或上它自己就是0,即:a ^ a = 0;
正确解法如下:
cpp
class Solution {
public:
vector<int> swapNumbers(vector<int>& numbers) {
numbers[0] = numbers[0] ^ numbers[1];
numbers[1] = numbers[0] ^ numbers[1]; // 此时number[1] = number[0]
numbers[0] = numbers[0] ^ numbers[1];
return numbers;
}
};
STEP | numbers[0] | numbers[1] |
---|---|---|
1 | numbers[0] ^ numbers[1] | numbers[1] |
2 | numbers[0] ^ numbers[1] | numbers[0] ^ (numbers[1] ^ numbers[1])= numbers[0] |
3 | numbers[0] ^ numbers[1] ^ numbers[0] = numbers[1] | numbers[0] |
位1的个数
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量).)。
提示:
- 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
- 在 Java 中,编译器使用 二进制补码 记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数
-3
。
示例 1:
输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:
输入:n = 128 (控制台输入 00000000000000000000000010000000)
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:
输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
提示:
- 输入必须是长度为
32
的 二进制串 。
题解
解法一
和整数按位取值的思路一样,当n不为0时(不为0说明一定有1在),取出低位进行判断:
cpp
class Solution {
public:
int hammingWeight(uint32_t n) {
int count = 0;
while (n > 0) {
if (n % 2) count++;
n = n / 2;
}
return count;
}
};
解法二
直接循环检查给定整数 n 的二进制位的每一位是否为 1。当检查第 i 位时,我们可以让 n 与 $2^i $ 进行与运算,根据结果判断这一位是否为1,如果这一位是1,那么与运算的结果就是1,反之为0。
cpp
class Solution {
public:
int hammingWeight(uint32_t n) {
int ret = 0;
for (int i = 0; i < 32; i++) {
if (n & (1 << i)) {
ret++;
}
}
return ret;
}
};
找到数组的中间位置
给你一个下标从 0 开始的整数数组 nums
,请你找到 最左边 的中间位置 middleIndex
(也就是所有可能中间位置下标最小的一个)。
中间位置 middleIndex
是满足 nums[0] + nums[1] + ... + nums[middleIndex-1] == nums[middleIndex+1] + nums[middleIndex+2] + ... + nums[nums.length-1]
的数组下标。
如果 middleIndex == 0
,左边部分的和定义为 0
。类似的,如果 middleIndex == nums.length - 1
,右边部分的和定义为 0
。
请你返回满足上述条件 最左边 的 middleIndex
,如果不存在这样的中间位置,请你返回 -1
。
示例 1:
输入:nums = [2,3,-1,8,4]
输出:3
解释:
下标 3 之前的数字和为:2 + 3 + -1 = 4
下标 3 之后的数字和为:4 = 4
示例 2:
输入:nums = [1,-1,4]
输出:2
解释:
下标 2 之前的数字和为:1 + -1 = 0
下标 2 之后的数字和为:0
示例 3:
输入:nums = [2,5]
输出:-1
解释:
不存在符合要求的 middleIndex 。
示例 4:
输入:nums = [1]
输出:0
解释:
下标 0 之前的数字和为:0
下标 0 之后的数字和为:0
提示:
1 <= nums.length <= 100
-1000 <= nums[i] <= 1000
题解
解法一:暴力求解
cpp
class Solution {
public:
int findMiddleIndex(vector<int>& nums) {
// 遍历数组,检查是否满足左侧和等于右侧和
for (int i = 0; i < nums.size(); ++i) {
int l = 0, r = 0;
for (int j = 0; j < i; ++j){ // 计算 i 的左侧和
l += nums[j];
}
for (int k = i+1; k < num.size(); ++k){ // 计算 i 的右侧和
r += nums[k];
}
if( l == r){
return i;
}
}
return -1; // 如果没有找到中间索引
}
};
解法二:前缀和
cpp
class Solution {
public:
int findMiddleIndex(vector<int>& nums) {
int totalSum = 0, leftSum = 0;
// 计算数组的总和
for (int num : nums) {totalSum += num;}
// 遍历数组,检查是否满足左侧和等于右侧和
for (int i = 0; i < nums.size(); ++i) {
// 右侧和 = 总和 - 左侧和 - 当前元素
int rightSum = totalSum - leftSum - nums[i];
if (leftSum == rightSum) {
return i;
}
leftSum += nums[i]; // 更新左侧和
}
return -1; // 如果没有找到中间索引
}
};
递推
递推算法是一种通过已知条件和特定递推关系式,逐步推导出问题结果的算法。它通常以一个明确的初始条件(或边界条件)开始,然后通过递推公式逐步求解后续结果。
递推算法的结构
- 初始条件
- 给定问题的边界或起点。
- 递推关系
- 根据问题规律,确定如何通过前面的结果推导当前结果。
- 终止条件
- 决定何时停止递推。
经典示例:斐波那契数列
斐波那契数列的定义:
F(0) = 0
F(1) = 1
F(n) = F(n−1) + F(n−2) (n≥2)
采用递推算法:
cpp
int fibonacci(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
int prev1 = 0, prev2 = 1; // 初始化 F(0) 和 F(1)
int current;
for (int i = 2; i <= n; ++i) {
current = prev1 + prev2; // F(i) = F(i-1) + F(i-2)
prev1 = prev2;
prev2 = current;
}
return current;
}
递归算法也可以,更容易理解,但有重复计算,可能会超时:
cpp
int fibonacci(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
实战
爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
1 <= n <= 45
题解
这道题其实是斐波那契数列的变形。
由于每次可以只能爬1个或2个台阶,所以,如果处在第 i 阶台阶,那么它只能是从第 i-1或者第i-2阶台阶爬上来的,那么就有 f(i) = f(i-1) + f(i-2)。
cpp
class Solution {
public:
int climbStairs(int n) {
int f = 0, f1 = 1, f2 = 2;
if (n == 1)
return f1;
if (n == 2)
return f2;
for (int i = 3; i <= n; ++i) {
f = f1 + f2;
f1 = f2;
f2 = f;
}
return f;
}
};
杨辉三角
给定一个非负整数 *numRows
,*生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
提示:
1 <= numRows <= 30
题解
cpp
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); // 每一行有 i+1 个元素
ret[i][0] = ret[i][i] = 1; // 首尾固定为 1
// 处理中间元素
for (int j = 1; j < i; ++j) {
ret[i][j] = ret[i - 1][j] + ret[i - 1][j - 1]; // 递推关系式
}
}
return ret;
}
};