目录
示例3:142. 环形链表 II - 力扣(LeetCode)
二分查找
二分查找法
- 使用前提:有序数组,且数组中无重复元素
- 关键点:确定查找区间,遵循循环不变量
二分查找法的模板:
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
注意细节:
- 计算mid时需要防止溢出
- while中的条件 left 可不可以和right 相等
- 修改区间范围,是否要mid - 1
寻找左右边界的二分查找
视频讲解:二分查找为什么总是写错?_哔哩哔哩_bilibili
# 伪代码
left = -1,right = nums.size() # 注意初始化值
while(left + 1 != right){
mid = (left + right)/2;
if IsBlue(mid)
left = mid; # 注意是mid
else
right = mid;
}
return right or left;
1.划分蓝红区域,确定IsBlue
2.返回right还是left
相关题目:
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
详细讲解看:代码随想录算法训练营第一天|704.二分查找,27.移除元素,977.有序数组的平方-CSDN博客
双指针法
双指针是指在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针进行遍历,从而达到相应的目的。
按照指针的移动方向,双指针分为同向双指针、异向双指针:
- 同向双指针,也称快慢指针(两个指针一快一慢);
- 异向双指针,分为对撞指针(从两边向中间移动)、背向指针(从中间向两边移动)。
双指针法在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
快慢指针
两个指针,初始在同一位置,然后向相同方向移动,一个移动速度快,一个移动速度慢。
关键点:明确快慢指针的含义
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
详细讲解看:代码随想录算法训练营第一天|704.二分查找,27.移除元素,977.有序数组的平方-CSDN博客
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
};
示例3:142. 环形链表 II - 力扣(LeetCode)
判断链表是否存在环?
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
详细讲解看:代码随想录算法训练营第四天|24. 两两交换链表中的节点 、 19.删除链表的倒数第N个节点 、面试题 02.07. 链表相交 、142.环形链表II-CSDN博客
对撞指针
两个指针,初始一个在左、一个在右,左指针向右移动,右指针向左移动,直到相撞停止。
示例1:977. 有序数组的平方 - 力扣(LeetCode)
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
vector<int> result(A.size(), 0);
int k = A.size() - 1; //指向result数组的更新位置
for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素
if (A[i] * A[i] < A[j] * A[j]) {
result[k--] = A[j] * A[j];
j--;
}
else {
result[k--] = A[i] * A[i];
i++;
}
}
return result;
}
};
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 去重a
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
详细讲解看:代码随想录算法训练营第六天|哈希表-CSDN博客
滑动窗口
滑动窗口算法的基本思想是维护一个窗口,通过移动窗口的两个边界来处理问题。
具体来说,我们可以使用两个指针
left
和right
分别表示滑动窗口的左右边界,然后通过不断移动右指针right
来扩大窗口,同时根据问题的要求调整左指针left
来缩小窗口。当右指针right
扫描到字符串或数组的末尾时,算法的执行就完成了。在扩大或缩小窗口的过程中,可以记录下一些中间结果,例如最大值、最小值、子串长度等等,从而求解问题的最终答案。
关键点:窗口是什么?、如何移动窗口?
示例1:209. 长度最小的子数组 - 力扣(LeetCode)
给定一个含有 n
个正整数的数组和一个正整数 target
。找出该数组中满足其总和大于等于 target
的长度最小的 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0
。
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
示例2:438. 找到字符串中所有字母异位词 - 力扣(LeetCode)
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> result;
unordered_map<char,int> pFreq;
unordered_map<char,int> windowFreq;
// 统计p中字符的频率
for(char c : p){
pFreq[c]++;
}
// 初始化窗口频率表
int left = 0,matched = 0,required = p.size();
for(int i = 0; i < p.size(); i++){
if(pFreq.find(s[i]) != pFreq.end()){
windowFreq[s[i]]++;
if(windowFreq[s[i]] == pFreq[s[i]]){
matched++;
}
}
}
// 如果初始窗口就是答案之一
if(matched == required){
result.push_back(left);
}
// 滑动窗口
for(int right = p.size(); right < s.size(); right++){
// 窗口右移,加入新字符
char rightChar = s[right];
if(pFreq.count(rightChar)){
windowFreq[rightChar]++;
if(windowFreq[rightChar] == pFreq[rightChar]){
matched++;
}
}
// 窗口左移,移除左边界字符
char leftChar = s[left];
if(pFreq.count(leftChar)){
if(windowFreq[leftChar] == pFreq[leftChar]){
matched--;
}
windowFreq[leftChar]--;
}
// 移动左边界
left++;
// 检查是否匹配
if(matched == required){
result.push_back(left);
}
}
return result;
}
};
详细讲解看:代码随想录算法训练营第六天|哈希表-CSDN博客
前缀和
前缀和算法(Prefix Sum)是一种用于快速计算数组元素之和的技术。它通过预先计算数组中每个位置前所有元素的累加和,将这些部分和存储在一个新的数组中,从而在需要计算某个区间的和时,可以通过简单的减法操作得到结果,而不必重新遍历整个区间。
前缀和的思想是重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。
前缀和 在涉及计算区间和的问题时非常有用!
示例1:58. 区间和(第九期模拟笔试) (kamacoder.com)
给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> vec(n);
vector<int> p(n);
int presum = 0;
//记录前缀和
for (int i = 0; i < n; i++) {
scanf("%d", &vec[i]);
presum += vec[i];
p[i] = presum;
}
while (~scanf("%d%d", &a, &b)) {
int sum;
//利用前缀和,计算指定区间的值
if (a == 0) sum = p[b];
else sum = p[b] - p[a - 1];
printf("%d\n", sum);
}
}
示例2:44. 开发商购买土地(第五期模拟笔试) (kamacoder.com)
在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。
现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。
然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。 为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。
注意:区块不可再分。
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main () {
int n, m;
cin >> n >> m;
int sum = 0; //记录所有土地的面积
vector<vector<int>> vec(n, vector<int>(m, 0)) ;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> vec[i][j];
sum += vec[i][j];
}
}
// 统计横向
vector<int> horizontal(n, 0);
for (int i = 0; i < n; i++) {
for (int j = 0 ; j < m; j++) {
horizontal[i] += vec[i][j];
}
}
// 统计纵向
vector<int> vertical(m , 0);
for (int j = 0; j < m; j++) {
for (int i = 0 ; i < n; i++) {
vertical[j] += vec[i][j];
}
}
int result = INT_MAX;
//列举,按行划分
int horizontalCut = 0;
for (int i = 0 ; i < n; i++) {
horizontalCut += horizontal[i];
result = min(result, abs(sum - horizontalCut - horizontalCut));
}
//列举,按行划分
int verticalCut = 0;
for (int j = 0; j < m; j++) {
verticalCut += vertical[j];
result = min(result, abs(sum - verticalCut - verticalCut));
}
cout << result << endl;
}
详细讲解看:代码随想录算法训练营第二天| 209.长度最小的子数组 、59.螺旋矩阵II、区间和、开发商购买土地-CSDN博客