主要资料来源:
数据结构与算法
提取码:1256
内涵《java数据结构与算法》一书
把数据元素按照一定的关系组织起来,用来组织和存储数据
从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象中数据元素之间的相互关系分类;
a.集合结构(元素之间无关系)
b.线性结构(元素之间一对一)
c.树形结构(元素之间一对多)
d.图形结构(元素之间多对多)
逻辑结构在计算机中真正的表示方式,即存储结构
a.顺序存储结构
把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的,如数组
b.链式存储结构
把数据元素存放在任意的存储单元,这组存储单元可以是连续的也可以是不连续的。因此在链式存储结构中引进了一个指针存放数据元素的地址,通过地址找到相关联数据元素的位置
在特定计算模型下,在信息处理过程中为了解决某一类问题而设计的一个指令序列。
算法具备的特性:
输入:待处理的信息,
输出:经过处理之后得到的信息,即问题的答案。
确定性:任一算法都可以描述为由若干种基本操作组成的序列。
可行性:在相应的计算模型中,每一基本操作都可以实现,且能够在常数时间内完成。
有穷性:对于任何输入,按照算法,经过有穷次基本操作都可以得到正确的输出。
总之,算法追求花最少的时间占用最少的内存空间解决问题。
计算1到100的和(使用高斯求和,花最少的时间,占用最少的内存空间):
int sum = 0;
int n=100;
sum = (n+1)*n/2;
(1)事后分析估算方法(不适用)
(2)事前分析估算方法:
高级语言编写的程序程序在计算机上运行所消耗的时间取决于下列因素:
1.算法采用的策略和方案;
2.编译产生的代码质量;
3.问题的输入规模(所谓的问题输入规模就是输入量的多少);
4.机器执行指令的速度;
抛开与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。
如:
计算100个1+100个2+100个3+…100个100的结果
代码:
int sum = 0;
int n = 100;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
sum += i;
}
}
在研究算法的效率时,只考虑核心代码的执行次数( sum += i;
),这样可以简化分析。
概念:
给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么f(n)的增长渐近快于g(n)
随着输入规模的增大,算法的常数操作可以忽略不计;
随着输入规模的增大,与最高次项相乘的常数可以忽略;
算法函数中n最高次幂越小,算法效率越高
(1)大O记法
定义:
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况并确定T(n)的量级。算法的时间复杂度,就是算法的时间量度,记作:T(n)=O(f(n))。它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模n的某个函数。执行次数=执行时间,一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法。
基于对函数渐近增长的分析,推导大O阶的表示法有以下几个规则可以使用:
1.用常数1取代运行时间中的所有加法常数;
2.在修改后的运行次数中,只保留高阶项;
3.如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数;
如:
public static void main(String[] args) {
int n = 100;
show(n);
for (int i = 0; i < n; i++) {
show(i);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println(j);
}
}
}
private static void show(int i) {
for (int j = 0; j < i; i++) {
System.out.println(i);
}
}
在show方法中,有一个for循环,所以show方法的时间复杂度为O(n),在main方法中,show(n)这行代码内部
执行的次数为n,第一个for循环内调用了show方法,所以其执行次数为n^2,第二个嵌套for循环内只执行了一行代
码,所以其执行次数为n^2,那么main方法总执行次数为n+n^2+n^2=2n^2+n。
根据大O推导规则,去掉n保留最高阶项,并去掉最高阶项的常数因子2,所以最终main方法的时间复杂度
为O(n^2)
(2)对常见时间复杂度的总结
描述 | 增长的数量级 | 说明 | 举例 |
---|---|---|---|
常数级别 | 1 | 普通语句 | 将两个数相加 |
对数级别 | logN | 二分策略 | 二分查找 |
线性级别 | N | 循环 | 找出最大元素 |
线型对数级别 | NlogN | 分治思想 | 归并排序 |
平方级别 | N^2 | 双层循环 | 检查所有元素对 |
立方级别 | N^3 | 三层循环 | 检查所有三元组 |
指数级别 | 2^N | 穷举查找 | 检查所有子集 |
O(1)
尽可能的追求O(1),O(logn),O(n),O(nlogn)这几种时间复杂度,如果发现算法的时间复杂度为平方阶、立方阶或者更复杂的,那算法需要优化。
(2)最坏情况
如:
有一个存储了n个随机数字的数组,请从中查找出指定的数字:
public int search(int num) {
int[] arr = {11, 10, 8, 9, 7, 22, 23, 0};
for (int i = 0; i < arr.length; i++) {
if (num == arr[i]) {
return i;
}
}
return -1;
}
最好情况:
查找的第一个数字就是期望的数字,那么算法的时间复杂度为O(1)
最坏情况:
查找的最后一个数字才是期望的数字,那么算法的时间复杂度为O(n)
平均情况:
任何数字查找的平均成本是O(n/2)
最坏情况是一种保证,在应用中,这是一种最基本的保障,即使在最坏情况下,也能够正常提供服务,所以,除非特别指定,运行时间都指的是最坏情况下的运行时间。
(2)计算机访问内存的方式都是一次一个字节:
(3)一个引用(机器地址)需要8个字节表示:
如: Date date = new Date(),则date这个变量需要占用8个字节来表示
(4)创建一个对象,如new Date(),除了Date对象内部存储的数据占用的内存,对象本身也有内存开销,每个对象的自身开销是16个字节,用来保存对象的头信息
(5)一般内存的使用,如果不够8个字节,都会被自动填充为8字节:
(6)java中数组被被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组一般需要24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存。
算法的空间复杂度计算公式记作:S(n)=O(f(n)),其中n为输入规模,f(n)为语句关于n所占存储空间的函数。
如:
public static int[] reverse1(int[] arr) {
int n = arr.length;//申请4个字节
int temp;//申请4个字节
for (int start = 0, end = n - 1; start <= end; start++, end--) {
temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
return arr;
}
public static int[] reverse2(int[] arr) {
int n = arr.length;//申请4个字节
int[] temp = new int[n];//申请n*4个字节+数组自身头信息开销24个字节
for (int i = n - 1; i >= 0; i--) {
temp[n - 1 - i] = arr[i];
}
return temp;
}
算法一:不管传入的数组大小为多少,始终额外申请4+4=8个字节;
算法二:4+4n+24=4n+28;
根据大O推导法则,算法一的空间复杂度为O(1),算法二的空间复杂度为O(n),所以从空间占用的角度讲,算法一要优于算法二。
java中有内存垃圾回收机制,并且jvm对程序的内存占用也有优化(如即时编译),由于现在的计算机设备内存一般都比较大,所以内存占用一般情况下并不是算法的瓶颈,普通情况下直接说复杂度,默认为算法的时间复杂度。