您的位置:首页 > 理论基础 > 数据结构算法

线性表(顺序表,链表的表示和实现)

2016-03-06 16:55 459 查看
本篇文章将从以下几点进行讲解:

 1.线性表的类型定义

 2.1线性表的顺序表示和实现

 2.2线性表的链式表示和实现

1.线性表的类型定义:

接下来介绍下什么是线性表,学习每一样东西都要从定义下手.

由n( n>= 0 )个数据特性相同的元素构成的有限序列称为线性表//这个是官方给出的定义有点生僻

换句话说,同一线性表中的元素必定 具有相同特性 , 也就是说属于同一数据对象(就是它们的结构类型是一样的,有着同个爸妈生出来的),还有一个最明显的特点就是 相邻数据元素之间存在着 序偶关系;那么问题又来了这里 为什么说 序偶 ,序偶 又是神马呢?这里先不解释,到后面 讲解完对比着去思考 线性表中的顺序表 和 链式表 你就迎刃而解了.

接下来在对线性表的定义再啰嗦几句- -,

   线性表中元素的个数为n( n >= 0 ) ,将这个n定义为线性表的长度, 当 n = 0 时称为空表;

   那么对于非空线性表或线性结构,有一下特点:

          (1)存在唯一的一个被称为 "第一个"的数据元素;

          (2)存在唯一的一个被称为"最后一个"的数据元素"

          (3)除第一个元素外,结构中每个元素均只有一个"前驱"

          (4)除最后一个素外,结构中每个元素均只有一个"后继"

       ps:这里的前驱和后继 你要是不理解的话对线性表的链式结构理解起来就有很大的问题了;在线性表的链式结构中,前驱和后继就是存放链式结构的 指针域,相信很多的同学都知道 链式存储可以的存储内存分配方式可以是一整块的内存 也可以是散列开的内存,相邻的两个链节点,前一结点的后继指向后一结点的前驱.还是没理解的小伙伴也不要太着急,在后面讲解到链式结构的时候我会图文进行讲解链式结构的线性表的讲解.

 

  对于线性表的概念定义把上面的内容理解完就差不多了,接下来就是我们的一个重点内容了:对线性表的顺序表现和实现的介绍,对上面概念每台弄懂的同学,可以再结合下顺序表的内容再一次加深

 2.1线性表的顺序表示和实现

      首先说说我对线性表为什么分为顺序表示和链式表示,都归结内存分配结构,顺序表的内存分配是有序的,链式线性表的内存分配可以是散列的分配,这个就是最本质的问题,所以也导致在后面出现将要引出的话题:顺序表和链式表的操作和差异,由于在数据的存储方式上的特点所以这两者的(增删改查)行为也是不一样的的.

还是老规矩我们先来一个官方的定义下什么是线性表的顺序表示:

     用一组地址连续的存储单元一次存储线性表的数据元素,通常把这种存储结构的线性表称为顺序表,他具有的特点:逻辑上相邻的数据元素,其物理次序也是相邻的.

     这里头举一个栗子:假设线性表的每个元素占 k 个存储单元,那么以第一个单元的存储结构作为起始地址,第 i + 1 个元素的地址是多少呢?

          Loc(a[i+1] ) = Loc(a[i]) + k

       由于上面这个特点,那么就可以引出顺序表的一个最重要的特性: 只要确定了线性表的起始位置,那么线性表中任意元素都可以随机存取,说以业界 又称顺序表存储结构是一种随机存取的存储结构.

2.1.2顺序表中的基本操作的实现

     介绍完顺序表,那么同学肯定很渴望知道顺序表的具体操作是怎么样的(增删改查,创建)

     1.顺序表的初始化//创建

//1.为顺序表动态的分配一个预定大小的数组空间,将elem指向这个段空间的基地址
//2.将表当前长度设置为0
Status InitList_Sq(SqList &L){
//构造一个空的顺序表L
L.elem = new ElemType[MaxSize];  //为顺序表动态分配一个大小为MaxSize的数组空间
if(!L.elem) exit(Error);         //存储分配失败
L.length = 0;                    //将表当前长度设置为0
return OK;
}


PS:动态的分配线性表的存储区域可以更有效的利用系统的资源,当不需要这个线性表的时候,可以用销毁操作及时的释放占用的空间.

     2.查找

    查找分为两种情况:一种是按序号查找,另一种是按值查找;

    由于书序表是随机存取的,所以按序号查找的话很简单,只需要将序号写到顺序表的角标里头就可以定位到要找的角标对应的那个元素了;

    下面给给小伙伴们介绍另一种情况:给按照所给定的值进行查找
//在顺序表中查找与给定值 e 相等的数据元素,如果找到返回其在表中的"位置序号",否则返回 0
//1.从第一个元素起,一次和 e 进行比较
//2.若比对成功,第 i 个元素的值与 e 值相同,则返回 i + 1
//3.若比对失败,返回 0
int LocateElem_Sq(SqList L,ElemType e){
for(int i=0;i <L.length; ++i)
if(L.elem[i]==e) return i+1;
return 0;
}


 ps:当顺序表中查找一个元素的时候,查找的时间主要花销在比对上面,而比对的次数又取决于查找元素的位置;所以时间的复杂度为在查找成功时的期望,平均查找长度,O(n)

  3.插入

   线性表要在第 i 的位置插入一个元素,即在 a[i - 1] 和a[i]间插入一个元素,由于顺序表的数据元素不仅在逻辑上相邻,在物理上也是相邻的.那么要在a[i-1]后面插入一个数据元素,那么剩下的a[i]到a
都要依次往后挪动,腾出位置来给 那个值 e插进去,为什么要依次挪动呢,因为 顺序表的数据结构觉得的:物理上相邻的,又由于顺序表只能使用系统的一块整的 连续的 存储空间,所以你要想实现中间插入一个,那么剩下的就得依次往后挪动一个位置
//假设在第i个元素之前插入一个元素,需要从最后一个元素n开始,依次向后移动一个位置,直至第i个元素
//期中启动了 (n - i + 1)个元素
//1.判断插入位置i是否合法(i值的合法范围 1 <= i <= n+1),判断是否越界
//2.判断顺序表的存储空间是否已经满,满还强行插入会造成内存溢出
//3.将第n个至第i个位置的元素依次向后移动一个位置,,空出第i个位置
//4.将要插入的新元素 e放到第i个位置
//表长度加1,插入成功返回OK
Status ListInsert_Sq(SqList &L,int i,ElemType e){
if(i<1 || i>L.length + 1) return Error; // i 值不合法
if(L.length == MaxSize) return Error; //当前存储空间已满
for(j=L.length -1;j>= i-1;j--)
L.elem[j+1] = L.elem[j];           //插入 i 到 n 的位置向后移动
L.elem[i-1] = e;                        //将值 e 插入到 第i个位置
++L.length;                             //表长叫1
return ok;
}


  Ps:为什么要判断是否已满,在上面的代码中没有做动态扩充,因此当表长达到预设的最大空间是,就不能再插入元素了

    与查找的算法相似,插入一个元素时,时间主要消耗在移动原始上,而移动元素的个数又取决于插入元素的位置,时间复杂度也是为 O(n)

    4.删除

   在线性表的顺序结构中,由于在存储结构上是连续的,所以在删除 a[i]这个元素的时候,a[i-1]和a[i+1]是联动跟着变化的,那么在删除第i个元素的时候,需要依次向前移动第i+1个到第n个元素的位置,共需要移动(n -i)个元素
//1.首先是判断删除的位置i是否合法,合法值为(1<= i <=n),若超出这个范围的话就返回error
//2.将要删除的数保存在e中(这里根据具体的需求,自由选择这一步)
//3.将第i+1到第n位置的元素依次向前移动一个位置
//4.表长度减1,删除成功返回ok
Status ListDelete_Sq(SqList &L,int i,ElemType e){
if(i<1 || i>L.length) return Error;  //i超出边界
e = L.elem[i-1];                     //保存删除的那个值
for(j=i;j<=L.length;++j)             //将第i-1 到第 n 个位置的元素依次向前移动一位
L.elem[j-1] = L.elem[j];
--L.length;                         //长度减1
return ok;
}


 PS:由于删除算法可以看做是插入算法的逆过程,所以也免不了要移动元素,时间的复杂度也是O(n)

 2.2线性表的链式表示和实现

    首先要了解线性表的链式表示,那么就不得不提线性表的链式结构的最典型的一种数据结构:单链表;学习都是从简单到复杂,复杂的东西就是在简单的基础上稍作变化.

   老规矩先介绍下什么是线性表的链式存储结构吧.线性表的链式存储结构的特点:用一组任意的存储单元存储线性表的数据(这组存储单元可以是连续的也可以是散列的);这样的每一个数据成为 结点 ,结点包含两个域: 数据域(存储元素的信息的) 指针域(存储直接后继存储位置,也就是用来指向下一个结点的地址信息,在c语言中有这么一句话,"地址就是指针,指针就是地址"),n个这样的节点就链结成一个链表.

    那么现在就可以引出  单链表(每个结点只含有一个指针域的链表);当然节点的指针域也可以是两个 ,那就叫双向链表咯;

   链表的种类有很多 大体可以分为 线性表 和非线性表 : 他们的分类依据是->所含指针的个数/指针指向/指针链接方式;

   市面上出现的链表可以大致分为 : 单链表,循环链表,双向链表,二叉链表,十字链表,邻接多链表;其中 单链表 循环链表 和双向链表 用于实现 线性表的链式结构

   绕了那么大个圈,现在就 再围绕本次的核心介绍 单链表 :首先要知道单链表的存储结构,整个链表必须从 头指针开始, 头指针指示链表中的第一个结点的存储位置(ps:为什么从头指针开始? 同学们没忘记单链表我们刚刚把他归结为 线性表的一种吧,所以单链表在逻辑结构上是连续,又由于单链表支持 连续空间和散列空间的存储结构 ,所以 就需要从第一的 那个头指针 一级一级的往下指,才能 索引出 链表中的所有结点 ). 同时 由于最后一个数据元素没有直接后继(在前面已经介绍了线性表的定义了 忘记的同学滑上去再看下),则单链表中最后一个结点的指针为
空(NULL).

 下面就贴一下单链表的表示,顺便图文表示下指针域的实际作用,这样对指针的理解就不会辣么抽象了(Ps:图画的太丑不要嫌弃,主要看气质)





Ps单链表的逻辑关系是由结点中的指针指向的

2.2.1单链表基本操作的实现

    1.初始化

          单链表的初始化操作就是构造一个空表



//单链表的存储结构
typedef struct LNode{
ElemType data;             //结点的数据域
struct LNode *next;        //结点的指针域
}LNode,*LinkList;             //LinkList 为指向结构体LNode的指针类型


Ps: LinkList 和 LNode* 同为结构体指针类型;在顺序表中,由于逻辑上相邻的两个元素在物理位置上也是紧密相邻的.然而在单链表中,两个元素的存储位置没有固定的相邻关系,但是每个元素的存储位置都包含在其直接前驱结点的信息中.假设p是链表指向第i个数据元素的指针,他的数据域为 a[i]; 那么 p->next是指向 第i+1 个数据元素的指针.则有  p-> data = a[i];

      p->next->data = a[i+1];

又由于单链表是非随机存取的存储结构,要去第i个元素 必须从 头指针出发顺链进行查找,顺序"存取"的存储结构,所以他的基本操作实现不同于顺序表
//1.生成一个新的结点作为头结点,用头指针L指向头结点
//2.头结点的指针域置空
Status InitList_L(LinkList &L){
L = new LNode;
L->next = NULL;      //头结点的指针域置空
return ok
}


2.按序号查找

从链表的第一个结点顺链扫描,用指针p指向当前扫描到的节点,p的初始值指向第一个结点(P = L-next) .用 j 做计数器,j的初始值设置为1,一直增加进去,知道 j 的值等于 我们要查找的第i个结点
status GetEleme_L(LinkList L,int i,ElemType &e){
p = L->next;j=1; //初始化,p指向第一个结点,j为计数器
while(p&&j<i){     //循环的跳出条件是p为空 ,计数器增加到i时
p= p->next;
++j;
}
if(!p||j>i) return Error;  //第i个元素不存在
e=p->data;                  //取出第i个元素的值
return ok;
}


Ps:这个算法的时间主要花销在,比较j和i的值,并且移动p指针,所以时间取决于 i 的值, 时间复杂度为O(n)

3.按值查找

 这个操作和顺序表的查找操作相似,从第一个结点开始,一起和e相比较,如果查找到该结点的数据域与e的值相等就跳出循环
LNode *LocateElem_L(LinkList L,ElemType e){
p = L->next;
while(p && p->data!=e)
p=p->next;              //寻找满足条件的指针,否则指向下一个结点
return p;
}


Ps:和顺序表的查找相似,所以时间复杂度为 o(n);

4.单链表的

插入

 还记得顺序表的插入吗?在第i个 位置插入一个数据元素,那么需要从第i到第n个数据元素依次向后移动一位.由于这种开销的巨大,所以才引进了链表结构,链表的一大亮点就是他的插入特点,只需要知道要插入的那个位置的指针域就可以了.

  假设s为指向结点x的指针

        s->next = p->next; p->next = s;



//将值为e作为一个新的节点插入到表的第a[i-1] 和 a[i]的位置之间
//1.找到结点a[i-1],并由指针p指向该结点
//2.生成一个新的节点*s
//3.将新结点*s的数据域置为e
//4.将新结点的指针域指向结点a
//5.令结点a[i-1]的指针域指向新结点*s
status ListInsert_L(LinkList &L,int i,ElemType e){
p = L; j =0;
while(p&&j<i-1){            //寻找第i-1个结点
p=p->next;++j;
}
if(!p||j>i-1)
return Error;
s = new LNode;    //生成一个新的节点
s->data = e;      //将该结点的数据域置为e

s->next = p->next;  //这两步是核心算法,你要将s插进入时,你要先把 p的后事先解决了再插
p->next = s;
}


Ps:因为第i个结点在插入之前,你不需要先从第一个结点开始索引找到第i-1个结点,所以还是要一个索引的过程,那么时间复杂度为O(n)

5.单链表的删除

 跟插入数据元素的算法差不多,你要删除之前必须先找到要删除的位置



//1.找到结点a[i-1],并由指针p指向该结点
//2.临时保存删除的结点a[i]在q中,以备释放(Ps:不释放的话,容易导致内存泄露)
//3.令p->next指向a[i]的直接后继结点
//4.将待删除结点的值保存在e中 (这里具体看需求,可以不用这行代码)
//5.释放a[i]控件
status ListDelete_L(LinkList &L,int i,ElemType &e){
p=L;j=0;
while(p->next&&j<i-1){
p=p->next;++j;      // 索引到第i-1个结点
}
if(!(p->next)|| (j>i-1))
return Error;

q=p->next;           //存放待删除的结点
p->next = q->next;
e = q->data;
delete q;
return ok;
}


6.1前插法创建单链表

    前插法是通过将新结点逐一插入链表的头部.首先要建立一个只有头结点 空链表,没读入一个数据元素就申请一个新结点,并将新结点插入到头结点之后



void CreateList_F(LinkList &L,int n){
//逆位序输入n个元素的值
L= new LNode;
L->next=NULL;
for(i=n;i>0;--i){
p=new LNode;              //创建新结点
cin>>p->data;             //输入数据元素的值
p->next=L->next;L->next=p;   //插入到头
}
}


6.2后插法创建单链表

   将新结点逐个插入到链表的尾部



void CreateList_L(LinkList &L,int n){
//正位序插入元素
L=new LNode;
L->next=NULL;
r = L;            //尾指针r指向头结点
for(i=0;i<n;++i){
p=new LNode;  //生成新结点
cin>>p->data;  //输入元素值
p->next = NULL;r->next=p;  //r指向新的尾结点
r=p;
}

}


总结
先感谢下那些耐心的读者能 读到这里,希望帮助同学对线性表有一个较为全面的理解

 

顺序表

链     表

空间

存储空间

预先分配,会导致内存闲置和溢出现象

动态分配,不会出现内存闲置和溢出现象

存储密度

密度为1

需要借助指针来体现元素间的逻辑关系,存储密度小于1

时间

存取元素

随机存取,时间复杂度O(1)

顺序存取,时间复杂度O(n)

插入\删除

平均移动约表中一般元素,时间复杂度O(n)

不需要移动元素,确定插入删除位置后,时间复杂度O(1)

适用情况

1.       表长变化不大,且事先确定变化的范围

2.       很少进行插入删除,经常按照元素序号访问数据元素

1.       长度变化不大

2.       频繁进行插入删除操作

存储密度:一个结点数据本身所占的存储空间和整个结点所占存储空间的比值
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息