1. 数据结构
1.1 核心名词解释
数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,并且确保经过这些运算后所得到的新结构仍然是原来的结构类型。
- 数据:程序的操作对象。能被输入到计算机中,且能被计算机处理。
- 数据项:数据的不可分割的最小单位。一个数据元素可由若干个数据项组成.(姓名,年龄,性别)
- 数据元素:组成数据对象的基本单位。(学生)
- 数据对象:性质相同的数据元素的集合(类型于数组)。(班级)
- 结构:数据元素之间不是独立的,存在特定的关系.这些关系即是结构;
- 数据结构:指的数据对象中的数据元素之间的关系
1.2 逻辑结构与物理结构
逻辑结构:
指数据与数据的逻辑关系。分为线性结构和非线性结构:
- 线性结构(数据元素之间一对一)
- 存在唯一的第一个和最后一个数据
- 除第一个元素外,每个元素都有一个前驱
- 除最后一个元素外,每个元素都有一个后驱
- 线性表,队列,栈,数组都是线性结构,字符串也是一种特殊的线性结构
- 非线性结构
集合:结构中的数据元素除了同属于一种类型外,别无其它关系。树形结构:数据元素之间一对多的关系(二叉树,红黑树)图状结构:结构中的数据元素之间存在多对多的关系
物理结构:
指数据的逻辑结构在计算机中的存储形式。物理结构是描述数据具体在内存中的存储.
顺序存储结构: 数据元素存放在一组存储地址连续的存储单元里,其数据元素间的逻辑关系和物理关系是一致的。链式存储结构: 数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。 | | 顺序存储结构 |链式存储结构 | |------|------------|------------| |存储分配的方式|静态分配|动态分配| |存储密度| 存储密度大(=1)| 存储密度小(<1) | |存储方式|随机存取,顺序存取|顺序存取| |插入/删除时移动元素个数| 平均需要移动近一半元素 |不需要移动元素,只需要修改指针|
存储密度 = 结点数据本身所占的存储量 / 结点结构所占的存储总量
顺序表适宜于做查找这样的静态操作;
链表宜于做插入、删除这样的动态操作。
若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。
2. 算法
算法是解决特定问题求解的描述,在计算机中指令的有序序列,并且每条指令表示一个或多个操作,就是求解一个问题的步骤。
2.1 数据结构与算法的关系
2.2 算法特性
- 输入输出: 算法具有零个或多个输入,至少有一个或多个输出。
- 有穷行: 算法可以在某一个条件下自动结束而不会出现无限循环
- 确定性: 算法执行的每一步骤在一定条件下只有一条执行路径,一个相同的输入对应相同的一个输出
- 可行性: 算法每一步骤都必须可行,能够通过有限的执行次数完成
2.3 算法效率衡量方法
- 正确性
- 可读性
- 健壮性
- 高效性
2.4 大O表示法
- 用常数1取代运行时间中所有常数 3->1 O(1)
- 在修改运行次数函数中,只保留最高阶项 n^3+2n^2+5 -> O(n^3)
- 如果在最高阶存在且不等于1,则去除这个项目相乘的常数 2n^3 -> n^3
2.5 算法时间复杂度
- 常数阶:
// 1+1+1+1+1+1+1 = 7 ->O(1)
void testSum2(int n) {
int sum = 0; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
printf("testSum2:%d\n",sum);//执行1次
}
- 线性阶:循环一次
// 1+(n+1)+n+1 = 3+2n ->O(n)
void testSum3(int n){
int i,sum = 0; //执行1次
for (i = 1; i <= n; i++) { //执行n+1次
sum += i; //执行n次
}
printf("testSum3:%d\n",sum); //执行1次
}
- 平方阶:循环二次
// x = x + 1; 执行n*n次 ->O(n^2)
void add3(int x,int n) {
for (int i = 0; i< n; i++) {
for (int j = 0; j < n ; j++) {
x = x + 1;
}
}
}
// 1+(n+1)+n(n+1)+n^2+n^2 = 2+3n^2+2n -> O(n^2)
void testSum5(int n) {
int i,j,x=0,sum = 0; //执行1次
for (i = 1; i <= n; i++) { //执行n+1次
for (j = 1; j <= n; j++) { //执行n(n+1)
x++; //执行n*n次
sum = sum + x; //执行n*n次
}
}
printf("testSum5:%d\n",sum);
}
- 立方阶:循环三次
void testB(int n){
int sum = 1; // 执行1次
for (int i = 0; i < n; i++) { // 执行n次
for (int j = 0 ; j < n; j++) { // 执行n*n次
for (int k = 0; k < n; k++) { // 执行n*n*n次
sum = sum * 2; // 执行n*n*n次
}
}
}
}
- 对数阶
// 2的x次方等于n x = log2n ->O(logn)
void testA(int n) {
int count = 1; // 执行1次
//n = 10
while (count < n) {
count = count * 2;
}
}
- 线性对数阶(nlogn)将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(nlogn),也就是了O(nlogn)。
for(m = 1; m < n; m++) {
i = 1;
while(i < n) {
i = i * 2;
}
}
- 指数阶:一般不考虑。除非是非常小的n,否则会造成噩梦般的时间消耗,这是一种不切实际的算法时间复杂度。
O(1) < O(log n) < O(n) < O(nlog n) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
我们查找一个有n个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为O(1),但也有可能这个数字就在最后一个位置,那么时间复杂度为O(n)。 平均运行时间是期望的运行时间。 最坏运行时间是一种保证。在应用中,这是一种最重要的需求,通常除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
2.6 算法空间复杂度
程序空间计算的因素有:1.寄存本身的指令;2.常数;3.变量;4.输入;5.对数据进行操作的辅助空间。
考量算法的空间复杂度,主要考虑算法执行时所需要的辅助空间
// 问题: 数组逆序,将一维数组a中的n个数逆序存放在原数组中.
int main(int argc, const char * argv[]) {
int n = 5;
int a[10] = {1,2,3,4,5,6,7,8,9,10};
//算法实现(1) 只用了一个temp ->O(1)
int temp;
for (int i = 0; i < n/2 ; i++) {
temp = a[i];
a[i] = a[n-i-1];
a[n-i-1] = temp;
}
//算法实现(2) -> O(n)
int b[10] = {0};
for (int i = 0; i < n;i++) {
b[i] = a[n-i-1];
}
for (int i = 0; i < n; i++) {
a[i] = b[i];
}
return 0;
}
对一个算法,其时间复杂度和空间复杂度往往会互相影响. 当追求一个较好的时间空间复杂度时,可能会导致占用较多的存储空间. 即可能会使用空间复杂度的性能变差.反之亦然. 不过,通常情况下,鉴于运算空间较为充足,人们都以算法时间空间复杂度作为算法优先的衡量指标。