数据结构与算法
程序是由数据结构加上算法组成的,数据结构相当于是程序的基石,而算法则是将这些基石构建出那些令世人惊叹的奇迹建筑的方法。
何为数据结构
要了解数据结构,那么就必须要先了解何为数据。
数据
数据就是描述客观事物的符号,他们可以输入到计算机中,可以被计算机程序进行处理,例如我们的整形,字符型,声音,视频都可以被称为数据,他们都可以通过一定的方式来让计算机能够处理这些数据。
数据之间的关系
- 数据元素:数据元素是组成数据的基本单位。
- 数据项:一个数据元素可以由若干个数据项来构成。
- 数据对象:是性质相同的数据元素的集合(其实这有点像java的面相对象的概念,在面向对象的概念中我们也会将具有相同的特征属性的元素定义为类对象)。
- 数据结构:数据结构,其实从字面上理解就是各个数据元素之间的关系与组合所形成的结构(其实这很类似于我们化学中的分子结构,他们也是各个元素之间以特定的关系与组合才形成了不同的分字子结构),所以我们把相互之间有一种或多种特定关系的数据元素的集合就称为数据结构。
何为算法
算法是解决特定问题的特定步骤,在计算机中表现为指令的有序序列,其实算法就是我们解决问题的核心程序步骤。
数据结构与算法的关系
有些时候可能会有人把他们分开来讲,但是其实他们之间的关系更像是共生的关系,他们既有自己的概念与体系,但是却又是不可分割的一对共生体,抛开算法我们只说数据结构就显得没有什么意义,因为我们只知道结构而不会运用,同样反之亦然,我只知道使用却不理解基础结构就会浮于表面无法得其真谛。
算法复杂度
算法设计的要求
一个算法的设计我们需要满足四个要求
- 正确性:语法的正确,输入输出的合法性,以及结果的正确性。
- 可读性:算法的设计的另一目的就是为了便于阅读,理解和交流。
- 健壮性:一个算法应该能对不同的数据输入都能做出相关的处理,不论是正确的还是错误的数据,而不是直接程序出错或者是得到不正确的结果。
- 时间短存储量少:好的算法在保证上面三点要求之外还应该是时间短存储量少的一个解决方式。(这一点会出现两个概念就是时间复杂度和空间复杂度的概念,这两个我们后面会细讲)
如何一个算法的好坏
一般我们会使用两种方式(但是正经人谁用这两种方式啊,不都是瞅一眼就知道了)
- 事后统计法:我更习惯称为“真男人方法”;可能同样时间、同样的地点、同一个人、同样的东西,你可能坚持个五秒钟就好了,其他人可能就坚持了半小时还没好,这时候我们就会说还是短点好啊,省时又省力。(其实这个方法就是通过测试设计好的程序和数据来比较那个的结果更好)
- 事前分析估算法:我们在编程之前就先对我们的算法进行统计学的方式进行估计,然后再判断算法是否符合要求,是否还可以更加的优化等。
如何判断一个算法的复杂度
这里我们以一个简单的例子来引出时间复杂度的概念,下面的例子很明显sum2函数的方式,所花的时间和步骤都要少于sum1函数,那么我就可以说sum2的时间复杂度优于sum1。
// 利用循环的方式实现1——n(令n为100)的和
public static void sum1() {
int result = 0;
// 这个算法的执行步骤是100次,时间复杂度是O(n)
for (int i=1; i<=100; i++) {
result += i;
}
System.out.println(result);
}
// 利用算法的方式实现求和(1-n)
public static void sum2() {
int result;
int n = 100;
// 真正的算法执行步骤,只需要一次,时间复杂度是O(1)
result = (1 + n) * n / 2;
System.out.println(result);
}
大O表示法
算法的时间复杂度我们有专门的表示方法和表示规则
时间复杂度的表示
- 时间复杂度表示规则是,只有常数就为1,含有变量就舍去常数、低阶项、把系数化为1的规则来进行表示。
- 时间复杂度的表示方法,是O(X),X为算法复杂度,他是经过表示规则的化简之后的值(当然这里X的值都是基于对应执行次数的极限思维得到估算结果)。
- 我们以一张常见的复杂度表来展示复杂度的表示方式。
时间复杂度的计算
我们以斐波那契数列的任意位置的求值计算来讲解一下时间复杂度的计算方式(当然这里的时间复杂度的计算方式都是满足上面我们所说的,表示规则与舍去规则)。
/**
* 利用不同的算法实现斐波那契数列的第任意个数据的求值
*/
public static int fib1(int n) { // 使用递归实现的Fibonacci数列
if (n<2) return n;
return fib1(n-1) + fib1(n-2);
// 他的结果是O(2^n),下面我们会单独分析这个例子
}
public static int fib2(int n) { // 通过数学分析循环实现前两个相加为后一个值
if (n<2) return n;
int f = 0;
int s = 1;
for(int i=0; i<(n-1); i++) {
s = f + s;
f = s - f;
// 我们只计算循环内部的语句和外部的语句,所以结果就是2+2n,
// 经过表示规则与常数、系数、低阶舍去,得到最终的结果O(n)。
}
return s;
// 他的结果是O(n)
}
public static int fib3(int n) {
// 这里我们可以发现其实斐波那契数列可以使用数学公式进行求解,也就是我们所说的特征方程
double x = Math.sqrt(5);
double y = (Math.pow((1 + x) / 2, n) - Math.pow((1 - x) / 2, n)) / x;
// 最后我们返回的结果是int类型,进行类型转换
return (int)y;
// 他的结果是O(1)
}
fib1函数的时间复杂度分析,递归的话我们一般是使用树来进行分析。我们以n为5来画图;得到的结果就是(1+2+4+8 = 2^0 + 2^1 + 2^2 + 2^3 = 2^4 - 1 = 0.5 * 2^n -1),所以时间复杂度就是O(2^n)。(注意:上面的公式并不是规律,对于n为6的情况就不能简单的在公式后面加上 2^4,想的话可以验证一下)。
多层循环的情况
多层循环需要对内外循环的控制条件产生相应的变化;如果是内外层循环次数没有关系一般我们会得到相应的高次的时间复杂度,如果有关系那么我们就要对循环控制条件进行判断与计算,一般会得到一些带有log或其他东西的结果,这个需要根据具体情况而定。
public void test1(int n) {
// 当n为16时;分析第一次16,二次8,三次4.四次2,结束一共四次运行,就是16 = 2^4
while ((n = n / 2) > 0) {
System.out.println("log2^n");
// n = n / 2;
}
// 他的结果是O(logn)
}
/*
这是多层循环的情况分析
*/
public void test2(int n) {
for (int i = 1; i < n; i = i * 2) {
// 外层循环其实和上面的例子本质是一样,除二与乘二其实都是对2求log,所以外层显然就是log2^n次
for (int j = 0; j < n; j++) {
// 内层就直接是n次了,所以附上外层就是nlog2^n
System.out.println("test");
}
}
// 他的结果是O(nlogn)
}
空间复杂度
其实对于空间复杂度我们一般不太会去计算,空间复杂度的计算就是看程序消耗的储存空间是多少;但是在时机情况中我们一般会在时间和空间上面用更多空间去换取更少的时间,因为大家都不喜欢等待嘛,喜欢的东西第一眼看见就立刻想要得到。就算麻烦一点也没关系。
多数据规模情况
当我们出现不止一个部分需要使用算法来实现目的的时候,其实和我们程序的执行是差不多的,我们只需要将不同部分的算法的时间复杂度相加求合即可。
public static void one(int n,int m){
for (int i=0; i<n; i++){
System.out.println(1);
}
for (int i=0; i<m; i++){
System.out.println(2);
}
// 这个算法的时间复杂度就是O(n+m)
}
算法的优化方向
- 用尽量少的存储空间和用尽量少的执行步骤。
- 可以根据情况进行时间与空间的取舍,不一定就非要保证是时间,或非要保证空间,视情况而定。