数据结构与算法—这可能是最易懂的快速幂讲解了

文章收录在公众号:[bigsai],任何转载需要注明来源。欢迎一键三连

前言

快速幂是什么?

  • 顾名思义,快速幂就是快速算底数的n次幂。

有多快?

  • 其时间复杂度为 O(log₂n), 与朴素的O(n)相比效率有了极大的提高。

用的多么?

  • 快速幂属于数论的范畴,本是ACM经典算法,但现在各厂对算法的要求越来越高,并且快速幂适用场景也比较低多并且相比朴素方法有了非常大的提高。所以掌握快速幂算法已经是一名更合格的工程师必备要求!

下面来详细看看快速幂算法吧!

快速幂介绍

先看个问题再说:

初探

首先问你一个问题,如果让你求 (2^10)%1000你可能会这样写:

int va=1;
for(int i=0;i<10;i++)
{
  va*=2;
}
System.out.println(va%10000);

熟悉的1024没问题,总共计算了10次。但是如果让你算 (2^50)%10000呢?

你可能会窃喜,小样,这就想难住我?我知道int只有32位,50位超出范围会带来数值越界的异常,我这次可以用long,long有64位呢!

long va=1;
for(int i=0;i<50;i++)
{
  va*=2;
}
System.out.println(va);
System.out.println(va%10000);

计算50次出了结果正当你暗暗私喜的时候又来了一个要命的问题:让你算 (2^1e10)%10000 且不许你用Java大数类,你为此苦恼不知所措。这时bigsai小哥哥让你百度下取模运算,然后你恍然大悟,在纸上写了几个公式:

(a + b) % p = (a % p + b % p) % p  (1(a - b) % p = (a % p - b % p ) % p (2(a * b) % p = (a % p * b % p) % p  (3)
a ^ b % p = ((a % p)^b) % p        (4

你还算聪明一眼发现其中的规律:

(a * b) % p = (a % p * b % p) % p   (3)
(2*2*2···*2) %1e10=[2*(2*2···*2)]%1e5=(2%1e5)*(2*2···*2%le5)%1e5

凭借这个递推你明白:每次相乘都取模。机智的你pia pia写下以下代码,却发现另一个问题:怎么跑不出来?

image-20201028160221192

咱们打工人需要对计算机运行速度和数值有一个大致的概念。循环体中不同操作占用时间不同,所以当你的程序循环次数到达1e6或1e7的时候就需要非常非常小心了。如果循环体逻辑或者运算较多可能非常非常慢。

image-20201028163737620

快速幂探索

机智的你不甘失败,开始研究其数的规律,将这个公式写在手上、膀子上、小纸条上。吃饭睡觉都在看:

image-20201028171029641

然后你突然发现其中的奥秘,n次幂可以拆分成一个平方计算后就剩余n/2的次幂了:

image-20201028174250098

现在你已经明白了快速幂是怎么回事,但你可能有点上头,还是给我讲了很多内容:

image-20201028180224832

快速幂实现

至于快速幂已经懂了,我们该怎么实现这个算法呢?

image-20201028185101226

说的不错,确实有递归和非递归的实现方式,但是递归使用的更多一些。在实现的时候,注意一下奇偶性、停止条件就可以了,奇数问题可以转换为偶数问题:

2*2*2*2*2=2 * (2*2*2*2) 奇数问题可以转化为偶数问题。

这里,递归的解法如下

long c=10000007;
public  long divide(long a, long b) {
		if (b == 0)
			return 1;
		else if (b % 2 == 0) //偶数情况
			return divide((a % c) * (a % c), b / 2) % c;
    else//奇数情况
			return a % c * divide((a % c) * (a % c), (b - 1) / 2) % c;
	}

非递归实现也不难,控制好循环条件即可:

//求 a^b%1000000007
long c = 1000000007;
public  long divide(long a, long b) {
  a %= c;
  long res = 1;
  for (; b != 0; b /= 2) {
    if (b % 2 == 1)
      res = (res * a) % c;
    a = (a * a) % c;
  }
  return res;
}

对于非递归你可能有点模糊为啥偶数情况不给res赋值。这里有两点:

  • 为奇数的情况res仅仅是收集相乘那个时候落单的a
  • 最终b均会降到1,a最终都会和res相乘,不用担心会漏掉
  • 理想状态一直是偶数情况,那最后直接获得a取模的值即可。

如果还是不懂,可以用这个图来解释一下:

image-20201028192842778

矩阵快速幂

你以为这就结束了?虽然快速幂主要内容就是以上内容,但是总有很多牛人能够发现很有趣的规律—矩阵快速幂。如果你没听过的话建议仔细看看了解一下。

大家都知道斐波那契数列: 的规则:

image-20201028193231170

前几个斐波那契的数列为:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …

斐波那契从递推式就可以看出是指数级别的增长,所以稍微多几个数字就是爆炸式增长,所以很多时候也会要求最后几位的结果。有了前面模运算公式溢出就不成问题,但n如果非常非常大怎么快速计算就成了一个新的问题。

我们看下面一组公式:

f(n+1) = f(n)   + f(n-1)
f(n)   = f(n)

如果那f(n)和f(n-1)放到一个矩阵中(一行两列):[f(n+1),f(n)] 能否找到和[f(n),f(n-1)]之间的什么规律呢?

答案是存在规律的,看上面的公式知道

[f(n+1),f(n)]
=[f(n)+f(n-1),f(n)]

                 [1  1]
=[f(n),f(n-1)]  *      
                 [1  0]
                 
                 [1  1] [1   1]
=[f(n-1),f(n-2)]*      *
                 [1  0] [1   1]  

=·······           

所以现在你可以知道它的规律了吧,这样一直迭代到f(2),f(1)刚好都为1,所以这个斐波那契的计算为:

image-20201028195631635

而这个矩阵有很多次幂,就可以使用快速幂啦,原理一致,你只需要写一个矩阵乘法就可以啦,下面提供一个矩阵快速幂求斐波那契第n项的后三位数的模板,可以拿这个去试一试poj3070的题目啦。

public int Fibonacci(int n)
    {
        n--;//矩阵为两项
        int a[][]= {{1,1},{1,0}};//进行快速幂的矩阵
        int b[][]={{1,0},{0,1}};//存储漏单奇数、结果的矩阵,初始为单位矩阵
        int time=0;
        while(n>0)
        {
            if(n%2==1)
            {
                b=matrixMultiplication(a, b);
            }
            a=matrixMultiplication(a, a);
            n/=2;
        }
        return b[0][0];
    }
 public  int [][]matrixMultiplication(int a[][],int b[][]){//
        int x=a.length;//a[0].length=b.length 为满足条件
        int y=b[0].length;//确定每一排有几个
        int c[][]=new int [x][y];
        for(int i=0;i<x;i++)
            for(int j=0;j<y;j++)
            {
                //需要确定每一个元素
                //c[i][j];
                for(int t=0;t<b.length;t++)
                {
                    c[i][j]+=(a[i][t]%10000)*(b[t][j]%10000);
                    c[i][j]%=10000;
                }
            }
        return c;
    }

结语

这篇到这里就肝完啦,其实快速幂的内容还不止这么多,尤其是矩阵快速幂,会有着各种巧妙的变形,不过跟数学有一些关系,这年头,不会点算法、不会点数学真的是举步维艰。所以大家要对本篇内容好好吸收,让我那么久的努力发挥出作用。

如果有疑问不懂得欢迎私聊我讨论。也希望大家点个在看,您的支持是我努力的不断动力。

关注bigsai,回复bigsai领取干货资源,回复进群加入力扣打卡群。下次再见,打工人!

image-20201028210802464

image-20201028210842709

Big sai CSDN认证博客专家 数据结构与算法 爬虫 Java
原创公众号:「bigsai」,回复【bigsai】获取珍藏pdf书籍资源,回复【进群】即可加入leetcode打卡群。分享Java,数据结构与算法,python爬虫知识,期待和优秀的你成为朋友!
本教程为授权出品 课程介绍: 1.算法是程序的灵魂,优秀的程序在对海量数据处理时,依然保持高速计算,就需要高效的数据结构算法支撑。 2.网上数据结构算法的课程不少,但存在两个问题: 1)授课方式单一,大多是照着代码念一遍,数据结构算法本身就比较难理解,对基础好的学员来说,还好一点,对基础不好的学生来说,基本上就是听天书了 2)说是讲数据结构算法,但大多是挂羊头卖狗肉,算法讲的很少。 本课程针对上述问题,有针对性的进行了升级  3)授课方式采用图解+算法游戏的方式,让课程生动有趣好理解  4)系统全面的讲解数据结构算法, 除常用数据结构算法外,还包括程序员常用10大算法:二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法、马踏棋盘算法。可以解决面试遇到的最短路径、最小生成树、最小连通图、动态规划等问题及衍生出的面试题,让你秒杀其他面试小伙伴 3.如果你不想永远都是代码工人,就需要花时间来研究下数据结构算法。 教程内容: 本教程是使用Java来讲解数据结构算法,考虑到数据结构算法较难,授课采用图解加算法游戏的方式。内容包括: 稀疏数组、单向队列、环形队列、单向链表、双向链表、环形链表、约瑟夫问题、栈、前缀、中缀、后缀表达式、中缀表达式转换为后缀表达式、递归与回溯、迷宫问题、八皇后问题、算法的时间复杂度、冒泡排序、选择排序、插入排序、快速排序、归并排序、希尔排序、基数排序(桶排序)、堆排序、排序速度分析、二分查找、插值查找、斐波那契查找、散列、哈希表、二叉树、二叉树与数组转换、二叉排序树(BST)、AVL树、线索二叉树、赫夫曼树、赫夫曼编码、多路查找树(B树B+树和B*树)、图、图的DFS算法和BFS、程序员常用10大算法、二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法马踏棋盘算法。 学习目标: 通过学习,学员能掌握主流数据结构算法的实现机制,开阔编程思路,提高优化程序的能力。
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值