线性表

目录

 

 

1 定义

 2 线性表的顺序存储结构

2.1 顺序结构的获取元素操作

2.2 顺序结构的插入操作

2.3 顺序结构的删除操作

2.4 顺序表结构的优缺点

3 线性表的链式存储结构

4 单链表的读取

5 单链表的插入与删除

5.1 单链表的插入

5.2 单链表的删除

6 单链表的整表创建

7 单链表的整表删除

8 单链表结构与顺序存储结构优缺点

9 静态链表

9.1 静态链表的插入操作

9.2 静态链表的删除操作

9.3 静态链表的优缺点

10 循环链表

11 双向链表

12 总结


 

1 定义


线性表:零个或多个数据原始的有限序列。
有限个元素,元素之间有顺序,第一个元素无前驱,最后一个元素无后继,其他元素有且只有一个前驱和后继。

 

 2 线性表的顺序存储结构


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

线性表的每个数据元素的数据类型都相同,所以可以使用一维数据来实现顺序存储结构,即把第一个数据元素存到数据下标为0的位置中,接着把线性表相邻的元素存储在数组中的相邻的位置。

存储器中的每个存储单元都有自己的编号,这个编号称为地址。
第i个元素与第i+1个元素的地址关系:LOC(a_{i+1}) = LOC(a_{i}) + C  ,其中C为数据类型占用的c个存储单元。

第i个元素与第1个元素的关系为: LOC(a_{i}) =  LOC(a_{1}) + (i - 1) *C

 

2.1 顺序结构的获取元素操作

对于线性表的顺序存储结构来说,如果要getElement操作,即将线性表L中的第i个位置元素值返回,只要i的数值在数组下标范围内,就把数组第i-1下标的值返回即可,时间复杂度O(1)。

 

2.2 顺序结构的插入操作

算法思路:

  1. 如果插入位置不在数组范围内,抛异常;
  2. 如果线性表长度大于等于数组长度,抛出异常或动态增加容量;
  3. 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置;
  4. 将要插入的元素填入位置i处;
  5. 表长加1;

2.3 顺序结构的删除操作

算法思路:

  1. 如果删除的位置不在数组范围内,抛异常;
  2. 取出删除元素;
  3. 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置;
  4. 表长减1;

如果元素要插入或删除最后一个元素,时间复杂度是O(1);如果插入或删除最后一个那么时间复杂度为O(n),至于平均情况,由于元素插入到第i个位置,或删除第i个元素,需要移动n-i个元素,根据概率原理,每个位置插入或删除的可能性是相同的,也就是说位置靠前移动元素多,位置考后,移动元素少,最终平均移动次数和最中间的那个元素的移动次数相等为(n-1)/2,由时间复杂度的推导,可以得出平均复杂度为O(n)

2.4 顺序表结构的优缺点

优点:

  1.   无须为表示表中元素之间的逻辑关系而增加额外的存储空间;
  2.   可以快速存取表中任一位置的元素

缺点:

  1. 插入和删除操作需要移动大量元素;
  2. 当前线性表长度变化较大,难以确定存储空间的容量;

3 线性表的链式存储结构

线性表的顺序存储结构,最大的缺点就是插入和删除时需要移动大量元素。

n个结点链接称一个链表,即为线性表的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。

链表中第一个结点的存储位置叫做头指针。在单链表的第一个结点前附设一个结点,称为头结点。

头指针:头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针,头指针具有标识作用,所以常用头指针冠以链表的名字,无论链表是否为空,头指针均不为空,头指针是链表的必要元素

头结点:头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义;有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了;头结点不一定是链表必须要素

结点由存放数据元素的数据域存放后继结点地址的指针组成

4 单链表的读取

算法思路:

  • 声明一个结点p指向链表第一个结点,初始化j从1开始;
  • 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
  • 若到链表末尾p为空,则说明第i个元素不存在;
  • 否则查找成功,返回结点p的数据;

5 单链表的插入与删除

5.1 单链表的插入

算法思路:

  • 声明一结点p指向链表第一个结点,初始化j从1开始;
  • 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
  • 若到链表末尾p为空,则说明第i个元素不存在;
  • 否则查找成功,在系统中生成一个空结点s;
  • 将数据元素e赋值给s->data;
  • 单链表的插入标准语句s->next = p->next; p -> next = s;
  • 返回成功

对单链表表头和表尾的插入

5.2 单链表的删除

算法思路:

  • 声明一结点p指向链表第一个结点,初始化j从1开始;
  • 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
  • 若到链表末尾p为空,则说明第i个元素不存在;
  • 否则查找成功,将欲删除的结点p->next 赋值给q;
  • 单链表的删除标准语句 p->next = q->next;
  • 将q结点中的数据赋值给e,作为返回;
  • 释放q结点;
  • 返回成功

6 单链表的整表创建

单链表整表创建的算法思路:

  • 声明一结点p和计数器变量i;
  • 初始化一空链表L;
  • 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
  • 循环:
  • 生成一新结点赋值给p;
  • 随机生成一数字赋值给p的数据域p->data;
  • 将p插入到头结点与前一新结点之间
/*随机产生n个元素的值,建立带表头结点的单链表线性表L(头插法)*/
void createListHead(LinkList *L, int n) {
   LinkList p;
   int i;
   srand(time(0));   /*初始化随机数种子*/
   *L = (LinkList)malloc(sizeof(Node));
   (*L) -> next = NULL;   /*先建立一个带头结点的单链表*/
   for(i=0; i<n; i++) {
      p = (LinkList)malloc(sizeof(Node));/*生成新结点*/
      p->data = rand()%100 + 1;    /*随机生成100以内的数字*/
      p->next = (*L)->next;
      (*L)->next = p;   /*插入到表头*/
   }
}

/*随机产生n个元素的值,建立带头结点的单链表L(尾插法)*/
void createListTail(LinkList *L, int n) {
   LinkList p,r;
   int i;
   srand(time(0));    /*初始化随机数种子*/
   *L = (LinkList)malloc(sizeof(Node)); /*创建整个线性表*/
   r = *L;  /*r为指向尾部的结点*/
   for(i=0; i<n; i++) {
      p = (Node *)malloc(sizeof(Node));  /*生成新结点*/
      p->data = rand()%100+1;  /*随机生成100以内的数字*/
      r->next = p;    /*将表尾终端结点的指针指向新结点*/
      r = p;   /*将当前的新结点定义为表尾终端即诶单*/
   }
   r->next = NULL;   /*当前链表结束*/
}

7 单链表的整表删除

单链表整表删除的算法思路:

  • 声明一结点p和q;
  • 将第一个结点赋值给p;
  • 循环:
  • 将下一结点赋值给q;
  • 释放p;
  • 将q赋值给p
/*初始条件:顺序线性表L已存在,操作结果:将L重置为空表*/
Status clearList(LinkList *L) {
   LinkList p,q;
   p = (*L)->next;  /*p指向第一个结点*/
   while(p) {
     q = p->next;
     free(p);
     p = q;
   }
   (*L)-> = NULL;   /*头结点指针域为空*/
   return OK;
}

8 单链表结构与顺序存储结构优缺点

存储分配方式:顺序存储结构用一段连续的存储单元依次存储线性表的数据元素;单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素

时间性能:

   查找:顺序存储结构O(1),单链表O(n)

   插入和删除:

        顺序存储结构需要平均移动表长一半的元素,时间为O(n);单链表在线出某位置的指针后,插入和删除时间仅为O(1)

   空间性能:

       顺序存储结构需要预分配存储空间,分大了,浪费,分小了易发生上溢;单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制

9 静态链表

用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法。

首先我们让数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个data和一个cur,数据域data,用来存放数据元素,也就是通常我们要处理的数据,而游标cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。

/*线性标的静态链表存储结构*/
#define MAXSIZE 1000 /*假设链表的最大长度是1000*/
typedef struct{
   ElemType data;
   int cur;   /*游标,为0时表示无指向*/
}Component, StaticLinkList[MAXSIZE];

另外我们对数组第一个最后一个元素作为特殊元素处理,不存数据,通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0

/*将一维数组space中各分量链表成一备用链表,space[0].cur为头指针,“0”表示空指针*/
Status InitList(StaticLinkList space) {
   int i;
   for(i=0; i<MAXSIZE-1; i++) {
       space[i].cur = i+1;
   }
   space[MAXSIZE-1].cur = 0;  /*目前静态链表为空,最后一个元素的cur为0*/
   return OK;
}

9.1 静态链表的插入操作

静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间分配,需要时申请,无用时释放。在动态链表中,结点的申请和释放分别借用malloc()和free()两个函数来实现,在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以我们需要自己实现这两个函数,才可以做删除和插入操作。

为来辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,没当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int malloc_sll(StaticLinkList space) {
    int i = space[0].cur;  /*当前数组第一个元素的cur存的值*/
                           /*就是要返回的第一个备用空闲的下标*/
    if(space[0].cur) {
       space[0].cur = space[i].cur; /*由于要拿出一个分量来使用来*/
                                    /*所以就得把它的下一个分量用来做备用*/
    }
   
}

/*在L中第i个元素之前插入新的数据元素e*/
Status ListInsert(StaticLinkList L, int i, ElemenType e) {
   int j,k,l;
   k = MAX_SIZE -1;  /*注意k首先是最后一个元素的下标*/
   if(i<1 || i>ListLength(L)+1) {
      return ERROR;
   }
   j = malloc_ssl(L);  /*获得空闲分量的下标*/
   if(j) {
      L[j].data = e;  /*将数据赋值给此分量的data*/
      for(l=1; l<=i-1; l++) {/*找到第i个元素之前的位置*/
         k = L[k].cur;
      }
      L[j].cur = L[k].cur; /*把第i个元素之前的cur赋值给新元素的cur*/
      L[k].cur = j;  /*把新元素的下标赋值给第i个元素之前元素的cur*/
      return OK;
   }
   return ERROR;
}

9.2 静态链表的删除操作

删除元素时,原来是需要释放结点的函数free(),现在我们也需要自己实现

/*删除在L中第i个数据元素e*/
Status listDelete(StaticLinkList L, int i) {
   int j, k;
   if(i<1 || i>listLength(L)) {
      return ERROR;
   }
   k = MAX_SIZE -1;
   for(j=1; j<=i-1; j++) {
      k = L[k].cur;
   }
   j = L[k].cur;
   L[k].cur = L[j].cur;
   free_ssl(L, j);
   return OK;
}

void free_ssl(StaticLinkList space, int k) {
   space[k].cur = space[0].cur; /*把第一个元素cur值赋给要删除的分量cur*/
   space[0].cur = k; /*把要删除的分量下标赋值给第一个元素的cur*/
}

/*初始条件:静态链表L已存在,操作结果:返回L中数据元素个数*/
int listLength(StaticLinkList L) {
   int j = 0;
   int i = L[MAXSIZE-1].cur;
   while(i) {
      i = L[i].cur;
      j++;
   }
   return j;
}

9.3 静态链表的优缺点

优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点

缺点:没有解决连续存储分配带来的表长难以确定的问题;失去了顺序存储结构随机存取的特性

10 循环链表

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。

为了使空链表和非空链表处理一致,通常设一个头结点,并不是说,循环链表一定要头结点。

带头结点的空链表:

对于非空的循环链表:

其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。

用指向终端结点的尾指针来表示循环链表,此时查找开始结点和终端结点都很方便,终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点,其实就是rear->next->next,其时间复杂度也为O(1);

11 双向链表

双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域,所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。

/*线性表的双向链表存储结构*/
typedef struct DulNode{
    ElemType data;
    struct DulNode *prior;   /*直接前驱指针*/
    struct DulNode *next;    /*直接后继指针*/
}DulNode, *DuLinkList;

双向链表的循环带头结点的空链表如下图:

非空的循环带头结点的双向链表如下图:

由于这是双向链表,那么对于链表中的某一个结点p,它的后继的前驱还是它自己,它的前驱的后继自己也是它自己,即:

p->next-prior = p = p->prior->next;

双向链表的插入:

s->prior = p;  /*把p赋值给s的前驱,如上图1*/
s->next = p->next; /*把p->next赋值给s的后继,如上图2*/
p->next->prior = s; /*把s赋值给p->next的前驱,如上图3*/
p->next = s;  /*把s赋值给p的后继,如上图4*/

双向链表的删除:

p->prior->next = p->next;  /*p->next赋值给p->prior的后继,如上图1*/
p->next->prior = p->prior;  /*把p->prior赋值给p->next的前驱,如上图2*/
free(p);  /*释放结点*/

12 总结

 

 

 

 

【返回目录】

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值