Java版数据结构和算法+AI算法和技能

106 阅读8分钟

Java版数据结构和算法+AI算法和技能

核心代码,注释必读

// download:3w ukoou com

数据结构和算法介绍 「算法」就是解决问题的方法或者过程。如果我们把问题看成是函数,那么算法就是将输入转换为输出的过程。「数据结构」是数据的计算机表示和相应的一组操作「程序」则是算法和数据结构的具体实现

「数据结构」 指的是:数据的组织结构,用来组织、存储数据

展开来讲,数据结构研究的是数据的逻辑结构、物理结构以及它们之间的相互关系,并对这种结构定义相应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。

数据结构的作用,就是为了提高计算机硬件的利用率。比如说:操作系统想要查找应用程序 「Microsoft Word」 在硬盘中的哪一个位置存储。如果对硬盘全部扫描一遍的话肯定效率很低,但如果使用「B+ 树」作为索引,就能很容易的搜索到 Microsoft Word 这个单词,然后很快的定位到 「Microsoft Word」这个应用程序的文件信息,从而从文件信息中找到对应的磁盘位置。

Java版数据结构和算法+AI算法和技能 - 算法复杂度

数据结构与算法经常是放在一起讲,这两者是没办法独立的,因为算法是为了达到某种目的的一种实现方式,而数据结构是一种载体,也就是说算法必须依赖数据结构这种载体,否则就是空谈。换句话说:数据结构是为算法服务的,而算法又需要作用在特定的数据结构之上。

一个算法到底好不好,我们如何去评价?前面我们提到了,你的代码好不好,最直观的就是看响应速度,算法也一样,同样实现一个目的(比如说排序),谁的算法速度快,我们就可以认为谁的算法更优,如果说两种算法实现的速度差不多,那么我们还可以去评价算法所占用的空间,谁占用的空间少,那么就可以认为谁的算法更优,这就是算法的基础:时间复杂度和空间复杂度。

学习算法之前,我们必须要学会如何分析时间复杂度和空间复杂度(也就是“快”和“省”),否则自己写出来的算法自己都不知道算法的效率。

时间复杂度大 O表示法

接触过算法的都知道,算法的时间复杂度是用大写的“O”来表示的,比如:O(1)O(n)O(logn)O(nlogn)O(n²) 等等。

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系,上面的这种时间复杂度表示法并不能真正反应一个算法的执行时间,反应的只是一个趋势,所以我们在分析复杂度的时候要关注“变”,忽略“不变”。

变指的是变量,也就是一段代码的执行时间是随着变量的变化而变化的,而不变指的是常量,也就是不论我的变量如何改变,执行时间都不会改变。

接下来我们就实际的来分析下常用时间复杂度的例子来练习一下。

O(1) 常数阶

0(1) 复杂度算法也称之为常数阶算法。这里的 1 是用来代指常量,也就是说这个算法的效率是固定的,无论你的数据量如何变化,效率都一样,这种复杂度也是最优的一种算法。

public static void print(int n){
    int a = 1;
    int b = 2;
    int c = 3;
    int sum = a + b + c;
    System.out.println(sum);
}

上面的示例中不论有多少行代码,时间复杂度都是属于常数阶。换言之:只要代码不存在循环递归等循环类调用,不论代码有多少行,其复杂度都是常数阶。

O(n) 线性阶

O(n) 复杂度算法也称之为线性阶。比如下面这个示例我们应该怎么分析复杂度呢?

public static void print1(int n){
    int a = 0;
    for (int i=0;i<n;i++){
        System.out.println(i);
    }
}

前面常量阶没分析是因为常量阶比较容易理解,接下来我们就以线性阶这个为例子来分析下具体是怎么得到的。

我们假设每一行代码的执行时间是 T,那么上面这段代码的执行复杂度是多少呢?

答案很明显,那就是 T+n*T,也就是 (n+1)T,而在算法中有一个原则,那就是常量可以被忽略,所以就得到了 nT,换成大 O 表示法就是 O(n)

这只是一个简略的计算过程,大家也不用较真说每行代码执行时间可能不一样之类的,也不要较真说 for 循环占用了一行,下面的大括号也占用了一行,如果要较真这个,那我建议可以去想一下 1=1 为什么等于 2

算法中的复杂度反应的只是一个趋势,这里 O(n) 反应的就是一个趋势,也就是随着 n 的变化,算法的执行时间是会降低的。

O(n²) 平方阶

知道了上面的线性阶,那么平方阶就很好理解了,双层循环就是平方阶,同理,三次循环就是立方阶,k 次循环就是 k 次方阶。

O(logn) 对数阶

O(logn) 也称之为对数阶,对数阶也很常见,像二分查找,二叉树之类的问题中会见到比较多的对数阶复杂度,但是对数阶也是比较难理解的一种算法复杂度。

下面我们还是来看一个例子:

public static void print2(int n){
    int i=1;
    while (i <= n) {
        i = i * 2;
    }
}

这段代码又该如何分析复杂度呢?这段代码最关键就是要分析出 while 循环中到底循环了多少次,我们观察这个循环,发现 i 并不是逐一递增,而是不断的翻倍:1->2->4->8->16->32->64 一直到等于 n 为止才会结束,所以我们得到了这样的一个公式:2^x=n

也就是我们只要计算出 x 的值,就得到了循环次数,而根据高中的数学知识我们可以得到 x=log2n2 在下面,是底数,试了几种方法都打不出来,放弃了),所以根据上面线性阶的分析方法,我们省略常量,就得到了示例中的算法复杂度为 O(log2n)

同样的分析方式,下面的例子,我们可以很快的分析出复杂度就为 O(log3n)

int i=1;
while (i <= n) {
    i = i * 3;
}

上面得到的 log3n 我们可以再做进一步的转换:log3n=log32 * log2n,而 log32(注意这几个地方的 3 是底数,在下面) 是一个常量,常量可以省略,所以也就得到了:O(log3n)=O(log2n)。同样的道理,不论底数是多少,其实最终都可以转化成和 O(log2n) 相等,正因为如此,为了方便,我们算法中通常就会省略底数,直接写作 O(logn)

上面的数学公式大家如果忘了或者看不懂也没关系,只要记住不论对数的底数是多少,我们都算作 O(logn),而对于一个算法的复杂度是否是对数阶,还有一个简易的判断方法:当循环中下标以指定倍数形式衰减,那么这就是一个对数阶

O(nlogn) 线性对数阶

如果理解了上面的对数阶,那么这种线性对数阶就非常好理解了,只需要在对数阶的算法中再嵌一层循环就是线性对数阶:

for (int j=1;j<=n;j++){
    int i=1;
    while (i <= n) {
        i = i * 2;
    }
}

分析了前面这些最常用的时间复杂度,其实我们可以得到以下规律:

  • 只要是常量级别,不论多大,效率都是一样的(如:常量阶复杂度例子)。
  • 分析一段代码的时间复杂度,只需要分析执行次数最多的一段代码(如:所以例子中我们只分析了循环体中代码执行次数)。
  • 嵌套代码的复杂度等于嵌套内外代码复杂度的乘积(如:分析线性对数阶复杂度例子)。