快速幂、滑动窗口

首页 / 算法笔记 / 正文

快速幂

通常关于快速幂的问题就是求x的n次方(不考虑大数),如果我们使用最简单的循环:

for(int ans = 1,i <= n; i++){
        ans *=x;
    }

它的时间复杂度就是o(n),但是通常这样的题都会要求时间复杂度低于o(n),那么我们就需要来优化这个代码。

首先我们假设x = 3,n = 10。

ans = 3^ 10 = (3 ^ 2) ^ 5

此时我们发现循环次数已经变为了原来的1/2,但是幂已经变成了奇数,对于奇数,我们还有继续优化的空间,所以我们可以将式子改写为

ans = (9 ^ 1) (9 ^ 4) = 9 (81 ^ 2) = 9 * 6561

所以 ans = 3^ 10 = (3 ^ 2) ^ 5 = (9 ^ 1) (9 ^ 4) = 9 (81 ^ 2) = 9 * 6561

用代码来表示则是

while(n > 0){
        if(n % 2 == 0){
            n /= 2;
            x *= x;
        }else{
            n = (n - 1) / 2;
            ans *= x;
            n /= 2;
            x *= x;
        }
    }

但是这段代码还有继续优化的空间。在之前的位运算中提到

1 & 1 = 1, 1 & 0 = 0

如果一个数是奇数,那么它的二进制表达式最后一位一定是1,反之则为0。

而且除以2可以用位运算右移1位则会变为原来的一半。

最后优化完的代码如下

while(n > 0){
        if((n & 1) == 1){
            ans *= x;
        }
        x *= x;
        n >>= 1;
    }

剑指Offer16

实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。

  • -100.0 < x < 100.0
  • -2^31 <= n <= 2^31-1
  • -10^4 <= xn <= 10^4

代码如下:

class Solution {
public:
    double myPow(double x, int n) {
        if(n == 0 || x == 1){
            return 1;
        }
        long b = n;
        if( b < 0){
            b = -b;
            x = 1 / x;
        }
        double ans = 1.00;
        while(b > 0){
            if((b & 1) == 1){
                ans *= x;
            }
            x *= x;
            b >>= 1;
        }
        return ans;
    }
};

滑动窗口

滑动窗口一般是用在数组当中的一种思想,这里我们直接配合例题来介绍。

剑指Offer48

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

输入: "abcabcbb"
输出: 3

  • 0 <= s.length <= 5 * 104`
  • s 由英文字母、数字、符号和空格组成

首先我们能直接想到的就是找出它所有的字符串,然后便利有没有重复元素,从而确定它的最长子字符串,这样的方法的时间复杂度为o(n ^ 3),这肯定不是我们需要的算法。但是我们可以运用滑动窗口来解决这个问题。

首先我们设定left = 0, right = 0,left不动,right持续+1,直到窗口里面有了一个重复的元素为止,当出现了重复元素时left持续+1,直到越过了right所指向的重复元素为止,不断的记录窗口的长度,从而得到最长字串的长度。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int ans = 0, right = 0, left = 0;
        vector<bool> hash(256, false);
        while(right < s.size()){
            while(hash[s[right]]){
                hash[s[left++]] = false;
            }
            ans = max(ans, right - left + 1);
            hash[s[right++]] = true;
        }
        return ans;
    }
};

但是我们还有继续优化的空间。如果重复字符在窗口中间,要越过重复数字,则需要非常多的left+1,那么我们可以选择直接让left直接移动到重复元素的后面。

我们要记录在滑动的过程中每个元素出现的位置,当重复元素出现时,只要重复元素之前出现的那个位置大于等于窗口左边界的下标,就可以直接跳过,反之则不移动左边界。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int ans = 0, right = 0, left = 0;
        vector<int> value(128,-1);
        while(right < s.size()){
            if(value[s[right]] != -1){
                left = max(left,value[s[right]] + 1);
            }
            value[s[right]] = right;
            ans = max(ans , right - left + 1);
            right++;
        }
        return ans;
    }
};

剑指Offer57-2

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列

输入:target = 9
输出:[[2,3,4],[4,5]]

思路:首先我们需要,因为至少含有两个数,所以我们的左边界最大值小于等于target / 2,确立了我们左边界的范围后,我们只需要确定不同情况下的滑动方向即可。

当窗口和小于target时,我们需要右边界右移

当窗口和大于target时,我们需要左边界右移

当窗口和等于target时,我们需要左边界右移寻找下一个解

代码如下:

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        int right = 1 , left = 1, sum = 0;
        vector<vector<int>> ans;
        while(left <= target / 2){
                if(sum < target){
                    sum += right;
                    right++;
                } 
                else if (sum > target){
                    sum -= left;
                    left++;
                }else{
                    vector<int> arr;
                    for(int k = left; k < right; k++){
                        arr.push_back(k);
                    }
                    ans.push_back(arr);
                    sum -= left;
                    left++;
                }
        }
        return ans;
    }
};
无标签
评论区
头像
文章目录