• 数据结构与算法概述


    1.数据

    数据是指能够直接输入计算机中,被计算机处理的符号和被计算机操作的对象。

    数据不仅包括整型、实型等数值数据,也包括声音、视频、图像等非数值数据。

    数据有两个必备条件:1.能直接输入计算机 2.能被计算机直接处理。

    2.数据元素

    数据元素是指数据结构中基本的独立单位,它也被叫作元素、结点、记录等。在复杂的数据结构中,数据元素往往由若干个数据项组成,数据项是具有独立含义的最小标识单位,也称为字段或域。

    3.数据对象

    数据对象是性质相同的数据元素的集合。所谓性质相同是指数据元素具有相同数量和类型的数据项。数据对象是数据的一个子集。

    数据结构是相互之间存在一种或多种关系的数据元素的集合,这种关系包括两个方面:逻辑关系与存储方式

    逻辑关系又称为逻辑结构,描述元素之间的额逻辑关系;而存储方式描述的是数据元素与数据元素之间的关系。

    逻辑结构

    逻辑结构反映的是数据元素之间的关系,他们与数据元素在计算机中存储的位置无关,是数据结构在用户面前所呈现的形式。根据不同的逻辑结构来分,数据结构可分为集合、线性结构、树形结构和图形结构4种形式。

    (1)集合

    在集合中,数据元素都属于这个集合,但数据元素之间并没有什么关系。

    (2)线性结构

    线性结构中的元素具有一对一的关系,通过前一个结点可以找到后一个结点。线性结构可分为顺序存储和链式存储两种。

    顺序存储是由一段地址连续的空间来存储元素;链式存储是由分散的单元空间来存储元素,存储单元由指针相连接。

    在线性结构中,除头尾结点外,可以通过前一个结点来寻找后一个结点也可以通过后一个结点寻找前一个结点。

    (3)树形结构

    树形结构中,数据元素之间存在一对多的层次关系。

    除根结点外,树形结构的每一个结点都必须有一个且只有一个前驱结点,但可以有任意个后续结点。这些数据元素有自顶向下的层次关系。

    (4)图形结构

    图形结构中的数据元素存在多对多的关系,每个结点的前驱和后继结点都可以是任意个。

    4.存储结构

    常用的存储方式有顺序存储结构、链式存储结构、索引存储结构和三列表4种。

    (1)顺序存储结构

    顺序存储结构是把逻辑上相邻的结点存储在地址连续的存储单元里,数据元素之间的关系由存储单元是否相邻来实现。这种逻辑结构通常用高级语言上的数组来描述,数据的逻辑关系与物理关系是一致的。以数组int a[5] = {100,20,2,56,266}为例,其中的元素a[0]~a[4]在逻辑上是连续的,在存储器中的物理地址也是连续的。

    使用顺序存储结构存储数据时,系统为数据分配一段 连续的地址空间。顺序存储结构可以提高空间利用率,而且对于随机访问元素,其效率非常高,因为逻辑上相邻的数据元素,其存储地址也是紧邻的,所以可以按元素序号来快速查找到某一个元素。

    但也因如此,如果要对顺序存储结构实现元素的插入和删除,效率则非常低,因为如果要插入一个元素,需要将这个位置之后的所有元素都向后移动一个位置;同样,如果要删除一个元素,需要将这个位置之后的所有元素都向前移动一个位置。

    顺序存储结构在使用时有空间限制,当需要存取元素的个数多于预先分配的空间时,会出现“溢出”问题;当元素个数少于预先分配的空间时,又会造成空间浪费。

    (2)链式存储结构

    链式存储结构在空间上是一些不连续的存储单元,这些存储单元的逻辑关系通常附加的指针字段来表示,例如C/C++语言中的指针类型,通过这些指针的指向来表明结点之间的联系。

    一般而言,链式存储在内存中分配的单元是不连续的,像散落的珠子,需要用丝线(指针)串起来,这种结构更有利于元素的插入和删除。但是链式存储结构无法进行元素的随机访问。

    对于链式存储结构而言,空间利用率也较低,因为分配的内存单元有一部分被用来存储结点之间的逻辑关系。但链式存储在存储元素时没有空间限制,顺序存储与链式存储都是按需分配的,只是链式存储可以在需要时方便地分配新空间,不会造成空间不足或者浪费。

    (3)索引存储结构

    这种存储结构主要是为了方便查找数据,它通常是在存储结点信息的同时,还建立附加的索引表。索引表中的每一项称为索引项,它由两个字段组成:关键字与地址。其中关键字唯一标识一个结点,地址是指向结点的指针

    这种索引表一个索引项对应一个结点,叫作稠密索引。如果索引表中一个索引项对应一组结点,叫作稀疏索引。

    索引表可以快速地对数据进行随机访问。又因为在进行数据的插入和删除时,只需要更改索引表中的地址值,不必移动结点,所以在数据更改方面也具有较高的效率。但是索引存储结构在建立结点时会额外分配空间来建立一个索引表,因此降低了空间利用率。

    (4)散列存储结构

    散列存储又称为哈希存储,是一种力图将数据元素的存储位置与关键字之间建立确定对应关系的查找技术。它的基本思想是通过一定的函数关系(散列函数,也称为哈希函数)计算出一个值,将这个值作为元素的存储地址。

    散列存储的访问速度是非常迅速的,只要给出相应结点的关键字,它会立即计算出该结点存储地址。因此,它是一种非常重要的存储方法。

    5.抽象数据类型

    抽象数据类型有两个重要特征:数据抽象与数据封装

    所谓数据抽象就是指用ADT描述程序处理的实体时,强调的是其本质的特征,无论内部结构如何变化,只要本质特征不变,就不影响其外部使用。例如,在程序设计语言中经常使用的数据类型int ,它就可以理解为是一个抽象数据类型,在不同的计算机或操作系统中,它的实现方式可能会有所不同,但它本质上的数学特性是保持不变。例如,int类型的数据指的是整数,可以进行加减乘除模等一些运算,int类型数据的这些数学特性是保持不变,那么在编程者来看,它们都是相同的。因此数据抽象的意义在于数据类型的数学抽象特性。

    所谓数st据封装是指用户在软件设计时从问题的数学模型中抽象出来的逻辑结构和逻辑数据结构上的运算,需要通过固有数据类型来实现,它在定义时必须给出名字及其能够进行的运算操作。一旦定义了一个抽象数据类型,程序设计中就可以像使用基本数据类型那样来使用它。例如,在同居学生信息时,经常会使用姓名、学号、成绩等信息,可以定义一个抽象数据类型student,它封装了姓名、学号、成绩3个不同类型的变量,这样操作student 的变量就能很方便地知道这些信息了。

    抽象数据类型的定义由**一个值域( Data )和定义在该值域上的一组操作( Operation )**组成,若按其值的不同特性,可将抽象数据类型细分为3类:
    (1)原子类型。这种类型的变量,值是不可分解的。例如 int 、 char 等这些数据类型就无法再分解,属于原子类型的数据类型。一般较少定义这种抽象数据类型,因为固有数据类型基本都能满足程序设计。
    (2)固定聚合类型。这种抽象数据类型,其值是由确定数目的成分按一定的结构组成的。例如,以上提到的 student 抽象数据类型由姓名、学号、成绩3个成分按照先后顺序组成
    (3)可变聚合类型。与固定聚合类型相比,构成可变聚合类型值的成分的数目不确定。例如,定义一个“字符序列”,其中序列长度是可变的,我们并不知道会有多少个字符来组成这个序列。
    抽象数据类型可以更好地使程序设计模块化,在模块内部给出这些数据的表示及其操作的细节,在模块外部使用抽象的数据和抽象的操作。而且,所定义的数据类型的抽象层次越高,含有该抽象数据类型的软件模块的复用程度也就越高。

    程序 = 数据结构 + 算法

    算法

    算法是解决特定问题的步骤描述,通俗地讲算法就是描述解决问题步骤的方法。

    在计算机中,算法也是对某一个问题的求解方法,只是它的表现形式是计算机指令的有序序列,执行这些指令就能解决特定的问题。

    算法有3种较为崇用的表示方法,伪代码法、 N - S 结构化流程图和流程图法,用得最多的是流程图法。
    流程图符号中列举了4个图框、1个流程线和1个连接点,具体说明如下:

    1.起止框用于表示流程的开始或结束。
    2.输人输出框用平行四边形表示,在平行四边形内可以写明输人或输出的内容。
    3.判断框用菱形表示,它的作用是对条件进行判断,根据条件是否成立来决定如何执行后续的操作。
    4.处理框用矩形表示,它代表程序中的处理功能,如算术运算和赋值等。
    5.流程线用单向箭线或直线表示,可以连接不同位置的图框。流程线的标准流向是从左到右和从上到下,可用直线表示,非标准流向的流线应使用箭头指示方向。

    6.连接点用圆形表示,用于流程图的延续。

    假设一个数组要从小到大排序,结合流程图来分析选择排序的过程:
    第一步,在数组中选择出最小的元素,将它与0角标元素交换,即放在开头第1位。

    第二步,除0角标元素外,在剩下的待排序元素中选择出最小的元素,将它与1角标元素交换,即放在第2位。

    第三步,依次类推,直到完成最后两个元素的排序交换,就完成了升序排列。
    这样根据流程图来编写算法的指令代码,就会变得清晰简单。读者在以后设计算法时,最好先根据设计思路画出算法的流程图,其次分析其可行性,最后再完成代码。

    算法的特性

    一个好的算法,尤其是一个成熟的算法,应该具有以下5个特性

    (1)确定性。

    算法的每一步都有确定的含义,不会出现二义性。即在相同条件下,只有一条执行路径,相同的输人只会产生相同的输出结果。

    (2)可行性。

    算法的每一步都是可执行的,通过执行有限次操作来完成其功能。

    (3)有穷性。

    一个算法必须在执行有穷步骤之后结束,且每一步都可在有穷时间内完成。这里的有穷概念不是数学意义上的,而是指在实际应用当中可以接受的、合理的时间和步骤。

    (4)输人。

    算法具有零个或多个输入。有些输人量需要在算法执行过程中输入;有的算法表面上没有输入,但实际上输入量已经被嵌入在算法之中。

    (5)输出。

    算法至少具有一个或多个输出。“输出”是一组与“输入”有对应关系的量值,是算法进行信息加工后得到的结果,而这种对应关系即为算法的功能。

    一个好的算法需要具备这儿个条件。那么在设计算法时,怎样才能设计出好的算法呢?这就需要在设计算法时有明确的目标。想要设计出一个好的算法,需要从以下4个方面来考虑:
    (1)正确性。算法能够正确地执行,实现预定的功能。这是算法最重要也是最基本的要求,它包括程序没有语法错误;对于合法的输入能够产生满足要求的输出结果;甚至对于非法的测试数据都能有满足要求的输出结果。一个好的算法必定经得住千锤百炼的测试。
    (2)可读性。算法应该易于理解,也就是可读性好,这就要求算法的逻辑必须是清晰、简单和结构化的。可读性好有助于程序员理解算法,晦涩难懂的算法往往会隐藏错误且不易被发现,难于调试和修改。
    (3)健壮性。要求算法具有高容错性,即提供异常处理,能够对不合理的数据进行检查,不经常出异常中断或死机现象。
    (4)高效率与低存储。算法的效率通常指的是算法的执行时间,对于同一个问题的多种算法,执行时间短的其效率就高。存储量指的是算法在执行过程中所需的最大存储空间,包括所用到的内存及外存。设计算法时应考虑到执行效率和存储需求,设计出一个“性价比”较高的算法。
    要设计出一个好的算法,就要综合考虑其正确性、可读性、健壮性,还要考虑其执行效率

    算法的复杂度

    分析一个算法主要看这个算法的执行需要多少机器资源。在各种机器资源中,时间和空间是两个最主要的方面。因此,在进行算法分析时,人们最关心的就是运行算法所要花费的时间和算法中使用的各种数据占用的空间资源。算法所花费的时间通常称为时间复杂度,使用的空间资源称为空间复杂度。

    1.时间复杂度

    在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,然后分析T(n)随n的变化。

    T(n) = O(f(n))

    这样用大写的O来标记算法的时间复杂度,称之为大O标记法。一般随n的增长,T(n)也会随之增长,其中T(n)增长最慢者就是性能最优的算法。

    算法复杂度关系

    T(n) = O(1) 常数阶

    T(n) = O(n) 线性阶

    T(n) = O(n^2) 平方阶

    T(n) = O(n^3) 立方阶

    T(n) = O(2^n) 指数阶

    T(n) = O(logn) 对数阶

    T(n) = O(nlogn) nlogn阶

    大O阶的推导方法:

    (1)用常数1取代运行中的所有加法常数。

    (2)在修改后的运行次数函数中,只保留最高阶项。

    (3)如果最高阶项存在,且不是1,则除去其常系数,得到的结果就是大O阶。

    int a = 100;				//执行一次
    int b = 200;                //执行一次
    int sum = a + b;            //执行一次
    printf("%d\n",sum);         //执行一次
    
    • 1
    • 2
    • 3
    • 4

    加起来一共执行了4次 ,f(n) = 4,即T(n) = O(4),将常数项以1代替,在保留其最高阶项时,发现其没有最高阶项,因此该算法的时间复杂度为O(1),为常数阶。

    void func()
    {
        int i,sum = 0;                    //执行一次
        for(i = 0;i < 100;i++)
        {
            sum +=i;                      //执行n次
            
        }
        printf("%d\n",sum);               //执行一次
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    该程序段的执行次数为1+n+1 ,则f(n) = n+2;即T(n) = O(n+2)。然后将常数项以1代替,且保留最高阶项,则得出T(n) = O(n),因此该算法的时间复杂度为O(n),为线性阶。

    void func()
    {
    	int i = 1;
        do
        {
            i*= 2;      
        }while(i<n);
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这个程序段中,当i

    2.空间复杂度

    空间复杂度是对一个算法在运行过程中所占存储空间大小的度量,一般也作为问题规模n的函数,以数量级形式给出,格式如下:

    S(n) = O(f(n))

    一个算法的存储量包括输入数据所占空间、程序本身所占空间和辅助变量所占空间。在对算法进行分析时,只考虑辅助变量所占空间。

    有时候,在写代码时可以用空间来换取书剑,例如,写一个算法来判断某年是否闰年,这样每输入一个年份都要调用算法去判断一下,在时间上就有点复杂为了提高效率,可以用空间来换取时间,即建立一个大小合适的数据,编号从0到n,如果是闰年,则存入数据1,否则存入数据0。这样只要通过判断年份编号上存储的是0还是1就知道该年份是否是闰年了。

    数据结构是算法的基础,算法是数据结构的灵魂。

    算法通常是决定程序效率的关键,但一切算法最终都要在相应的数据结构上实现,许多算法的精髓就是在于选择了合适的数据结构作为基础。在程序设计中,不但要注重算法设计,也要正确选择数据结构,这样往往能够事半功倍。

  • 相关阅读:
    带你了解数据分布式存储原理
    怎么把图片变成圆角?
    文件包含漏洞
    【《C Primer Plus》读书笔记】第10章:数组和指针
    webWorker
    如何在Web前端实现CAD图文字全文搜索功能之技术分享
    【CS231N】b站同济子豪兄全视频笔记
    <多态>——《C++高阶》
    python和Springboot如何交互?
    Blender程序化建模简明教程【PCG】
  • 原文地址:https://blog.csdn.net/weixin_52190799/article/details/126051688