数据结构与算法再探(五)贪心-双指针-滑动窗口

贪心算法

贪心算法是一种常用的算法设计策略,旨在通过局部最优选择来构建全局最优解。它的基本思想是:在每一步选择中,都选择当前看起来最优的选项,而不考虑后续的影响。贪心算法通常用于解决最优化问题,尤其是在某些特定条件下能够得到全局最优解的问题

1、分发饼干

455. 分发饼干 - 力扣(LeetCode)

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。输入: g = [1,2,3], s = [1,1]: 输出: 1

贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干:

C++双指针

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        int j=0;
        for(int i=0;i<s.size();++i){
             if (j<g.size()&&s[i]>=g[j])//满足条件的饼干,才能让饥饿度后移
             {
                j++;
             }
        }
        return j;
    }
};

python3

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        n=len(g)
        i=0
        for x in s:
            if i<n and x>=g[i]:
                i+=1
        return i

2、分发糖果

135. 分发糖果 - 力扣(LeetCode)

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。你需要按照以下要求,给这些孩子分发糖果: 每个孩子至少分配到 1 个糖果。 相邻两个孩子评分更高的孩子会获得更多的糖果。请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

肯定需要遍历,但是一次遍历考虑左右有些复杂化,可以从左到右和从右到左遍历,都是O(N)

C++

class Solution {
public:
    int candy(vector<int>& ratings) {
        int size = ratings.size();
        if (size < 2) {
            return size;
        }
        vector<int> num(size, 1);
        for (int i = 1; i < size; ++i) {
            if (ratings[i] > ratings[i - 1]) {
                num[i] = num[i - 1] + 1;
            }
        }
        for (int i = size - 1; i > 0; --i) {
            if (ratings[i] < ratings[i - 1])
              //在第一次遍历的时候也许已经满足条件要考虑相等,所以用max
                num[i - 1] = max(num[i - 1], num[i] + 1);
        }
        return accumulate(num.begin(), num.end(), 0);
    }
};

python3

class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        if n == 0: return 0
        candy_nums = [1] * n
        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                candy_nums[i] = candy_nums[i - 1] + 1
        for i in range(n - 1, 0, -1):
            if ratings[i - 1] > ratings[i]:
                candy_nums[i - 1] = max(candy_nums[i - 1], candy_nums[i] + 1)
        return sum(candy_nums)

3、无重叠区域

435. 无重叠区间 - 力扣(LeetCode)

给定多个区间,计算让这些区间互不重叠所需要移除区间的最少个数。起止相连不算重叠,在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此采取的贪心策略为,优先保留结尾小且不相交的区间。

C++

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
       sort(intervals.begin(),intervals.end(),[](vector<int>a, vector<int>b){return a[1]<b[1];});
       int n=intervals.size();
       if(n<=1) return 0;
       int res=0;
       int j=0;
       for(int i=1;i<n;++i){
        if(intervals[j][1]>intervals[i][0]){
            res++;
        }else{
            j=i;
        }
       }
       return res;
    }
};

python3

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        intervals.sort(key=lambda x:x[1])
        n=len(intervals)
        if n<=1:
            return 0
        j=0
        sum=0
        for i in range(1,n):
            if intervals[j][1]>intervals[i][0]:
                sum+=1
            else:
                j=i
        return sum

双指针

双指针是一种常用的算法技巧,双指针技术通常涉及两个指针在同一数据结构上移动,以达到特定的目的。特别适用于处理数组或链表等线性结构的问题,通过使用两个指针,可以有效地减少时间复杂度,简化代码逻辑。

左右指针:两个指针分别从数组的两端向中间移动。
快慢指针:一个指针移动较快,另一个指针移动较慢,常用于链表中查找中间节点或检测环。
滑动窗口:用于处理子数组或子串问题,动态调整指针以满足特定条件。

常见应用场景
查找特定元素:如在排序数组中查找两个数的和。
反转字符串或数组:通过左右指针交换元素。
合并两个有序数组:使用两个指针遍历两个数组。
滑动窗口问题:如最长无重复子串、最小覆盖子串等。

1、两数之和

167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)

给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列  ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。

C++

使用左右指针,左指针从数组的开始位置,右指针从数组的结束位置,逐步逼近

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int l=0,r=numbers.size()-1,sum;
        while (l<r)
        {
            sum=numbers[l]+numbers[r];
            if(sum==target) break;
            if(sum<target){
                ++l;
            }else{
                --r;
            }
        }
        return vector<int>{l+1,r+1};
    }
};

python3

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        l,r=0,len(numbers)-1
        while l<r:
            sum=numbers[l]+numbers[r]
            if sum==target:
                break
            if sum>target:
                r-=1
            else:
                l+=1
        return l+1,r+1

2、合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序排列。

数组合并到nums1中,两数组都是非递减的,两个指针指向两个数组末尾,第三个指针指向nums1需要填充的位置。

88. 合并两个有序数组 - 力扣(LeetCode)

C++

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int pos=m-- +n-- -1;
        while (m>=0&&n>=0)
        {
            if(nums1[m]>nums2[n]){
                nums1[pos--]=nums1[m--];
            }else{
               nums1[pos--]=nums2[n--];
            }
            //如果 nums2 的数字已经复制完,剩余nums1 的数字不需要改变,因为它们已经被排好序
        }
        while (n>=0)
        {//nums2 的数字有可能没有处理完
            nums1[pos--]=nums2[n--];
        }
    }
};

滑动窗口

释义:滑动窗口的核心思想是使用两个指针(通常称为左指针和右指针)来表示一个窗口的范围。随着右指针的移动,窗口逐渐扩大;当满足某些条件时,左指针也会移动,从而缩小窗口。这个过程可以在一次遍历中完成,因此时间复杂度通常为 O(n)。

滑动窗口的类型:1)固定大小窗口:窗口的大小是固定的,适用于需要计算固定长度子数组的情况。2)动态大小窗口:窗口的大小是动态变化的,适用于需要满足特定条件的子数组或子串。

应用场景:1)最长无重复子串:找出字符串中最长的无重复字符的子串。2)最小覆盖子串:在一个字符串中找到包含另一个字符串所有字符的最小子串。3)固定大小的子数组:计算固定大小窗口的和、平均值等。4)子数组的最大/最小值:在给定范围内查找最大或最小值。

1、无重复的最长字串

使用动态大小窗口,维护一个字符集合,动态调整左右指针

3. 无重复字符的最长子串 - 力扣(LeetCode)

C++

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
      unordered_map<char,int> res;
      int len=0;
      int left=0;
      int right=0;
      while(right<s.length()){
        while(res[s[right]]){
            res[s[left++]]--;//移除重复字母
        }
        res[s[right]]++;
        len=max(len,right-left+1);
        right++;
      }  
      return len;
    }
};

python3

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        ans=left=0
        winds=set()
        for right,c in enumerate(s):
            while c in winds:
                winds.remove(s[left])
                left+=1
            winds.add(c)
            ans=max(ans,right-left+1)
        return ans

2、最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

76. 最小覆盖子串 - 力扣(LeetCode)

class Solution {
public:
    string minWindow(string s, string t) {
        vector<int> chars(128,0);
        vector<bool> flag(128,false);
        for(int i=0;i<t.size();++i){
            flag[t[i]]=true;
            ++chars[t[i]];
        }
        int cnt=0, l=0,min_l=0,mini_size=s.size()+1;
        for(int r=0;r<s.size();++r){
            if(flag[s[r]]){
                if(--chars[s[r]]>=0){
                    ++cnt;
                }
                while(cnt==t.size()){
                    if(r-l+1<mini_size)
                    {
                        min_l=l;
                        mini_size=r-l+1;
                    }
                    if(flag[s[l]] &&( ++chars[s[l]]>0)){
                        --cnt;
                    }
                    ++l;
                }
            }
        }
        return mini_size>s.size()?"":s.substr(min_l,mini_size);
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值