java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析

系列文章目录

(一)引论——为后续章节搭建一个学习平台


(二)算法分析——时间复杂度的分析法则及3个经典算法案例分析


(三)链表——ArrayList与LinkedList源码解析和应用场景以及手写实现LRU缓存淘汰算法


(四)队列——线程池中有限资源请求队列排队功能的实现原理及队列的手写实现


(五)栈——用户界面的前进跳转及回退机制如何实现及栈的手写实现


(六)Hash表——HashMap 的实现原理精讲及Hash思想在ThreadLocal与数据库索引中的应用


(七)Java容器结构总结


(八) 树——基本概念,以及huffman编码、二叉排序树、二叉平衡树的原理及手写实现


(九)散列和优先队列(堆)以及不相交集类


(十)图论(1)——拓扑排序、最短路径算法、网络流问题


(十 一)图论(2)——最小生成树、深度优先搜索的应用、NP-完全性介绍


(十二)算法设计技巧——贪婪算法、分治算法、动态规划算法、随机化算法和回溯算法


(十三)摊还分析——二项队列、斜堆、斐波那契堆、伸展树


(十四)高级数据结构及实现——自顶向下伸展树、红黑树、treap树、后缀数组与后缀树、k-d树、配对树


(十五)【面试】常用算法实战——排序算法、二分查找算法、kmp匹配算法、递归算法、大数据判存算法


(十六)【面试】B+树——MySql数据库索引是如何实现的


文章目录

  • 一、数学基础
  • 二、要分析的问题
  • 三、时间复杂度分析法则
    • 1、一个简单的例子
    • 2、分析法则
  • 四、3个经典算法案例
    • 1、斐波那契数列——递归算法?
    • 2、求解最大子序列和——动态规划算法
    • 3、计算最大公因数——欧几里得算法


前  言

算法(algorithm)是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。对于一个问题,一旦某种算法给定并且(以某种方式)被确定是正确的,那么重要的一步就是确定该算法将需要多少诸如时间或空间等资源量的问题。如果一个问题的求解算法竟然需要长达一年时间,那么这种算法就很难能有什么用处。同样,一个需要若干个GB( gigabyte)的内存的算法在当前的大多数机器上也是无法使用的。
\qquad
今天流弊要给大家分享的内容如下:
\qquad
java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析_第1张图片


正  文

一、数学基础


java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析_第2张图片
java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析_第3张图片
java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析_第4张图片

二、要分析的问题


通常,要分析的最重要的资源就是运行时间
影响运行时间的主要因素有:

  • 所使用的编译器和计算机——这超出了理论模型的范畴,不予考虑
  • 所使用的算法以及对该算法的输入
    \quad 平均运行时间——反映典型的行为
    \quad 最坏运行时间——对任何情形的一种保证

三、时间复杂度分析法则


估计一个算法的运行时间通常有两种方法:

  • 编码并运行——精准,但同一个算法在不同机器和不同编译器中运行的时间不一样,缺乏普遍性
  • 分析算法——大O标记法,关注的是算法运行时间的增长率问题,而这恰好能反映一个算法的优劣

1、一个简单的例子

下面是计算 ∑ i = 0 N i 3 的一个简单的程序片段 \text {下面是计算}\sum_{i=0}^N i^3\text {的一个简单的程序片段} 下面是计算i=0Ni3的一个简单的程序片段

public static int sum( int n ){
                  
	int partialSum;                         
	partialSum = 0;							
	for( int i = 1; i <= n; i++ ){
               
		partialSum += i * i * i;			
	}										
	return partialSum;						
}											

\qquad 对这个程序段的分析是简单的。所有的声明均不计时间。第3行和第7行各占一个时间单 元。第5行每执行一次占用4个时间单元(两次乘法,一次加法和一次赋值),而执行n次共占 用4n个时间单元。第4行在初始化 i、判断 i<=n 和对 i 的自增运算隐含着开销。所有这些的总开销是:初始化1个单元时间,所有的测试为n + 1个单元时间,而所有的自增运算为 n 个单元 时间,共2n + 2个时间单元。我们忽略调用方法和返回值的开销,得到总量是6n + 4个时间单元。因此,我们说该方法是O(N)。

\qquad 如果每次分析一个程序都要演示所有这些工作,那么这项任务很快就会变成不可行的负 担。幸运的是,由于我们有了大O的结果,因此就存在许多可以釆取的捷径并且不影响最后的结果。例如,第5行(每次执行时)显然是O(1)语句,因此精确计算它究竟是2、3还是4个时间单元是愚蠢的;这无关紧要。第3行与for循环相比显然是不重要的,所以在这里花费时间也是不明智的。


2、分析法则

法则1——for循环

一个for循环的运行时间至多是该for循环内部那些语句(包括判断语句)的运行时间乘以迭代的次数。

\qquad

法则2——嵌套的for循环

从里向外分析这些循环。在一组嵌套循环内部的一条语句总的运行时间为该语句的运行时 间乘以该组所有的for循环的大小的乘积。
例如,下列程序片段为O ( n 2 ) (n^2) (n2)

for( i=0;i<n;i++){
     
	for( j = 0; j < n; j++ ){
     
		k++
	}
}

\qquad

法则3——顺序语句

将各个语句的运行时间求和即可(这意味着,其中的最大值就是所得的运行时间)。
例如,下面的程序片段先是花费O(n), 接着是O ( n 2 ) (n^2) (n2), 因此总量也是O ( n 2 ) (n^2) (n2)

for( i = 0; i < n; i++ ){
     
	a[i] = 0;
}
for( i = 0; i < n; i++ ){
     
	for( j = 0; j < n; j++ ) {
     
		a[ i ] += a[ j ] + i + j;
	}
}

\qquad

法则4——if /else语句
if( condition ){
     
	s1
}else{
     
	s2
}

一个if /else语句的运行时间从不超过判断的运行时间再加上S1和S2中运行时间长者的总的运行时间。
\qquad

法则5——递归调用

如果有递归过程,那么存在两种情况:

1、若递归实际上只是被薄面纱遮住的for循环,则分析通常是很简单的

例如,下面的 factorial 方法(该方法对递归的使用并不算好)

PS:实际上就是一个简单的循环从而其运行时间为O(n)。

public static long factorial( int n ){
     
	if( n <= 1 ){
     
		return 1;
	}else
		return n * factorial( n - 1 );
	}
}

\qquad

2、当递归被正常使用时,分析将涉及求解一个递推关系

例如下面的例子(实际上它对递归的使用效率低的令人惊诧!稍后会讲解一种有效的方法)

下面算法的时间复杂度为:
T ( n ) = { O ( 1 ) , (  n ≤ 1  ) Ω ( ( 3 2 ) n ) , (  n > 1  ) T(n) = \begin{cases} O(1), & \text{( $n \leq 1$ )} \\ \Omega ((\frac {3} {2})^n), & \text{( $n > 1$ )} \end{cases} T(n)={ O(1),Ω((23)n),n1 )n>1 )
PS:运行时间以指数的速度增长

public static long fib( int n ){
     
	if( n <= 1 ){
     
		return 1;
	}else{
     
		return fib(n-l)+fib(n-2);
	}
}


初看起来,该程序似乎对递归的使用非常聪明。可是,如果将程序编码并在 n 值为50左右时运行,那么这个程序让人感到效率低得吓人。分析是十分简单的。令 T ( n ) T(n) T(n)为调用函数 f i b ( n ) fib(n) fib(n)的运行时间。如果 n = 0 或 n = 1,则运行时间是某个常数值,即第2行上做判断以及返回所用的时间。因为常数并不重要,所以我们可以说
T ( 0 ) = T ( 1 ) = 1 T(0)=T(1)=1 T(0)=T(1)=1
对于n的其他值的运行时间则相对于基准情形的运行时间来度量。若n>2,则执行该方法的时间是第2行上的常数工作加上第 5 行上的工作。第5行由一次加法和两次方法调用组成。由于方法调用不是简单的运算,因此 必须用它们自己来分析它们。第一次方法调用是 f i b ( n − 1 ) fib(n-1) fib(n1),从而按照T的定义它需要 T ( n − 1 ) T(n-1) T(n1)个时间单元。类似的论证指出,第二次方法调用需要 T ( n − 2 ) T(n-2) T(n2)个时间单元。此时总的时间需求为 T ( n − 1 ) + T ( n − 2 ) + 2 T(n-1) + T(n-2) +2 T(n1)+T(n2)+2,其中2指的是第2行上的工作加上第5行上的加法。于是对于 n ≥ 2 n \geq 2 n2 , 有下列关于 f i b ( n ) fib(n) fib(n)的运行时间公式:
T ( n ) = T ( n − 1 ) + T ( n − 2 ) + 2 T(n) =T(n-1) +T(n-2) +2 T(n)=T(n1)+T(n2)+2
但是 f i b ( n ) = f i b ( n − 1 ) + f i b ( n − 2 ) fib(n) =fib(n-1) +fib(n-2) fib(n)=fib(n1)+fib(n2),因此由归纳法容易证明
T ( n ) ≥ f i b ( n ) T(n) \geq fib(n) T(n)fib(n)
在上一篇文章中我们证明过
f i b ( n ) < ( 5 3 ) n fib(n) < (\frac {5} {3})^n fib(n)<(35)n
类似的计算可以证明
f i b ( n ) ≥ ( 3 2 ) n ( n > 4 ) fib(n) \geq (\frac {3} {2})^n \quad \text{( n > 4 )} fib(n)(23)n( n > 4 )

T ( n ) ≥ ( 3 2 ) n ( n > 4 ) T(n) \geq (\frac {3} {2})^n \quad \text{( n > 4 )} T(n)(23)n( n > 4 )
根据 定义2.2 可知
T ( n ) = Ω ( ( 3 2 ) n ) (  n > 4  ) T(n) = \Omega((\frac {3} {2})^n) \quad \text{( $n > 4$ )} T(n)=Ω((23)n)n>4 )
从而这个程序的运行时间以指数的速度增长。这大致是最坏的情况。通过保留一个简单的数组并使用一个for 循环,运行时间可以显著降低。

这个程序之所以运行缓慢,是因为存在大量多余的工作要做,违反了在上一篇文章中叙述的递归的第四条主要法则(合成效益法则)。注意,在第5行上的第一次调用即 f i b ( n − 1 ) fib(n-1) fib(n1)实际上在某处计算 f i b ( n − 2 ) fib(n-2) fib(n2)。这个信息被抛弃而在第5行上的第二次调用时又重新计算了一遍。抛弃的信息量递归地合成起来并导致巨大的运行时间。

\qquad

小结

显然在某些情形下这么估计有些过头,但决不会估计过低。

其他的法则都是显然的,但是,分析的基本策略是从内部(或最深层部分)向外展开工作 的。如果有方法调用,那么要首先分析这些调用。

\qquad

四、3个经典算法案例


1、斐波那契数列——递归算法?

流弊想通过跳槽涨薪,在一次面试中:
\qquad
面试官:请在5分钟内完成下面题目

java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析_第5张图片

\qquad

被面试官鄙视的解法

流弊:没问题(心想:这还不简单)
\qquad 啪!啪!啪!三下五除二,1分钟流弊就完事了(效率杠杠滴),

public class Fibonacci {
     
    /**
     * 递归实现
     */
    public static long fib_rec(int n){
     
        int[] arr = {
     0,1};
        if(n <= 2){
     
            return arr[n-1];
        }
        return fib_rec(n-1)+fib_rec(n-2);
    }
}

然后屁颠屁颠的拿给面试官看(心想:这下加薪有望了~~)

面试官一看,给了流弊一个鄙视的眼神

面试官: 你回去等通知吧!

。。。。。。

\qquad

流弊郁闷了(心想:我明明就写出来了,而且还是在1分钟内写出来的,怎么就被鄙视了呢?),到底是哪里出了问题呢?流弊百思不得骑姐!

于是流弊回家问了下度娘,终于找到了原因,原来是使用递归算法求斐波那契数列第n项,效率极其低下,其算法的时间复杂度是指数级增长的!
\qquad

面试官期待的解法

为了以后不踩同样的坑,流弊通过查阅各种资料,终于找到了一种高效的解法(时间复杂度为O(n)),代码如下:

public class Fibonacci {
     
     /**
     * 数组+for循环实现(其实就是 动态规划 思想)
     */
    public static long fib_for(int n){
     
        int[] arr = {
     0,1};
        if(n < 2){
     
           return arr[n];
        }
        long first,sencond,res;
        first=0;
        sencond=1;
        res=0;
        for (int i=2;i<n;i++){
     
            res = first + sencond;
            first = sencond;
            sencond = res;
        }
        return res;
    }
}

至此,流弊终得骑姐!
java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析_第6张图片

\qquad

装逼专用解法

通常面试到这里也就差不多了,尽管我们还有比这更快的O(logn)算法。由于这个算法需要用到一个很生僻的数学公式,因此很少有面试官会要求我们掌握(估计有些面试官自己都不知道)。不过为了装逼,我们还是简要介绍下。

介绍这个方法前,先介绍一个数学公式:
在这里插入图片描述

java数据结构与算法:算法分析—时间复杂度分析法则及3个经典算法案例分析_第7张图片

由于很少有面试官要求编程实现这种思路(这种解法编码复杂,且隐含的时间常数较大,不太适用面试,但是可以拿出来装下逼,展示你的学习能力,给面试官留下更好的印象),这里我就不做长篇大论了,有兴趣的读者可以根据这个思路自行尝试实现。

PS:若大家实在有这个需求,可在评论区留言,如果超过1000人有这个需求,我会单独拿出来进行详细深入的讲解,哈哈

在这里插入图片描述

\qquad

2、求解最大子序列和——动态规划算法

面试题描述

输入一个整型数组,数组里正负数都可能有,数组中的一个或者连续的多个整数组成一个子数组。 求所有子数组的和的最大值,要求时间复杂度为O(n)

这个题目可以使用递归算法、分治算法、贪心算法以及动态规划算法等多种算法实现,我们这里使用动态规划算法实现,代码如下:

public int maxSubArray(int[] nums) {
     
    if (nums == null || nums.length == 0) {
     
        return 0;
    }

    int thisSum = nums[0];
    int result = thisSum ;

    for (int i = 1; i < nums.length; ++i) {
     
        thisSum  = Math.max(thisSum , 0) + nums[i];
        result = Math.max(result, thisSum );
    }
    
    return result;
}

PS:如果详细了解分析思路,可以看我这篇文章:动态规划? so easy!!!

\qquad

3、计算最大公因数——欧几里得算法

public class GreatestCommonFactor {
     
    /**
     * 欧几里得算法——辗转相除法
     */
    public  static long gcf(long m, long n){
     
        long rem=0;
        while (n != 0){
     
            rem = m % n;//若m < n,接下来的代码其实就是将m , n 的值做个交换
            m = n;
            n = rem;
        }
        return m;
    }

    public static void main(String[] args){
     
            System.out.println(gcf(7,9));
    }
}

\qquad

总  结

今天主要给大家分享了下,算法分析中的时间复杂度的分析法则,以及通过3个经典面试题讲解了下递归算法、动态规划算法以及欧几里得算法。

下一章的内容:
《链表——ArrayList与LinkedList源码解析和应用场景以及手写实现LRU缓存淘汰算法》

你可能感兴趣的:(#,数据结构与算法,算法,数据结构,java,面试)