通过对基础数据结构和算法的学习,能更深层次的理解程序,提升编写代码的能力,让程序的代码更优雅,性能更高。
内容:
1.数据结构和算法概述
2.算法分析
3.排序
4.线性表
5.符号表
6.树
7.堆
8.优先队列
9.并查集
10.图
一、数据结构和算法概述
1.1什么是数据结构
官方:数据结构是一门研究非数值计算的程序问题中的操作对象,以及他们之间的关系和操作等相关问题的学科
大白话:数据结构就是把数据元素一定的关系组织起来的集合,用来组织和存储数据
1.2数据结构分类
传统上,我们可以把数据结构分为逻辑结构和物理结构两大类
逻辑结构分类:
逻辑结构是从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象数据元素之间的互相分类
a.集合结构:集合结构中数据元素除了属于同一个集合外,他们之间没有任何其他的关系
b.线性结构:线性结构中的数据元素之间存在一对一的关系
c.树形结构:树形结构中的数据元素之间存在一对多的层次关系
d.图形结构:图形结构的数据元素是多对多的关系
物理结构分类:
逻辑结构在计算机中的真正的表达方式(又称为映像)成为物理结构
顺序存储结构:
把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的
查找元素很方便
顺序存储存在一定的弊端:插入元素或删除元素后,这个时候整个结构都处于变化中,此时需要链式存储结构
链式存储结构:
是把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的。此时,数据元素之间并不能反映元素间的逻辑关系,因此在链式存储结构中引进了一个指针存放数据元素的地址 ,这样通过地址就可以找到相关联数据元素的位置
二、算法分析
1.1算法的时间复杂度分析
事后分析估算方法:
利用计算机计时器对不同的算法编制的程序的运行时间进行比较,从而确定算法效率的高低。但是这种方法存在很大的缺陷:必须依据算法实现编制好的测试程序,通常要花费大量的精力和时间,测试完如果发现测试的是非常糟糕的算法,那么之前所做的事情就全部白费了,并且不同的测试环境(硬件环境)的差别导致测试的结果差异也很大。
事前分析估算方法:
在计算机程序编写前,依据统计方法对算法进行估算,经过总结,我们发现一个高级语言编写的程序在计算机上运行所消耗的时间取决于下列因素:
1.算法采用的策略和方案
2.编译产生的代码质量——————例如javac,无法控制
3.问题的输入规模(所谓的问题输入规模就是输入量的多少)
4.机器执行指令的速度——————硬件决定的,无法干预
由此可见,一个程序的运行时间依赖于算法的好坏和问题的输入规模,如果算法固定,那么该算法的执行时间就只和问题的输入规模有关
举例
一、计算1到100的和
第一种解法:
第二种解法:
如果把第一种算法的循环体看作是一个整体,忽略结束条件的判断,那么其实这两个算法运行时间的差距就是n和1的差距。
为什么循环判断在算法1里执行了n+1次,看起来是个不小的数量,但是可以忽略呢?
例子:
计算100个1+100个2+100个3+...+100个100的结果
这个例子中,如果我们要精准的研究循环的条件执行了多少次,是一件很麻烦的事,由于真正计算机的代码是内循环的循环体,所以,在研究算法的效率时,我们只考虑核心代码的执行次数,这样可以简化分析。
分析一个算法的运行时间,最重要的就是把核心的次数和输入的输入规模关联起来
1.1.1函数渐进增长
给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么f(n)的渐进增长快于g(n)
1、随着输入规模的增大,算法的常规操作可以忽略不计。
2、随着输入规模的增大,与最高次项相乘的常数可以忽略。
3、最高此项的指数大的,随着n的增长,结果也会变得增长特别快。
4、算法函数中n最高次幂越小,算法效率越高
5、算法函数中的常数可以忽略
6、算法函数中最高次幂的常数因子可以忽略
1.1.2算法时间复杂度
1.1.2.1大O记法
定义:用大写O()来体现算法时间复杂度的记法,称之为大O记法。
算法一:
算法二:
算法三:
如果忽略判断条件的执行次数和输出语句的执行次数,那么当输入规模为n时,以上算法执行的次数分别为:
算法一:3次
算法二:n+3次
算法三:n^2+2次
大O记法规则
1.用常数1取代运行时间中的所有加法常数
2.在修改后的运行次数中,只保留高阶项
3.如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数
所以,上述算法的大O记法为:
算法一:O(1)
算法二:O(n)
算法三:O(n^2)
1.1.2.2常见的大O阶
1、线性阶
一般含有非嵌套循环涉及线性阶,线性阶就是随着输入规模的扩大,对应计算次数呈直线增长
上面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码需要执行n次
2.平方阶
一般嵌套循环属于这种时间复杂度
这段代码的时间复杂度是O(n^2)
3.立方阶
一般三层嵌套循环属于这种时间复杂度
这段代码的时间复杂度是O(n^3)
4.对数阶
由于每次i*2之后,就距离n更进一步,假设x个2相乘后大于n,则会退出循环,由于是2^x=n,得到x=log(2)n;所以这个循环的时间复杂度为O(logn)
对于对数阶,由于随着输入规模n的增大,不管底数为多少,他们的增长趋势是一样的,所以忽略底数
5.常数阶
一般不涉及循环操作的都是常数,因为它不会随着n的增长而增加操作次数
上述代码,不管输入规模n是多少,都执行2次,根据大O推导法则,常数用1来替换,所以上述代码的时间复杂度为O(1)
下面是对常见时间复杂度的一个总结
他们的复杂程度从低到高依次为:
O(1)
从平方阶开始,随着输入规模的增大,时间成本会急剧增大,所以我们的算法,尽可能的追求的是O(1)、O(logn)、O(n)、O(nlogn)这几种时间复杂度,但如果发现的时间复杂度为平方阶、立方阶或者更复杂的,那可以分为这种算法是不可取的,需要优化
1.1.2.3函数调用的时间复杂度分析
案例一:
在main方法中,有一个for循环,循环体调用了show方法,由于show方法内部只执行一行代码,所以show方法的时间复杂度为O(1),那main方法的时间复杂度就是O(n)
案例二:
在main方法中,有一个for循环,循环体调用了show方法内部也有一个for循环,所以show方法的时间复杂度为O(n^2)
案例三:
在show方法中,有一个for循环,所以show方法的时间复杂度为O(n),在mian方法中,show(n)这行代码内部执行的次数为n,第一个for循环调用了show方法,所以其执行次数为n^2,第二个嵌套for循环内只执行一行代码,所以其执行次数为n^2,那么mian方法总执行次数为n+n^2+n^2=2n^2+n。根据大O推导规则,去掉n保留最高阶项,并去掉最高阶项的常数因子2,所以最终main方法的时间复杂度为O(n^2)
1.1.2.4最坏情况
例子:
有一个存储了n个随机数字的数组,请从中查找出指定的数字
最好的情况:查找的第一个数字就是期望的数字,那么算法的时间复杂度为O(1)
最坏的情况:查找最后一个数字,才是期望的数字,那么算法的时间复杂度为O(n)
平均情况:任何数字查找的平均成本是O(n/2)
最坏情况是一种保证,在应用中,这是一种最基本的保障,即使在最坏的情况下,也能够正常提供服务,所以,除非特别指定,我们提到的运行时间都指的是最坏的情况下的运行时间
1.2算法的空间复杂度分析
1.2.1java中常见内存占用
1.基本数据类型内存占用情况:
2.计算机访问内存的方式都是一次一个字节
3.一个引用(机器地址)需要8个字节表示:
例如:Date date = new Date(),则date这个变量需要占用8个字节来表示
4.创建一个对象,比如new Date(),除了Data对象内部存储的数据(例如年月日等信息)占用的内存,该对象本身也有内存开销,每个对象的自身开销是16个字节,用来保存对象的头信息
5.一般内存的使用,如果不够8个字节,都会被自动填充成8字节
6.java中数组被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组一般需要24个字节的头信息(16个自己的对象开销,4个字节用于保存长度以及4个填充字节)再加上保存值所需的内存
1.2.2算法的空间复杂度
案例:对指定的数组元素进行反转,并返回反转的内容
解法一:
解法二:
忽略判断条件占用的内存,得出的占用情况如下:
解法一:不管传入的数组大小为多少,始终额外申请4+4=8个字节
解法二:4+4n+24=4n+28
根据大O推导法则,算法一的空间复杂度为O(1),算法二的空间复杂度为O(n),所以从空间复杂度的角度讲,算法一要优于算法二
了解java的基本内存占用,是我们对java程序的内存占用情况进行估算