第一章 什么是算法和数据结构
1.什么是算法
算法,对应的英文是algorithm,这是一个很古老的概念,最早来自于数学领域。在数学领域,算法是用于解决某一类问题的公式或思想。
而我们所说的算法是计算机领域的算法,它的本质是一系列的指令,用于解决特定的运算和逻辑问题。
算法有简单的和复杂的
简单的算法,如一列整数找出最大值。
复杂的算法,如在多种物品里选择装入背包的物品,使背包里的总价值最大 ,或者找出一个城市到另一个城市的最短距离。
算法有高效的,也有拙劣的
在计算机领域衡量算法好坏的重要标准是:空间复杂度(运行时间的长短)、时间复杂度(占用空间的大小)
1.1时间复杂度
1.1.1 基本操作次数
为了方便计算,我们对程序基本操作执行次数的统计,设T(n)为程序基本操作次数的函数(也可以认为是相对执行时间),n为输入规模。
- 场景一 T ( n ) = 3 n T(n) = 3n T(n)=3n,执行次数是线性的
void eat1(int n){
for(int i=0; i<n; i++){
System.out.println("等待1分钟");
System.out.println("等待1分钟");
System.out.println("吃1cm 面包");
}
}
- 场景二 T ( n ) = 3 l o g n T(n) = 3logn T(n)=3logn,执行次数是用对数计算的。
void eat2(int n){
for(int i=n; i >1; i/=2){
System.out.println("等待1分钟");
System.out.println("等待1分钟");
System.out.println("吃1cm 面包");
}
}
- 场景三 T(n) = 3 执行次数是常量
void eat3(int n){
System.out.println("等待1分钟");
System.out.println("等待1分钟");
System.out.println("吃1cm 面包");
}
- 场景四 T ( n ) = 0.5 n 2 + 0.5 n T(n) = 0.5n^2 + 0.5n T(n)=0.5n2+0.5n,执行次数是用多项式计算的。
void eat4(int n){
for(int i=0; i<n; i++){
for(int j = 0; j<i;j++){
System.out.println("等待1分钟");
}
System.out.println("吃1cm 面包");
}
}
1.1.2 渐进时间复杂度
官方定义如下:
若存在函数f(n),使得当n趋近与无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n) = O(f(n)),称为O(f(n)),O为算法的渐进时间复杂度,简称为时间复杂度。
因为渐进时间复杂度用大写O来表示,所有也被称为大O表示法。
如何推导出时间复杂度?如下有几个原则:
- 如果运行时间是常数量级,则用常数1表示
- 只保留时间函数中的最高阶项
- 如果最高阶项存在,则省钱最高阶项前面的系数
- 场景一 T ( n ) = 3 n , T(n) = 3n, T(n)=3n,最高阶项为3,省去系数3,则转化的时间复杂度为: T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)
- 场景二 T ( n ) = 3 l o g n , T(n) = 3logn, T(n)=3logn,最高阶项为3,省去系数3,则转化的时间复杂度为: T ( n ) = O ( l o g n ) T(n) = O(logn) T(n)=O(logn)
- 场景三 T ( n ) = 3 , T(n) = 3, T(n)=3,最高阶项为3,省去系数3,则转化的时间复杂度为: T ( n ) = O ( 1 ) T(n) = O(1) T(n)=O(1)
- 场景四
T
(
n
)
=
0.5
n
2
+
0.5
n
,
T(n) = 0.5n^2 + 0.5n,
T(n)=0.5n2+0.5n,最高阶项为
0.5
n
2
0.5n^2
0.5n2,省去系数0.5,则转化的时间复杂度为:
T
(
n
)
=
O
(
n
2
)
T(n) = O(n^2)
T(n)=O(n2)
这四种时间复杂度当n的取值足够大时,可以得出如下结论: O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n 2 ) O(1)<O(logn)<O(n)<O(n^2) O(1)<O(logn)<O(n)<O(n2)
1.2空间复杂度
1.2.1 什么是空间复杂度
在运行一段程序时,我们不仅需要执行各种运算指令,同时还会根据需要储存一些临时的中间数据,以便后续指令可以方便地继续执行。储存中间数据所需要的内存大小我们叫做——空间复杂度,同样用大O表示,记作 S ( n ) = O ( f ( n ) ) S(n) = O(f(n)) S(n)=O(f(n))
1.2.2 空间复杂度的计算
- 常量空间
当算法空间大小固定,和输入规模没有直接关系时,空间复杂度记做 O ( 1 ) O(1) O(1)。如下代码
void fun1(int n) {
int var = 3;
...
}
- 线性空间
当算法分配的空间是一个线性的集合(如数组),并且集合大小和输入规模n成正比时,空间复杂度记作 O ( n ) O(n) O(n)。如下代码
void fun2(int n) {
int[] array = new int[n];
...
}
- 二维空间
当算法分配的空间是一个二维数组集合,并且集合的长度和宽度都与输入规格n成正比时,空间复杂度记作 O ( n 2 ) O(n^2) O(n2)。如下代码
void fun3(int n) {
int[][] array = new int[n][n];
...
}
- 递归空间
递归是一个比较特殊的场景。虽然递归代码中并没有显式地声明变量或者集合,但是计算机在执行程序时,会专门分配一块内存用来储存”方法调用栈“。
”方法调用栈“包括进栈和出栈两个行为。
当进入一个新方法时,执行入栈操作,把调用的方法和参数信息压入栈中。当方法返回时,执行出栈操作,把调用的方法和参数信息从栈中弹出。如下代码
void fun4(int n) {
if(n<1) {
return;
}
fun4(n-1);
...
}
从”方法调用栈“的出入栈操作可以看出,其内存空间和递归深度成正比。纯粹的递归操作的空间复杂度也是线性的,所以空间复杂度是 O ( n ) O(n) O(n)
2.什么是数据结构
数据结构,对应的英文单词是data structure,是数据的组织、管理和储存格式,其使用的目的是为了高效地访问和修改数据。
数据结构主要有哪些组成方式?
-
线性结构
线性结构是最简单的数据结构,包括数组、链表,以及由它们衍生出来的栈、队列、哈希表。 -
树
树是相对复杂的数据结构,其中比较有代表性的是二叉树,由它又衍生出了堆之类的数据结构。 -
图
图是更为复杂的数据结构,因为在图中会呈现出多对多的关联关系。 -
其他数据结构
除以上的几种数据结构以外,还有一些其他的千奇百怪的数据结构。它们由基本数据结构变形而来,用于解决特定问题,如跳表、哈希链表、位图等。