数据结构篇

         一.绪论

                  数据结构主要研究非数值性程序设计中所出现的计算机操作对象以及他们之间的关系和运算等

        术语  

        数据(Data):信息    对在计算机科学中指所有能输入到计算机中并被计算机程序处理的符号的总称;

                              例如 在结客观事物的符号表示,构体中,结构体中的所描绘的内容都是数据

        数据元素(Data Element):数据的基本单位  

        数据项(Data Item)是有独立含义的最小单位       数据项构成数据元素

         数据元素可以包含一个或者几个数据项,例如数据库表里边的某一列,就是一个数据项,每一行就是一个数据元素

        数据的基本单位,在计算程序中通常作为一个整体进行考虑和处理,有时,一个数据元素可由若干个数 据项(data item)组成; 如:一本书的数目信息为一个数据元素, 而书目信息的每一项(书名、作者名等) 为一个数据项, 数据项是数据不可分割的最小单位.  在数组中int a[10]里面有10个元素,结构体整体才是一个数据元素

        数据对象(Data Object)元素的集合

是性质相同的数据元素的集合,是数据的一个子集; 
如:整数数据对象N = {0, ±1, ±2, ±3, …},字母字符数据对象C = {‘A’, ‘B’, ‘C’, …, ‘Z’}

       数据结构(Data Structure)

                 是相互之间存在的一种或者多种特定关系的数据元素的集合.(通常有四类基本结构)

                         集合: 结构中的数据元素除了”同属于一个集合”的关系外,别无其他关系;

                         线性结构: 结构中的数据元素之前存在一对一关系  除第一个元素外,其他元素只有一个前驱,除最后一个元素外                                            其他元素只有一个后继

                        树形结构: 结构中的数据元素之间存在一对多关系    即一个元素只有一个前驱    但可以有多个后继

                        图状或网状结构: 结构中的数据元素存在多对多关系   元素之间的逻辑关系可以是任意的

                 从上可以看出      数据结构就是带有结构的数据元素的集合 

     数据结构三要素

(1)逻辑结构: 数据之间关系的描述,虚的

逻辑结构形式上用二元组,B=K,RK是结点的集合,RK上关系的集合,例如<k,k>代表kk’前驱kk后继,为相邻结点       逻辑结构独立于存储结构  而存储结构依赖逻辑结构

      (1.1) 线性结构:有且只有一个开始结点和一个终端结点,并且所有结点都最多只有一个直接前驱和一个直接后    继(11),典型的有:线性表、栈、队列、数组、串

      (1.2)  非线性结构:每个结点可以有不止一个直接前驱和直接后继(11),典型的树、图、集合

 (2) 存储结构: 数据结构在计算机中映射称为存储结构,实的    

                             存储结构是逻辑关系的映象与元素本身的映象,存储结构实质上是内存分配

      注:在计算机中存储信息的最小单位叫做位(bit),8位表示一个字节(byte),二个字节称为一个字(word),字节,字或更多得二进制位可称为位串,这个位串称为元素或结点。当数据元素由若干个数据项组成时,则位串中对应于每个数据项的子位串称为数据域

        (2.1)顺序存储结构(sequentialstorage structure):把逻辑上相连的结点存储在物理位置上相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现。一般采用数组或者结构体数组来描述。  

       (2.2)链式存储结构(LinkedStorage Structure):其不要求逻辑上相邻的结点,在物理位置上相邻,结点间的逻辑关系由附加的(指针)引用字段表示。一个结点的引用字段往往指导下一个结点的存放位置。一般采用链表来描述

       (2.3)索引存储方式(index:索引存储方式是采用附加的索引表的方式来存储节点信息的一种存储方式。索引表由若干索引项组成。索引存储方式中索引项的一般形式为(关键字、地址)。其中,关键字是能够唯一标识一个节点的数据项。

      (2.4)散列存储方式(hash):根据结点的关键字通过散列函数直接计算出该结点的存储地址

  (3)数据运算:一些操作,增、删、改、查

       数据类型(Data Type)类型

一个值的集合和定义在这个值集上的一组操作的总称(例如,C语言中的整型变量,其值集为某个区间上的整数),定义在其上的操作为加减乘数模运算等算术运算),按”值”的不同特性,高级程序语言中的数据类型可分为两类

            非结构的原子类型: 原子类型是不可分解的.例如:C的基本类型(整型,实型,字符型和枚举类型)、指针类型和空类型.

           结构类型: 结构类型的值是由若干成为按照某种结构组成的,因此是可以分解的,并且它的成分可以是非结构的,也可以是结构的. 例如:数组的值由若干分量组成,每个分量可以是整数,可以是数组等.在某种意义上,数据结构可以看成是”一组具有相同结构的值”,则数据结构可以看成由一种数据结构和定义在其上的一组操作组成.

            抽象数据类型(Abstract Data Type, ADT): 指一个数学模型及定义在该模型上的一组操作.抽象数据类型的定义仅取决于它的一组逻辑特性内,而与其在计算机内部表示和实现无关,即无论其部结构如何变化,只要它的数学特性不变,都不影响其外部的使用.

       抽象数据类型 (Abstract Data TypeADT)模型、类型

一个含抽象数据类型的软件模块通常应包含”定义,表示实现“3个部分
抽象数据类型的定义由一个值域和定义在该值域的一组操作组成.若按照其值的不同特性,可分为3种类型

           原子类型(atomic data type)属原子类型的变量的值是不可分解的.这类抽象数据类型较少,因为一般情况下,已有的固有数据类型足以满足需求,但有时也有必要定义新的原子数据类型,例如整位为100的整数;

            结构类型

                     固定聚合类型(fixed-aggregate data type)属该类型的变量,其值由确定数目的成分按某种结构组成.例如,复数是两个实数依确定的次序关系组成.

                     可变聚合类型(variable-aggregate date type)和固定聚合类型相比较,构成可变聚合类型”值”的成分的数目是不确定.例如,可定义一个”有序整数序列”的抽象数据类型,其中序列的长度是可变的.固定聚合类型与可变聚合类型可以统称为

            多形数据类型(polymorphic data type)是指其值的成分不确定的数据类型,抽象数据类型中,不论其元素具有何种特性,元素之间的关系相同,基本操作也相同,从抽象数据类型的角度看,具有相同的数学抽象特性,故称之为多形数据类型.

     二.算法

       1.概念

          算法+数据结构=程序,说明数据结构和算法是程序的两大要素,二者相辅相成,缺一不可。算法是程序的灵魂。

          算法:算法是指解决问题的一种方法或一个过程

          程序:程序是算法用某种程序设计语言的具体实现

          算法程序都是用来表达解决问题的逻辑步骤,但算法独立于具体的计算机,与具体的程序设计语言无关,而程序正好相反;程序是算法,但算法不一定是程序

        2.算法的特性

           有限性:算法必须在执行有穷步之后结束,而每一步都必须在有穷时间内完成。

           确定性:算法中每一步操作的含义都必须是确定的,不能有二义性。

           输    入:一个算法可以有零个或多个输入。 

           输    出:一个算法有一个或多个输出。

           可行性:一个算法必须是可行的,即算法中每一操作都能通过已知的一组基本操作来实现。

        数据结构的表示(存储结构)用类型定义(typedef)来描述。数据元素类型约定为ElemType

       3.时间复杂度

算法执行时间 :一个算法的执行时间大致上等于其所有语句执行时间的总和,对于语句的执行时间是指该条语句的执行次数和执行一次所需时间的乘积。

语句频度:指该语句在一个算法中重复执行的次数,算法中所有语句频度之和 记作T(n),基本运算频度为f(n), T(n)=O(f(n)) 表示随问题规模n的增大,算法的执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度,所以算时间复杂度相当于算频度

时间复杂度就是执行语句被调用了多少次

时间复杂度依赖于问题规模,和数据的初始状态

选取T(n)=O(f(n))中增长速度最快的项,且系数得写1如果f(n)跟n没有关系时,即频度是个常量,即可以明确表示的数字时,O(1)

最坏时间复杂度和平均时间复杂度  最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。 

规则

T(n)=T1(n)+T2(n)=O(f(n)+g(n))=O(max(f(n),g(n))

T(n)=T1(n)*T2(n)=O(f(n)*g(n))=O(f(n)*g(n))

一般计算最深层循环内的语句频度是时间复杂度

常用时间复杂度

常用的时间复杂度:O(1) <O(log2n) < O(n) < O(n log2n) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

怎么计算时间复杂度

(1)如果只调用了一次,如:
x=5; 
if(x<-4)
{x=x+4;}
else
{x=x+3;}
在大括号中的内容,只会调用一个语句,那么O(n)=1;  
(2)如果调用了两次,如:
x=5;
if(x<-4)
{x=x+4;}
else
{x=x+3;}
x=x+56;
在大括号中的内容,只会调用一个语句,但是在最后,还有一个计算公式要调用语句;总共加起来就是调用2次。那么O(n)=2;
(3)用1个FOR循环调用
for(x=0;x<n;x++)
{x=x+1;}
x会从0到n-1循环,执行的语句就是将当前x值加入新的x中,总共调用n次;那么O(n)=n;
(4)用2个嵌套FOR循环调用
for(x=0;x<n;x++)
{
for(y=1;y<=n;y++)
{x=x+y;}
}
遇到嵌套循环,可以先将外面的FOR语句中的变量固定为初始值x=0,主要看里面的FOR语句的时间复杂度,很明显,里面语句执行次数是从1到n总共调用n次,O(n)=n;这还只是x=0时的调用。x可以从0到n-1,共n次。每次调用都会执行n次调用y的情况,因此,执行语句x=x+y;总共会调用n*n次。O(n)=n^2。

数执行语句的执行次数,就是时间复杂度。注意:
(1)找到正确的执行语句。
(2)for循环中的初始值和终止值。
for(i=0;i<n;i++) i值变化是从0到n-1,共n次。
for(i=0;i<=n;i++) i值变化是从0到n,共n+1次。
(3)注意for循环的调用顺序,从里面到外面进行的。

注:访问第i个结点,属于随机  时间复杂度为o(1);   不需要移动的为o(1)  需要移动的为o(n);

         4.空间复杂度 

关于算法的存储空间需求,类似于算法的时间复杂度, 我们采用空间复杂度作为算法所需存储空间的度量S,记作: S(n)=O(f(n)) ,其中,n为问题的规模,O表示数量级。

对于输入数据所占的具体存储量只取决于问题本身,与算法无关,因此,只需分析算法在实现时所需辅助空间即可。若所需辅助空间相对于输入数据而言是个常数,则称该算法为原地工作,辅助空间为O(1)

          三.线性表

     1.定义

线性表(Linear_List)简称为表nn≥0个具有相同类型的数据元素的有限序列  其中n为表长,当n=0 时该线性表是一个空表。若用L命名线性表,则其一般表示如下:
    L=(a1, a2, ..., ai, ai+1, ..., an)

线性表是一种最简单,最基本,也是最常用的一种线性结构(线性结构的特点是数据元素之间存在一种线性关系)。它有顺序结构存储和链式结构存储,它的主要基本操作有插入、删除和查找等。在一个线性表中,数据元素的类型是相同的,或者说线性表是由同一类型的数据元素构成的线性结构。

线性表的长度: 线性表中数据元素的个数

空表 :长度等于零的线性表

除第一个元素外,其他每一个元素有且仅有一个直接前驱。

除最后一个元素外,其他每一个元素有且仅有一个直接后继。

    2.特点

同一性:线性表由同类数据元素组成,每一个ai必须属于同一数据对象

有穷性:线性表由有限个数据元素组成, 表长度就是表中数据元素的个数

有序性:线性表中表中相邻数据元素之间存在着序偶关系<ai, ai+1>

  • 表中元素的个数有限。
  • 表中元素具有逻辑上的顺序性,在序列中各元素排序有其先后次序。
  • 表中元素都是数据元素,每一个表元素都是单个元素。
  • 表中元素的数据类型都相同。这意味着每一个表元素占有相同数量的存储空间。

唯一首元素,唯一尾元素除首元素外,任何元素都有一个前驱除尾元素外,任何元素都有一个后继每个元素有一个位序

注:注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要将其混淆 

 3.线性表顺序存储

        线性表的顺序存储(Sequential Mapping,简称顺序表):指用一组地址连续的存储单元依次存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中。

         由于C语言的一维数组在内存中所占的正是一个地址连续的存储区域,顺序存储最核心的则是数组

         采用顺序存储结构的线性表通常称为顺序表

        假设线性表中有n个元素,每个元素占k个单元(字节),第一个元素的地址为loc(a1),则可以计算出第i个元素的地址loc(ai)

                 loc(ai) =loc(a1)+(i-1)×k

其中loc(a1) 称为基址。      区分元素的序号和数组的下标,a1的序号为1,而其对应的数组下标为0

         顺序表的特点:随机存取,查找时间复杂度O(1),删除、插入得移动大量元素

          顺序表结构描述    

               结构类型的声明:

                          typedef struct List

                             {

                                       ElemType data[MAXSIZE]; //数组存储数据元素     

                                       int length;                //线性表当前长度

                              }SqList, *list;

注:顺序表的运算

int isEmpty(SqList &L);//判断表是否为空
int getElem(SqList L, int i);//返回第i个位置的值
int listInsert(SqList &L, int i, int e);//在指定位置第i处插入数据e
int listDelete(SqList &L, int i);//删除指定位置的元素
void printList(SqList &L);//打印线性表
int listLength(SqList &L);//求线性表的长度
void initList(SqList &L);//初始化线性表
int locateElem(SqList &L, int x)//返回该值的位置
int destroylist(sqlist &l)//销毁链表

4.顺序表的动态分配

       静态分配的顺序表因为内存是固定的,内存分配少了,容易产生溢出,内存分配多了又浪费内存,所以最好是动态分配,但仍然属于顺序表,而不是链表

     malloc()函数用来动态分配内存空间

 注:静态分配的释放是由程序决定的,主函数运行结束,才释放内存

        动态分配的释放是手动释放的,关键字为free(参数);

     realloc() 函数用来重新分配内存空间,其原型为: 
        void* realloc (void* ptr, size_t size);

【参数说明】ptr 为需要重新分配的内存空间指针,size 为新的内存空间的大小。

realloc() 对 ptr 指向的内存重新分配 size 大小的空间,size 可比原来的大或者小,还可以不变。当 malloc()calloc() 分配的内存空间不够用时,就可以用 realloc() 来调整已分配的内存。

如果 ptr 为 NULL,它的效果和 malloc() 相同,即分配 size 字节的内存空间。

如果 size 的值为 0,那么 ptr 指向的内存空间就会被释放,但是由于没有开辟新的内存空间,所以会返回空指针;类似于调用 free()

几点注意:

  • 指针 ptr 必须是在动态内存空间分配成功的指针,形如如下的指针是不可以的:int *i; int a[2];会导致运行时错误,可以简单的这样记忆:用 malloc()、calloc()、realloc() 分配成功的指针才能被 realloc() 函数接受。
  • 成功分配内存后 ptr 将被系统回收,一定不可再对 ptr 指针做任何操作,包括 free();相反的,可以对 realloc() 函数的返回值进行正常操作。
  • 如果是扩大内存操作会把 ptr 指向的内存中的数据复制到新地址(新地址也可能会和原地址相同,但依旧不能对原指针进行任何操作);如果是缩小内存操作,原始据会被复制并截取新长度。


【返回值】分配成功返回新的内存地址,可能与 ptr 相同,也可能不同;失败则返回 NULL。

注意:如果分配失败,ptr 指向的内存不会被释放,它的内容也不会改变,依然可以正常使用

补:引用的用法

       引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。是属于同一个存储单元

 
 引用的声明方法:类型标识符 &引用名=目标变量名;

【例1】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名

  说明:

  (1)&在此不是求地址运算,而是起标识作用。

  (2)类型标识符是指目标变量的类型。

  (3)
声明引用时,必须同时对其进行初始化。

  (4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。

   ra=1; 等价于 a=1;

  
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。

  (6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。

引用应用

  1、引用作为参数

  引用的一个重要作用就是作为函数的参数
。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C 中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。


  (1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

  (2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

  (3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

  如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。

  2、常引用

  常引用声明方式:const 类型标识符 &引用名=目标变量名;

  用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。

  原因在于foo( )和"hello world"串都会产生一个临时对象,而在C 中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。

  引用型参数应该在能被定义为const的情况下,尽量定义为const 。

  3、引用作为返回值

  要以引用返回函数值,则函数定义时要按以下格式:

类型标识符 &函数名(形参列表及类型说明)
{函数体}

  说明:

  (1)以引用返回函数值,定义函数时需要在函数名前加&

  (2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。

  引用作为返回值,必须遵守以下规则:

  (1)不能返回局部变量的引用。这条可以参照Effective C [1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。

  (2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C [1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。

  (3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C [1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
  4、引用和多态

  引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。

  【例7】:

class  A;
class  B:public A{……};
B  b;
A  &Ref = b; // 用派生类对象初始化基类对象的引用

  Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。

  引用总结

  (1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。

  (2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。

    5.链表

  1. 头插法:插入的顺序和链表中的顺序是相反的
  2. 尾插法:插入的顺序和链表中的顺序是一样的

        (1)特点

          逻辑次序和物理次序不一定相同,物理内存不要求连续                元素之间的逻辑关系用指针表示

        (2)结点

         为了正确地表示结点间的逻辑关系,必须在存储线性表的每个数据元素值的同时,存储指示其后继结点的地址(或位置)信息,这两部分信息组成的存储映象叫做结点(Node

        (3)单链表

            单链表

猜你喜欢

转载自blog.csdn.net/Wang_kang1/article/details/82966533
今日推荐