浅谈线性表

线性表

1. 线性表的基本概念与实现

1.1 线性表的定义

线性表是具有相同特性数据元素的一个有限序列

  • 如何理解 相同特性有限序列
    相同特性:所有元素属于同一数据类型
    有限:元素个数有限
    序列:序列不是指的特定的顺序的排列,而是根据数据元素插入顺序保存的。

该序列中所含元素的个数叫作线性表的长度,用 n (n≥0) 表示。注意,n可以等于零,表示线性表是一个空表。


线性表是一种简单的数据结构,可以把它想象成一队学生。

在这里插入图片描述

学生人数对应线性表的长度,学生人数是有限的,这里体现了线性表是一个有限序列;队中所有人的身份都是学生,这里体现了线性表中的数据元素具有相同的特性;线性表可以是有序的,也可以是无序的,如果学生按照身高来排队,矮在前, 高在后,则体现了线性表的有序性。

1.2 线性表的逻辑特性

继续拿定义中的例子来进行说明。在一队学生中,只有一个学生在队头,同样只有一个学生在队尾。

在队头的学生的前面没有其他学生,在队尾的学生的后边也没有其他学生。

除了队头和队尾的学生以外, 对于其他的每一个学生,紧挨着站在其前面和后面的学生都只有一个。

在这里插入图片描述

线性表也是这样,只有一个表头元素,只有一个表尾元素,表头元素没有前驱,表尾元素没有后继,除表头和表尾 元素之外,其他元素只有一个直接前驱,也只有一个直接后继。以上就是线性表的逻辑特性。

以下图数据中的元素 3 来说,它的直接前驱是 2,直接后继是 4 。

在这里插入图片描述

1.3 线性表的存储结构

线性表的存储结构有顺序存储结构链式存储结构两种。前者称为顺序表,后者称为链表。下面通过对比来介绍这两种存储结构。

(1)顺序表

顺序表就是把线性表中的所有元素按照其逻辑顺序,依次存储到从指定的存储位置开始的一块连续的存储空间中。这样,线性表中第一个元素的存储位置就是指定的存储位置,第 i+1个元素的存储位置紧接在第 i 个元素的存储位置的后面。

在这里插入图片描述

(2)链表

在链表存储中,每个结点不仅包含所存元素的信息,还包含元素之间逻辑关系的信息,如单链表中前驱结点包含后继结点的地址信息,这样就可以通过前驱结点中的地址信息找到后继结点的位置。

在这里插入图片描述

(3)两种存储结构的比较

顺序表就好像图a所示的一排房间,每个房间左边的数字就是该房间到 0 点的距离,同时也代表了房间号,房间的长度为1。因此,只要知道 0 点的位置,然后通过房间号就可以马上找到任何一个房间的位置,这就是顺序表的第一个特性——随机访问特性。由图a还可以看出,5 个房间所占用的地皮是紧挨着的,即连续地占用了一片空间,并且地皮的块数 6 是确定的,若在地皮上布置新的房间或者拆掉老的房间(对顺序表的操作),地皮的块数不会增加,也不会减少。这就是顺序表的第二个特性,即顺序表要求占用连续的存储空间。存储分配只能预先进行,一旦分配好了,在对其操作的过程中始终不变。

在这里插入图片描述

再看链表,如图b所示,4 个房间是散落存在的,每个房间的右边有走向下一个房间的方向指示箭头。因此,如果想访问最后一个房间,就必须从第一个房间开始,依次走过前3个房间才能来到最后一个房间,而不能直接找出最后一个房间的位置,即链表不支持随机访问。通过图b还可以知道,链表中的每一个结点需要划出一部分空间来存储指向下一个结点位置的指针,因此链表中结点的存储空间 利用率较顺序表稍低一些。链表中当前结点的位置是由其前驱结点中的地址信息所指示的,而不是由其相对于初始位置的偏移量来确定。因此,链表的结点可以散落在内存中的任意位置,且不需要一次性地划分所有结点所需的空间给链表,而是需要几个结点就临时划分几个。由此可见,链表支持存储空间的动态分配

在这里插入图片描述

图a 所示的顺序表中最右边的一个表结点空间代表没有被利用(顺序表还有剩余空间来注入新数据),如果想在1 号房间和 2 号房间之间插入一个房间,则必须将 2 号以后的房间都往后移动一个位置(假设房间是可以随意搬动的),即顺序表做插入操作的时候要移动多个元素。而链表就无须这样,如图b 所示的链表,如果想在第一个和第二个房间之间插入一个新房间,则只需改动房间后边的方向指示箭头即可,将第一个房间的箭头指向新插入的房间,然后将新插入的房间的箭头指向第二个房间,即在链表中进行插入操作无须移动元素

(4)链表的5种形式

1)单链表

在每个结点中除了包含数据域外,还包含一个指针域,用以指向其后继结点。

这里要区分一下带头结点的单链表不带头结点的单链表

①带头结点的单链表中,头指针 head 指向头结点,头结点的值域不含任何信息,从头结点的后继结点开始存储数据信息。头指针 head 始终不等于 NULL, head->next 等于 NULL 的时候,链表为空。

在这里插入图片描述

②不带头结点的单链表中的头指针 head 直接指向开始结点,当 head 等于 NULL的时候,链表为空。

在这里插入图片描述

总之,两者最明显的区别是,带头结点的单链表中有一个结点不存储信息(仅存储一些描述链表属性的信息,如表长),只是作为标志,而不带头结点的单链表的所有结点都存储信息。

2)双链表

单链表只能由开始结点走到终端结点,而不能由终端结点反向走到开始结点。如果要求输出从终端 结点到开始结点的数据序列,则对于单链表来说操作就非常麻烦。为了解决这类问题,构造了双链表。

如下图所示为带头结点的双链表。双链表就是在单链表结点上增添了一个指针域,指向当前结点的前驱。 这样就可以方便地由其后继来找到其前驱,从而实现输出终端结点到开始结点的数据序列。

在这里插入图片描述

同样,双链表也分为带头结点的双链表不带头结点的双链表,情况类似于单链表。带头结点的双链表,当 head->next 为NULL时链表为空;不带头结点的双链表,当 head 为NULL时链表为空

在这里插入图片描述

3)循环单链表

知道了单链表的结构之后,循环单链表就显得比较简单了。只要将单链表的最后一个指针域(空指针)指向链表中的第一个结点即可(这里之所以说第一个结点而不说是头结点是因为:如果循环单链表是带头结点的,则最后一个结点的指针域要指向头结点;如果循环单链表不带头结点,则最后一个指针域要指向开始结点)。

循环单链表可以实现从任一个结点出发访问链表中的任何结点,而单链表从任一结点出发后只能访问这个结点本身及其后边的所有结点。带头结点的循环单链表,当head等于head->next时,链表为空不带头结点的循环单链表,当head等于NULL 时,链表为空

如下图所示为带头结点的循环单链表:

在这里插入图片描述

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

在这里插入图片描述

4)循环双链表

和循环单链表类似,循环双链表的构造源自双链表,即将终端结点的 next 指针指向链表中的第一个结点,将链表中第一个结点的 prior 指针指向终端结点,如下图所示:

在这里插入图片描述

循环双链表同样有带头结点和不带头结点之分。当head等于NULL时,不带头结点的循环双链表为空。带头结点的循环双链表中是没有空指针的,其空状态下,head->nexthead->prior 必然都等于 head。所以判断其是否为空,只需要检查 head->next 和 head,prior 两个指针中的任意一个是否等于head指针即可。如下图所示:

在这里插入图片描述

因此,以下四句中的任意一句为真,都可以判断带头结点的循环双链表为空。

head->next == head;
head->prior == head;
head->next == head && head->prior == head;
head->next == head || head->prior == head;

上面4种链表可以用4种道路来形象的比喻,如下图所示:

在这里插入图片描述

单链表就像图a所示的单行车道,只允许车辆往一个方向行驶;双链表就像图b所示的双向车道,车辆既可以从左往右行驶,也可以从右向左行驶;循环单链表就像图c所示的单向环形车道, 车辆可沿着一个方向行驶在这条车道上;循环双链表就像图d所示的双向环形车道,车辆可以沿着两个方向行驶在这条车道上。

5)静态链表

静态链表借助一维数组来表示,如下图所示:

在这里插入图片描述

左图是静态链表,右图是其对应的一般链表。一般链表结点空间来自于整个内存,静态链表则来自于一个结构体数组。数组中的每一个结点含有两个分量:一个是数据元素分量data;另一个是指针分量,指示了当前结点的直接后继结点在数组中的位置(这和一般链表中next指针的地位是同等的)。

注意:静态链表中的指针不是我们通常所说的C语言中用来存储内存地址的指针型变量,而是一个存储数组下标的整型变量,通过它可以找到后继结点在数组中的位置,其功能类似于真实的指针,因此称其为指针。

2. 线性表的结构体定义和基本操作

2.1 线性表的结构体定义

#define maxSize 100	// 这里定义一个整型常量maxSize,值为100
  1. 顺序表的结构体定义
typedef struct 
{
    int data [maxSize];	// 存放顺序表元素的数组
    int length;	        // 存放顺序表的长度
}Sqlist;               // 顺序表类型的定义

如下图所示,一个顺序表包括一个存储表中元素的数组data[]和一个指示元素个数的变量length

在这里插入图片描述

  1. 单链表结点定义
typedef struct LNode
{
    int data;           // data 中存放结点数据域
    struct LNode *next; // 指向后继结点的指针
}LNode;                 // 定义单链表结点类型

如下图所示为单链表的结点结构图:

在这里插入图片描述

  1. 双链表结点定义
typedef struct DLNode
{
    int data;             // data 中存放结点数据域
    struct DLNode *prior; // 指向前继结点的指针
    struct DLNode *next;  // 指向后继结点的指针
}DLNode;                  // 定义双链表结点类型

如下图所示为双链表的结点结构图:

在这里插入图片描述

2.2 顺序表的操作

(1) 按元素值的查找算法

在顺序表中查找第一个值等于e的元素,并返回其下标。

查找操作代码如下:

int findElem (Sqlist L,int e)
{
	int i;
	for(i=0;i<L.length;++i)
	if(e == L.data[i])
		return i;     // 若找到,则返回下标
	return -1;        // 没找到,返回-1,作为失败标记
}
(2) 插入数据元素的算法

在顺序表L的p (0≤p≤length)位置上插入新的元素e。如果p的输入不正确,则返回0,代表插入失败;如果p的输入正确,则将顺序表第p个元素及以后元素右移一个位置,腾出一个空位置插入新 元素,顺序表长度增加1,插入操作成功,返回1。

插入操作代码如下:

int insertElem (Sqlist &L, int p, int e) // L本身要发生改变,所以用引用型
{
	int i;
	if (p<0 || p>L.length || L.length == maxSize) //位置错误或者表长已经达至
		return 0;	                               //顺序表的最大允许值,此时插入不成功,返回0
	for(i=L.length-1;i>=p;--i)
		L.data[i+1] = L.data[i];     // 从后往前,逐个将元素往后移动一个位置
	L.data[p] = e;	                 // 将e放在插入位置p上
	++(L.length);	                 //表内元素多了一个,因此表长自增1
	return 1;	                     //插入成功,返回1
}
(3) 删除数据元素的算法

删除顺序表L中下标为p (O≤p≤length-1)的元素,成功返回1,否则返回0,并将被删除元素的值赋给e。

分析:

要删除表中下标为 p 的元素,只需将其后边的元素逐个往前移动一个位置,将p位置上的元素覆盖掉,就达到了删除的目的。只需将插入操作中的元素右移改成元素左移即可。插入操作中右移的时候需要从最右边的元素开始移动,这里很自然想到在删除操作中左移的时候需要从最左边的元素开始移动。

删除操作代码如下:

int deleteElem (Sqlist &L, int p,int &e) // 需要改变的变量用引用型
{
	int i;
	if (p<0 || p>L.length-1)
		return 0;	            // 位置不对返回0,代表删除不成功
	e=L.data[p];	            // 将被删除元素赋值给e
    for (i=p;i<L.length-1;++i)	// 从p位置开始,将其后边的元素逐个前移一个位置
		L.data[i] = L.data[i+1];
    --(L.length);	            // 表长减 1
    return 1;	                // 删除成功,返回1
}

2.3 单链表的操作

(1) 插入操作

假设 p 指向一个结点,要将 s 所指结点插入 p 所指结点之后的操作如下图所示:

在这里插入图片描述

其语句如下:

s->next = p->next;
p->next = s;
(2) 删除操作

假设 p 指向一个结点,要将 p 所指结点后面的 s 所指结点删除的操作如下图所示:


其语句如下:

p->next = s->next;
free(s); // 调用函数free()来释放s所指结点的内存空间
(3) 建表操作
  • 尾插法

尾插法

假设有 n 个元素已经存储在数组 a 中,用尾插法建立链表 C。

void createlistR(LNode *&C,int a[], int n)
{
    LNode *s,*r; //s用来指向新申请的结点,r始终指向C的终端结点
    int i;
    C=(LNode*)malloc(sizeof(LNode)); // 申请C的头结点空间
    C->next=NULL;
    r=C;         //r指向头结点,因为此时头结点就是终端结点
    for(i=0;i<n;++i)
    {
        s=(LNode*)malloc(sizeof(LNode)); //s指向新申请的结点
        s->data=a[i]; //用新申请的结点来接收a 中的一个元素
        r->next=s;    //用r来接纳新结点
        r=r->next;    //r指向终端结点,以便接纳下一个到来的节点
    }
    r->next=NULL;   //数组a中所有的元素都已装入链表C,C的终端结点指针域置为NULL,C建立完成
}
  • 头插法

头插法建单链表过程如图所示:

在这里插入图片描述

void createlistF(LNode *&C,int a[],int n)
{
    LNode *s;
	int i;
    C=(LNode*)malloc(sizeof(LNode));
	C->next=NULL;
    for (i=0;i<n;++i)
    {
        s=(LNode*)malloc(sizeof(LNode));
        s->data=a[i];
        /*下边两句是头插法的关键步骤*/
        s->next=C->next; //s所指新结点的指针域next指向C中的开始结点
        C->next=s;	//头结点的指针域next指向s结点,使得s成为新的开始
    }
}

2.4 双链表的操作

(1) 插入结点

假设在双链表中 p 所指的结点之后插入一个结点 s

其操作语句如下:

s->next=p->next;
s->prior=p;
p->next=s;
s->next->prior=s; //假如p指向最后一个结点,则本行可去掉

指针变化过程如下图所示:

在这里插入图片描述

(2) 删除结点

设要删除双链表中 p 结点的后继结点

其操作语句如下:

q=p->next;
p->next=q->next;
q->next->prior=p;
free(q);

指针变化过程如下图所示:

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

King_960725

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

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

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

打赏作者

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

抵扣说明:

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

余额充值