首页 > 编程语言 >快速幂算法

快速幂算法

时间:2023-04-09 12:33:06浏览次数:39  
标签:case 10 return int break 算法 快速 public

快速幂算法

设计一个算法计算\(x^n\)的值。

根据定义最常见也最能瞬间想到的是如下的算法:

// 递归写法
public int pow1(int x, int n) {
  if (n == 0) return 1;
  if (n == 1) return x;
  return x * pow1(x, n - 1);
}
// 循环写法
public int pow2(int x, int n) {
  int y = 1;
  while (n) {
    y *= x;
    n--;
  }
  return y;
}

但上面的算法的时间复杂度是\(O(n)\)

下面采用快速幂算法来解决这个问题。

在解决它之前先来看一下原理:

\[x^{n}=x^{n-a}x^a \]

所以我们可以对本身要求的\(x^n\)对半分来求,只求一半的数,然后乘以自己本身就可以达到\(x^n\)

但是会出现的情况就是,对半分的时候会出现小数的情况,所以一定要分奇数和偶数的情况。

如果n分半了之后是偶数,那就直接对半分,如果是奇数则在对半分之后还要乘以一个x。

所以可以有下面的规律:

f(x, n) = {
  f(x, n/2)*f(x, n/2),    // 当n为偶数
  x*f(x, n/2)*f(x, n/2)   // 当n为奇数
}

所以得出快速幂算法1:

public int qPow1(int x, int n) {
  if (n == 0) return 1;
  if (n == 1) return x;
  if (n % 2 == 1) return x*f(x, n/2)*f(x, n/2);
  return f(x, n/2)*f(x, n/2);
}

但是上面的算法明显没有任何增进,因为f(x, n/2)要算两次,那和之前的\(O(n)\)的算法没什么区别。所以使用一个中间变量去接受一下,就可以提高算法效率。

public int qPow1(int x, int n) {
  if (n == 0) return 1;
  if (n == 1) return x;
  int t = f(x, n/2)
  if (n % 2 == 1) return x * t * t;
  return t * t;
}

上面的快速幂算法还是比较好理解的,下面的快速幂算法就比较的炫技了我觉得,但是也就那样(原理还是上面的,只是不是对半分而已,而是根据进制数来分)。

下面采用二进制数来分。

假设我们要计算的是\(x^{10}\),那么10的二进制数是1010,所以有如下公式及变换:

\[x^{10}=x^{(10)_{10}}=x^{(1010)_2}=x^{1*2^3+0*2^2+1*2^1+0*2^0}=x^{8+2}=x^8x^2 \]

\[x^{10}=x^8x^2 \]

和上面第一种快速幂的算法类似,只不过上一种采用的分法是:

\[x^{10}=x^5x^5 \]

那么不管怎样分,最后肯定会被分到1,因为\(x^0=1\),其实上面的分法都隐藏了一个\(x^0\),即:

\[x^{10}=x^8x^2x^0 \]

所以状态是怎么转移的,即每一次迭代都是怎样变化的。初始化\(t=x^{2^0}=x^1=x\),那么下一代的变化是\(x^{2^1}\),它是由\(x^{2^0*2}\)变化而来,因为采用的是二进制。所以指数部分要想从\(2^0\)变换到\(2^1\)就需要乘以一个2.那也就是说,\(x\)变到\(x^2\).那么迭代变化过程就是\(t=t*t\).

\(x^{2^0*2}=(x^{2^0})^2\)

所以得到第二种快速幂算法代码:

public int qPow2(int x, int n) {
  int y = 1;
  int t = x;
  while (n > 0) {
    switch (n % 2) {
      case 1: y = y * t;  // 这里不要写break
      case 0: t = t * t;
    }
    n = n / 2;
  }
  return y;
}

那么这个采用二进制的方法分,当然也有三进制的,四进制的,五进制的等等。十六进制就不要搞了,因为不是进制越高就越快。

通过我对上面的二进制写法的快速幂就可以看出来我还会有其他进制的写法。那么下面就来看一下三进制的写法,然后四进制的就顺其自然就明白了。

那么三进制的推导过程也是和二进制的推导过程是类似的。假设计算的是\(x^{10}\).

\[x^{10}=x^{(10)_{10}}=x^{(101)_3}=x^{1*3^2+0+3^1+1*3^0}=x^9x \]

所以从二进制的分法和三进制的分法可以看出,不管怎么分都是可以合起来达到10.只要能达到10的说明采用什么进制分法都是可以的。但并不是说采用的进制越高就越好。

那么初始化\(t=x^{3^0}=x\),那么下一代的变化是\(x^{3^1}\),它是由\(x^{3^0*3}\)变化而来,因为采用的是三进制。所以指数部分要想从\(3^0\)变换到\(3^1\)就需要乘以一个3.那也就是说,\(x\)变到\(x^3\).那么迭代变化过程就是\(t=t*t*t\).

\(x^{3^0*3}=(x^{3^0})^3\)

对比一下二进制和三进制的区别,所以四进制往后的就不需要我一个个推导了吧。

直接得出算法代码:

public int qPow3(int x, int n) {
  int y = 1;
  int t = x;
  while (n > 0) {
    switch (n % 3) {
      case 2: y = y * t;  // 这里不要写break
      case 1: y = y * t;  // 这里不要写break
      case 0: t = t * t * t;
    }
    n = n / 3;
  }
  return y;
}

直接得出四进制版本的代码:

public int qPow4(int x, int n) {
  int y = 1;
  int t = x;
  while (n > 0) {
    switch (n % 4) {
      case 3: y = y * t;  // 这里不要写break
      case 2: y = y * t;  // 这里不要写break
      case 1: y = y * t;  // 这里不要写break
      case 0: t = t * t * t * t;
    }
    n = n / 4;
  }
  return y;
}

直接得出五进制版本的代码:

public int qPow5(int x, int n) {
  int y = 1;
  int t = x;
  while (n > 0) {
    switch (n % 5) {
      case 4: y = y * t;  // 这里不要写break
      case 3: y = y * t;  // 这里不要写break
      case 2: y = y * t;  // 这里不要写break
      case 1: y = y * t;  // 这里不要写break
      case 0: t = t * t * t * t;
    }
    n = n / 5;
  }
  return y;
}

以此类推。。。就不写了。

可以去测试一下:

public class Main {
        public static void main(String[] args) {
                System.out.println("计算2的10次方:");
                int x = 2, n = 10;
                Solution s = new Solution();
                showMessage("pow1: ",  s.pow1(x, n));
                showMessage("pow2: ",  s.pow2(x, n));
                showMessage("qPow1: ", s.qPow1(x, n));
                showMessage("qPow2: ", s.qPow2(x, n));
                showMessage("qPow3: ", s.qPow3(x, n));
                showMessage("qPow4: ", s.qPow4(x, n));
                showMessage("qPow5: ", s.qPow5(x, n));
        }
        public static void showMessage(String str, int result) {
                System.out.println("---------------------");
                System.out.println(str + result);
                System.out.println("---------------------");
        }
}

class Solution {
        public int pow1(int x, int n) {
                if (n == 0) return 1;
                if (n == 1) return x;
                return x * pow1(x, n - 1);
        }
        public int pow2(int x, int n) {
                int y = 1;
                while (n > 0) {
                        y = y * x;
                        n--;
                }
                return y;
        }
        public int qPow1(int x, int n) {
                if (n == 0) return 1;
                if (n == 1) return x;
                int t = qPow1(x, n / 2);
                if (n % 2 == 1) return x * t * t;
                return t * t;
        }
        public int qPow2(int x, int n) {
                int y = 1;
                int t = x;
                while (n > 0) {
                        switch (n % 2) {
                                case 1: y = y * t; // 这里不要写break
                                case 0: t = t * t;
                        }
                        n = n / 2;
                }
                return y;
        }
        public int qPow3(int x, int n) {
                int y = 1;
                int t = x;
                while (n > 0) {
                        switch (n % 3) {
                                case 2: y = y * t;  // 这里不要写break
                                case 1: y = y * t;  // 这里不要写break
                                case 0: t = t * t * t;
                        }
                        n = n / 3;
                }
                return y;
        }
        public int qPow4(int x, int n) {
                int y = 1;
                int t = x;
                while (n > 0) {
                        switch (n % 4) {
                                case 3: y = y * t;  // 这里不要写break
                                case 2: y = y * t;  // 这里不要写break
                                case 1: y = y * t;  // 这里不要写break
                                case 0: t = t * t * t * t;
                        }
                        n = n / 4;
                }
                return y;
        }
        public int qPow5(int x, int n) {
                int y = 1;
                int t = x;
                while (n > 0) {
                        switch (n % 5) {
                                case 4: y = y * t;  // 这里不要写break
                                case 3: y = y * t;  // 这里不要写break
                                case 2: y = y * t;  // 这里不要写break
                                case 1: y = y * t;  // 这里不要写break
                                case 0: t = t * t * t * t * t;
                        }
                        n = n / 5;
                }
                return y;
        }
}

终端输出:

计算2的10次方:
---------------------
pow1: 1024
---------------------
---------------------
pow2: 1024
---------------------
---------------------
qPow1: 1024
---------------------
---------------------
qPow2: 1024
---------------------
---------------------
qPow3: 1024
---------------------
---------------------
qPow4: 1024
---------------------
---------------------
qPow5: 1024
---------------------

然后来分析一下它们的执行效率。

一般的\(O(n)\)就不说了,肯定是比\(O(log_2n)\)差的。主要是看是不是进制越高就越好?先给出答案,并不一定是。看着qPow5好像可以更快收到答案,但我们忘了看\(t=t*t*t*t*t\)这段代码和它上面的那一坨。然后再放大一点看,如果我采用的是十进制的写法。那会发现状态转移是\(t=t*t*t...(10个t)\)那和一般的pow有什么区别?所以,从这一点可以看出来并不是进制越高就越好。就好像是用\(x^8x^2\)和\(x^2x^8\)比效率一样。都是一样的嘛,如果外层循环少了,那里面的乘法就多了。所以得找个平衡的点。那这个平衡的点一般也就是对半的时候(并不是所有情况都是),所以我们折腾了那么久又回到了二进制的版本。因为计算机底层是二进制,所以我们就采用二进制的版本,然后再采用代码上的语法优化,这样应该是更好一点,因为其它进制的版本可优化的点并不多。下面给出二进制优化的版本。

public int qPow2(int x, int n) {
  int y = 1;
  int t = x;
  while (n > 0) {
    if (n % 2 == 1) y *= t;
    t *= t;
    n >>= 1;
  }
  return y;
}

因为Java语法本身的原因在做位运算的时候不能像C/C++一样可以用非零当作真。所以if (n % 2 == 1) y *= t;并不改变。但如果是C/C++的话可以采用下面的版本:

int qPow2(int x, int n) {
  int y = 1;
  while (n) {
    if (n & 1) y *= t;
    t *= t;
    n >>= 1;
  }
  return y;
}

快速幂算法就先到这里结束了。

标签:case,10,return,int,break,算法,快速,public
From: https://www.cnblogs.com/cukor-zhong-520/p/17300134.html

相关文章

  • 算法学习之冒泡排序【C语言】
    冒泡排序排序规则冒泡排序的规则是相邻的两个数字依次比较,如果前面的数字比后面的数字大,则交换它们的位置,否则保持不变,直到遍历完所有的数字。这个过程会不断地进行,直到所有的数字都按照从小到大的顺序排列好。双层循环在冒泡排序的算法中,需要使用两层循环来实现排序功能。for(int......
  • Ficow 的 AI 平台快速上手指南(ChatGPT, NewBing, ChatGLM-6B, cursor.so)
     本文首发于FicowShen'sBlog,原文地址:Ficow的AI平台快速上手指南(ChatGPT,NewBing,ChatGLM-6B,cursor.so)。 内容概览前言OpenAI——ChatGPT微软——NewBing智谱AI——ChatGLM-6BAI生成代码——cursor.so总结 前言 现在各种AI工具大爆发,赶紧......
  • 算法-递归三(树形结构)
    publicclassSolution{publicIList<IList<int>>Permute(int[]nums){varrtItem=newList<int>();varvisited=newDictionary<int,bool>();IList<IList<int>>rt=newList<IList<int&......
  • 基于TiDB+Flink实现的滑动窗口实时累计指标算法
    作者:Jellybean前言在不少的支付分析场景里,大部分累计值指标可以通过T+n的方式计算得到。随着行业大环境由增量市场转为存量市场,产品的运营要求更加精细化、更快速反应,这对各项数据指标的实时性要求已经越来越高。产品如果能实时把握应用的整体运行情况或特征用户的状态,就可......
  • 算法学习之选择排序【C语言】
    选择排序排序规则选择排序是一种简单直观的排序算法,其基本思想是每次从待排序的数据元素中选出最小(或最大)的一个元素,存放到序列的起始位置,直到全部元素排序完成。具体步骤如下:1.从第一个数开始,与其后的数一一比较,如后小前大,则交换,依次比较直至最后一组数。2.通过上述步骤,得到参加循......
  • ssh服务支持弱加密算法
    详情描述:远程SSH服务器配置为使用arcfour流密码或无任何密码。RFC4253不建议使用arcfour弱算法。https://tools.ietf.org/html/rfc4253#section-6.3解决方案:解决方法:*在SSH会话中仅使用CTR模式加密算法,如AES-CTR。https://tools.ietf.org/html/rfc4253#section-6.3方案......
  • 基于蛙跳算法的最优值计算matlab仿真
    1.算法描述            蛙跳算法是基于种群进化的元启发式算法之一,通过模拟自然界中青蛙觅食过程中种群所体现出的交流与合作行为,以实现对问题的求解。在一片湿地中,分布着一群青蛙,每只青蛙有自己的想法,每只青蛙的想法则被定义为一个解。每只青蛙找到食物时,都会......
  • C4.5分类树算法介绍
    为什么C4.5会出现?因为ID3算法节点的分支越多,信息增益也就越大,这会出现过拟合的现象,因此提出C4.5算法。图1C4.5的属性选择方法——获利比例获利比例=信息增益/分支度IV分支度IV与各分支下的类别数目之比成负相关:假如14个样本一共分4支:划分方法1为:分支1数目:分支2数目:分支......
  • 随机森林算法深入浅出
    目录一随机森林算法的基本原理二随机森林算法的优点1.随机森林算法具有很高的准确性和鲁棒性2.随机森林算法可以有效地避免过拟合问题3.随机森林算法可以处理高维度数据4.随机森林算法可以评估特征的重要性三随机森林算法的缺点1.随机森林算法对于少量数据集表现不佳2.随......
  • 理解回溯算法——从全排列问题开始
    一、简介回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。 二、从全排列问题开始理解回溯算法以数组[1,2,3]的全排列为例。先写以1开头的全排列,它们是:[1,2,3],[1,......