1. 数据算法与结构 - 概述
http://dongxicheng.org/structure/structure-algorithm-summary/
https://www.cnblogs.com/zhuzhenwei918/p/6227448.html 总结
http://www.cnblogs.com/zhuzhenwei918/p/6384675.html
http://www.cnblogs.com/zhuzhenwei918/p/6309101.html
http://blog.csdn.net/l_215851356/article/details/77659462
http://blog.fishc.com/category/structure/page/10 小甲鱼
http://www.cnblogs.com/alex3714/articles/5474411.html
1. 数据算法与结构 - 概述
程序设计 = 数据结构 + 算法
1. 数据和数据结点
数据是对客观事物的描述形式和编码形式的统称
数据是由数据元素组成的, 数据元素又称为数据结点, 简称结点.
每个数据元素又包括多个数据项, 每个数据项又称为结点的域, 其中, 用来唯一标识结点的域称为关键字.
2. 数据结构, 逻辑结构, 物理结构(存储结构)
数据结构分为逻辑结构和物理结构
逻辑结构: 数据对象中数据元素之间的相互关系
物理结构: 数据的逻辑结构在计算机中的存储方式(如何把数据元素存储到计算机的存储器中, 存储器是针对内存)
例:
一个有穷的结点集合D, 以及该集合中各结点之间的关系R, 组成一个数据结构, 表示成B=(D, R)
D和R是对客观事物的抽象描述, R表示结点间的逻辑关系, 所以(D, R)表示的数据的逻辑结构
而数据结构在计算机内的存储形式称为存储结构, 也称为物理结构
五大逻辑结构:
- 集合结构: 数据元素同属于一个集合
- 线性结构: 数据元素之间是一对一的关系
- 树形结构: 数据元素之间存在一对多的层次关系
- 图形结构: 数据元素是多对多的关系
- 散结构: 结点之间没有关系, 或者说存在特殊关系-无关关系
两种数据元素的存储结构形式:
顺序存储: 把数据元素存放在地址连续的存储单元里, 其数据间的逻辑关系和物理关系是一致的. Eg, 数组
链式存储: 把数据元素存放在任意的存储单元里, 这组存储单元可以是连续的, 也可以是不连续的. 链式存储结构的数据元素存储关系不能反映其逻辑关系, 因此需要用一个指针存放数据元素的地址, 这样子通过地址就可以找到相关联数据元素的位置.
3. 数据类型, 抽象, 抽象数据类型
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称. 例如: 整型, 浮点型, 字符型....
作用: 对数据类型进行分类, 然后根据数据类型来分配内存空间.
数据额类型分为两类:
原子类型: 不可以再分解的基本类型, 例如整型, 浮点型, 字符型等.
结构类型: 由若干个类型组合而成, 是可以再分解的, 例如整型数组是由若干整型数据组成的.
抽象 - 从一般意义上讲, 抽象是指"抽取事物的共性, 忽略个性; 体现外部特征, 掩饰具体细节". 抽象是一种思考问题的方式, 它隐藏了繁杂的细节.
抽象数据类型简称ADT(Abstract Data Type), 对已有的数据类型进行抽象. 它是将"数据"连同对其的"处理操作"(即运算)封装在一起而形成的复合体. 注意: ADT是对一个确定的数学模型, 以及定义在该模型上的一组操作的抽象表示, 不涉及具体的实现.
4. 算法的定义, 算法与数据结构, 程序的关系, 算法的描述
算法(Algorithm)是解决特定问题求解步骤的描述, 在计算机中表现为指令的有限序列, 并且每条指令表示一个或多个操作 – 解决问题的技巧和方式. 算法能够对一定规范的输入, 在有限时间内获得所要求的输出. 如果一个算法有缺陷, 或不适合于某个问题, 执行这个算法将不会解决这个问题. 不同的算法可能用不同的时间, 空间或效率来完成同样的任务. 一个算法的优劣可以用空间复杂度与时间复杂度来衡量.
一个算法应该具有以下七个重要的特征:
①有穷性(Finiteness):算法的有穷性是指算法必须能在执行有限个步骤之后终止;
②确切性(Definiteness):算法的每一步骤必须有确切的定义;
③输入项(Input):一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
④输出项(Output):一个算法有一个或多个输出,以反映对输入数据加工后的结果. 没有输出的算法是毫无意义的;
⑤可行性(Effectiveness):算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步,即每个计算步都可以在有限时间内完成(也称之为有效性);
⑥高效性(High efficiency):执行速度快, 占用资源少;
⑦健壮性(Robustness):对数据响应正确.
5. 时间复杂性与空间复杂性
算法的时间需求称为算法的时间复杂性(时间复杂度), 指运行时间的需求
算法的空间需求称为算法的空间复杂性(空间复杂度), 指空间需求
算法的空间复杂度通过计算算法所需的存储空间实现, 算法的空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数.
在写代码时, 可以通过一笔空间上的开销来换取计算时间的开销, 这取决用于什么地方.
6. 时间复杂性的大O表示法*****
计算机科学中, 算法的时间复杂性(Time Complexity)是一个函数, 它定量描述了该算法的运行时间, 时间复杂性常用大O符号(Big O notation)表示, 用于描述函数渐进行为. 大O记法, 简而言之可以认为它的含义是"order of"(大约是).
算法的时间复杂性是指输入数据量n(即问题实例的规模)的函数, 也可以说是一个算法中的语句执行次数称为语句频度或时间频度, 记为T(n), 可以理解为T就是指Time, 即时间 (执行次数 = 时间).
比如, 某排序算法, 对10000个元素排序所用的时间要比100个元素排序的时间长, 其中10000和100就是输入数据量. 评价一个算法的时间复杂性, 就是要设法找出函数T(n).
一般, 不可能精确计算, 而只能估算T(n). 做法:求出当输入数据量n逐渐加大时, 也就是当n趋近于无穷的时候T(n)的极限情况, 称为算法的"渐近时间复杂性".
推导大O阶方法
1. 用常数1取代运行时间中的所有加法常数
2. 在修改后的运行次数函数中, 只保留最高阶项(降低了计算难度, 也能比较客观的反映出当n很大时,算法的时间性能)
3. 如果最高阶项存在且不是1, 则去除与这个项相乘的常数
- O(1) 常数阶, 常数时间, 即算法时间和输入量n没有关系
- O(logn) 对数阶, 对数时间. 对数阶不写底数(因为与之无关)
- O(n) 线性阶, 线性时间, 即算法时间用量和n成正比
- O(nlogn) 线性对数阶, 非常理想的阶
- O(n2) 平方阶时间
- O(n3) 立方阶
- O(2n) 指数阶
常用的时间复杂度所耗费的时间从小到大依次是:O(1) < O(logn) < (n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
O(1) - 常数阶
Temp=i; i=j; j=temp;
以上三条单个语句的频度均为1, 该程序段的执行时间是一个与问题规模n无关的常数.
此算法的运行次数函数是f(n) = 3. 算法的时间复杂度为常数阶, 记作T(n)=O(1). 如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数. 此类算法的时间复杂度是O(1).
O(logn) - 对数阶
对数: a的x次方等于N (a>0 & a!=1), 记做x = logaN. Eg. 52 = 25 <-> 2 = log525
int count = 1; // 语句1 while (count <= n){ count = count * 2; // 语句2, 时间复杂度为O(1)的程序步骤序列 }
解: 语句1的频度是1,
设语句2的频度是f(n), 则: 2f(n)<=n; f(n)<=log2n
取最大值f(n) = log2n,
T(n) = O(log2n) = O(logn)
由于每次count乘以2之后, 就距离n更近了一分, 假设有x个2相乘后大于或等于n, 则会退出循环.
于是由2x=n得到x=log2n. 所以这个循环的时间复杂度为O(logn).
O(n) - 线性阶
a=0;
b=1; //语句1
for (i=1;i<=n;i++){ //语句2
s=a+b; //语句3
b=a; //语句4
a=s; //语句5
}
解: 语句1的频度: 2,
语句2的频度: n,
语句3的频度: n-1,
语句4的频度: n-1,
语句5的频度: n-1,
T(n) = 2+n+3(n-1) = 4n-1 = O(n).
O(n2) - 平方阶
执行时间随问题规模增长呈正比例增长
//交换i和j的内容 sum=0; //1次 for(i=1;i<=n;i++) //n次 for(j=1;j<=n;j++) //n2次 sum++; //n2次
解:T(n)= 2n2+n+1 = O(n2)
for(i=1;i<n;i++){ y=y+1; //语句1 for (j=0;j<=(2*n);j++) x++; //语句2 }
解: 语句1的频度是n-1
语句2的频度是(n-1)*(2n+1) = 2n2-n-1
f(n) = 2n2-n-1+(n-1) = 2n2-2
该程序的时间复杂度T(n) = O(n2).
int i, j, n = 100; for( i=0; i < n; i++ ){ for( j=i; j < n; j++ ){ printf("I am Charon”); } }
解: 由于当i=0时. 内循环执行了n次, 当i=1时, 内循环则执行n-1次……当i=n-1时, 内循环执行1次,所以总的执行次数应该是:
n+(n-1)+(n-2)+…+1 = n(n+1)/2. 所以, n(n+1)/2 = n2/2+n/2.
最终T(n) = O(n2)
O(n3) - 立方阶
for(i=0;i<n;i++){ for(j=0;j<i;j++){ for(k=0;k<j;k++) x=x+2; } }
解:当i=m, j=k的时候, 内层循环的次数为k. 当i=m时, j可以取 0,1,...,m-1, 所以这里最内循环共进行了0+1+...+m-1=(m-1)m/2次. 所以, i从0取到n, 则循环共进行了: 0+(1-1)*1/2+...+(n-1)n/2 = n(n+1)(n-1)/6. 所以时间复杂度为O(n3).
7. 最坏情况和平均情况
所谓最坏情况是指, 对于具有相同输入数据量的不同输入数据, 算法时间用量的最大值.
所谓平均情况是指, 对于所有相同输入数据量的各种不同输入数据, 算法耗时的平均值
递归和分治思想
递归定义
使用递归能使程序的结构更清晰、更简洁、更容易让人理解, 从而减少读懂代码的时间.
int Fib(int i){ if( i < 2 ) return i == 0 ? 0 : 1; return Fib(i-1) + Fib(i-2); }
int factorial( n ){ if( 0 == n ) return 1; else return n * factorial( n - 1 ); }
假设传入的值为5:
红: 1*1=1 return 黄: 2*1=2 return 绿:3*2=6 return 青: 4*6=24 return 蓝: 5*24 return
例3: 将输入的任意长度的字符串反向输出, 遇#结束输入
void print() { char a; scanf(“%c”, &a); if( a !=‘#’) print(); if( a !=‘#’) printf(“%c”, a); }
输入字符串:ABC#
分治思想
当一个问题规模较大且不易求解的时候, 可以考虑将问题分成几个小的模块, 逐一解决.
采用分治思想处理问题, 其各个小模块通常具有与大问题相同的结构, 这种特性十分适合递归.
例: 折半查找算法(二分法搜索)的递归实现
基本思想: 减小查找序列的长度, 分而治之地进行关键字的查找.
折半查找的实现过程是:先确定待查找记录的所在范围,然后逐渐缩小这个范围,直到找到该记录或查找失败(查无该记录)为止。
例如有序列:1 1 2 3 5 8 13 21 34 55 89(该序列包含 11 个元素,而且关键字单调递增。),现要求查找关键字 key 为 55 的记录。
我们可以设指针 low 和 high 分别指向关键字序列的上界和下界,指针 mid 指向序列的中间位置,即 mid = (low+high)/2。
#include<stdio.h> #define SIZE 10 typedef int ElemType; int refind(ElemType *data,int begin,int end,ElemType num); int main(void){ ElemType data[SIZE]={10,20,30,40,50,60,70,80,90,100}; ElemType num; for(int i = 0;i<SIZE;i++) printf("%d ",data[i]); printf("\n请输入要查找的数据:\n"); scanf("%d",&num); int flag = refind(data,0,SIZE,num); printf("位置为:%d\n",flag); return 0; } / //递归 int refind(ElemType *data,int begin,int end,ElemType num) { if(begin > end) { printf("没找到\n"); return -1; } int mid = (begin+end)/2; if(data[mid] == num) { return mid; }else if(data[mid] <= num) return refind(data,mid+1,end,num); else return refind(data,begin,mid-1,num); }
递归函数时间复杂度分析
例子:求N!。
这是一个简单的"累乘"问题,用递归算法也能解决。
n! = n * (n - 1)! n > 1
0! = 1, 1! = 1 n = 0,1
因此,递归算法如下:
Java代码
fact(int n) {
if(n == 0 || n == 1)
return 1;
else
return n * fact(n - 1);
}
以n=3为例,看运行过程如下:
fact(3) ----- fact(2) ----- fact(1) ------ fact(2) -----fact(3)
------------------------------> ------------------------------>
递归 回溯
递归算法在运行中不断调用自身降低规模的过程,当规模降为1,即递归到fact(1)时,满足停止条件停止递归,开始回溯(返回调用算法)并计算,从fact(1)=1计算返回到fact(2);计算2*fact(1)=2返回到fact(3);计算3*fact(2)=6,结束递归。
算法的起始模块也是终止模块。
(2) 递归实现机制
每一次递归调用,都用一个特殊的数据结构"栈"记录当前算法的执行状态,特别地设置地址栈,用来记录当前算法的执行位置,以备回溯时正常返回。递归模块的形式参数是普通变量,每次递归调用得到的值都是不同的,他们也是由"栈"来存储。
(3) 递归调用的几种形式
一般递归调用有以下几种形式(其中a1、a2、b1、b2、k1、k2为常数)。
<1> 直接简单递归调用: f(n) {...a1 * f((n - k1) / b1); ...};
<2> 直接复杂递归调用: f(n) {...a1 * f((n - k1) / b1); a2 * f((n - k2) / b2); ...};
<3> 间接递归调用: f(n) {...a1 * f((n - k1) / b1); ...},
g(n) {...a2 * f((n - k2) / b2); ...}。
2. 递归算法效率分析方法
递归算法的分析方法比较多,最常用的便是迭代法。
迭代法的基本步骤是先将递归算法简化为对应的递归方程,然后通过反复迭代,将递归方程的右端变换成一个级数,最后求级数的和,再估计和的渐进阶。
<1> 例:n!
算法的递归方程为: T(n) = T(n - 1) + O(1);
迭代展开: T(n) = T(n - 1) + O(1)
= T(n - 2) + O(1) + O(1)
= T(n - 3) + O(1) + O(1) + O(1)
= ......
= O(1) + ... + O(1) + O(1) + O(1)
= n * O(1)
= O(n)
这个例子的时间复杂性是线性的。
<2> 例:如下递归方程:
T(n) = 2T(n/2) + 2, 且假设n=2的k次方。
T(n) = 2T(n/2) + 2
= 2(2T(n/2*2) + 2) + 2
= 4T(n/2*2) + 4 + 2
= 4(2T(n/2*2*2) + 2) + 4 + 2
= 2*2*2T(n/2*2*2) + 8 + 4 + 2
= ...
= 2的(k-1)次方 * T(n/2的(i-1)次方) + $(i:1~(k-1))2的i次方
= 2的(k-1)次方 + (2的k次方) - 2
= (3/2) * (2的k次方) - 2
= (3/2) * n - 2
= O(n)
这个例子的时间复杂性也是线性的。
<3> 例:如下递归方程:
T(n) = 2T(n/2) + O(n), 且假设n=2的k次方。
T(n) = 2T(n/2) + O(n)
= 2T(n/4) + 2O(n/2) + O(n)
= ...
= O(n) + O(n) + ... + O(n) + O(n) + O(n)
= k * O(n)
= O(k*n)
= O(nlog2n) //以2为底
一般地,当递归方程为T(n) = aT(n/c) + O(n), T(n)的解为:
O(n) (a<c && c>1)
O(nlog2n) (a=c && c>1) //以2为底
O(nlogca) (a>c && c>1) //n的(logca)次方,以c为底
上面介绍的3种递归调用形式,比较常用的是第一种情况,第二种形式也有时出现,而第三种形式(间接递归调用)使用的较少,且算法分析
比较复杂。 下面举个第二种形式的递归调用例子。
<4> 递归方程为:T(n) = T(n/3) + T(2n/3) + n
为了更好的理解,先画出递归过程相应的递归树:
n --------> n
n/3 2n/3 --------> n
n/9 2n/9 2n/9 4n/9 --------> n
...... ...... ...... ....... ......
--------
总共O(nlogn)
累计递归树各层的非递归项的值,每一层和都等于n,从根到叶的最长路径是:
n --> (2/3)n --> (4/9)n --> (12/27)n --> ... --> 1
设最长路径为k,则应该有:
(2/3)的k次方 * n = 1
得到 k = log(2/3)n // 以(2/3)为底
于是 T(n) <= (K + 1) * n = n (log(2/3)n + 1)
即 T(n) = O(nlogn)
由此例子表明,对于第二种递归形式调用,借助于递归树,用迭代法进行算法分析是简单易行的。
迭代法就是迭代的展开方程的右边,直到没有可以迭代的项为止,这时通过对右边的和进行估算来估计方程的解。比较适用于分治问题的求解,为方便讨论起见,给出其递归方程的一般形式:
【举 例】下面我们以一个简单的例子来说明:T(n)=2T(n/2)+n2,迭代过程如下:
容易知道,直到n/2^(i+1)=1时,递归过程结束,这时我们计算如下:
到这里我们知道该算法的时间复杂度为O(n2),上面的计算中,我们可以直接使用无穷等比数列的公式,不用考虑项数i的约束,实际上这两种方法计算的结果是完全等价的,有兴趣的同学可以自行验证。