什么是数据结构和算法?
数据结构和算法与做饭非常相似,首先我们先来看一下如何做一个煎饼:
- 将面粉、发酵粉、盐和糖在碗中混合
- 倒入牛奶和鸡蛋
- 搅拌到均匀
- 将煎锅加热
- 把面糊倒入锅中
- 将饼煎至两面金黄
一个算法就和上面煎饼过程非常相似,算法就是一步一步的执行计算机的指令。
配料: 面粉、发酵粉、盐、糖、鸡蛋、牛奶 就相当于算法中处理的数据。数据以一种形式(原始的)输入,然后以另一种形式出来。
所以数据结构是什么呢? 他们是在算法处理数据时保存数据的容器,就像是在上面例子中装面粉的袋子,煎饼的煎锅,盛放成品的盘子。
数据结构
基本概念和术语
数据结构中的基本概念:数据 数据元素 数据项 数据对象 数据结构
- 数据
是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。
- 数据元素
是具有一定意义的基本单位,在计算机通常作为整体处理。
例如在我们生活中,一个人、一辆车都是一个数据元素。
- 数据项
一个数据元素由若干个数据项组成,例如一个人的数据项是身高、体重、年龄等组成。
数据项是数据不可分割的最小单位
- 数据对象
是性质相同元素的集合,是数据的子集。
例如由若干个学生组成的班级就是一个数据对象。
- 数据结构
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
代码
下面是使用C语言表达数据之间的关系:
struct Person {
char * name; // 数据项(数据项)
int age; // 数据项(数据项)
float height; // 身高(数据项)
};
int main(int argc, const char * argv[]) {
struct Person person; // 数据元素
struct Person personArray[10]; // 数据对象
person.name = "leeyii"; // 数据项
person.age = 18; // 数据项
person.height = 180; // 数据项
return 0;
}
数据元素由若干个数据项组成。struct Person
是一个数据元素,它由 char *name
、 int age
和 float height
三个数据项组成。
数据对象是性质相同的元素的结合。struct Person personArray[10]
是一个数据对象,它是由10个相同性质的 struct Person
组成的数组。
由此我们可以总结出如下关系:
逻辑结构和物理结构
数据结构又可以分为逻辑结构和物理结构两种:
逻辑结构
逻辑结构是一种抽象的表达,他表示数据元素之间的逻辑关系。
根据数据对象之间的数据元素关系,我们可以将逻辑结构分为以下四种:
- 集合结构
集合结构中的元素除了同属一个集合外,他们之间没有其他的关系。
- 线性结构
线性结构中的元素具有一对一的关系。如:队列、线性表、栈、数组等。
- 树形结构
树形结构中的元素存在一对多的关系。如:二叉树、B树、红黑树。
- 图形结构
图形结构中的元素存在多对多的关系。如: 临近矩阵、邻接表等。
上面四种逻辑关系是抽象出来用来解决实际问题的,通过这样的关系能够清晰的表达数据元素之间的关系。
物理结构
物理结构也可以称作物理储存结构表示数据在计算机中内存中储存的形式。
数据的储存结构可以分为以下两种:
- 顺序储存结构
顺序储存结构就是在内存中开辟出连续的内存空间用来储存数据元素。
- 链式储存结构
链式储存结构可以将数据元素放在任意的储存单元,这些单元之间可以是连续的也可以是独立,因此我们需要在数据元素中添加指针,来关联与其相关联的数据元素。
总结:
顺序结构相比较链式储存结构的优点在于,每一个元素的位置是固定的,因此需要查找某一个位置的元素就非常的方便。但是对于一些复杂的结构,要改变元素之间的关系就非常困难了。
逻辑结构是面向问题的,而物理结构就是面向计算机的. 其基本的目标就是将数据以及逻辑关系存储到计算机的内存中.
算法
算法:是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作.
算法的特性
- 输入输出
一个算法需要有输入条件和输出结果。输出结果是一定要有的,如果没有结果,那么设计算法就没有意义了。
- 有穷性
在有限的步骤内能够解决问题,如果一个算法是无线循环会导致程序卡死,这样的算法肯定是不可取的。
- 确定性
算法的每一个步骤都需要具有明确的含义,不能有二义性。
- 可行性
算法的每一步都是可执行的。
算法设计的要求
- 正确性
正确性是算法的前提,一个算法最重要的就是计算出来的结果,与设计算法预期结果是一致的。
- 可读性
一个好的算法需要便于阅读和理解,如果一个算法用简单的几步完成,但是代码晦涩难懂,那么他也不能称之为好的算法。设计算法的时候可以辅以注释来增强可读性。
并不是代码越少,算法就越牛逼!!!
- 健壮性
一个好的算法应该能够应对各种复杂的条件,对输入进行判断,各种边界情况的处理。考虑即便算法的输入是不合法的,算法是否能够正常的运行。
- 时间效率高和存储量低
时间效率和空间效率是衡量一个算法好坏的关键,能够用越少的时间和空间来实现算法是再好不过的。
如何衡量一个算法
用来衡量一个算法好坏的两个关键指标是:时间复杂度和空间复杂度。
时间复杂度
算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
- 常数阶O(1)
int testSum1(int n) {
return (1 + n) * n / 2;
}
- 对数阶O(logn)
void test2(int n) {
int x = 1;
while (x < n) {
x = x * 2;
}
}
- 线性阶O(n)
int testSum2(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum = sum + i;
}
return sum;
}
- 线性对数阶O(nlogn)
void test3(int n) {
int x = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j = j * 2) {
x++;
}
}
}
- 平方阶O(n^2)
void test4(int n) {
int x = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j = j++) {
x++;
}
}
}
- 立方阶O(n^3)
- 指数阶O(2^n)
上面时间复杂度性能排序如下:
O(1) < O(log n) < O(n) < O(nlog n) < O(n^2) < O(n^3) < O(2^n)
最坏情况与最好情况
如果在数组中查找一个数字,如果查找的数字在数组的第一个,那么时间复杂度为O(1)
,如果查找的数字在最后一个,那么就是最坏的情况时间复杂度为O(n)
。
最坏的情况运行时间是一种保证, 那就是运行时间将不会比这更坏了. 在应用中,这是一种最重要的需求,通常除非特别指定,我们提到的运行时间都是最坏情况下的运行时间.
空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记做: S(n) = n(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数.
一个算法所需要的空间包括:
- 寄存本身的指令
- 常数
- 变量
- 输入
- 对数据进行操作的辅助空间
我们计算空间复杂度的时候一般考虑的都是对数据进行操作的辅助空间。
下面是反转一个数组的两种实现方式:
/// 算法实现1
void reversalArr1(int *arr, int length) {
int temp;
for (int i = 0; i < length / 2; i++) {
temp = arr[i];
arr[i] = arr[length - i - 1];
arr[length - i - 1] = temp;
}
}
/// 算法实现2
void reversalArr2(int *arr, int length) {
int b[length] = {};
for (int i = 0; i < length; i++) {
b[i] = arr[length - 1 - i];
}
for (int i = 0; i < length; i++) {
arr[i] = b[i];
}
}
上面两种实现中算法1只需要一个临时变量 temp
, 与问题规模 length
的大小无关,所以他的空间复杂度为 O(1)
.
算法2中需要借助一个临时数组 int b[length]
, 所以它的空间复杂度为 O(n)
.
参考
001--数据结构与算法之美(基础)
时间复杂度 百度百科