《数据结构》第一章 绪论

1.1数据抽象与数据结构

1.1.1抽象与结构

抽象是对一类事物或一个系统的简化描述,它舍弃个别的、非本质的属性,抽取共同的、本质的属性,是形成概念的必要手段。

抽象涉及对一个或一组对象的本质属性的发现和命名。

人的记忆分为短期记忆和长期记忆两个阶段,在短期记忆中,记住事物的细节的能力是很有限的,这对人的智力是极大的限制。

对事物抽象的结果往往表现为发现对象的结构。结构是各个组成部分的搭配和排列,可以形式地表示为各成分之间的关系。

抽象结构是人们克服复杂性的重要认知形式。在认识事物时,对结构的分解和合成十分重要。

1.1.2抽象与封装

对事物的抽象不仅可以表现为结构形式,而且面对特定的需求,可以进一步划分为“外部结构”和“内部结构”。

隐藏了与使用操作无直接关系的内部结构的做法称为封装

封装具有一下几个特点:

  1. 简单:尽可能易学和易用。
  2. 完整:封装成一个整体,便于管理和移动。
  3. 一致:同类设备的内部结构或者功能实现细节可以不同,但可以具有相同或相似的外观和操作接口。
  4. 保密:不公开内部结构和实现细节。
  5. 易维护:对设备内部进行专业维修后,设备功能不变,外部结构不变,使用操作也不变。
  6. 安全:避免对设备内部的不合适操作和破坏。

1.1.3程序设计的抽象

现实世界的各种事物都可以称为对象。复杂对象由简单对象组成。现实世界的对象可以通过抽象,用某种符号形式表示为计算机程序处理的数据对象。所以数据对象统称为数据。在特定问题中,基本的数据对象称为数据元素

在程序设计中,使用最多的抽象是功能抽象数据抽象。此外,面向对象程序设计还采用了更多形式的抽象,如封装继承多态等。

1.功能抽象

功能抽象是为一段代码命名,以便能用其名字来执行它。

功能抽象也称为过程抽象,变量参数化是它的一个主要方面。可以将函数中的常量和变量分离,并对变量初始化。在许多情况下,函数操作的数据由变量组成。参数化后,函数可以得到良好的封装,变得更加通用。

允许在一个函数中调用另一个函数,就有了从低级抽象建立高级抽象的基础。

函数的嵌套调用体现了多层抽象。在层次设计中,人们把整个程序设计项目看作一个主函数(main)。可以把功能复杂的函数分解成若干个较小的子函数。如果子函数的功能仍然过于复杂,则可以进一步分解。在较高层次上,设计者只须按功能需要调用低层函数,不必关注其实现细节。

2.数据抽象

数据抽象可以表示为一个三元组:

(S,D,P)

其中,S为数据对象的结构,D为数据对象的取值范围(也称为值域),P为数据基本操作集。本质上,数据抽象是把具有相同取值范围D并可以进行相同操作的P的一批数据抽象为一个数据类型,并限定只能用这些操作来引用和修改数据的值。

数据抽象最本质的是封装,把数据类型的使用与实现分离,使程序设计者能够实现操作:

(1)把大的系统分解为多个小的部分,每部分具有对所处理的数据的操作接口。

(2)这些操作接口是其对应部分的功能抽象,是外部可见的,而它的具体实现则是对外部隐藏的。

(3)对外部隐藏数据,外部只能通过操作接口访问数据。

数据类型T是一个值域D以及D上的一组基本操作P的集合:

                           T={D,P}

例如:整型int的值域为整数区间[-215,215-1],操作集为{+,-,*,/,%......}

使用基本数据类型即可编写程序,但在更高层次数据抽象之上编写程序会更加方便。定义和使用数据类型的过程称为数据抽象

按“值”的不同特性,高级语言的数据类型可以分为两类:

①非结构的原子类型:该类型的值不可分解,例如整型int,浮点型float等;

②结构类型:该类型的值可以分解,由若干成分按某种结构组成,其成分可以是非结构的,也可以是结构的,如:数组的值由若干分量组成,每个分量可以是整型,也可以是数组等。

1.1.4 数据结构

从数据类型的观点看,原子类型变量的值不可分解。在一般情况下,程序设计语言提供的基本数据类型可以满足需求。但有时也需要重新定义新的原子数据类型。

结构类型的值可以分解,并可以划分为两种类型:

(1)固定聚合类型:这个类型变量的值由确定的数目的成分按某种结构组成。例如,复数有由两个实数依确定的<实部,虚部>次序构成。

(2)可变聚合类型:和固定聚合类型相比,构成可变聚合类型“值”的成分的数目不确定。例如,可以定义一个“有序整数序列”的抽象数据类型,其中序列的长度是可变的。

一个数据类型的数据对象的结构S由其元素集C和元素之间的关系集R组成:

S=(C,R)

S称为数据逻辑结构,简称数据结构。由元素集C构建一个数据结构时,通常是根据操作集P的需求,定义C的元素之间的关系集R。这些关系有些是自然存在的,有些是为了满足某些操作的需求而人为添加的。

在实际问题中,数据元素通常不是孤立存在的,它们之间可能存在某些关系,并组成特定的结构。根据数据元素之间的不同特性,可以将数据结构分为4类基本结构:

集合:数据元素之间未定义特定的关系;

线性结构:数据元素之间存在一个对一个的线性关系;

树形结构:数据元素之间存在一个对多个的层次关系;

图结构或网状结构:数据元素之间存在多个对多个的网状关系。

数据结构在计算机中的表示(又称为映像)称为数据的物理结构或者存储结构。包括数据元素的表示和关系的表示。

在计算机中,表示信息的最小单位是二进制数的一位,简称。可以由若干位组成的位串表示一个数据元素(比如用一个字长的位串表示一个整数,用一个字节表示一个ASCII字符),通常称这个位串为元素结点。当数据元素由若干数据项组成时,位串中对应于各个数据项的子位串称为数据域。

在计算机中,数据元素之间的关系主要有两种不同的表示方法:顺序映象非顺序映象,并由此分别得到顺序存储结构链式存储结构

顺序映象借助元素在存储器中的相对位置表示数据元素之间的关系。

非顺序映象借助指示元素存储地址的指针表示数据元素之间的逻辑关系。

数据的逻辑结构和物理结构密切相关。

一个算法的设计取决于选定的数据逻辑结构,而算法的实现依赖于采用的存储结构。

1.2 抽象数据类型与应用程序接口

1.2.1 抽象数据类型

如果一个类型的定义更注重它的行为而非表示,那么该类型就称为抽象数据类型(ADT)。依据数据抽象的观点,一个抽象数据类型可表示为一个三元组(C,R,P),其中(C,R)是一个数据结构S,P是作用于S的基本操作集。

抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关,即不论其内部存储结构如何变化,只要它的愁绪爱那个特性不变,都不影响其外部的使用。

一个含抽象数据类型的软件模块通常应包含定义、表示和实现3个部分,分别是定义数据逻辑结构,通过类型定义表示数据存储结构,编写代码实现基本操作集。

1.2.2 接口和实现

有必要强调库自身和调用库的其他程序(称为库的客户)之间的区别,也就是要关注库和它的客户之间的边界,这种边界也称为接口。接口不仅提供了交流的渠道,而且还阻止了复杂细节相互影响。接口是现代库的核心部分。

接口的基本意思是两种不同实体之间的界限。

在编程中,接口是一个概念性边界,而不是一个物理边界,它是库的实现和其用户之间的边界。

接口的目的是在不显示库的实现细节情况下,为客户提供有关使用库的信息。

接口不仅是客户与库之间的沟通渠道,也是分离二者的屏障。通过促进双方之间的交流,确保接口一方的细节不会暴露出来,防止使另一方的编码复杂化,这样使得编程过程中的概念复杂性大大降低。

在计算机中,接口是一个概念实体。它提供了两边需要的信息,使实现库的程序员和调用库的程序员之间相互理解。在编写C程序时,必须有某种方法来使得概念性接口成为实际程序的一部分。在C语言中,接口由头文件来表示。它一般拥有和实现它的文件相同的名子,只是扩展名为.h而不是.c。

将函数原型放入接口中,使之对客户来说,这称为导出这些函数。

尽管函数原型是一个接口中最普遍的元素,但是接口还可以导出其他的定义。

1.2.3 良好的接口设计规则

要设计一个有效的接口,必须遵守几个准则。一般来说,应该生成这样的接口文件:
(1)一致性:一个接口应该定义一个具有统一主题的抽象。如果某个函数不适合该主题,该函数就应该在其他接口中定义。

(2)简单性:底层的实现本身通常很复杂,接口要对客户尽可能地隐藏其复杂性。

(3)充分性:当客户用到一个抽象时,接口必须提供足够的函数来满足其需要。如果接口缺少一些关键操作,客户就会放弃使用该接口,转而开发自己地更加强大的抽象。设计者应避免一个接口过于简单而变得毫无用处,这和简单性同样重要。

(4)通用性:一个好的接口可以灵活地满足许多客户的需求。专门为某客户设计的接口,不会像可以在很多种情况下使用的接口那么灵活。

(5)稳定性:在接口中定义的函数即使在底层的实现变化时,也会保持一样精确的结构和效果。接口行为的变化使得客户不得不改变他们的程序,那么这将使接口的价值大打折扣。

大部分抽象数据类型是通过一个接口定义的,该接口导出的抽象数据类型,以及定义该数据类型行为的一个函数集合。优点:

  1. 简单性:对客户隐藏类型的内部表示,这意味着客户可以更少关心细节。
  2. 灵活性:由于ADT是在类型的行为方面定义的,实现ADT的程序员可以自由地改变它的底层。对于任何抽象,都可以改变它的实现,只要保留它的接口不变。
  3. 安全性:接口的边界像墙一样将客户和实现隔开。如果客户程序可以访问到类型地表示,那么它将会以意想不到的方式改变底层的数据结构的值。使用ADT就可以防止这种修改。

1.3 算法和算法设计

1.3.1 算法和算法描述

1.算法和程序

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

  1. 有穷性:一个算法必须总是(对任何合法的输入值)在执行有穷步之后结束,且每一步都可在有穷时间内完成。
  2. 确定性:算法中的每条指令必须有确切的含义,读者理解时不会产生二义性,并且在任何条件下,算法只有唯一的一条执行路径,即对于相同输入只能得到相同的输出。
  3. 可行性:一个算法是可行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现。
  4. 输入:一个算法有一个或多个输入,这些输入取自于特定的对象合集。
  5. 输出:一个算法有一个或多个输出。这些输出是同输入有着某种特定关系的量。

一个计算机程序是用某种程序设计语言对相关的数据结构和算法的具体实现。语言通常用函数来实现算法。一个算法可以有多种语种的多个实现程序(或函数)。

算法设计的要求

设计一个好算法应该达到以下目标:

  1. 正确性:算法应当满足具体问题的需求。

正确一词的4个层次:

    • 程序不含语法错误;
    • 程序对于几组输入数据能够得出满足规格说明要求的结果。
    • 程序对于精心选择的典型、苛刻而带有刁难性的机组输入数据能够得到满足说明规格的结果。
    • 程序对于一切合法的输入数据都能产生满足规格说明要求的结果。
  1. 可读性:算法主要是为了人的阅读与交流,其次才是机器执行。可读性好助于人对算法的理解;晦涩难懂的程序易于隐藏较多错误,难以调试和修改。
  2. 健壮性:当输入非法数据时,算法能适当地作出反应或进行处理,不会产生莫名其妙地输出结果。处理出错的方法是返回一个表示错误或错误性质的值,以便在更高的抽象层次上进行处理,而不是打印错误信息或终止程序的执行。
  3. 高效率和低存储量需求:效率是指算法执行的时间。对于求解同一个问题的多个算法,执行的时间越短,其效率越高。存储量需求是指算法执行过程中所需要的最大存储空间。效率和存储量需求都与问题的规模有关。

1.3.2 算法分析基础

1.算法效率的度量

算法执行时间可由其实现程序在计算机运行所需的时间的度量。

度量一个程序的执行时间的两种方法:

  1. 事后统计方法:计算机内部的计时功能可精确到毫秒级,不同算法的程序可通过一组或若干组相同的统计数据以分辨优劣。

缺陷:

    • 必须运行依据算法编制的程序;
    • 所得时间的统计量依赖于计算机的硬件、软件等环境因素,又是容易掩盖算法本身的优劣。
  1. 事前分析估算法。消耗时间取决于一下因素:
    • 依据的算法选用何种策略。
    • 问题的规模。
    • 书写程序的语言。
    • 编译程序所产生的机器代码的质量。
    • 机器执行指令的速度。

一个算法是由控制结构(顺序、分支和循环结构3种)和原操作(指固有数据类型的操作)构成,执行时间却决于两者的综合效果。

关于算法效率的定性分析有助于理解因问题规模变化而引发的算法性能的变化。问题规模容易量化。

  1. 对数字进行操作的算法可用数字本身代表问题规模。
  2. 大多数对数组进行操作的算法可用数组元素个数反映问题规模。
  3. 大多数对数据结构进行操作的算法可用数据元素集合的大小反映问题规模。

一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间度量记作:

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

它表示随问题规模n的增大,,算法执行时间的增长率不超过f(n)的增长率,称作算法的渐近时间复杂度,简称时间复杂度

由于算法的时间复杂度不是定量指标,反映的只是对于问题规模n的增长率,所以在难以精确计算基本操作执行次数(频度)的情况下,只需求出它关于n的增长率或阶即可。大O符号可以简化圆括号内的公式,让其以最简单的形式表示算法的定性行为。最常用的简化方式有两种:

  1. 删除那些随n值增大而对总和不再重要的低阶项。
  2. 删除所以常数因子。

大部分情况下,单个表达式和语句的运行时间是常数,可认为与问题规模n无关,记为O(1)(即O(n0),除非其中调用了需单独计算时间的函数。

一般情况下,通过找出执行次数最多的代码段,把它们的运行语句的总次数表示为n的函数,就能确定算法的计算复杂度。

对于并列循环结构:取各循环结构计算时间的最大值。

对于嵌套循环结构:取最内层循环体中语句的执行次数。

如果各层循环的次数是相互无关的,可简单采用乘法法则:各层循环的次数相乘。

递归算法的运行时间可分解为两部分:

  1. 执行目前层次的递归分解运算所需时间。
  2. 执行递归调用所需的时间。

有的情况下,算法中基本操作的重复执行次数还随问题的输入数据集不同而异。

  1. 算法的存储空间需求

类似于时间复杂度,数据结构以空间结构作为算法所需存储空间的度量,记作:

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

其中n为问题的规模(或大小)。

若输入数据所占空间之却决于问题本身,和算法无关,则只需要分析除输入和程序之外的额外空间,否则应考虑输入本身所需空间(和输入数据的表示形式有关)。

若额外空间相对于输入数据量来说是常熟,则称此算法为原地工作。

1.4 数据结构与算法的描述和实现

1.4.1 一维数组

一维数组的封装

当序列的长度待定时,可将其元素的存储空间动态分配,并与其长度封装为一个结构体:

    typedef struct

    {

        ElemType *elem;

        int length;

    }Sequence;

声明一个序列变量时,启动前需要对其分配动态存储空间,初始化就可以实现这个操作:

Status intitSequence(Sequence&s,int n){ //序列s初始化

    s.elem = (ElemType*)malloc(n*sizeof(ElemType)); //分配n个元素空间

    if(NULL == s.elem)

    {

        return OVERFLOW;

    }

    s.length = n;

}

C语言的数组名实际上就是第0元素的地址,s.elem是这个数组的首地址,也等价于&s.elem[0],该数组中第一个元素可表示为s.elem[0]。

取第i元素的值,可直接表示为s.elem[i],也可利用指针操作:  

  ElemType *q;

    q =&as.elem[0];

此时*q也表示第i元素的值。

1.4.2 指针与结构体

1.结构体

C语言允许用户自定义结构体结构体数据类型。

结构体类型定义的一般形式如下:

    struct 结构体名{

           成员列表

    }

结构体的成员也称为域。举例:

struct student{          //含三个成员(域)

           int number;        //学号

           char name[10];     //姓名

           float score;       //  成绩

}

结构体名可以用来定义结构体变量

如:struct student stu1,stu2,stu3;

为了使代码简洁,采用关键字typedef定义结构体类型的方法,其一般形式如下:

typedef struct 结构体名{        //若在定义成员表时未用到

           成员列表

    }结构体类型名表列;

举例:

typedef struct 结构体名{

           int number;        //学号

           char name[10];     //姓名

           float score;       //  成绩

    }stuType, *stuPtrType;

其中,stuPtrType是指向学生结构体的指针类型。定义一个学生结构体变量的语句:

    stuType stu1,stu2,stu3;

还可以很方便地定义指向学生结构体地指针变量:

    stuPtrType stuPtr1,stuPtr2,stuPtr3;

结构体地成员变量也可以是结构体类型,成员变量也已是指向结构体自身的指针,

如:

    typedef struct{

        int year;

        int month;

        int day;

    }data;//日期类型

typedef struct student{

        int number;     //学号

        char name[10];  //姓名

        data birthday;  //出生日期

        float score;    //成绩

        struct student *nextStu;//结构体指针,指向学号相邻的下一位学生

        }stuType;      //学生类型

引用结构体变量中的成员的一般形式如下:

    结构体变量名.成员名

引用结构体指针变量的一般形式如下:

    (*结构体指针变量).成员名

    结构体指针变量->成员名

2.实现一个简单的链表

链表是C语言中一种应用广泛的结构,用一组任意的存储单元存放数据元素,链表的存储单元可以是地址连续的,也可以是不连续的。

在链表中中为每个元素增设指针域来表示元素逻辑上的相邻关系,指针域中存储的信息称作指针或链,用来指向后继元素,用指针来表示元素逻辑上的相邻关系。

此外,每个元素由两部分组成,存储元素本身信息的域称作数据域,存储后继元素地址的域称为指针域,这两部分组成一个元素的结点

对于访问第i个元素的操作:

(1)在数组方式下,可以直接访问该元素;

(2)在链表方式下,则必须从头指针指向的第一个元素开始沿着指针链依次计数,直到“数到“该元素。

对于插入元素操作:

  1. 数组方式下,要把插入位置后面的原来元素往后移动。
  2. 在链表方式下,就不需要移动任何一个记录,只需要先分配一个空间,然后使前一个元素的指针指向插入元素的空间,再把插入空间的指向后一个元素的空间。

删除元素也是如此。

在C语言中,链表可以利用指针和结构体实现。链表中一个结点的C原因呢定义:

typedef struct LNode{

        ElemType data;      //数据域

        struct LNode *next; //指针域

    }LNode,*LinkList;      //结点类型和链表的指针类型

链表的一些基本操作:

  1. 分配、初始化和释放一个结点。建立一个链表,需要用malloc函数为链表中的每个结点分配空间。

LNode *p;

p=(LNode*)malloc(sizeof(LNode));

        (b)中的意义就是将p结点的数据域data初始化为10,指针域next初始化为NULL。对应C代码:

        if(p!=NULL)

        {

           p->data = 10;

           p->next = NULL;

        }

注意:当引用p结点中的任意一个域时,其域指示符是“->”而不是“.”

删除一个结点后,应该将结点空间释放,以便重用。释放p结点的语句如下:

  

      free(p);
  1. 链接和交换两个结点。

现在设有p结点和q结点。(a)

令p结点的指针域指向q结点,将两个结点链接起来。(b)对应语句:

    p->next = q;

交换两个结点是改变两个结点的逻辑关系,令原前驱点变为后继结点语句如下:

    q->next = p;

    p->next = NULL;
  1. 遍历一个链表。遍历是一个对数据结构的全部元素访问且仅访问一次的操作。可定义一个ElemType类型的数据元素进行访问的函数,此后的遍历操作都通过调用该函数访问数据元素。

最简单的访问函数是直接输出数据元素:

Status printElement(ElemType e){

         printf(“%c”,e);      //假设元素类型ElemType为char

         return OK;

}

算法:链表的遍历

void TraverseLinkList(LinkList L,Status(*visit)(ElemType e)){

//从头到尾遍历指针L指向的链表

    LNode *p;

    p=L;

    while(p!=NULL)

    {

        visit(p->data);

        p=p->NULL;

    }

}

函数TraverseLinkList的第二个参数visit是函数指针。函数指针是指向函数的指针变量,本质是一个指针变量。

函数指针声明格式如下:

类型说明符(*函数名)(参数);

例如:

    Status(*fpt)(ElemType e);//声明一个函数指针

函数指针的初始化就是将函数的地址赋值给函数指针。

例如:

        fpt = printElement;     //将printElement函数的首地址赋给fpt

对于函数指针参数,实参是一个具有相同返回类型和参数表的函数名或函数指针。例如:

    TraverseLinkList(L,printElement);

    TraverseLinkList(L,fpt);

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

提刀立码,调参炼丹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值