第一部分:和数组相关的问题

本系列的博客主要是整理刘宇波老师《玩转算法面试》课程的相关知识点,本系列博客主要有八个部分:

(1)数组中常见的问题

(2)查找表相关问题

(3)链表相关问题

(4)栈、队列和优先队列

(5)二叉树和递归

(6)递归和回溯法

(7)动态规划

(8)贪心算法

 

在第一部分中,主要介绍了和数组相关的问题,大体上的问题解决方案都和如何划分区域有关。区域的形成离不开左右索引,根据索引的数量和如何动态改变分成了三个部分:单索引、对撞指针和滑动窗口。下面主要针对每一部分进行介绍并针对leetcode相关例题给出练习。

1 单索引

单索引不是说区域只由一个索引构成,而是说在构成区域的两个索引中,只由一个索引会改变,另外的一个索引是固定不动的。如下图中,左边界并不进行移动,只有右边界不断的向右进行移动,用来扩大有限范围。如何理解有限范围中的“有效”呢?你可以理解这个有效范围是指“符合题意”的范围。

 

1.1 移动零 (leetcode 283)

我们拿leetcode的283题为例,来详细介绍一下“有效范围”的含义和移动的规则。

题目是说给定一个随机的数组,我们需要将非零的元素全部移动到前半区域,然后将元素零全部移动到数组的后半区域。这样,整个数组通过一个索引index,就可以分成[1,3,12]和[0,0]两个部分。而前半部分[1,3,12]就是“有效区域”,而它所对应的索引范围就是[0,index]。

在初始化的时候,为了保证有效区域一开始就是空的,我们要初始化index为-1,这样[0,-1]之间其实就没有元素,这是index初始化的原则。因此在[0,index]中,我们就赋予其范围中所有元素都是非零的含义,而index+1之后的元素就都是元素0。因此,我们可以遍历整个数组,一旦我们遇到了非零的元素,我们就将其和index+1的元素进行置换,然后通过++index来扩大有效的范围。通过这样一个逻辑,我们就可以实现元素0的移动。下面可以配合一个动态图来理解一下:

​算法的思想和算法的实现是两种不同的能力,了解思想之后,最重要的就是使用代码将思想呈现出来。下面是整个代码:

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        // 在[0,k]之间填充非零元素
        int k = -1;
        for (int i = 0;i < nums.size();i++)
            // 遇到非零数据表示就需要进行交换了
            if (nums[i] != 0){
                swap(nums[++k], nums[i]);
            }           
    }
};

 

1.2 删除元素 (leetcode 27)

其实这一题和上面“移动零”两者在本质上是一样的,只不过上面将所有的元素零移动到了数组的后半区域,而这一题是将所有的val移动到数组的后半区域,算法思想如法炮制,只不过代码需要稍微修改一下:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        // [0, k]之间的数据都是非val的
        int k = -1;
        for(int i = 0;i<nums.size();i++)
            if(nums[i] != val)
                swap(nums[i],nums[++k]);
        return k+1;
    }
};

 

1.3 删除有序数组中重复的元素 (leetcode26 )

​在这个问题中,我们可以假定[0,index]之间的元素都是独一无二的元素,又因为数组本身是有序的,只要当前遍历的元素与index元素不相同,我们就可以进行置换!因为要索引[0,index]中的元素,因此我们设定index从0开始进行索引!,下面是hi相关代码:

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        // [0,k]之间是独一无二的数据,置换所有与num[k]不相同的数据
        if(!nums.size())
            return 0;
        int k = 0;
        for(int i = k+1; i<nums.size(); i++)
            if(nums[i] != nums[k])
                swap(nums[++k],nums[i]);
        return k+1;
    }
};

 

1.4 删除有序数组中重复的元素(leetcode 80)

​在这一题中,我们设定[0,index]中的元素最多有两个相同的元素。为了在遍历数组中每个元素到底应不应该添加到[0,index]的范围中,我们需要记录index以及index-1这两个元素是否相同,我们使用flag变量来表示。当flag为false的时候,我们就将当前的元素纳入到该范围中,并更新flag的值;如果flag为ture,并且index+1的元素和当前的元素不相同的时候,我们进行置换,然后再更新inde的范围。

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        // 如果没有元素,返回0
        if (!nums.size())
            return 0;
        // [0,k]表示符合的区域,flag表示前两个元素是否相同
        bool flag = false;
        int  k = 0;	// k从0开始索引
        for (int i = 1;i < nums.size();i++) {
            // 如果前两个元素不相同,我们就接收i这个元素
            if (flag == false) {
                // 接受这个元素要判断这两个相邻的元素是否相同
                flag = (nums[k] == nums[i]);
                swap(nums[++k], nums[i]);
            }
            else if (flag && nums[i] != nums[k]) {
                // 如果相邻的元素相等,我们就需要向下寻找和nums[k]不同的元素,并与nums[k+1]进行置换,并++k
                swap(nums[++k], nums[i]);
                flag = false;
            }    
        }
        return k + 1;
    }
};

 

1.5 小结

在上面的几个例题中,我们都给[0,index]区域赋予了一定的意义,然后在根据题目不断的扩大这个有效的区域,直到整个区域的所有元素都遍历完了。

 

2 对撞指针

对撞指针是说,我们使用两个索引left,right从数组的两端相向而行,直到它们碰到一起;在碰到一起之前,我们需要进行处理或者进行某些操作。

​ 2.1 两个数之和 (leetcode 167)

​因为整个数组是升序排列的,也就是说整个数组是按照从小到大的顺序进行排列的。我们从数组两端开始进行索引,如果[left, right]中的所有元素和恰好等于target, 说明left和right就是我们要找的索引;如果小于targe,说明我们需要改变小端的索引,也就是让left向后移动;如果大于target,也就是说我们要改变大端的索引,即让right向前移动。

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int smaller = 0, bigger = numbers.size() - 1;
        vector<int> rst(2);
        int sum = 0;
        // smaller < bigger进入循环,因为不能找两个相同的数
        while (smaller < bigger) {
            // 如果两个数加起来比target要小,要增加smaller的索引
            sum = numbers[smaller] + numbers[bigger];
            if (sum < target)
                smaller++;
            else if (sum > target)
                bigger--;
            else{
                rst[0] = smaller+1;
                rst[1] = bigger+1;
                return rst;
            }
        }
        // 抛出异常
        throw invalid_argument("the input has no solution");
    }
};

 

2.2 验证回文串 (leetcode 125)

回文串就比较简单了,我们只比较数字或者字母的对称性。如果left端不是,我们就移动left;如果right不是,我们就移动right。否则,我们就比较对称性:
 

class Solution {
public:
    bool isPalindrome(string s) {
        int left = 0, right = s.size() - 1;
        while (left<=right) {
            // 如果左边的元素不是数字或者字符
            if (!isalnum(s[left]))
                left++;
            // 如果右边的元素不是数组或者字符
            else if (!isalnum(s[right]))
                right--;
            // 左边和右边的与元素都是字符或者数字
            // 并且它们是相同的字符,就更新left和right
            else if (tolower(s[left]) == tolower(s[right])) {
                left++;
                right--;
            }
            // 他们不是相同的字符,说明不是回文串
            else
                return false;
        }
        return true;
    }
};

 

2.3 盛水最多的容器 (leetcode 11)

这里的思路和上面的大差不差,只不过我们要考虑到底是移动right还是left呢?我们要根据height[left]和height[right]来进行判断,从而进行移动。因为要求得最大的区域,我们只能够移动height最小的索引。

class Solution {
public:
    int maxArea(vector<int>& height) {
        // 求[left, right]区域的面积
        int left = 0, right = height.size()-1;
        int maxA = 0;   // 最大的面积
        int area = 0;   // 当前的面积
        while(left < right){
            if(height[left]<height[right])
                area = (right-left)*height[left++];
            else 
                area = (right-left)*height[right--];
            if(area>maxA)
                maxA = area;
        }
        return maxA;
    }
};

 

3 滑动窗口

滑动窗口的概念很重要!它是说,我们找[left,right]区域从数组的首端开始,不断改变这个区域的范围,直到滑动过整个完整的数组。

3.1 长度最小的子数组 (leetcode 209)

我们需要从空开始不断改变[left, right]的区域,然后使用变量sum来记录整个区域中所有元素的和。如果sum小于s的话,说明我们要增加区域的范围,将right+1对应的元素纳入到整个范围中;如果大于等于的话,我们需要将left的元素从中删除。在更新数组范围之后,我就需要判断[left,right]中所有元素的大小和s的关系了。(先更新区域,后进行判断)

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        // [left, right] < s
        int left = 0, right = -1;
        int sum = 0;    // [left, right]区间中素有元素的和
        int minLen = nums.size() + 1; //  [left, right]最长的长度
        while(left < nums.size()){
            // 如果sum < s,将nums[++right]加到区间中
            if(sum < s && right+1 < nums.size())
                sum += nums[++right];
            else
                sum -= nums[left++];
            // 更新最长长度
            if(sum >= s && (right-left+1) < minLen)
                minLen = right-left+1;
        }
        if(minLen == nums.size() + 1)
            return 0;
        return minLen;
    }
};

从中,我们可以得到滑动窗口的基本框架:

3.2 无重复字符的最长子串 (leetcode 3)

我们的整体思路和上面的差不多,但是其中的一个非常重要的技巧就是如何判断一个字符是否已经在一个字符串中出现了呢?我们可以使用一个数组保存所有元素是否被访问的状态,该数组默认是0,如果被访问了就置为1。因为字符在低层就是数字,其低层数组可以当做是状态数组的索引。

因此,整个算法的逻辑就是我们假定[left, right]中所有的元素都是独一无二的,如果right+1的元素在[left, right]中没出现,我们就更新剔除最后的元素;如果没有出现,我们就将right+1这个元素纳入到有效的区间中!整个算法的代码就是:

class Solution {
public:
   
    int lengthOfLongestSubstring(string s) {
        int freq[129] = { 0 };
        // [left, right] 之间的是没有重复字符的子数组
        int left = 0, right = -1, maxLen = 0;
        while (left < s.size()) {
            // 如果s[right+1]的元素在[left, right]中没有重复的字符
            // 就更新right和freq的数组
            if (right + 1 < s.size() && freq[s[right + 1]] == 0)
                freq[s[++right]] = 1;                        
            else
                freq[s[left++]] = 0;
            // 更新maxLen长度
            if (right - left + 1 > maxLen)
                maxLen = right - left + 1;
        }
        return maxLen;
    }
};

 

3.3 找到字符长中所有字母异位词 (leetcode 438)

在这个问题中非常重要的问题就是如何求一个两个字符串是不是字母异位词呢?我们可以记录两个字符串中每个字符出现的次数,然后比较这两个频率数组是否相等。如果相等,说明它们是字母异位词;否则,就不是字母异位词。

 // 判断两个vector是否相同
 bool same(vector<int>& sFreq, vector<int>& pFreq){
     if(sFreq.size() != pFreq.size())
         return false;
     // 遍历两个vector的所有元素,依次进行比较
     for(int i = 0;i < sFreq.size(); i++)
         if(sFreq[i] != pFreq[i])
             return false;
     return true;
 }

它的整体逻辑和上面滑动指针的思想基本相同,只不过我们在滑动的时候需要更新该区域中每个字符出现的频率。如果该区域的长度短于p的长度,我们就将right+1这个元素纳入到[left, right]的区域中,并更新频率数组。否则,就将left元素吐出来。更新之后我们就判断整个区域是否和p成字母异位词,本质上就是判断两个字符串的频率数组是否相同

class Solution {
public:

    // 判断两个vector是否相同
    bool same(vector<int>& sFreq, vector<int>& pFreq){
        if(sFreq.size() != pFreq.size())
            return false;
        // 遍历两个vector的所有元素,依次进行比较
        for(int i = 0;i < sFreq.size(); i++)
            if(sFreq[i] != pFreq[i])
                return false;
        return true;
    }

    vector<int> findAnagrams(string s, string p) {
        // [left, right]表示框定的区域的字符
        // 首先需要计算p的频率
        vector<int> rst;
        // 如果s短于p,就返回空的vector
        if(s.size() < p.size())
            return rst;
        // 计算p中字符出现的频率
        vector<int> pFreq(26,0);
        vector<int> sFreq(26,0);
        for(int i = 0; i < p.size(); i++)
            // 从0开始计算每个字符出现的次数
            pFreq[p[i]-'a']++;
        // 滑动窗口[left, right]
        int left = 0, right = -1;
        while(left<s.size()){
            // 如果窗口宽度小于p的宽度的话,就right++,并计算sFreq中该字符出现的频率
            if((right-left+1) < p.size() && (right+1)<s.size())
                sFreq[s[++right]-'a']++;
            else
                sFreq[s[left++]-'a']--;
            // 判断长度是否相同,并且是字母异位词
            if((right-left+1) == p.size() && same(sFreq,pFreq))
                // 记录当前的left,并更新left
                rst.push_back(left);
        }
        return rst;
    }
};

3.4 小结

在滑动窗口中,最重要的就是窗口什么时候进行滑动滑动之后的相关处理是什么

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值