数组篇章总结|二分查找、双指针、前缀和

目录

二分查找

二分查找法的模板:

双指针法

快慢指针

示例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

相关题目:

704. 二分查找 - 力扣(LeetCode)

35. 搜索插入位置 - 力扣(LeetCode)

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

详细讲解看:代码随想录算法训练营第一天|704.二分查找,27.移除元素,977.有序数组的平方-CSDN博客

双指针法

双指针是指在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针进行遍历,从而达到相应的目的。

按照指针的移动方向,双指针分为同向双指针、异向双指针:

  1. 同向双指针,也称快慢指针(两个指针一快一慢);
  2. 异向双指针,分为对撞指针(从两边向中间移动)、背向指针(从中间向两边移动)。

双指针法在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。

快慢指针

两个指针,初始在同一位置,然后向相同方向移动,一个移动速度快,一个移动速度慢。

关键点:明确快慢指针的含义

示例1:27. 移除元素 - 力扣(LeetCode)

给你一个数组 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博客

示例2:206. 反转链表 - 力扣(LeetCode)

给你单链表的头节点 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;
    }
};

示例2:15. 三数之和 - 力扣(LeetCode)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != 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博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值