数据结构和算法总结(一)

任何一位有志于驾驭计算机的学生,都应该从这些方面入手,重点是:不断学习,反复练习,勤于总结。

究竟什么是算法呢?所谓算法,是指基于特定的计算机模型,旨在解决某一问题而设计的一个指令序列。

算法应具有以下流程:输入与输出;基本操作即加减乘除;确定性即明确的指令序列,可行性即可在对应计算机模型实现的指令序列;有穷性和正确性,有限步骤和正确结果。

算法的特性:计算效率主要包括时间复杂度和空间复杂度。同一种算法也有多种不同的实现方式从而效率不同。

在算法的输入,输出还是中间结果,在计算机中都可以数据的形式表示,以及数据的存储,组织,转移和变换等操作,不同计算平台和模型都会影响整体算法的效率。即数据结构的重要性。我认为算法更像一种思想,但依托于具体问题,具体数据结构的。所以以具体的数据结构来总结与学习算法。最终的结果就是找到最优解或者忧解,也就是归结于问题结果的查找问题。

时间复杂度:随着输入规模的扩大,算法的执行时间将如何增长?

n\rightarrow T(n)的表示方法。但是对于小规模可忽略,但对于大规模可关注其渐进上界的大小可用\sigma 来表示表示,

T(n)= \sigma (f(n))(n)=\sigma (f(n)),有两种意图:有函数的常系数可以忽略为1,多项式中的低次项可忽略。

如何计算f(n)呢?其等于算法所执行基本操作的总次数。基本操作为算数运算、比较、分支、子程序调用与返回等:而这些基本操作均可在常数时间内完成。在最坏情况下的响应速度才是唯一的指标。

当然还有对于应的最好时间复杂度\Omega,以及中间时间复杂度\Theta

空间复杂度:时间复杂度是其天然上界,现在主要是用加空间减时间。时间的重要性更强。

关于时间复杂度的取值情况:常数,对数(对于其底数的取值无所谓),对数多项式复杂度(虽不如常数复杂度但仍可接近)

,线性,多项式(实际应用中可接受),指数(实际应用无解,但小规模可能低于多项式)。

实际情况:绝大多数问题并不存在多项式时间算法,有些问题还需要无穷时间。

输入规模:用以描述输入所需的空间规模。

数据结构:数据项之间的某种逻辑次序。包括线性结构,半线性结构,非线性结构。

线性结构:各数据项按照一个线性次序构成一个整体,主要包括向量,列表,栈与队列。

半线性结构:树结构,只要附加某种约束(遍历),便可以在树的元素之间确定某种线性次序。

非线性结构:相互之间均可能存在二元关系的一组对象,此类一般性二元关系,即为图论。

基本数据结构有:向量,列表,栈与队列,二叉树,图,搜索树。

高级数据结构:高级搜索树,词典,优先级队列。

现代数据结构普遍遵从“信息隐藏”的理念,通过统一的接口和内部封装,构成ADT,抽象数据类型。

2.递归

递归是允许函数和过程进行自我调用。递归 的特点:简洁,减少代码量,提高算法的可读性,保证算法的整体效率。

递归形式:线性递归,二分递归,多分支递归,实现遍历,分治等算法策略。

1.线性递归

每一递归实例对自身的调用至多一次。于是每一层上至多只有一个实例,且它们构成一个线性的次序关系。

应用问题可分解为两个独立的子问题,一个对应于单独的某个元素,直接求解。另一个与原问题相同。往往对应与减而治之的策略:没深入一层,问题规模均缩减为一个常数。实例:数组求和。

递归跟踪求递归的时间复杂度和空间复杂度:所有递归实例的创建、执行和销毁所需的时间总和。正比于递归深度。

多递归基,多向递归。

尾递归:在线性递归算法中,若递归调用在递归实例中恰好以最后一步操作的形式出现,则称作尾递归。

尾递归的写法只是具备了使当前函数在调用下一个函数前把当前占有的栈销毁,但是会不会真的这样做,是要具体看编译器是否最终这样做,如果在语言层面上,没有规定要优化这种尾调用,那编译器就可以有自己的选择来做不同的实现,在这种情况下,尾递归就不一定能解决一般递归的问题。例如:

一般递归:在函数 A 执行的时候,如果在第二步中,它又调用了另一个函数 B,B 又调用 C.... 栈就会不断地增长不断地装入数据,当这个调用链很深的时候,栈很容易就满 。
int func(int n)
{
    if (n <= 1) return 1;

    return (n * func(n-1));
}
理论上,在 func(n-1) 返回之前,func(n),不能结束返回。因此func(n)就必须保留它在栈上的数据,直到func(n-1)先返回。
int tail_func(int n, int res)
{
     if (n <= 1) return res;

     return tail_func(n - 1, n * res);
}
从上可以看到尾递归把返回结果放到了调用的参数里。这个细小的变化导致,tail_func(n, res)不必像以前一样,非要等到拿到了tail_func(n-1, n*res)的返回值,才能计算它自己的返回结果 -- 它完全就等于tail_func(n-1, n*res)的返回值。因此理论上:tail_func(n)在调用tail_func(n-1)前,完全就可以先销毁自己放在栈上的东西。

实际上属于尾递归的算法均可以简单的转化为相应的迭代版本。

二分递归:面对输入规模巨大时运用分而治之策略。每一次递归实例都可能做多次递归,故称作“多路递归”。递归深度为O(logn)。必须保证子问题独立。

若子问题不独立呢?解决方法是:借助一定量的辅助空间,在各子问题求解之后,及时记录下其对应的解答。没当遇到一个子问题,都首先查验它是否已经计算过,以期通过直接查表获得解答,从而避免重复计算。即所谓制表和记忆策略。也可以从递归基出发,自底而上递推得出各子问题的解,即所谓的动态规划算法。

猜你喜欢

转载自blog.csdn.net/u013070875/article/details/84921339