数据结构(C语言)第二章 线性表

线性表

1.线性表的基本概念

线性表(Linear List)是由n(n≥0)个数据元素(a1,a2,…,an)构成的有限序列,记作L=(a1,a2,…,an)。其中线性表的表长即是表中所包含数据元素的个数,而空表指的是不含数据元素的线性表。

  • 线性表在理论上的定义如下:

对于线性表 L=(a1,a2,…,ai-1, ai, ai+1,…,an),有

(1)ai-1ai之前,称 ai-1ai的直接前驱(1<i≤n)

(2)ai+1ai之后,称 ai+1ai的直接后继(1≤i<n)

(3)首元素 a1没有前驱;

(4)尾元素 an没有后继;

(5)任意元素 ai(1<i<n)有且仅有一个直接前驱和一个直接后继。

image-20240512154755344

​ 线性表的抽象数据类型从逻辑上定义线性表这种数据结构的数据对象、数据对象之间的关系, 以及相关的基本操作。其中,数据对象说明线性表中的每个数据元素均属于某个类型(如整型、实型或字符型等),用一个集合表示:

D={ai|ai∈ElemSet,i=1,2,,n,n≥0}

其中,线性关系运用<ai-1,ai>序偶对来表示前驱和后继的关系。

  • 线性表的抽象数据类型定义如下:

image-20240512152759040

2.线性表顺序存储结构定义及实现

线性表顺序存储结构指的是将线性表中的数据元素依次存放到计算机存储器内一组地址连续的存储单元中,这种分配方式称为顺序分配或顺序映像。在顺序存储结构中,逻辑上相邻的两个数据元素在物理存储空间中也相邻。

  • 线性表(a1,a2,…,an)顺序存储结构的一般形式如下图所示。

下图用一个“井”状结构来表示内存,每一个格子表示存储一个数据元素的空间,a1,a2,…,an 按顺序存储在以 b 开始的连续地址空间中。

b:表示表的首地址/基地址/元素 a1的地址。
p:表示 1 个数据元素所占据存储单元的数量。
MaxLength:表示最大长度,通常为某个常数。

image-20240512161154703

  • 线性表顺序存储结构的定义如下:

    #define  MAXSIZE 100     //最大长度
    typedef  struct 
    {
     	 ElemType  *elem;      //指向数据元素的基地址
      	int  length;                 //线性表的当前长度                        
     }SqList;
    
    

    其中,elem 是一个大小为 MaxLength 的数据元素数组;length 为线性表表长;SqList 为此结构类型定义的名称。

    静态分配是指在编译时分配给线性表一个固定大小的存储空间(一般是程序运行时逻辑空间的栈空间(Stack Space))。

如果插入的元素超过这个存储空间的大小,就会发生溢出。为解决这一问题,引入了顺序存储结构的动态分配。

动态分配,即当数据元素超过所分配存储空间的大小时,在堆空间中再找一片更大的连续空间重新分配,将所有的数据元素放入。顺序存储的动态分配定义如下:

​ 其中,LIST_INIT_SIZE 表示第一次为顺序表分配的存储空间大小;LISTINCREMENT 表示次需要扩充存储空间时的增量;elem 是一个元素的指针,保存存储空间中第一个数据元素的地址,并且一旦需要扩充线性表的存储空间,可能需要改变 elem,使其指向新空间的起始位置。

2.1顺序表的基本操作

  • 初始化线性表L (参数用引用)

    tatus InitList_Sq(SqList &L)                   //构造一 个空的顺序表L
    {
        L.elem=new ElemType[MAXSIZE];   //为顺序表分配空间
        if(!L.elem) exit(OVERFLOW);           //存储分配失败
        L.length=0;				     //空表长度为0
        return OK;
    }
    
  • 初始化线性表L (参数用指针)

    Status InitList_Sq(SqList *L) //构造一个空的顺序表L
    {
    	L-> elem=new ElemType[MAXSIZE];   //为顺序表分配空间
        if(! L-> elem) exit(OVERFLOW);       //存储分配失败
        L-> length=0;	            	  //空表长度为0
        return OK;
    }
    
  • 销毁线性表L

    void DestroyList(SqList &L)
    {
      if (L.elem) delete[]L.elem;   
     //释放存储空间
    }
    
  • 清空线性表L

    void ClearList(SqList &L) 
    {
       L.length=0;               
     //将线性表的长度置为0
    }
    
  • 求线性表L的长度

    int GetLength(SqList L)
    {
       return (L.length);             
    }
    
  • 判断线性表L是否为空

    int IsEmpty(SqList L)
    {
      if (L.length==0) return 1;      
       else return 0;
    }
    
  • 取值(根据位置i获取相应位置数据元素的内容)

    //获取线性表L中的某个数据元素的内容,随机存取
    int GetElem(SqList L,int i,ElemType &e)
    {
      if (i<1||i>L.length) return ERROR;   
       //判断i值是否合理,若不合理,返回ERROR
      e=L.elem[i-1];   //第i-1的单元存储着第i个数据
      return OK;
    }
    
  • 查找(根据指定数据获取数据所在的位置)

    在线性表L中查找值为e的数据元素
    int LocateELem(SqList L,ElemType e)
    {
      	for (i=0;i< L.length;i++)
          	if (L.elem[i]==e) return i+1;                
     	 return 0;
    }
    
  • 插入元素(插在第 i 个结点之前)

​ 顺序表插入新元素:设 L.elem[0]…L.elem[MaxLength-1]中有 length 个元素,在第 i 个元素(L.elem[i-1])之前插入新元素 e,其中 1≤i≤length+1,当 i 等于 length+1 时,表示在尾部添加一个新元素。

在线性表 L=(a1,a2,…,ai-1, ai, ai+1,…,an)中的第 i 个元素前插入元素 e,an、an-1……ai+1、ai均要依次往后移动,那么移动元素的下标范围是 i-1~n-1i-1~L.length-1,这里L.length 的值为 n。注意移动元素的方向是从 anai方向依次把每个元素往后移动一个位置,如下图所示。

image-20240512170911243

算法步骤:

  1. 判断插入位置i 是否合法。
  2. 判断顺序表的存储空间是否已满。
  3. 将第n至第i 位的元素依次向后移动一个位置,空出第i个位置。
  4. 将要插入的新元素e放入第i个位置。
  5. 表长加1,插入成功返回OK。
//在线性表L中第i个数据元素之前插入数据元素e 
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];    //插入位置及之后的元素后移
    L.elem[i-1]=e;                     //将新元素e放入第i个位置
    ++L.length;		     	//表长增1
    return OK;
}

算法分析:插入在尾结点之后,则根本无需移动(特别快);若元素全部后移(特别慢);

若要考虑在各种位置插入(共n+1种可能)的平均移动次数,该如何计算?

:在一个最大容量为9个数据元素的存储空间中,当前表长为6,现要在第3个元素8前插入一个新元素6,插入6前后的顺序存储结构如图所示。

image-20240512171527972

  • 静态分配算法
#include <stdio.h>
#include <stdlib.h>

#define OK 1
#define ERROR 0
#define OVERFLOW -1
#define MaxLength 100

typedef int Status;
typedef int ElemType; //数据元素类型定义

//静态分配
typedef struct {
    ElemType elem[MaxLength];//下标:0,1,..., MaxLength -1
    int length;               //表长
 } SqList;

Status Insert(SqList *L,int i,ElemType e)
{
    int j;
    if (i<1 || i>L->length + 1) return ERROR;            //i值不合法
    if (L->length >= MaxLength) return OVERFLOW;    //溢出
    for (j = L->length - 1; j >= i - 1; j--)
        L->elem[j + 1] = L->elem[j];                 //向后移动元素
    L->elem[i - 1] = e;                           //插入新元素
    L->length++;                              //长度变量增1
    return OK;                              //插入成功
}


int main()
{
    SqList L;
    int arr[]={2,5,8,20,30,35};
    int len=sizeof(arr)/sizeof(int);
    int i;
    for (i=0;i<len;i++){
        L.elem[i]=arr[i];
    }
    L.length=len;
    printf("插入前:\n");
    for(i=0;i<L.length;i++){
        printf("%d ",L.elem[i]);
    }
    printf("\n");
    printf("插入后:\n");
    Insert(&L,3,6);
    for(i=0;i<L.length;i++){
        printf("%d ",L.elem[i]);
    }
    return 0;
}
>>>
插入前:
2 5 8 20 30 35 
插入后:
2 5 6 8 20 30 35 
sandbox> exited with status 0
  • 动态分配算法
#include <stdio.h>
#include <stdlib.h>

#define OK 1
#define ERROR 0
#define OVERFLOW -1
#define MaxLength 100

typedef int Status;
typedef int ElemType; //数据元素类型定义

#define LIST_INIT_SIZE 100
#define LISTINCREMENT  10

//动态分配
typedef struct{  //顺序表(顺序结构)的定义
      ElemType * elem;
      int length;
      int listsize;
 }SqList;
	
//动态分配顺序表插入算法:
Status Insert(SqList *L, int i, ElemType e)
{
    int j;
    if (i<1 || i>L->length+1)     //i的合法取值为1至n+1
        return ERROR;
    if (L->length>=L->listsize)    /*溢出时扩充*/
    {
        ElemType *newbase;
        newbase=(ElemType *) realloc(L->elem,(L->listsize+LISTINCREMENT)*sizeof(ElemType));
        if (newbase==NULL) return OVERFLOW;   //扩充失败
        L->elem=newbase;
        L->listsize+=LISTINCREMENT;
    }
    for(j=L->length-1;j>=i-1;j--){ //向后移动元素,空出第i个元素的分量elem[i-1]
        L->elem[j+1]=L->elem[j];
    }
    L->elem[i-1]=e;                   //新元素插入
    L->length++;                    //线性表长度加1
    return OK;
}

int main () {
	SqList L;
    int arr[]={2,5,8,20,30,35};
    int len=sizeof(arr)/sizeof(int);
    int i;
    L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
    for (i=0;i<len;i++){
        L.elem[i]=arr[i];
    }
    L.length=len;
    printf("插入前:\n");
    for(i=0;i<L.length;i++){
        printf("%d ",L.elem[i]);
    }
    printf("\n");
    printf("插入后:\n");
    Insert(&L,3,6);
    for(i=0;i<L.length;i++){
        printf("%d ",L.elem[i]);
    }
	return 0;
}

>>>
插入前:
2 5 8 20 30 35 
插入后:
2 5 6 8 20 30 35 
sandbox> exited with status 0
  1. 顺序表插入算法分为静态分配顺序表插入算法和动态分配顺序表插入算法。
  2. 静态分配插入算法的基本思想为:先判断插入的位置是否合理,接着判断表长是否达到分配空间的最大值,然后从线性表中的最后一个元素到插入位置的所有元素,依次往后移动一个元素的位置,这样给待插入的元素留出一个空位置,最后把新增元素插入这个空位置,表长增加 1, 插入成功返回。
  3. 在动态分配空间的顺序表中,如果插入元素使得表长超过所分配的空间大小,就利用 realloc这个函数寻找更大片的连续地址空间。如果没有找到,说明还是会溢出(当然这种情况很少发生);如果找到了,就将基地址改为新的地址,分配的存储空间也增加。而动态分配插入新元素所做的操作与静态分配是一样的,均是移动相应元素。
  • 删除元素

​ 在顺序表中删除元素也存在移动元素的过程,不过移动方向与插入元素相反。假如在顺序表中删除第 i 个元素 ai,1≤i≤length,那么就将元素 ai+1,…,an 依次往前移动,使得元素 ai-1 与元素ai+1相邻,ai元素就删除了,如下图所示:

image-20240512172859397

​ 顺序表删除元素算法的基本思想为:首先判断删除元素的下标是否存在,然后用一个 for 循环来移动元素,移动元素下标范围为 i~length-1,最后修改表长为原表长减 1。

算法步骤:

  1. 判断删除位置i 是否合法(合法值为1≤i≤n)。
  2. 将欲删除的元素保留在e中。
  3. 将第i+1至第n 位的元素依次向前移动一个位置。
  4. 表长减1,删除成功返回OK。
//将线性表L中第i个数据元素删除
Status ListDelete_Sq(SqList &L,int i)
{
   if((i<1)||(i>L.length)) return ERROR;	 //i值不合法
   for (j=i;j<=L.length-1;j++)                   
    L.elem[j-1]=L.elem[j];       //被删除元素之后的元素前移  
   --L.length;               	                 //表长减1
  return OK;
}

算法分析:

  1. 若删除尾结点,则根本无需移动(特别快);
  2. 若删除首结点,则表中n-1个元素全部前移(特别慢);
  3. 若要考虑在各种位置删除(共n种可能)的平均移动次数,该如何计算?
  • 例:顺序表删除元素

    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    #define OVERFLOW -1
    #define MaxLength 100
    
    typedef int Status;
    typedef int ElemType; //数据元素类型定义
    
    #define LIST_INIT_SIZE 100
    #define LISTINCREMENT  10
    
    //动态分配
    typedef struct{  //顺序表(顺序结构)的定义
          ElemType *elem;
          int length;
          int listsize;
     }SqList;
    
    int Delete(SqList* L, int i);
    
    int main()
    {
        SqList L;
        int arr[]={2,5,8,20,30,35};
        int len=sizeof(arr)/sizeof(int);
        int i;
        L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
        for (i=0;i<len;i++){
            L.elem[i]=arr[i];
        }
        L.length=len;
        printf("删除前:\n");
        for(i=0;i<L.length;i++){
            printf("%d ",L.elem[i]);
        }
        printf("\n");
        printf("删除后:\n");
        Delete(&L,2);
        for(i=0;i<L.length;i++){
            printf("%d ",L.elem[i]);
        }
        return 0;
    }
    
    //顺序表删除元素
    int Delete(SqList* L, int i)
    {
        if (i<1 || i>L->length)
                return ERROR;
        int j;
        for(j=i;j<=L->length-1;j++){
            L->elem[j-1]=L->elem[j];
        }
        L->length--;
        return OK;
    }
    
    删除前:
    2 5 8 20 30 35 
    删除后:
    2 8 20 30 35 
    sandbox> exited with status 0
    
  • 顺序表(顺序存储结构)的特点

  1. 利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
  2. 在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
  • 顺序表的优缺点

    优点:

    • 时间:可以随机存取表中任一元素
    • 空间:存储密度大(结点本身所占存储量 / 结点结构所占存储量)

    缺点:

    时间:在插入、删除某一元素时,需要移动大量元素

    空间:浪费存储空间,属于静态存储形式,数据元素的个数不能自由扩充

3.线性表链式存储结构定义及实现

线性表的链式表示又称为非顺序映像或链式映像。

链式存储结构是指将线性表中的数据元素存放到计算机存储器内一组非连续存储单元中。在链式结构中,只能通过指针来维护数据元素间的关系。由于这个原因,对线性表中的元素只能进行顺序访问。

单链表是链式存储结构中最基础、也是最具代表的一种存储结构形式。单链表是指线性表的每个结点分散地存储在内存空间中,先后依次用一个指针串联起来。单链表可以分为不带表头结点和带表头结点两种情形。

3.1与链式存储有关的术语

各结点由两个域组成:

  • 数据域:存储元素数值数据
  • 指针域:存储直接后继结点的存储位置

image-20240512214600031

  1. 结点:数据元素的存储映像。由数据域和指针域两部分组成

  2. 链表: n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构

  3. 单链表、双链表、循环链表:

    结点只有一个指针域的链表,称为单链表或线性链表

    有两个指针域的链表,称为双链表

    首尾相接的链表称为循环链表

循环链表示意图:

image-20240512214850413

  1. 头指针、头结点和首元结点

image-20240512215109606

  1. 头指针是指向链表中第一个结点的指针
  2. 首元结点是指链表中存储第一个数据元素a1的结点
  3. 头结点是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息
  • 如何表示空表?

    有头结点时,当头结点的指针域为空时表示空表

    image-20240512215355685

  • 在链表中设置头结点有什么好处?

    1.便于首元结点的处理首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;

    2.便于空表和非空表的统一处理无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。

  • 头结点的数据域内装的是什么?

    头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。

3.2链表(链式存储结构)的特点

  1. 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻

  2. 访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等

  • 链表的优缺点

    优点:

    1. 时间:插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高

    2. 空间:数据元素的个数可以自由扩充

    缺点:

    1. 时间:存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)
    2. 空间:存储密度小

3.3单链表的定义和实现

不带表头结点的单链表如下图所示:

image-20240512220506967

带表头结点的单链表:

① 非空表。单链表中至少存储一个元素为非空表;其中,头指针 head 指向表头结点,表头结点的数据域不放元素,指针域指向首元素结点 a1

image-20240512220607643

② 空表。单链表中还没有存储数据元素为空表;当head->next= =NULL 时,表示为空表,否则表示为非空表。

image-20240512220646471

  • 单链表的存储结构定义

    typedef struct Lnode
    {
         ElemType   data;       //数据域
         struct LNode  *next;   //指针域
    }LNode,*LinkList;   
    // *LinkList为Lnode类型的指针
    //指针变量p:表示结点地址,结点变量*p:表示一个结点
    

    image-20240512220930235

  • 单链表基本操作的实现

    初始化(构造一个空表 )

    算法步骤:

    (1)生成新结点作头结点,用头指针L指向头结点。

    (2)头结点的指针域置空。

    Status InitList_L(LinkList &L){ 
       L=new LNode;                    	
       L->next=NULL;     
       return OK; 
    } 
    
  • 销毁单链表

    Status DestroyList_L(LinkList &L)
    {
        LinkList p;
           while(L)
            {
                p=L;  
                L=L->next;
                delete p;  
            }
         return OK;
     }
    
  • 清空单链表

    Status ClearList(LinkList & L){
      // 将L重置为空表 
       LinkList p,q;
       p=L->next;   //p指向第一个结点
       while(p)       //没到表尾 
          {  q=p->next; delete p;     p=q;   }
       L->next=NULL;   //头结点指针域为空 
       return OK;
     }
    
  • 求表长

    image-20240512221705205

    求表长
    int  ListLength_L(LinkList L){
    //返回L中数据元素个数
        LinkList p;
        p=L->next;  //p指向第一个结点
         i=0;             
         while(p){//遍历单链表,统计结点数
               i++;
               p=p->next;    } 
        return i;                             
     }
    
  • 判断表是否为空

    //判断表是否为空
    int ListEmpty(LinkList L)
    { 
    	//若L为空表,则返回1,否则返回0 
       if(L->next)   //非空 
         return 0;
       else
         return 1;
     }
    
    
  • 取值(根据位置i获取相应位置数据元素的内容)

    链表的查找:要从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构

    算法步骤:

    1. 从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值p = L->next

    2. j做计数器,累计当前扫描过的结点数,j初值为1。

    3. p指向扫描到的下一结点时,计数器j加1。

    4. j = i时,p所指的结点就是要找的第i个结点。

//获取线性表L中的某个数据元素的内容
Status GetElem_L(LinkList L,int i,ElemType &e){ 
    p=L->next;j=1; //初始化
     while(p&&j<i){	//向后扫描,直到p指向第i个元素或p为空 
       p=p->next; ++j; 
     } 
     if(!p || j>i)return ERROR; //第i个元素不存在 
     e=p->data; //取第i个元素 
     return OK; 
}//GetElem_L 
  • 查找(根据指定数据获取数据所在的位置)

    算法步骤:

    1. 从第一个结点起,依次和e相比较。

    2. 如果找到一个其值与e相等的数据元素,则返回其在链表中 的“位置”或地址;

    3. 如果查遍整个链表都没有找到其值和e相等的元素,则返回0 或“NULL”。

    //在线性表L中查找值为e的数据元素
    LNode *LocateELem_L (LinkList L,Elemtype e) {
     //返回L中值为e的数据元素的地址,查找失败返回NULL
      p=L->next;
      while(p &&p->data!=e)  
            p=p->next;                		
      return p; 	
    } 
    
    //在线性表L中查找值为e的数据元素
    int LocateELem_L (LinkList L,Elemtype e) {
     //返回L中值为e的数据元素的位置序号,查找失败返回0 
      p=L->next; j=1;
      while(p &&p->data!=e)  
            {p=p->next;  j++;}          		
      if(p) return j; 
      else return 0;
    } 
    
  • 插入(插在第 i 个结点之前)

    先进先出单链表单链表建立后,如果想要输出单链表中元素的值,则先加入单链表的元素会先输出、后加入的后输出。所以这个单链表也可以称为“先进先出”单链表。

    可以通过尾插法创建单链表,算法步骤如下。

    (1)生成表头结点,headtail 都指向表头结点。

    (2)输入元素的值 e,当元素不是结束标记时,重复下列操作,否则转至步骤(3)。① 生成新结点 p,e 保存到 p 结点的数据域。② 使用 tail->next=p;将 p 结点链接到单链表的表尾。③ 使用 tail=p;让 tail 指向当前的表尾结点。

    (3)使用 tail->next=NULL;将最后一个结点的指针域赋值为空。

    (4)返回 head,完成“先进先出”单链表的创建。

    ​ 先进后出单链表单链表建立后,如果想要输出单链表中元素的值,则先加入单链表的元素会后输出、后加入的先输出。这个单链表称为“先进后出”单链表。为实现创建“先进后出”单链表,每当输入一个元素后,生成的结点不是放在表尾而是插入表头,成为新的首元素结点,使用这种插入方式创建单链表的方法俗称首插法,首插法具体如下图所示。当前单链表中已有 i 个元素结点,元素输入次序为 a1,…,ai,现输入第 i+1 个元素 ai+1,具体操作为: 第①步生成新结点 p,并保存新元素 ai+1;第②步通过 p->next=head->next 使得新结点指针指向原首结点;第③步通过 head->next=p 让表头结点的指针域指向新结点 ai+1,不再指向 ai,将新结点作为首元素,即可完成将新结点插入表头的操作。

    image-20240512224113669

    一般情况下插入元素的操作如下:

    (1)在已知 p 指针指向的结点后插入一个元素 x首先用一个指针 f 指向新结点,该结点的数据域为 x,然后此新结点 next域赋值为 p 指针指向结点的 next 域,最后 p 指针指向结点的 next 域赋值为 f。

    image-20240512224443628

    (2)在已知 p 指针指向的结点前插入一个元素x因为单链表每个结点只有一个指针指向其后继结点,如果在结点前插入一个新结点,就需要得到 p 指向结点的前驱结点指针,假设该指针为 q,如下图所示。这样问题就转换成在指针 q 指向的结点之后插入一个结点,即将该问题(2)转换成问题(1)求解。

    image-20240512224536020

    将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间

    image-20240512222803122

算法步骤:

  1. 找到ai-1存储位置p
  2. 生成一个新结点*s
  3. 将新结点*s的数据域置为x
  4. 新结点*s的指针域指向结点ai*
  5. 令结点p的指针域指向新结点*s

image-20240512223103740

//在L中第i个元素之前插入数据元素e 
Status ListInsert_L(LinkList &L,int i,ElemType e){ 
     p=L;j=0; 
      while(p&&j<i−1){p=p->next;++j;}	//寻找第i−1个结点 
      if(!p||j>i−1)return ERROR;	//i大于表长 + 1或者小于1  
      s=new LNode;			//生成新结点s 
      s->data=e;      		           //将结点s的数据域置为e 
      s->next=p->next;	   	          //将结点s插入L中 
      p->next=s; 
      return OK; 
}//ListInsert_L 

  • 删除(删除第 i 个结点)

    算法步骤:

    (1)找到ai-1存储位置p

    (2)保存要删除的结点的值

    (3)令p->next指向ai的直接后继结点

    (4)释放结点ai的空间

    image-20240512235808157

image-20240512235846175

//将线性表L中第i个数据元素删除
 Status ListDelete_L(LinkList &L,int i,ElemType &e){
    p=L;j=0; 
    while(p->next &&j<i-1){                  //寻找第i个结点,并令p指向其前驱 
        p=p->next; ++j; 
    } 
    if(!(p->next)||j>i-1) return ERROR; //删除位置不合理 
    q=p->next;                                        //临时保存被删结点的地址以备释放 
    p->next=q->next; 	                  //改变删除结点前驱结点的指针域 
    e=q->data; 	                                //保存删除结点的数据域 
    delete q; 	                                //释放删除结点的空间 
 return OK; 
}//ListDelete_L 
  • 单链表的建立(前插法)

    从一个空表开始,重复读入数据:

    生成新结点将读入数据存放到新结点的数据域中将该新结点插入到链表的前端

    image-20240513000526469

image-20240513000641641

算法如下:

void CreateList_F(LinkList &L,int 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; 	//插入到表头 
     } 
}//CreateList_F 
  • 单链表的建立(尾插法)

    从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。

    初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。

    image-20240513001001746

    算法如下:

    void CreateList_L(LinkList &L,int n){ 
          //正位序输入n个元素的值,建立带表头结点的单链表L 
          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=p; 	                                  //r指向新的尾结点 
          } 
    }//CreateList_L 
    

3.4循环链表

有的时候,为了应用方便,我们可以将链表中最后一个结点的 next 域指向链表的第一个结点而形成一个环,这种单链表称为循环单链表。

带表头结点的循环单链表示意图如下图所示。其中(a)为非空循环单链表,(b)为空循环单链表。

image-20240513001748184

从循环链表中的任何一个结点的位置都可以找到其他所有结点,而单链表做不到;

在有些情况下,需要对单链表尾结点进行访问。为了提高算法的效率,此时还可采用只带尾指针tail,不带头指针的循环链单链表。在这种只带尾指针的循环单链表方式中,tail->next 的值就是表头结点的指针,其相当于头指针,所以能方便地完成头指针循环单链表的各种操作。其中,(a)为只带尾指针的非空循环单链表,(b)为空循环单链表。

image-20240513002120144

3.5双向链表

双向链表:单链表和循环单链表每个结点中只有一个指针指向其后继。对于循环单链表,一个结点需要访问其前驱结点时要顺着 next 域扫描整个链表一遍,此时效率显然不高。这里为了方便访问结点的前驱结点而引入双向链表。在双向链表中,其中,每个结点除了数据域之外,还有两个指针域(一个指向其直接前驱结点,另一个指向其直接后继结点)。

数据结构的定义如下。

image-20240513012758782

image-20240513012829984

  • 双向链表的一般形式如图(a)和图 (b)所示,它们分别列出了非空双向链表和空双向链表。
L 为头指针,L 指向表头结点;通过首结点的 next 指针域可以依次访问各个结点;通过尾结点的 prior 指针域可以逆序访问链表中的各个结点。我们可以根据 L->next == NULL 是否成立来判断是否为空表。

image-20240513013248822

​ 在实际应用中,一般会在双向链表中加上循环,形成双向循环链表。双向循环链表的一般形式如下图所示,我们可以根据 L->next == LL->prior == L 是否成立来判断是否为空表

image-20240513013609428

​ 从双向链表中删除结点时,需要注意两个指针的变化。例如,已知双向链表中包含结点 A、B、C,指针 p 指向结点 B,删除 B,那么所做的操作如下。

image-20240513013710488

​ 向双向链表中插入结点时,也需要注意两个指针的变化。例如,已知双向链表中包含两个相邻结点 A 和 C,指针 p 指向结点 C,现在插入一个新的结点到 A 和 C 之间,由 f 指向该待插入的结点 B,那么所做的操作如下。

image-20240513013749172

  • 双向链表的插入

image-20240513013927016

Status ListInsert_DuL(DuLinkList &L,int i,ElemType e){
   if(!(p=GetElemP_DuL(L,i))) return ERROR;
    s=new DuLNode; 
   s->data=e;
   s->prior=p->prior;  
   p->prior->next=s;
   s->next=p;  
   p->prior=s;
   return OK;
}
  • 双向链表的删除

image-20240513014244022

Status ListDelete_DuL(DuLinkList &L,int i,ElemType &e)
{
   if(!(p=GetElemP_DuL(L,i)))     return ERROR;
   e=p->data;
   p->prior->next=p->next;
   p->next->prior=p->prior;
   delete p; 
   return OK;
}

4.顺序表与链表的比较

优点缺点
顺序表(1)顺序表是一种随机存取结构,存取任何元素的时间是一个常数,速度快;
(2)结构简单,逻辑上相邻的元素在物理上也是相邻的;
(3)不需要使用指针,节省存储空间;
(4)顺序表在实现上使用的是连续的内存空间,我们可以借助 CPU 的缓存机制预读顺序表中的数据,所以访问效率更高
(1)插入和删除元素要移动大量元素,需要消耗大量时间;
(2)需要一块连续的存储空间;
(3)插入元素可能发生“溢出”,不易扩充;
(4)自由区中的存储空间不能被其他数据占用(共享),存在浪费空间的问题
链表(1)插入和删除元素不需要移动大量元素,不需要消耗大量时间;
(2)不需要一块连续的存储空间;
(3)链表本身没有大小的限制,天然地支持动态扩容,插入元素一般不会发生“溢出”;
(4)用多少就取多少,不存在自由区中的存储空间不能被其他数据占用(共享)的情况,从而避免浪费空间的问题
(1)不是一种随机存取结构;
(2)逻辑上相邻的元素在物理上不一定是相邻的;
(3)需要使用指针,存储密度小于 1,浪费一定的空间;
(4)额外存储指针结点,频繁进行增删操作容易造成内存碎片;
(5)链表在内存中并不是连续存储,对 CPU 缓存不友好,无法有效预读

image-20240513015322870

5.线性表的应用

5.1线性表的合并

  • 问题描述:

    假设利用两个线性表La和Lb分别表示两个集合 A和B,现要求一个新的集合 A=AB

  • 算法步骤:次取出Lb 中的每个元素,执行以下操作:

    1. 在La中查找该元素
    2. 如果找不到,则将其插入La的最后

    单链表合并算法:将两个带表头结点的有序单链表 LaLb 合并为有序单链表 Lc,该算法利用原单链表的结点。单链表合并示意图如图所示。

    image-20240513020813516

算法如下:

void union(List &La, List Lb){
  La_len=ListLength(La);
  Lb_len=ListLength(Lb); 
  for(i=1;i<=Lb_len;i++){
      GetElem(Lb,i,e);
      if(!LocateElem(La,e)) 
           ListInsert(&La,++La_len,e);                     
  }
}

5.2有序表的合并

5.2.1有序的顺序表合并

问题描述:

已知线性表La 和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列。

例:

La=(1 ,7, 8)
Lb=(2, 4, 6, 8, 10, 11)
Lc=(1, 2, 4, 6, 7 , 8, 8, 10, 11) 

算法步骤c

  1. 创建一个空表Lc

  2. 依次从 La 或 Lb 中“摘取”元素值较小的结点插入到 Lc 表的最后,直至其中一个表变空为止

  3. 继续将 La 或 Lb 其中一个表的剩余结点插入在 Lc 表的最后

算法:

void MergeList_Sq(SqList LA,SqList LB,SqList &LC){ 
     pa=LA.elem;  pb=LB.elem;     //指针pa和pb的初值分别指向两个表的第一个元素 
     LC.length=LA.length+LB.length;      	//新表长度为待合并两表的长度之和 
     LC.elem=new ElemType[LC.length];    	//为合并后的新表分配一个数组空间 
     pc=LC.elem;                         		//指针pc指向新表的第一个元素 
     pa_last=LA.elem+LA.length-1; 	//指针pa_last指向LA表的最后一个元素 
     pb_last=LB.elem+LB.length-1; 	//指针pb_last指向LB表的最后一个元素 
     while(pa<=pa_last && pb<=pb_last){  	//两个表都非空 
      if(*pa<=*pb) *pc++=*pa++;        	//依次“摘取”两表中值较小的结点      
      else *pc++=*pb++;      } pa++;             //LB表已到达表尾
     while(pb<=pb_last)  *pc+
     while(pa<=pa_last)  *pc++=*+=*pb++;          //LA表已到达表尾 
}//MergeList_Sq 
5.2.2有序链表合并
  • 算法步骤:
  1. 将这两个有序链表合并成一个有序的单链表。

  2. 表中允许有重复的数据。

  3. 要求结果链表仍使用原来两个链表的存储空间, 不另外占用其它的存储空间。

image-20240513021806881

  • 初始化

    image-20240513022425668

  • pa->data<=pb->data

    image-20240513022612003

    image-20240513022712234

    image-20240513022738219

  • pa->data>pb->data

    image-20240513023341129

image-20240513023415011

image-20240513023445972

image-20240513023507151

image-20240513023527550

  • 算法描述

    void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
       pa=La->next;  pb=Lb->next;
       pc=Lc=La;                    //用La的头结点作为Lc的头结点 
       while(pa && pb){
          if(pa->data<=pb->data){ pc->next=pa;pc=pa;pa=pa->next;}
          else{pc->next=pb; pc=pb; pb=pb->next;}
       pc->next=pa?pa:pb;      //插入剩余段  
       delete Lb;                     //释放Lb的头结点}  
    
  • 将两个非递减的有序链表合并为一个非递增的有序链表,如何实现?

    1. 要求结果链表仍使用原来两个链表的存储空间, 不另外占用其它的存储空间。

    2. 表中允许有重复的数据。

  • 算法步骤

    (1)Lc指向La

    (2)依次从 La 或 Lb 中“摘取”元素值较小的结点插入到 Lc 表的表头结点之后,直至其中一个表变空为止

    (3)继续将 La 或 Lb 其中一个表的剩余结点插入在 Lc 表的表头结点之后

    (4)释放 Lb 表的表头结点

记得关注我哦!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@杨星辰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值