程序 = 数据结构 + 算法
一、数据结构基本概念
数据结构就是带“结构”的数据元素的集合,“结构”就是指数据元素之间存在的关系。
数据结构有逻辑结构和存储结构两个层次。
1. 逻辑结构
与数据存储无关,独立于计算机,从逻辑关系上描述数据。
两个要素:一是数据元素,二是关系。
根据数据元素之间关系的不同特性,有四类基本结构,复杂程度依次递进。
(1)集合结构数据元素 除了“属于同一集合”的关系外,别无其他关系。
例如,确定一名学生是否为班级成员,只需将班级看做一个集合结构。
(2)线性结构数据元素 存在一对一的关系。
例如,将学生信息数据按照其入学报到的时间先后顺序进行排列,将组成一个线性结构。
(3)树结构数据元素 存在一对多的关系。
例如,在班级的管理体系中,班长管理多个组长,每位组长管理多名组员,从而构成树形结构。
(4)图结构或网状结构数据元素 存在多对多的关系。
其中集合结构、树结构和图结构都属于非线性结构。
线性结构包括线性表、栈和队列(具有特殊限制的线性表,数据操作只能在表的一端或两端进行)、字符串(也是特殊的线性表,其特殊性表现在它的数据元素仅由一个字符组成)、数组(是线性表的推广,它的数据元素是一个线性表)、广义表(也是线性表的推广,它的数据元素是一个线性表,但不同构,即或者是单元素,或者是线性表)。
非线性结构包括树(具有多个分支的层次结构)和二叉树(具有两个分支的层次结构)、有向图(一种图结构,边是顶点的有序对)和无向图(另一种图结构,边是顶点的无序对)。这几种逻辑结构可以用一个层次图描述,如图所示。
2. 存储结构
把数据对象存储到计算机时,通常要求既要存储数据,又要存储逻辑关系,数据元素在计算机内用一个结点来表示。
有两种基本的存储结构,顺序存储结构和链式存储结构。
2.1 顺序存储结构
是把逻辑相邻的结点存储在物理位置相邻的存储单元中。
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。
顺序存储结构的主要优点是节省存储空间,分配给数据的存储单元全存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。
可实现对结点的随机存取,每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。
优点:随机存取表中元素、储存密度大。
缺点:插入和删除操作需要移动元素。
2.2 链式存储结构
用一组任意的存储单元存储线性表的数据元素(任意代表可以连续的,也可以不连续),不要求逻辑上相邻的元素在物理位置上也相邻,因此没有顺序存储结构所具有的弱点,但也同时失去了顺序表可随机存取的优点.
二、算法基本概念
算法(Algorithm)是为了解决某类问题而规定的一个有限长的操作序列。
1. 五个特性
(1)有穷性。一个算法必须总是在执行有穷步后结束,且每一步都必须在有穷时间内完成。
(2)确定性。对于每种情况下所应执行的操作,在算法中都有确切的规定,不会产生二义性,使算法的执行者或阅读者都能明确其含义及如何执行。
(3)可行性。算法中的所有操作都可以通过已经实现的基本操作运算执行有限次来实现。
(4)输入。一个算法有零个或多个输入。当用函数描述算法时,输入往往是通过形参表示的,在它们被调用时,从主调函数获得输入值。
(5)输出。一个算法有一个或多个输出,它们是算法进行信息加工后得到的结果,无输出的算法没有任何意义。当用函数描述算法时,输出多用返回值或引用类型的形参表示。
2 算法优劣的基本标准
(1)正确性。在合理的数据输入下,能够在有限的运行时间内得到正确的结果。
(2)可读性。一个好的算法,首先应便于人们理解和相互交流,其次才是机器可执行性。
可读性强的算法有助于人们对算法的理解,而难懂的算法易于隐藏错误,且难于调试和修改。
(3)健壮性。当输入的数据非法时,好的算法能适当地做出正确反应或进行相应处理,而不会产生一些莫名其妙的输出结果。
(4)高效性。高效性包括时间和空间两个方面。时间高效是指算法设计合理,执行效率高,可以用时间复杂度来度量;空间高效是指算法占用存储容量合理,可以用空间复杂度来度量。时间复杂度和空间复杂度是衡量算法的两个主要指标。
3 算法优劣的衡量标准
3.1时间复杂度
算法的时间复杂度是一个函数,定性描述了该算法的运行时间。
时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数 。
使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。
3.1.1 时间复杂度的排序
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) <O(n!) < O(n^n)
3.1.2 常见时间复杂度
对于时间复杂度,很多人的概念是比较模糊的。
下面举例子说明一些时间复杂度。
- O(1): 常数函数
- a=15
- O(logn): 对数函数
- 分析for(int i=1;i<n;i*=2):假设执行t次使得i=n;有2^t=n; t=log2~n,为log级别时间复杂度为O(logn)。
- 还有典型的二分查找,拓展欧几里得,快速幂等算法均为O(logn)。属于高效率算法。
- O(n): 线性函数
- for (int i=0;i<n;i++)
- 比较常见,能够良好解决大部分问题。
- O(nlogn):
- for (int i=1;i<n;i++)for (int j=1;j<i;j*=2)
- 常见的排序算法很多正常情况都是nlogn,比如快排、归并排序。这种算法效率大部分也还不错。
- O(n^2)
- for(int i=0;i<n;i++)for(int j=0;j<i;j++)
- 其实O(n^2)的效率就不敢恭维了。对于大的数据O(n^2)甚至更高次方的执行效果会很差。
如果同样是n=10000,那么不同时间复杂度额算法执行次数、时间也不同。
具体 | n | 执行次数 |
O(1) | 10000 | 1 |
O(log2n) | 10000 | 14 |
O( n^1/2) | 10000 | 100 |
O(n) | 10000 | 10000 |
O(nlog2 n) | 10000 | 140000 |
O(n^2) | 10000 | 100000000 |
O(n^3) | 10000 | 1000000000000 |
3.1.3 降低算法复杂度的方法
有些会靠数据结构的特性和优势,比如二叉排序树的查找,线段树的动态排序等等,这些数据结构解决某些问题有些非常良好的性能。
还有的是靠算法策略解决,比如同样是排序,冒泡排序这种笨而简单的方法就是O(n2),但快排、归并等聪明方法就能O(nlogn)。
要想变得更快,就得掌握更高级的数据结构和更精巧的算法。
3.1.4 时间复杂度计算
时间复杂度计算一般步骤:
1、找到执行次数最多的语句; 2、计算语句执行的数量级 ; 3、用O表示结果。
并且有两个规则:
加法规则:同一程序下如果多个并列关系的执行语句那么取最大的那个,eg:
T(n)=O(m)+O(n)=max(O(m),O(n));
T(n)=O(n)+O(nlogn)=max(O(n),O(nlogn))=O(nlogn);
乘法规则:循环结构,时间复杂度按乘法进行计算,eg:
T(n)=O(m)*O(n)=O(mn)
T(n)=O(m)*O(m)=O(m^2)(两层for循环)
当然很多算法的时间复杂度还跟输入的数据有关,分为还会有最优时间复杂度(可能执行次数最少时),最坏时间复杂度(执行次数最少时),平均时间复杂度,这在排序算法中已经具体分析,但我们通常使用平均时间复杂度 来衡量一个算法的好坏。
3.2 空间复杂度
关于算法的存储空间需求,类似于算法的时间复杂度,采用渐近空间复杂度(Space Complexity)作为算法所需存储空间的量度,简称空间复杂度,它也是问题规模n的函数,记作:S(n)=O(f (n))
一般情况下,一个程序在机器上执行时,除了需要寄存本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的辅助存储空间。其中,对于输入数据所占的具体存储量取决于问题本身,与算法无关,这样只需分析该算法在实现时所需要的辅助空间就可以了。
若算法执行时所需要的辅助空间相对于输入数据量而言是个常数,则称这个算法为原地工作,辅助空间为O(1)。
常量空间 O(1):存储空间大小固定,和输入规模没有直接的关系。
线性空间 O(n):分配的空间是一个线性的集合,并且集合大小和输入规模n成正比。
二维空间 O(n^2):分配的空间是一个二维数组集合,并且集合的长度和宽度都与输入规模n成正比。
递归空间 O(logn):递归是一个比较特殊的场景。虽然递归代码中并没有显式的声明变量或集合,但是计算机在执行程序时,会专门分配一块内存空间,用来存储“方法调用栈”。执行递归操作所需要的内存空间和递归的深度成正比。