大话数据结构学习笔记(三)——线性表

1 线性表的定义

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

首先线性表是一个序列。也就是说,元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。

线性表元素的个数n(n≥0)定义为线性表的长度,当n=0时,称为空表。
在非空表中的每个数据元素都有一个确定的位置,如a[1]是第一个数据元素,a[n]是最后一个数据元素,a[i]是第i个数据元素,称i为数据元素a[i]在线性表中的位序。

image-20220426172540770

2 线性表的抽象数据类型

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

ADT 线性表(List)
Data
	线性表的数据对象集合为{a[1], a[2], a[3], ......, a[n]},每个元素的类型相同,其
中,除了第一个元素a[1]外,每个元素有且只有一个直接前驱元素,除了最后一个元素a[n]外,每一个元素有且只有一个直接后继元素。线性表中数据元素之间的关系是一对一的关系。
Operation
	InitList(*L): 		初始化操作,建立一个空的线性表L。
	ListEmpty(L): 		若线性表为空,则返回true,否则返回false。
	ClearList(*L): 		将线性表清空。
	GetElem(L, i, *e): 	将线性表L中的第i个位置的元素值返回。
	LocateElem(L, e): 	在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序列号。
	

3 线性表的顺序存储结构

3.1 顺序存储定义

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

3.2 顺序存储方式

线性表的顺序存储的结构代码:

// 存储空间初始分配量
#define MAXSIZE 20
// ElemType类型根据实际情况而定,这里假设为int
typedef int ElemType;
typedef struct
{
    // 数组存储数据元素,最大值为MAXSIZE
    ElemType data[MAXSIZE];
    // 线性表当前长度
    int length;
} SqList;

这里,我们就发现描述顺序存储结构需要三个属性:

  • 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
  • 线性表的最大存储容量:数组长度MAXSIZE
  • 线性表的当前长度length

3.3 数组长度和线性表长度区别

数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。

线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度应该小于等于数组的长度。

3.4 地址计算方法

image-20220426172703774

用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度。
其实,内存中的地址,就和图书馆或电影院里的座位一样,都是有编号的。存储器中的每个存储单元都有自己的编号,这个编号称为地址。

由于每个数据元素,不管它是整型、实型还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC(x)表示获得存储位置的函数):

LOC(ai+1)=LOC(ai)+C

所以对于第i个数据元素a[i]的存储位置可以由a[1]推算得出:

LOC(ai)=LOC(a1)+(i1)×C

image-20220426172732637

通过这个公式,可以随时算出线性表中任意位置的地址,不管它是第一个还是最后一个,都是相同的时间。那么我们对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数,因此用我们算法中学到的时间复杂度的概念来说,它的存取时间性能为O(1)。 我们通常把具有这一特点的存储结构称为随机存取结构

3.5 顺序存储结构的各类操作

3.5.1 获得元素操作

对于线性表的顺序存储结构来说,如果我们要实现GetElem操作,即将线性表L中的第i个位置元素值返回,其实是非常简单的。就程序而言,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可。

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
// Status是函数的类型,其值是函数结果状态代码,如OK等
// 初始条件: 顺序线性表L已存在,且1 <= i <= ListLength(L)
// 操作结果: 用e返回L中第i个数据元素的值
Status GetElem(SqList L, int i, ElemType *e)
{
    if (L.length == 0 || i < 1 || i > L.length)
        return ERROR;
    *e = L.data[i - 1];
    return OK;
}

3.5.2 插入操作

插入算法的思路:

  • 如果插入位置不合理,抛出异常。
  • 如果线性长度大于等于数组长度,则抛出异常或动态增加容量。
  • 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。
  • 将要插入元素填入位置i处,线性表长加1。

实现代码如下:

// 初始条件: 顺序线性表L已存在,且1 <= i <= ListLength(L)
// 操作结果: 在L中第i个位置之前插入鑫的数据元素e,L的长度加1
Status ListInsert(SqList *L, int i, ElemType e)
{
    int k;
    // 线性表容量已满
    if (L->length == MAXSIZE)
        return ERROR;
    // 当i不在范围内时
    if (i < 1 || i > L->length + 1)
        return ERROR;
    // 若插入数据位置不在表尾
    if (i <= L->length)
    {
        // 将要插入位置之后的数据元素向后移动一位
        for (k = L->length - 1; k >= i - 1; k --)
            L->data[k + 1] = L->data[k];
    }
    // 将新元素插入
    L->data[i - 1] = e;
    L->length ++;
    return OK;
}

3.5.3 删除操作

删除算法的思路:

  • 如果删除位置不合理,抛出异常。
  • 取出删除元素。
  • 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置。
  • 表长减1.

实现代码如下:

// 初始条件: 顺序线性表L已存在,且1 <= i <= ListLength(L)
// 操作结果: 删除L的第i个数据元素,并用e返回其值,L的长度减1
Status ListDelete(SqList *L, int i, ElemType *e)
{
	int k;
	// 线性表为空
	if (L->length == 0)
		return ERROR;
	// 删除位置不正确
	if (i < 1 || i > L->length)
		return ERROR;
	*e = L->data[i - 1];
	// 如果删除不是最后位置
	if (i < L->length)
	{
		// 将删除位置后继元素前移
		for (k = i; k < L->length; k++)
			L->data[k - 1] = L->data[k];
	}
    L->length --;
	return OK;
}

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

优点:

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

缺点:

  • 插入和删除操作需要移动大量元素
  • 当线性表长度变化交大时,难以确定存储空间的容量
  • 容易造成存储空间的“碎片”

4 线性表的链式存储结构

4.1 线性表链式存储结构定义

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。

image-20220426172835600

以前在顺序结构中,每个数据元素只需要存数据元素信息就可以了。现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
因此,为了表示每个数据元素a[i]与其直接后继数据元素a[i+1]之间的逻辑关系, 对数据元素a[i]来说, 除了存储其本身的信息之外, 还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。 指针域中存储的信息称做指针或链。 这两部分信息组成数据元素a[i]的存储映像, 称为结点(Node)。
n个结点(a[i]的存储映像)链结成一个链表,即为线性表(a[1],a[2],...,a[n])的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。

image-20220426172921789

有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,谁叫它是第一个呢,有这个特权。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针,如下图:

image-20220426173130420

4.2 头指针与头结点的异同

头指针

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

头结点

  • 头结点是为了操作的统一和方便而设立的,放在第一位元素的结点之前,其数据域一般无意义(也可存放链表信息,如长度)。
  • 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
  • 头结点不一定是链表必要元素

4.3 线性表链式存储结构代码描述

image-20220426173221521

// 线性表的单链表存储结构
typedef struct Node
{
    ElemType data;
    struct Node *next;
} Node;
// 定义LinkList
typedef struct Node *LinkList;

5 单链表的读取

获得链表第i个数据的算法思路:

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

实现代码算法如下:

// 初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
// 操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L, int i, ElemType *e)
{
    int j;
    LinkList p; 		// 声明一指针p
    p = L -> next; 		// 让p指向链表L的第一个结点
    j = 1; 				// j为计数器
    while (p && j < i) 	// p不为空且计数器j还没有等于i时,循环继续
    {
        p = p -> next; 	// 让p指向下一个结点
        ++j;
    }
    if (!p || j > i)
        return ERROR; 	// 第i个结点不存在
    *e = p -> data; 	// 取第i个结点的数据
    return OK;
}

6 单链表的插入与删除

6.1 单链表的插入

实现代码算法如下:

// 初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
// 操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度加1
Status ListInsert(LinkList *L, int i, ElemType e)
{
    int j;
    LinkList p, s;
    p = *L;
    j = 1;
    // 寻找第i-1个结点
    while (p && j < i)
    {
        p = p -> next;
        ++ j;
    }
    // 第i个结点不存在
    if (!p || j > i)
        return ERROR;
    // 生成新结点(C标准函数)
    s = (LinkList)malloc(sizeof(Node));
    s -> data = e;
    // 将p的后继结点赋值给s的后继
    s -> next = p -> next;
    // 将s赋值给p的后继
    p -> next = s;
    return OK;
}

6.2 单链表的删除

单链表第i个数据删除结点的算法思路:

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

实现代码算法如下:

// 初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
// 操作结果:删除L的第i个结点,并用e返回其值,L的长度减1
Status ListInsert(LinkList *L, int i, ElemType e)
{
    int j;
    LinkList p, q;
    p = *L;
    j = 1;
    // 寻找第i-1个结点
    while (p && j < i)
    {
        p = p -> next;
        ++ j;
    }
    // 第i个结点不存在
    if (!p || j > i)
        return ERROR;
    q = p -> next;
    // 将q的后继赋值给p的后继
    p ->next = q -> next;
    // 将q结点中的数据给e
    *e = q -> data;
    // 让系统回收此结点,释放内存
    free(q);
    return OK;
}

7 单链表的整表创建

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

  1. 声明一指针p和计数器变量i;
  2. 初始化一空链表L;
  3. 让L的头结点的指针指向NULL,即简历一个带头结点的单链表;
  4. 循环
    • 生成一新结点赋值给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));
        // 随机生成100以内的数字
        p -> data = rand() % 100 + 1;
        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指向尾部的结点
    r = *L;
    for (i = 0; i < n; i ++)
    {
        // 生成新结点
        p = (LinkList) malloc(sizeof(Node));
        // 随机生成100以内的数字
        p -> data = rand() % 100 + 1;
        // 将表尾终端结点的指针指向新结点
        r -> next = p;
        // 将当前的新结点定义为表尾终端结点
        r = p;
    }
    // 表示当前链表结束
    r -> next = NULL;
}

8 单链表的整表删除

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

  1. 声明一指针p和q;
  2. 将第一个结点赋值给p;
  3. 循环:
    • 将下一结点赋值给q;
    • 释放p;
    • 将q赋值给p;

实现代码算法如下:

// 初始条件:顺序线性表L已存在,操作结果:将L重置为空表
Status ClearList(LinkList *L)
{
    LinkList p, q;
    // p指向第一个结点
    p = (*L) -> next;
    // 没到表尾
    while (p)
    {
        q = p -> next;
        free(p);
        p = q;
    }
    // 头结点指针域为空
    (*L) -> next = NULL;
    return OK;
}

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

存储分配方式

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

时间性能

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

空间性能

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

通过上面的对比,可以得出一些经验性的结论:

  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。

10 静态链表

有些早期高级语言没有指针,链表结构按照前面我们的讲法,它就没法实现了。怎么办呢?

有人就想出来用数组来代替指针,来描述单链表。
首先我们让数组的元素都是由两个数据域组成,datacur。也就是说,数组的每个下标都对应一个data和一个cur。数据域data,用来存放数据元素,也就是通常我们要处理的数据;而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把cur叫做游标
我们把这种用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法

为了我们方便插入数据,我们通常会把数组建立得大一些,以便有一些空闲空间可以便于插入时不至于溢出。

image-20220426173320908

// 线性表的静态链表存储结构
// 假设链表的最大长度是1000
#define MAXSIZE 1000
typedef struct
{
    ElemType data;
    // 游标(Cursor),为0时表示无指向
    int cur;
} Component;
// 对于不提供结构struct的程序设计语言,可以使用一对并行数组data和cur来处理。
StaticLinkList[MAXSIZE];

静态链表优缺点:

  • 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的特点。
  • 没有解决连续存储分配带来的表长难以确定的问题
  • 失去了顺序存储结构随机存取的特性

总的来说,静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。

11 循环链表

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

循环链表的合并:

image-20220426173357922

image-20220426173411935

// rear为尾指针,指向链表终端结点
// 保存A表的头结点,如图中①
p = rearA -> next;
// 将本是指向B表的第一个结点(不是头结点)
rearA -> next = rearB -> next -> next;
// 赋值给rearA->next,如图中②
q = rearB -> next;
// 将原A表的头结点赋值给rearB->next,如图中③
rearB -> next = p;
// 释放q
free(q);

12 双向链表

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

双向链表存储结构:

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

image-20220426173629490

双向链表的插入

image-20220426172156369

// 把p赋值给s的前驱,如图中①
s -> prior = p;
// 把p->next赋值给s的后继,如图中②
s -> next = p ->next;
// 把s赋值给p->next的前驱,如图中③
p ->next -> prior = s;
// 把s赋值给p的后继,如图中④
p -> next = s;

双向链表的删除

image-20220426173705811

// 把p->next赋值给p->prior的后继,如图中①
p -> prior -> next = p -> next;
// 把p->prior赋值给p->next的前驱,如图中②
p -> next -> prior = p -> prior;
// 释放结点
free(p);

13 总结回顾

posted @   JapserTang  阅读(222)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
Live2D
欢迎阅读『大话数据结构学习笔记(三)——线性表』
点击右上角即可分享
微信分享提示