数据结构与算法

数据结构与算法

引用

数据结构主要学习用计算机实现数据组织和数据处理的方法

随着计算机应用领域的不断扩大,非数值计算问题占据了当今计算机应用的绝大部分,简单的数据类型已经远远不能满足需要,各数据元素之间的复杂关系已经不是普通数学方程式所能表达的了,无论是设计系统软件还是应用软件都会用到各种复杂的数据结构

一个好的程序无非是选择了一个合理的数据结构和一个好的算法,而好的算法的选择很大程度上取决于描述实际问题所采用的数据结构

数据结构定义

数据

数据是描述客观事物的数和字符的集合。例如,日常生活中使用的各种文字,数字和特定符号都是数据。从计算机的角度看,数据是所有能被输入到计算机中,且能被计算机处理的符号的集合,它是计算机能操作的对象的总称,也是计算机处理信息的某种特定的符号表示(例如,201班学生数据就是该班全体学生记录的集合)

数据元素

人们通常以数据元素作为数据的基本单位(例如,201班中的每个学生记录都是一个数据元素)。在有些情况下,数据元素也被称为元素、节点、顶点、记录等。

数据项

有时候,一个数据元素可以由若干个数据项组成,数据项是具有独立含义的最小数据单位,也称为字段或域(例如,201班中每个数据元素即学生记录是由学号、姓名等数据项组成)。数据对象是性质相同的数据元素的集合,它是数据的子集。

数据结构

数据结构是指所有数据元素以及数据元素之间的关系,可以看作是相互之间存在着某种特定关系的数据元素的集合,即可把数据结构看成是带结构的数据元素的集合。数据结构包括如下几个方面:

  1. 数据元素之间的逻辑关系,即数据的逻辑结构,它是数据结构在用户面前呈现的形式。
  2. 数据元素及其关系在计算机存储器中的存储方式,即数据的存储结构,也称为数据的物理结构。
  3. 施加在数据上的操作,即数据的运算。

数据的逻辑结构是从逻辑关系(主要是指数据元素的相邻关系)上描述数据的,它与数据的存储无关,是独立于计算机的。因此,数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。

数据的存储结构是逻辑结构用计算机语言的实现或在计算机中的表示(亦称为映像),也就是逻辑结构在计算机中的存储方式,它是依赖于计算机语言的。一般只在高级语言(例如C/C++)的层次上讨论存储结构。

数据的运算是定义在数据的逻辑结构之上的,每种逻辑结构都有一组相应的运算。例如最常用的运算有检索、插入、删除、更新、排序等。数据的运算最终需要在对应的存储结构上用算法实现。

逻辑结构

定义

数据元素之间的逻辑关系,即数据的逻辑结构,它是数据结构在用户面前呈现的形式。

在不产生混淆的前提下,常常将数据的逻辑结构称为数据结构。

分类

  1. 集合

    集合是指数据元素之间除了【同属于一个集合】的关系外,别无其它关系。

  2. 线性结构

    线性结构是指该结构中的节点之间存在一对一的关系,其特点是开始节点和终端节点都是唯一的,除了开始节点和终端节点以外,其余节点都有且仅有一个前驱,有且仅有一个后继。线性表就是一种典型的线性结构。

  3. 树形结构

    树形结构是指该结构中的节点之间存在一对多的关系。其特点是每个节点最多只有一个前驱,但可以有多个后继,且终端节点可以有多个。二叉树就是一种典型的树形结构。

  4. 图形结构

    图形结构是指该结构中的节点之间存在多对多的关系。其特点是每个节点的前驱和后继的个数都可以是任意的。因此,图形结构可能没有开始节点和终端节点,也可能由多个开始节点,终端节点。

树形结构和图形结构统称为非线性结构,该结构中的节点之间存在一对多或多对多的关系。由图形结构、树形结构和线性结构的定义可知,线性结构是树形结构的特殊情况,而树形结构又是图形结构的特殊情况。

存储结构

定义

数据元素及其关系在计算机存储器中的存储方式,即数据的存储结构,也称为数据的物理结构。

分类

  1. 顺序存储结构

    该结构是把逻辑上相邻的节点存储在物理位置上相邻的存储单元里,节点之间的逻辑关系由存储单元的邻接来体现。通常顺序存储结构是借助于计算机程序设计语言(C/C++)的数组来描述的

    顺序存储方法的主要优点是节省存储空间,因为分配给数据的存储单元全用于存放节点的数据(不考虑C/C++语言中数组需指定最大大小的情况),节点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对节点的随机存取,即每个节点对应一个序号,由该序号可直接计算出节点的存储地址(例如,对于数组A,节点序号为数组元素的下标,数组元素A[i]可以通过*(A+i)进行存取)。但顺序存储方法的主要缺点是不便于修改,对节点的插入,删除运行时,可能要移动一系列的节点。

  2. 链式存储结构

    该结构不要求逻辑上相邻的节点在物理位置上也相邻,节点间的逻辑关系是由附加的指针字段表示的。通常链式存储结构要借助于计算机程序设计语言(C/C++)的指针类型来描述。

    链式存储方法的主要优点是便于修改,在进行插入、删除运算时,仅需修改相应节点的指针域,不必移动节点。但与顺序存储相比,链式存储方法的主要缺点是存储空间的利用率低,因为分配给数据的存储单元有一部分被用来存储节点之间的逻辑关系了,另外,由于逻辑上相邻的节点在存储空间中不一定相邻,所以不能对节点进行随机存取。

  3. 索引存储结构

    该结构通常是在存储节点信息的同时,还建立附加的索引表。索引表中的每一项称为索引项,索引项的一般表现形式是:(关键字,地址),其中关键字唯一标识一个节点,地址是指向节点的指针。这种带有索引表的存储结构可以大大提高数据查找的速度。

    线性结构采用索引存储方法后,可以对节点进行随机访问。在插入、删除运算时,只需要移动索引表中对应节点的存储地址,而不必移动节点表中节点的数据,所以仍保持较高的数据修改运算效率,索引存储方法的缺点是增加了索引表,因而降低了存储空间的利用率。

  4. 散列(或哈希)存储结构

    该结构的基本思想是根据节点的关键字通过哈希(或散列)函数直接计算出一个值,并将这个这个指作为该节点的存储地址。

    哈希存储方法的优点是查找速度快,只要给出待查节点的关键字,就可以立即计算出该节点的存储地址。但与前三种存储方法不同的是,哈希存储方法只存储节点的数据,不存储节点之间的逻辑关系。哈希存储方法一般只适合要求对数据进行快速查找和插入的场合。

上述4种的存储方法,既可以单独使用,也可以组合起来对数据结构进行存储映像,同一种逻辑结构采用不同的存储方法,可以得到不同的存储结构,选择何种存储结构来表示相应的逻辑结构,应视具体要求而定,主要考虑的是运算是否方便即算法的时空要求。

数据类型

在用高级程序语言编写的程序中,必须对程序中出现的每个变量、常量或表达式明确说明他们所属的数据类型。不同类型的变量,其所能取的值的范围不同,所能进行的操作不同。数据类型是一组性质相同的值的集合和定义在此集合上的一组操作的总称。

如在高级语言中已实现了的,非高级语言直接支持的数据结构即为数据类型。在程序设计语言中,一个变量的数据类型不仅规定了这个变量的取值范围,而且定义了这个变量可用的运算,如C语言中定义变量i为int类型,则它的取值范围为-32768~32767(16位系统),可用的运算有+,-,*,/,%等

数据结构是指计算机处理的数据元素的组织形式和相互关系,而数据类型是某种程序设计语言中是实现的数据结构。在程序设计语言提供的数据类型的支持下,就可以根据从问题抽象出来的各种数据模型,逐步构造出描述这些数据模型的新的数据结构

C、C++语言常用数据类型

  1. C/C++语言的基本数据类型

    • int 整型,可以有三个限定词 short(短整数),long (长整数)和 unsigned(无符号整数)
    • bool 逻辑型 其取值只能是false(假)和true(真)
    • float 单精度浮点型
    • double 双精度浮点型
    • char 字符型,用于存放单个字符
  2. C/C++语言的指针类型

  3. C/C++语言的数组类型

  4. C/C++语言的结构体类型

  5. C/C++语言的共用体类型

  6. C/C++语言的自定义类型

  7. C/C++语言的引用运算符

抽象数据类型

抽象数类型(Abstract Data Type,ADT)指的是用户进行软件系统设计时从问题的数学模型中抽象出来的逻辑数据结构和逻辑数据结构上的运算,不考虑计算机的具体存储结构和具体实现算法。抽象数据类型中的数据对象和数据运算的声明与数据对象的表示和数据运算的实现相互分离

一个具体问题的抽象数据类型的定义通常采用简洁、严谨的文字描述,一般包括数据对象(即数据元素的集合)、数据关系和基本运算三方面的内容。抽象数据类型可用(D,S,P)三元组来表示。其中,D是数据对象;S是D上的关系集;P是D中数据对象的基本运算集。其基本格式为:

ADT 抽象数据类型名
{
	数据对象:数据对象的声明
	数据关系:数据关系的声明
	基本运算:基本运算的声明
}ADT 抽象数据类型名

其中基本运算的声明格式为:
基本运算名(参数表):运算功能描述
基本运算有两种参数:
1. 赋值参数,只为运算提供输入值;
2. 引用参数,已&打头,除可提供输入值外,还将返回运算结果。

例如,抽象数据类型复数的定义如下:

ADT Complex
{
	数据对象:
		D = {e1, e2 | e1,e2均为实数}
    数据关系:
    	R = {<e1,e2>| e1是复数的实数部分, e2是复数的虚数部分}
    基本运算:
    	AssignComplex(&Z,v1,v2):构造复数Z,其实部和虚部分别赋以参数v1和v2的值
    	DestoryComplex(&Z):销毁复数Z
    	GetReal(Z,&real):用real返回复数Z的实部值
    	GetImag(Z,&Imag):用Imag返回复数Z的虚部值
    	Add(z1,z2,&sum):用sum返回两个复数z1,z2的和值
} ADT Complex

抽象数据类型有两个重要特征:数据抽象和数据封装,所谓数据抽象,是指用ADT描述程序处理的实体时,强调的是其本质的特征,其所完成的功能以及它和外部用户的接口(即外界使用他的方法)。所谓数据封装,是指将实体的外部特性和其内部实现细节分离,并且对外部用户隐藏其内部实现细节。抽象数据类型需要通过固有数据类型(高级编程语言中已实现的数据类型)如C++中的类来实现

算法

算法定义

数据元素之间的关系有逻辑关系和物理关系,对应的运算有逻辑结构上的运算(抽象运算)和具体存储结构上的运算(运算实现)。

算法是在具体存储结构上实现某个抽象运算

算法是对特定问题的求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示计算机的一个或多个操作。一个算法具有五个重要的特性:

  1. 有穷性。一个算法必须总是(对任何合法的输入值)在执行有穷步骤之后结束,且每一步都可在有穷时间内完成。
  2. 确定性。对于各种情况下执行的操作,在算法中都有确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行,并且在任何条件下,算法都只有一条执行路径。
  3. 可行性。算法中的所有操作都必须足够基本,算法可以通过执行有限次基本操作来完成其功能。
  4. 有输入。作为算法加工对象的量值,通常体现为算法中的一组变量。有些输入量需要在算法执行过程中输入。有的算法表面上没有输入,但输入量实际上已被嵌入在算法之中。
  5. 有输出。输出是一组与输入有确定关系的量值,是算法进行信息加工后得到的结构,而这种确定对应关系即为算法的功能。

算法和程序不同,程序可以不满足有穷性。例如,一个操作系统(如DOS和Windows)在用户未操作时一直处于等待的循环中,直到出现用户操作为止。也就是说,程序可以无穷循环,不一定满足有穷性,但算法必须满足有穷性。

算法描述

例:设计一个算法:求一元二次方程a x 2 x^2 x2 + bx + c = 0的根。

解:该算法由如下步骤构成:
    (1)计算d = b*b -4*a*c
    (2)如果d>0,则转(5)
    (3)如果d=0,则转(9)
    (4)如果d<0,则转(12)
    (5)计算x1 = (-b+sqrt(d))/(2*a)
    (6)计算x2 = (-b-sqrt(d))/(2*a)
    (7)显示两个实根x1和x2的值
    (8)转(13)
    (9)计算x = (-b)/(2*a)
    (10)显示一个实根x的值
    (11)转(13)
    (12)显示没有实根的信息
    (13)算法结束

上例的算法采用文字描述的方式,而算法的描述其实可以有多种方式,如语言方式、图形方式和表格方式等。C/C++语言是一种高效,灵活和精练的高级程序设计语言,他的优点是类型丰富,语句精炼,编写的程序结构化程度高、可读性强。

void solution(float a, float b, float c)
{
    float d,x,x1,x2;
    d = b*b-4*a*c;
    if (d > 0) {
        x1 = (-b + sqrt(d)) / (2*a);
        x2 = (-b - sqrt(d)) / (2*a);
        printf("两个实根:x1=%f,x2=%f\n",x1,x2);
    }else if (d == 0) {
        x = (-b) / (2*a);
        printf("一个实根:x=%f\n", x);
    }else {
        printf("没有实根\n");
    }
}

算法分析

算法设计的目标
  1. 正确性。 要求算法能够正确的执行 ,实现预先规定的功能和性能。这是最重要也最基本的要求
  2. 可使用性。要求算法能够很方便地使用。这个特性也叫用户友好性
  3. 可读性。算法应易于理解,也就是可读性好。为了达到这个要求,算法的逻辑必须是清晰的,简单的和结构化的
  4. 健壮性。要求算法具有很好的容错性,即提供异常处理,能够对不合理的数据进行检查,不经常出现异常中断或死机现象
  5. 高效率和低存储量要求。通常算法的效率主要指算法的执行时间。对于同一个问题如果有多种算法可以求解,执行时间短的算法效率高。算法存储量指的是算法执行过程中所需要的最大存储空间。效率与存储量这两者都与问题的规模有关
算法效率分析

通常有两种衡量算法效率的方法:事后统计法和事前分析估算法。前者存在以下缺点:一是必须执行程序;二是存在其它因素掩盖算法本质,所以一般采用事前分析估算法来分析算法效率

一个算法用高级语言实现后,在计算机上运行时所消耗的时间与很多因素有关,如计算机的运行速度、编写程序所采用的计算机语言、编译产生的机器语言代码质量和问题的规模等。在这些因素中,前三个都与具体的机器有关。撇开这些与计算机硬件、软件有关的因素,仅考虑算法本身的效率高低,可以认为一个特定算法的运行工作量的大小,只依赖于问题的规模(通常用n表示),或者说,它是问题规模的函数。

一个算法是由控制结构(顺序、分支、循环3种结构)和原操作(指对固有数据类型的操作)构成的,算法的运行时间按取决于两者的综合效果。为了便于比较同一问题的不同算法,通常从算法中选取一种对于所研究的问题来说是基本运算的原操作(以下将基本运算的原操作称为基本运算),算法执行时间大致为基本运算所需的时间与其运算次数(一条语句的运行次数称为语句频度)的乘积。被视为算法基本运算的一般是最深层循环内的语句。

显然,在一个算法中,执行基本运算的次数越少,其运行时间也就越少;执行基本运算的次数越多,其运行时间也就越多。也就是说,一个算法的执行时间可以由其中基本运算的执行次数来计量。

算法中基本运算执行次数T(n)是问题规模n的某个函数f(n),记作:

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

记号O读作"大O"(是Order的简写,意指数量级),它表示随问题规模n的增大,算法执行时间的增长率和**f(n)**的增长率相同

O的形式定义为:若**f(n)是正整数n的一个函数,则T(n)=O(f(n))**表示存在一个正的常数c和 n 0 n_0 n0,使得当n>= n 0 n_0 n0时都满足T(n)<=cf(n)。也就是只求出T(n)的最高阶项,忽略其低阶项和常系数,这样既可简化T(n)的计算,又能比较客观的反映出当n很大时算法的时间性能

一个没有循环的算法中基本运算次数与问题规模n无关,记作O(1),也称作常数阶。

一个只有一重循环的算法中基本运算次数与问题规模n呈线性关系,记作O(n),也称线性阶乘。

其余常用的还有平方阶O( n 2 n^2 n2),立方阶O( n 3 n^3 n3),对数阶O( l o g 2 n log_2n log2n),指数阶O( 2 n 2^n 2n)等,各种不同数量级对应的值存在如下关系:

O(1) < O( l o g 2 n log_2n log2n) < O(n) < O( n l o g 2 n nlog_2n nlog2n) < O( n 2 n^2 n2) < O( n 3 n^3 n3) < O( 2 n 2^n 2n​) < O(n!)

算法的时间复杂度(用f(n)表示)采用这种数量级的形式后,只需要分析算法中影响算法执行时间的主要部分即可,不必对每一步都进行详细的分析。

算法效率分析例子
  1. 求两个n阶方阵的和C=A+B的算法如下,分析时间复杂度

    # define MAX 20 // 定义最大的方阶
    void matrixadd(int n, int A[MAX][MAX], int B[MAX][MAX], int C[MAX][MAX])
    {
        int i, j;
        for (i = 0; i < n; i++) { //语句①
            for (j = 0; j < n; j++) { // 语句②
                C[i][j] = A[i][j] + B[i][j]; // 语句③
            }
        }
    }
    

    该算法包括3个可执行语句①②③。语句①中循环控制变量i要从0增加到n,i=n时循环才会终止,故语句①的频度时n+1,但他的循环体却只执行n次。语句②作为语句①的循环体内的语句只执行n次,但语句②本身要执行n+1次,所以语句②的频度时n(n+1)。同理可得语句③的频度为n 2 ^2 2,因此该算法所有语句频度之和为:

    T(n) = n+1+n(n+1)+n 2 ^2 2=2n 2 ^2 2+2n+1=O(n 2 ^2 2)

    另外,该算法中的基本运算是两重循环中最深层的语句③,分析他的频度即:

    T(n) = O(n 2 ^2 2)

    从两种方式得到的算法的时间复杂度均为O(n 2 ^2 2),而后者计算过程要简单的多,所以一般采用后一种方式分析算法的时间复杂度

  2. 分析以下算法的时间复杂度

    int fun(int a[], int n, int k)
    {
        int i;
        i = 0; //语句①
        while (i < n && a[i] != k) {//语句②
            i++;//语句③
        }
        return i;//语句④
    }
    

    该算法完成在一维数组a[n]中查找给定值k的功能。语句③的频度不仅与问题的规模n有关,还与输入实例中a的各元素取值以及k的取值有关,即与输入实例的初始状态有关。如a中没有与k相等的元素,则语句③的频度为n,若a中的第一个元素a[0]等于k,则语句三的频度是0.在这种情况下,可用最坏情况下的时间复杂度作为算法的时间复杂度,在此例中即为O(n),记作T(n)=O(n)。这样做的原因是:最坏情况下的时间复杂度是在任何输入实例上运行时间的上界。

    有时也可以选择将算法的期望(或平均)时间复杂度作为讨论目标,所谓的平均时间复杂度是指在所有可能的输入实例以等概率出现的情况下算法的期望运行时间与问题规模n的数量级关系,此列中,k出现在任何位置的概率相同,都为1/n,则语句③的平均执行频度为 0+1+2+…+(n-1) / 2 = n-1 / 2

    它决定了此程序段的平均时间复杂度的数量级为O(n),其中n为数组a中元素的个数。

算法存储空间分析

线性表

线性表的定义

线性表是具有相同特性的数据元素的一个有限序列。该序列中所含元素的个数叫做线性表的长度。用n表示,n>=0。当n=0时,表示线性表是一个空表,即表中不包含任何元素。设序列中第i(i表示逻辑序号)个元素为a i _i i(1<=i<=n),则线性表的一般表示为:

(a1,a2,…,a i _i i, a i a_i ai + _+ + 1 _1 1,…,a n _n n)

其中a 1 _1 1为第一个元素,又称表头元素,a 2 _2 2为第二个元素,a n _n n为最后一个元素,又称作表尾元素。

一个线性表可以用一个标识符来命名,如用L命名上面的线性表,则:

L = (a1,a2,…,a i _i i, a i a_i ai + _+ + 1 _1 1,…,a n _n n)

线性表中的元素是与位置有关的,即第i个元素a i _i i处在第i-1个元素a i _i i − _- 1 _1 1额后面和第i+1个元素a i _i i + _+ + 1 _1 1的前面。这种位置上的有序性就是一种线性关系,所以线性表是一种线性结构,用二元组表示为:L = (D,R),其中:

D={a i _i i | 1<=i<=n,n>=0,a i _i i为ElemType类型(ElemType是自定义的类型标识符)}

R={r}

r={<a i _i i,a i _i i + _+ + 1 _1 1> | 1<=i<=n-1}

线性表的抽象数据类型描述

抽象数据类型的线性表定义如下:

ADT List
{
    数据对象:
    	D = {a[i] | 1<=i<=n,n>=0,a[i]为ElemType类型}
    数据关系:
    	R = {<a[i],a[i+1]>|a[i],a[i+1]∈D,i=1,...,n-1}
    基本运算:
    	InitList(&L):初始化线性表,构造一个空的线性表L.
        DestroyList(&L):销毁线性表,释放线性表L占用的内存空间.
        ListEmpty[L]:判断线性表是否为空表,如L为空表,则返回真,否则返回假.
        ListLength[L]:求线性表的长度,返回L中的元素个数.
        DispList(L):输出线性表,当线性表L不为空时,顺序显示L中各节点的值域.
        GetElem(L,i,&e):求线性表中某个数据元素值,用e返回L中第i(1<=i<=n)个元素的值.
        LocateElem(L,e):按元素值查找,返回L中第一个值域与e相等的序号,若这样的元素不存在,则返回0.
        ListInsert(&L,i,e):插入数据元素,在L的第i(1<=i<=n)个元素之前插入新的元素e,L的长度增1.
        ListDelete(&L,i,$e):删除数据元素,删除L的第i(1<=i<=n)个元素,并用e返回其值,L的长度减1.
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值