数据结构和算法2 线性表的物理结构

1 线性表的定义

        线性表(List):由零个或多个数据元素组成的有限序列。

        这里需要强调几个关键的地方: 首先它是一个序列,也就是说元素之间是有个先来后到的。 若元素存在多个,则第一个元素无前驱,而最后一个元素无后继其他元素都有且只有一个前驱和后继。 另外,线性表强调是有限的,事实上无论计算机发展到多强大,它所处理的元素都是有限的。

       如果用数学语言来进行定义,可如下: 若将线性表记为(a1,…,ai-1,ai,ai+1,…an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。 所以线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。

       

2 数据类型

        数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。 例如很多编程语言的整型,浮点型,字符型这些指的就是数据类型

          在计算机中,内存也不是无限大的,你要计算入1+1=2这样的整型数字的加减乘除运算,显然不需要开辟很大的内存空间。 而如果要计算1.23456789+2.987654321这样带大量小数的,就需要开辟比较大的空间才存放的下。 于是计算机的研究者们就考虑,要对数据类型进行分类,分出多种数据类型来适合各种不同的计算条件差异。

          我们对已有的数据类型进行抽象,就有了抽象数据类型。 抽象数据类型(Abstract Data Type,ADT)是指一个数学模型及定义在该模型上的一组操作。 抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。 比如1+1=2这样一个操作,在不同CPU的处理上可能不一样,但由于其定义的数学特性相同,所以在计算机编程者看来,它们都是相同的

         为了便于在之后的讲解中对抽象数据类型进行规范的描述,我们给出了描述抽象数据类型的标准格式:

        ADT 抽象数据类型名

        Data

                   数据元素之间逻辑关系的定义

       Operation

                   操作

       endADT

         抽象数据类型就是把数据类型和相关操作(相关操作又称为运算)捆绑在一起。

          数据结构的主要运算包括:建立create,消除destroy,从一个数据结构中删除delete一个数据元素,把一个数据元素插如insert到一个数据结构中,对一个数据结构进行访问access,对要给数据结构中的数据元素进行修改modify,对一个数据结构的数据元素进行排序sort,对要给数据结构中的数据元素进行查找search

3 线性表的抽象数据类型

    ADT 线性表(List)

    Data

            线性表的数据对象集合为{a1,a2,a3,a4,...an},每个元素的类型均为DataType,其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素,数据元素之间的关系是一对一的关系。

   Operation

            InitList(*L):初始化操作,建立要给空的线性表L

            ListEmpty(L):判断线性表是否为空表,若线性表为空,返回true,否则返回false

            ClearList(*L):将线性表清空

            GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e

            LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号表示成功,否则,返回0表示失败

            ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e

            ListDelete(*l,i,*e):删除线性表L中第i个位置元素,并用e返回其值

     endADT

4 线性表的顺序存储结构    

4.1   常规知识

           线性表有两种物理存储结构:顺序存储结构和链式存储结构。

           线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

           线性表(a1,a2,a3,...,an)的顺序存储如下:

           

             物理上的存储方式事实上就是在内存中找个初始地址,然后通过占位的形式,把一定的内存空间给占了,然后把相同数据类型的数据元素依次放在这块空地中

            接下来看一下线性表顺序存储的结构代码

#define MAXSIZE 20
typedef int ElemTyep;
typedef struct
{
	ElemTyep data[MAXSIZE];
	int length;
}SqList;

           大家可以看到,其就是对数组进行封装,同时增加了一个当前长度的变量

           总结:顺序存储结构封装需要三个属性:1、存储空间的起始位置,数组data,他的存储位置就是线性表存储空间的存储位置,即data数组第一个元素的存储地址就是该线性表的起始存储位置;2、线性表的最大存储容量:数组的长度MAXSIZE;3、线性表的当前长度length

           注意1:数组的长度和线性表的当前长度需要区分一下【数组的长度是存储线性表的存储空间的总长度,一般初始化后不变。而线性表的当前长度是线性表中元素的个数,是会变化的】

           注意2:线性表的计数从1开始

           注意3:假设ElemType占用的是c个存储单元(字节),那么线性表中第i+1个数据元素和第i个数据元素的存储位置的关系是【LOC(ai+1)=LOC(ai)+c】                                                                                  

           所以:对于第i个数据元素ai的存储位置可以由a1推算得出LOC(ai)=LOC(a1)+(i-1)*c

           

           注意4:通过上述公式,我们可以随时计算出线性表中任意位置的地址,不管他是第一个还是最后一个,都是相同的时间,那么他的存储时间性能当然就是O(1),我们通常称为随机存储结构

4.2 顺序存储的线性表的基本操作

        顺序存储的线性表中,很容易实现线性表的一些操作:初始化、赋值、查找、修改、插入、删除、求长度等

4.2.1 获取指定位置元素值

    实现GetElem的具体操作,即将线性表L中的第i个位置元素值返回,就程序而已,我们只需要把数组的i-1下标的值返回即可

#define ok 1
#define error 0

typedef int status;

status GetElem(SqList L, int i, ElemTyep *e)
{
	if (L.length == 0 || i<1 || i>L.length)
		return error;
	*e = L.data[i - 1];
	return ok;
}

             注意:上述代码返回值类型status是一个整型,约定返回1代表ok,返回0表示error,时间复杂度为o(1)

4.2.2、插入操作

              插入操作ListInsert(*L,i,e),即在线性表L中的第i个位置插入新元素e

              插入算法的思路如下:如果插入位置不合理,抛出异常(或返回error状态);如果线性表长度大于等于数组长度(即插入此元素后元素个数超出列表可以存放的元素最大值)则抛出异常或动态增加数组容量;从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。将要插入元素填入位置i处【注意:位置i处在数组中下标为i-1】;线性表长度加1

status ListInsert(SqList *L, int i, ElemTyep e)
{
	/*step1:判断插入位置是否合理,不合理则直接返回*/
	if (i<1 || i>L->length+1)  //情况1:插入位置不合理,注意:可以在线性表尾部添加元素
		return error;
	if (L->length == MAXSIZE)  //情况2:线性表长度大于等于数组长度
		return error;
	/*step2:移动相应位置元素,将列表中下标为[i-1,length-1]之间所有元素向后移动*/
     if(i<=L->length)  //注意:i等于L->length+1时表示在表尾添加元素,此时无需移动元素
    {
	    for (int k = L->length-1;k >= i - 1;k--)  //情况3:插入位置在[1,length]之间
	    {
		    L->data[k+1] = L->data[k];
	    }
    }
	/*step3:将元素e插入线性表中下标为i-1的地方,同时线性表长度加一*/
	L->data[i - 1] = e;
	L->length++;
	return ok;
}

         分析时间复杂度如下:

         最好的情况:插入操作刚好要求在线性表尾部【例如一共有3个元素,此时下标最大为2,若要求在第4个位置处添加元素,则无需移动,直接data[3]=value即可】,此时无需移动元素,所以此时的时间复杂度为o(1)

         最坏的情况:如果插入操作的位置在第一个元素位置,那么就意味着要将所有元素都将后面移动一位,所以时间复杂度为O(n)

         平均的情况:取最好情况和最坏情况的中间值O((n-1)/2)

         综上所述:插入操作的时间复杂度为O(n)

4.2.3 删除操作

            删除操作算法的思路:如何删除位置不合理,抛出异常;取出删除元素的元素值;从删除元素位置开始遍历到最后一个元素位置,分别将它们向前移动一个位置;表长减一

status ListDelte(SqList *L, int i, ElemTyep *e)
{
	/*step1:判断插入位置是否合理,不合理则直接返回*/
	if (i<1 || i>L->length)  //情况1:删除位置不合理
		return error;
	if (L->length == 0)      //情况2:线性表长度已经为0,没有可以删除的元素
		return error;
	/*step2:将被删位置i处元素值返回给e*/
	*e = L->data[i - 1];
	/*step3:移动相应位置元素,将列表中下标为[i,length-1]之间所有元素向前移动*/
	if (i < L->length)  //如果删除元素是最后一个,即i ==L.length,则不需要移动元素
	{
		for (int k = i;k >= L->length - 1;k++)  //情况3:删除元素位置在[1,length]之间
		{
			L->data[k - 1] = L->data[k];
		}
	}
	/*step4:线性表长度减一*/
	L->length--;
	return ok;
}

        分析时间复杂度如下:

         最好的情况:删除操作刚好要求在线性表最后一个元素【例如一共有3个元素,此时下标最大为2,若要求删除第三个元素,则无需移动,直接将线性表的长度减一即可】,此时无需移动元素,所以此时的时间复杂度为o(1)

         最坏的情况:如果删除操作的位置在第一个元素位置,那么就意味着要将所有元素都将向前移动一位,所以时间复杂度为O(n)

         平均的情况:取最好情况和最坏情况的中间值O((n-1)/2)

         综上所述:删除操作的时间复杂度为O(n)

4.3 线性表顺序存储结构的优缺点

         线性表的顺序存储结构,在存【注意:存即是赋值】、读数据时,不管是哪个位置,时间复杂度都是O(1),而在插入或删除时,时间复杂度都是O(n)

         这就说明:它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的是存取数据的应用

          线性表顺序存储结构的优点如下:

          1、无需为表示表中元素之间的逻辑关系而增加额外的存储空间;

          2、可以快速地存取表中任意位置的元素

          线性表顺序存储结构的缺点如下:

          1、插入和删除操作需要移动大量元素,此为最大缺点

          2、当线性表长度变化较大时,难以确定存储空间的容量

          3、容易造成存储空间的“碎片”

           上述讲的线性表的顺序存储结构,它最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。那我们能不能针对这个缺陷或者说遗憾提出解决的方法呢?要解决这个问题,我们就得考虑一下导致这个问题的原因。

            为什么当插入和删除时,就要移动大量的元素?原因就在于相邻两元素的存储位置也具有邻居关系,它们在内存中的位置是紧挨着的,中间没有间隙,当然就无法快速插入和删除

            解决方法就是:每个元素多用一个位置来存放指向下一个元素位置的指针,这样子从第一个元素就可以找到第二个元素,第二个元素可以找到第三个元素,以此类推,所有的元素我们就都可以通过遍历而找到,即线性表的链式存储即可解决这一缺陷


拓展知识1  结构体类型

                       https://www.cctry.com/thread-289320-1-1.html

                      1、自定义数据类型:
                      C/C++语言本身提供了很多基本数据类型,例如:int、float、char 等供我们使用。但是程序编写的过程中问题往往比较复杂,基本的数据类型有时候不能满足我们的需求,所以C/C++语言允许开发者根据自己的需要自定义数据类型,接下来要讲解的结构体struct、联合体union、枚举类型enum,类类型class 等就是用户自定义的数据类型。这些用户自定义的数据类型跟C/C++语言提供的基本类型一样,都可以用来定义变量。只不过在这些自定义数据类型在使用之前要先由用户声明出来才行。

                     2、定义结构体的必要性:
                    例如一个学生的信息包括:姓名,学号,性别,年龄 等等。按照我们之前的做法,可以使用数组来定义:

string name[100]; //姓名
int num[100]; //学号
char sex[100]; //性别
int age[100]; //年龄

                   这样虽然也能满足需求,但是比较麻烦,如果想获得一个学生的信息需要从4个数组中分别找到该学生的所有信息,而且没有什么关联性,容易乱,能不能把一个学生的信息都统一到一起呢?把他们当做一个组合项,在一个组合项中包含若干个类型的数据项。C/C++语言允许用户自己定义这样的数据类型,这样的类型就称作结构体。例如:

struct Student
{
    string name;
    int num;
    char sex;
    int age;
};

            这样就声明了一个结构体类型 Student,struct 是结构体类型的关键字,不能省略。

            3、结构体类型的声明:

struct 结构体类型名
{
    //成员表;
};

               struct 是声明该类型为结构体类型的关键字,不能省略。结构体类型名就是该结构体类型的名字,以后可以直接拿这个类型名来定义变量,就跟使用int,double一样用。类型名的命名规则跟变量一样,可以是数字、字母、下划线,且数字不能开头。上面例子中的 Student 就是结构体的类型名。接下来的一对大括号内的成员表包含了该结构体中的全部成员。上例中的 name、num、sex、age 都是结构体中的成员。在声明一个结构体类型时必须对各成员进行类型声明,即:
              类型名 成员名;
              例如:int num;
             备注:C语言中结构体的成员只能是数据,C++对此进行了扩充,结构体的成员既可以包含数据,也可以包含函数,其实在C++中 struct 跟 class 从使用角度来说差别不大!

            4、结构体类型变量的定义及初始化:
            A、定义:
          结构体类型声明完了之后就可以定义变量了,如:Student zhangsan, lisi;
          这种是非常常用的一种定义结构体类型变量的方法。当然也可以在声明结构体类型的时候就定义变量,当然这种是不常用的方法:

struct Student
{
    string name;
    int num;
    char sex;
    int age;
} zhangsan, lisi;

             B、初始化: Student zhangsan = {"张三", 1001, 'm', 25};
备注:初始化参数的顺序一定要和结构体类型声明的成员表顺序一致才行,不然会报错而且会错误的赋值。

           5、结构体类型变量成员的访问:
           结构体变量名.成员名

Student zhangsan = {"张三", 1001, 'm', 25};
zhangsan.num = 29;
int num = zhangsan.num;

拓展知识2    结构体声明时注意事项

                       

                 注意上述两段声明方式,左图使用typedef+struct来声明结构体,SqList是结构体类型名字,右图只是使用struct来声明结构体,lisi是结构体类型变量

拓展知识3  typedef和define具体的详细区别

                         https://www.cnblogs.com/rainbow70626/p/8809013.html

                         1) #define是预处理指令,在编译预处理时进行简单的替换,不作正确性检查,不关含义是否正确照样带入,只有在编译已被展开的源程序时才会发现可能的错误并报错。
                        2)typedef是在编译时处理的。它在自己的作用域内给一个已经存在的类型一个别名                                                                                           3)注意到#define 不是语句 不要在行末加分号,否则 会连分号一块置换。


5 线性表的链式存储结构(单链表)

          注意:线性表的链式存储结构包含:单链表、单循环链表、双向链表,双循环链表,本章主要介绍单链表,第六章主要介绍单循环链表,第七章注主要介绍双向链表,第八章介绍双循环链表

5.1 基础知识

            线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置

            存储链表中结点的任意一组的存储单元可以是连续的也可以是不连续的,甚至是零散分布在内存中的任意位置上,故链表中结点的逻辑顺序和物理顺序不一定相同

           比起顺序存储结构的每个数据元素只需要存储一个位置就可以了,现在的链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址,称为指针pointer或链link

           也就是说除了存储其本身的信息外,还需存储一个指示其直接后继的存储位置的信息

           我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成的数据元素为存储映像,称为节点node

            n个节点链成一个链表,即为线性表(a1,a2,a3,...,an)

            因为此链表的每个节点中只包含一个指针域,所以叫做单链表

             

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值