文章目录
- 算法技巧理论基础
-
- [1. 算法技巧概述](#1. 算法技巧概述)
-
- [1.1 常见算法技巧](#1.1 常见算法技巧)
- [1.2 技巧的特点](#1.2 技巧的特点)
- [2. 位运算技巧](#2. 位运算技巧)
-
- [2.1 异或运算的基本性质](#2.1 异或运算的基本性质)
- [2.2 只出现一次的数字(136)](#2.2 只出现一次的数字(136))
- [2.3 位运算的其他应用](#2.3 位运算的其他应用)
- [3. 摩尔投票法](#3. 摩尔投票法)
-
- [3.1 摩尔投票法的基本思想](#3.1 摩尔投票法的基本思想)
- [3.2 多数元素(169)](#3.2 多数元素(169))
- [3.3 摩尔投票法的扩展](#3.3 摩尔投票法的扩展)
- [4. 三指针技巧(荷兰国旗问题)](#4. 三指针技巧(荷兰国旗问题))
-
- [4.1 三指针的基本思想](#4.1 三指针的基本思想)
- [4.2 颜色分类(75)](#4.2 颜色分类(75))
- [4.3 三指针技巧的扩展](#4.3 三指针技巧的扩展)
- [5. 排列问题技巧](#5. 排列问题技巧)
-
- [5.1 下一个排列的基本思想](#5.1 下一个排列的基本思想)
- [5.2 下一个排列(31)](#5.2 下一个排列(31))
- [5.3 排列问题的扩展](#5.3 排列问题的扩展)
- [6. 快慢指针技巧(Floyd判圈算法)](#6. 快慢指针技巧(Floyd判圈算法))
-
- [6.1 快慢指针的基本思想](#6.1 快慢指针的基本思想)
- [6.2 寻找重复数(287)](#6.2 寻找重复数(287))
- [6.3 快慢指针的其他应用](#6.3 快慢指针的其他应用)
- [7. 常见题型总结](#7. 常见题型总结)
-
- [7.1 位运算类](#7.1 位运算类)
- [7.2 投票算法类](#7.2 投票算法类)
- [7.3 指针技巧类](#7.3 指针技巧类)
- [7.4 排列问题类](#7.4 排列问题类)
- [8. 技巧选择指南](#8. 技巧选择指南)
-
- [8.1 何时使用位运算](#8.1 何时使用位运算)
- [8.2 何时使用投票算法](#8.2 何时使用投票算法)
- [8.3 何时使用指针技巧](#8.3 何时使用指针技巧)
- [8.4 何时使用排列技巧](#8.4 何时使用排列技巧)
- [9. 总结](#9. 总结)
算法技巧理论基础
1. 算法技巧概述
算法技巧是在解决特定类型问题时,使用的一些巧妙的方法和思路。这些技巧通常能够以较低的时间复杂度和空间复杂度解决问题,是算法竞赛和面试中的重要工具。
1.1 常见算法技巧
- 位运算技巧:异或、与、或等位运算的巧妙应用
- 双指针技巧:快慢指针、左右指针、三指针等
- 数学技巧:摩尔投票法、排列组合等
- 数组技巧:原地修改、交换元素等
- 链表技巧:快慢指针找环、找中点等
1.2 技巧的特点
- 高效性:通常时间复杂度较低,空间复杂度也较低
- 巧妙性:需要一定的数学或逻辑思维
- 适用性:适用于特定类型的问题
- 简洁性:代码通常比较简洁
核心思想:
算法技巧的核心是:用巧妙的方法解决复杂的问题。
关键点:
1. 理解问题的本质
2. 找到合适的技巧
3. 掌握技巧的原理
4. 灵活应用技巧
2. 位运算技巧
2.1 异或运算的基本性质
异或运算(XOR):相同为0,不同为1。
真值表:
输入 A 输入 B 输出 A ⊕ B
0 0 0
0 1 1
1 0 1
1 1 0
基本性质:
a ^ a = 0:任何数与自己异或结果为0a ^ 0 = a:任何数与0异或结果为自己a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b:交换律和结合律a ^ b = b ^ a:交换律(a ^ b) ^ c = a ^ (b ^ c):结合律
示例:
5 = 0101
0 = 0000
5 ^ 0 = 0101 = 5
5 = 0101
5 = 0101
5 ^ 5 = 0000 = 0
5 = 0101
3 = 0011
5 ^ 3 = 0110 = 6
2.2 只出现一次的数字(136)
题目:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
核心思路:
- 利用异或运算的性质:
a ^ a = 0,a ^ 0 = a - 所有出现两次的数字异或后为0
- 0与只出现一次的数字异或,结果就是该数字
完整过程示例:
数组:[4, 1, 2, 1, 2]
初始:res = 0
步骤1:res = 0 ^ 4 = 4
步骤2:res = 4 ^ 1 = 5
步骤3:res = 5 ^ 2 = 7
步骤4:res = 7 ^ 1 = 6
步骤5:res = 6 ^ 2 = 4
结果:4
验证:
4 ^ 1 ^ 2 ^ 1 ^ 2
= 4 ^ (1 ^ 1) ^ (2 ^ 2)
= 4 ^ 0 ^ 0
= 4
代码:
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int res = 0;
for (int x : nums) {
res ^= x;
}
return res;
}
};
关键点:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 利用异或运算的交换律和结合律
2.3 位运算的其他应用
应用场景:
- 找只出现一次的数字
- 找两个只出现一次的数字
- 判断数字的奇偶性:
n & 1 - 快速计算2的幂:
1 << n - 判断是否为2的幂:
n & (n - 1) == 0
3. 摩尔投票法
3.1 摩尔投票法的基本思想
**摩尔投票法(Boyer-Moore Voting Algorithm)**是一种用于寻找数组中占多数的元素的算法。
核心思想:
- 维护一个候选元素和计数器
- 遍历数组,如果当前元素等于候选元素,计数器+1;否则计数器-1
- 当计数器为0时,更新候选元素
- 最后剩下的候选元素就是多数元素
原理:
假设多数元素数量 = M
其他元素数量 = N
题目保证:M > N
每一次 count--,本质是:
一个多数元素 + 一个非多数元素 → 同时消失
由于 M > N,最后一定剩下多数元素
3.2 多数元素(169)
题目:给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊n/2⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
完整过程示例:
数组:[3, 2, 3]
初始:candidate = 0, count = 0
步骤1:x = 3
count == 0,更新 candidate = 3
x == candidate,count = 1
步骤2:x = 2
count != 0
x != candidate,count = 0
步骤3:x = 3
count == 0,更新 candidate = 3
x == candidate,count = 1
结果:candidate = 3
验证:3出现2次,2出现1次,3是多数元素
更复杂示例:
数组:[2, 2, 1, 1, 1, 2, 2]
初始:candidate = 0, count = 0
步骤1:x = 2
count == 0,更新 candidate = 2
x == candidate,count = 1
步骤2:x = 2
count != 0
x == candidate,count = 2
步骤3:x = 1
count != 0
x != candidate,count = 1
步骤4:x = 1
count != 0
x != candidate,count = 0
步骤5:x = 1
count == 0,更新 candidate = 1
x == candidate,count = 1
步骤6:x = 2
count != 0
x != candidate,count = 0
步骤7:x = 2
count == 0,更新 candidate = 2
x == candidate,count = 1
结果:candidate = 2
验证:2出现4次,1出现3次,2是多数元素
代码:
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = 0;
int count = 0;
for (int x : nums) {
if (count == 0) {
candidate = x;
}
if (x == candidate) {
count++;
} else {
count--;
}
}
return candidate;
}
};
关键点:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 前提条件:多数元素必须存在且数量大于n/2
3.3 摩尔投票法的扩展
应用场景:
- 找出现次数大于n/2的元素
- 找出现次数大于n/3的元素(需要两个候选元素)
- 找出现次数大于n/k的元素(需要k-1个候选元素)
4. 三指针技巧(荷兰国旗问题)
4.1 三指针的基本思想
三指针技巧是用于将数组分成多个区域的方法,常用于排序和分类问题。
核心思想:
- 使用多个指针维护不同的区域边界
- 通过交换元素来调整区域
- 保持区域的性质不变
4.2 颜色分类(75)
题目:给定一个包含红色、白色和蓝色,共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
核心思路:
- 把数组划分成3个区域:[0区 | 1区 | 未处理 | 2区]
- 使用三个指针:
left:0区右边界(不包含)right:2区左边界(不包含)i:当前扫描指针
区域含义:
[0, left-1] → 全是 0
[left, i-1] → 全是 1
[i, right] → 未处理
[right+1, end] → 全是 2
完整过程示例:
数组:[2, 0, 2, 1, 1, 0]
初始:left = 0, right = 5, i = 0
状态:[未处理: 2, 0, 2, 1, 1, 0]
步骤1:i=0, nums[0]=2
2在右边,交换nums[0]和nums[5]
数组:[0, 0, 2, 1, 1, 2]
right = 4, i不变(因为交换后nums[0]可能是0或1,需要再检查)
状态:[未处理: 0, 0, 2, 1, 1 | 2]
步骤2:i=0, nums[0]=0
0在左边,交换nums[0]和nums[0](自己),left++
数组:[0, 0, 2, 1, 1, 2]
left = 1, i++
状态:[0 | 未处理: 0, 2, 1, 1 | 2]
步骤3:i=1, nums[1]=0
0在左边,交换nums[1]和nums[1](自己),left++
数组:[0, 0, 2, 1, 1, 2]
left = 2, i++
状态:[0, 0 | 未处理: 2, 1, 1 | 2]
步骤4:i=2, nums[2]=2
2在右边,交换nums[2]和nums[4]
数组:[0, 0, 1, 1, 2, 2]
right = 3, i不变
状态:[0, 0 | 未处理: 1, 1 | 2, 2]
步骤5:i=2, nums[2]=1
1在中间,i++
数组:[0, 0, 1, 1, 2, 2]
状态:[0, 0 | 1 | 未处理: 1 | 2, 2]
步骤6:i=3, nums[3]=1
1在中间,i++
数组:[0, 0, 1, 1, 2, 2]
状态:[0, 0 | 1, 1 | 未处理: | 2, 2]
步骤7:i=4 > right=3,结束
结果:[0, 0, 1, 1, 2, 2]
代码:
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
int left = 0; // 0 区右边界
int right = nums.size() - 1; // 2 区左边界
int i = 0; // 当前扫描指针
while (i <= right) {
if (nums[i] == 0) {
swap(nums[i], nums[left]);
left++;
i++;
}
else if (nums[i] == 1) {
i++;
}
else { // nums[i] == 2
swap(nums[i], nums[right]);
right--;
// 注意:这里i不变,因为交换后nums[i]可能是0或1,需要再检查
}
}
}
};
关键点:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 注意:当nums[i] == 2时,交换后i不变,因为交换后的元素需要再检查
- 含义始终保持:[0, left-1]全是0,[left, i-1]全是1,[i, right]未处理,[right+1, end]全是2
4.3 三指针技巧的扩展
应用场景:
- 颜色分类(荷兰国旗问题)
- 将数组分成三部分
- 快速排序的partition操作
5. 排列问题技巧
5.1 下一个排列的基本思想
下一个排列是字典序中当前排列的下一个排列。
核心思路:
- 从后向前找到第一个升序对(nums[i] < nums[i+1])
- 在i的右边找到比nums[i]大的最小数nums[j]
- 交换nums[i]和nums[j]
- 反转i右边的数组(使其升序,保证字典序最小)
原理:
- 从后向前找升序对,找到可以增大的位置
- 只变最小幅度保证字典序下一个
- 反转保证右边升序(最小)
5.2 下一个排列(31)
题目:整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] 。整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
完整过程示例:
数组:[1, 2, 3]
步骤1:从后向前找第一个升序对
i = 1, nums[1] = 2, nums[2] = 3
2 < 3,找到升序对,i = 1
步骤2:在i的右边找比nums[i]大的最小数
j = 2, nums[2] = 3 > nums[1] = 2
找到j = 2
步骤3:交换nums[i]和nums[j]
数组:[1, 3, 2]
步骤4:反转i右边的数组
i = 1,右边是[2],反转后还是[2]
数组:[1, 3, 2]
结果:[1, 3, 2]
更复杂示例:
数组:[1, 2, 3, 5, 4]
步骤1:从后向前找第一个升序对
i = 2, nums[2] = 3, nums[3] = 5
3 < 5,找到升序对,i = 2
步骤2:在i的右边找比nums[i]大的最小数
i的右边是[5, 4],nums[i] = 3
5 > 3, 4 > 3,最小的是4
找到j = 4, nums[4] = 4
步骤3:交换nums[i]和nums[j]
数组:[1, 2, 4, 5, 3]
步骤4:反转i右边的数组
i = 2,右边是[5, 3],反转后是[3, 5]
数组:[1, 2, 4, 3, 5]
结果:[1, 2, 4, 3, 5]
代码:
cpp
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
int i = n - 2;
// 1️找到第一个升序对
while (i >= 0 && nums[i] >= nums[i + 1]) i--;
if (i >= 0) {
// 2️ 找到右边比 nums[i] 大的最小数
int j = n - 1;
while (nums[j] <= nums[i]) j--;
swap(nums[i], nums[j]);
}
// 3️ 反转 i 右边的数组
reverse(nums.begin() + i + 1, nums.end());
}
};
关键点:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 从后向前找突破口
- 只变最小幅度保证字典序下一个
- 反转保证右边升序(最小)
5.3 排列问题的扩展
应用场景:
- 下一个排列
- 上一个排列
- 全排列
- 排列组合
6. 快慢指针技巧(Floyd判圈算法)
6.1 快慢指针的基本思想
**快慢指针(Floyd判圈算法)**是一种用于检测链表中是否存在环的算法,也可以用于找环的入口。
核心思想:
- 使用两个指针,一个快指针(每次走两步),一个慢指针(每次走一步)
- 如果存在环,快指针和慢指针一定会相遇
- 相遇后,将慢指针重置到起点,两个指针都每次走一步,再次相遇的点就是环的入口
原理:
设环的长度为C,环入口到起点的距离为L,相遇点到环入口的距离为X
慢指针走的距离:L + X
快指针走的距离:L + X + nC(n为快指针在环中多走的圈数)
由于快指针速度是慢指针的2倍:
2(L + X) = L + X + nC
L + X = nC
L = nC - X
这意味着:从起点到环入口的距离 = 从相遇点到环入口的距离(考虑环的长度)
6.2 寻找重复数(287)
题目:给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
核心思路:
- 将数组看作链表,nums[i]表示下一个节点的索引
- 由于有重复数字,链表中一定存在环
- 使用快慢指针找环的入口,入口就是重复的数字
完整过程示例:
数组:[1, 3, 4, 2, 2]
将数组看作链表:
索引0 -> nums[0] = 1 -> 索引1
索引1 -> nums[1] = 3 -> 索引3
索引3 -> nums[3] = 2 -> 索引2
索引2 -> nums[2] = 4 -> 索引4
索引4 -> nums[4] = 2 -> 索引2(形成环)
链表:0 -> 1 -> 3 -> 2 -> 4 -> 2 -> 4 -> ...
步骤1:找相遇点
初始:slow = nums[0] = 1, fast = nums[0] = 1
第1步:
slow = nums[1] = 3
fast = nums[nums[1]] = nums[3] = 2
第2步:
slow = nums[3] = 2
fast = nums[nums[2]] = nums[4] = 2
相遇:slow = 2, fast = 2
步骤2:找环入口
重置:slow = nums[0] = 1, fast = 2
第1步:
slow = nums[1] = 3
fast = nums[2] = 4
第2步:
slow = nums[3] = 2
fast = nums[4] = 2
相遇:slow = 2, fast = 2
结果:重复数字是2
更直观示例:
数组:[3, 1, 3, 4, 2]
链表:0 -> 3 -> 4 -> 2 -> 3 -> 4 -> 2 -> ...
步骤1:找相遇点
初始:slow = nums[0] = 3, fast = nums[0] = 3
第1步:
slow = nums[3] = 4
fast = nums[nums[3]] = nums[4] = 2
第2步:
slow = nums[4] = 2
fast = nums[nums[2]] = nums[3] = 4
第3步:
slow = nums[2] = 3
fast = nums[nums[4]] = nums[2] = 3
相遇:slow = 3, fast = 3
步骤2:找环入口
重置:slow = nums[0] = 3, fast = 3
第1步:
slow = nums[3] = 4
fast = nums[3] = 4
相遇:slow = 4, fast = 4
但这不是入口,继续:
第2步:
slow = nums[4] = 2
fast = nums[4] = 2
相遇:slow = 2, fast = 2
继续:
第3步:
slow = nums[2] = 3
fast = nums[2] = 3
相遇:slow = 3, fast = 3
实际上,我们需要找到的是值,不是索引。
重复数字是3(因为nums[0] = 3,nums[2] = 3)
代码:
cpp
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = nums[0];
int fast = nums[0];
// 找到相遇点
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// 找环入口
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};
关键点:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 将数组看作链表,利用快慢指针找环
- 环的入口就是重复的数字
6.3 快慢指针的其他应用
应用场景:
- 检测链表中是否有环
- 找链表中环的入口
- 找链表的中点
- 找链表的倒数第k个节点
- 寻找重复数
7. 常见题型总结
7.1 位运算类
-
只出现一次的数字
- 136.只出现一次的数字:利用异或运算的性质
-
位运算的其他应用
- 判断数字的奇偶性
- 快速计算2的幂
- 判断是否为2的幂
7.2 投票算法类
-
多数元素
- 169.多数元素:摩尔投票法找出现次数大于n/2的元素
-
扩展应用
- 找出现次数大于n/3的元素
- 找出现次数大于n/k的元素
7.3 指针技巧类
-
三指针问题
- 75.颜色分类:荷兰国旗问题,三指针划分区域
-
快慢指针问题
- 287.寻找重复数:快慢指针找环的入口
7.4 排列问题类
-
下一个排列
- 31.下一个排列:字典序下一个排列
-
扩展应用
- 上一个排列
- 全排列
8. 技巧选择指南
8.1 何时使用位运算
使用场景:
- 需要找只出现一次的数字
- 需要快速判断数字的某些性质
- 需要节省空间
判断标准:
- 问题涉及数字的位操作
- 需要O(1)空间复杂度
- 可以利用位运算的性质
8.2 何时使用投票算法
使用场景:
- 需要找出现次数大于n/k的元素
- 需要O(1)空间复杂度
- 多数元素一定存在
判断标准:
- 问题涉及找多数元素
- 空间复杂度要求O(1)
- 多数元素存在且数量大于n/k
8.3 何时使用指针技巧
使用场景:
- 需要将数组分成多个区域
- 需要找链表的某些节点
- 需要检测环
判断标准:
- 问题涉及数组或链表的操作
- 需要O(1)空间复杂度
- 可以通过指针维护区域或状态
8.4 何时使用排列技巧
使用场景:
- 需要找下一个或上一个排列
- 需要生成全排列
- 需要处理排列组合问题
判断标准:
- 问题涉及排列
- 需要按字典序处理
- 需要找到特定的排列
9. 总结
算法技巧是解决特定类型问题的重要工具,掌握这些技巧可以大大提高解题效率。
核心要点:
- 位运算:利用异或等位运算的性质,解决只出现一次的数字等问题
- 投票算法:利用计数器的增减,找出现次数大于n/k的元素
- 指针技巧:利用多个指针维护区域或状态,解决数组和链表问题
- 排列技巧:利用字典序的性质,找下一个或上一个排列
使用建议:
- 理解技巧的原理和适用场景
- 掌握技巧的实现细节
- 注意边界条件处理
- 灵活应用技巧解决实际问题
常见题型总结:
- 位运算:只出现一次的数字、位操作问题
- 投票算法:多数元素、出现次数大于n/k的元素
- 指针技巧:颜色分类、寻找重复数、链表问题
- 排列问题:下一个排列、全排列
学习路径:
- 理解每个技巧的基本原理
- 掌握技巧的实现方法
- 通过例题加深理解
- 灵活应用技巧解决实际问题