[从今天开始修炼数据结构]线性表及其实现以及实现有Itertor的ArrayList和LinkedList
[从今天开始修炼数据结构]栈、斐波那契数列、逆波兰四则运算的实现
[从今天开始修炼数据结构]队列、循环队列、PriorityQueue的原理及实现
从双十一低价购入了一批书,拖到今天还没开始看,实在不该不该。所以我决定从今天开始修炼数据结构和算法,打下坚实基础!
学习路线:《大话数据结构》程杰 和 《算法》Robert Sedgewick 两本书对照学习。 再加上网上搜集的优秀博文的参考。代码实现使用Java,写下此篇博文作为记录。 —— 2019.11.25 8:49在B326实验室。
另外,这两本书缺少动态规划这一部分,如果有幸有人看到我这篇随笔,请推荐你觉得优秀的动态规划教程给我~
下面开始正式内容:
一、什么是数据结构
学习数据结构,首先要搞清楚什么是数据,数据相关的概念。
数据就是计算机可以识别、操作的符号集合。
数据元素是组成数据的、有一定意义的基本单位。 也被称为记录。
数据项:一个数据元素可以由若干个数据项组成。 比如人这个数据元素,可以有眼睛、耳朵、鼻子等若干数据项。
数据对象是性质相同的数据元素的集合。 比如所有的人,都具有眼睛、耳朵、鼻子,那么人的集合就是数据对象。
数据结构:相互之间存在一种或多种特定关系的数据元素的集合。 编写一个好的程序,要分析、组织、处理对象的特性和处理对象之间的关系。
数据结构分为 逻辑结构 和 物理结构。
逻辑结构:集合结构 线性结构 树形结构 图形结构
物理结构:顺序存储 链式存储
在高级语言中,数据类型可以分为 原子类型(不可分解的基本类型) 和结构类型(对象、结构体等)。二者都属于抽象数据类型。下面给出描述抽象数据类型的标准格式。
ADT 抽象数据类型名
Data
数据元素之间逻辑关系的定义
Operation
操作1
初始条件
操作结果描述
操作2
……
操作n
……
endAD
二、什么是算法
算法是解决特定问题求解步骤的描述。在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。简单说算法也就是解决问题的方法。
一个好的算法要满足
1,正确性
没有语法错误,对于合法的输入能产生满足要求的输出;对于非法的输入能够得出满足规格说明的结果;对于刁难的测试数据也有满足要求的输出
2,可读性
算法设计的目的之一是便于阅读、理解和交流。
3,健壮性
一个好的算法应该能对输入数据不合法的情况做合适的处理,而不是产生异常或者莫名其妙的结果。
4,时间效率高和存储量低
时间和空间复杂度分析。事后统计方法有很大缺陷,不科学,不准确,我们一般使用事前分析估算方法来评判算法的效率。在计算机程序编制前,依据统计方法对算法进行估算。
我们在分析一个算法的运行时间时,重要的是把基本操作的数量与输入规模关联起来,即基本操作的数量必须表示成输入规模的函数。随着n值越来越大,它们在时间效率上的差异也就越来越大。
那么我们如何来表示算法在时间上的差异呢?这就引入了算法时间复杂度
三、时间复杂度
判断一个算法好不好,我们通过少量的数据是不能做出准确判断的,我们可以对比算法的关键执行次数函数的渐进增长性,得到某个算法随着n的增大,越来越优于(或劣于)另一算法。
在判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。分析高阶项,关键就是分析循环结构的运行情况。
算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的时间复杂度。其中f(n)是问题规模n的某个函数。
1,常数阶:对于单纯的分支结构(不包含在循环结构里),执行次数是恒定的,不会随着n的变大而发生变化,其时间复杂度是O(1).
2,线性阶:对于下面这段代码,因为循环体中的代码要执行n次,所以它的时间复杂度为O(n)
int i;
for(i = 0; i < n; i++)
3,对数阶:看下面这段代码。
int count = 1;
while (count < n){
count = count * 2;
}
count每次循环之后乘2,距离n更接近了一倍。也就是说,有多少个2相乘后大于n,则会退出循环。由2x=n得到x = log2n。所以这个循环的时间复杂度是O(logn)
4,平方阶:对于常规的循环嵌套
for(i = 0; i < n; i++){
for(j = 0; j < n; j++){
//一段O(1)的程序
}
}
这段代码的时间复杂度为O(n2);如果外循环的循环次数改为m。时间复杂度就变为O(m×n)。
我们可以总结得出:循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。
看下面这个循环嵌套。
for (i = 0; i < n; i++){ for (j = i; j < n; j++){
//一段O(1)的程序 } }
当i = 0时,内循环执行n次;当i = 1时,内循环执行了n - 1次;…… ; 当i= n -1 时,内循环执行了一次。所以总执行次数为
n + (n - 1 )+ (n - 2) + …… + 1 = n(n + 1)/2 = n2/2 + n/2 。去除不予考虑的常熟、低阶项等,得到时间复杂度为O(n2).
常用的时间复杂度所耗费的时间从小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
O(nlogn)会在后面讨论到,待补充。 O(n3) 及更高的时间复杂度太高,一般不讨论。
在实际情况中,遇到的问题都会有好有坏。比如在数组中顺序查找一个数字,最好的情况一开始找第一个位置就是目标;最坏的情况找到最后一个位置才发现目标。那么对应的最好情况的时间复杂度就是O(1),最坏情况就是O(n)。除非特别指定,我们提到的时间复杂度都是按照最坏情况来考虑的运行时间。那么我们在设计算法的时候希望的是哪个运行时间呢?我们希望的是平均运行时间,对于上面的例子就是n/2.平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。
四、空间复杂度
我们在写代码时,完全可以用空间换取时间。 举个例子,要判断某年是否是闰年:常规方法时写一个算法,设置判断条件,通过计算得到是否是闰年。而另一个方法是,设置一个足够长的,比如2050个元素的数组,把所有年份按下标对应,如果是闰年,数组值设为1,如果不是值设为0.这样判断是否是闰年可以直接到数组中按照下标取值。这样我们的运行时间将为了O(1) ,但是硬盘或者内存要存储这样一个很长的数组。这就是空间换取时间的做法。
到底哪个好?看你要用在什么地方。
算法的空间复杂度通过计算算法所需的存储空间实现,算法弓箭复杂度的计算公式记作:S(n) = O(f(n))。 n为问题规模,f(n)为语句关于n所占存储空间的函数。