【数据结构】线性表

在这里插入图片描述

上期回顾: 【数据结构】入门基础
个人主页:C_GUIQU
归属专栏:数据结构

在这里插入图片描述

目录

正文

1. 引言

数据结构在计算机科学领域中起着至关重要的作用,它关乎着数据的组织、存储以及操作方式。线性表作为一种基础且常用的数据结构,广泛应用于各类程序和算法之中。理解线性表的概念、特点以及其具体实现和操作对于深入学习数据结构和后续的编程实践都有着重要意义。本文将详细介绍线性表相关知识,并通过C++代码示例来展示其具体的实现和使用方法。

1.1 数据结构概述

数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。它主要研究的是数据的逻辑结构、存储结构以及在这些结构上定义的运算。逻辑结构描述了数据元素之间的逻辑关系,比如线性结构、树形结构、图状结构等。存储结构则关注数据元素及其关系在计算机存储器中的存储方式,常见的有顺序存储、链式存储等。通过合理地选择和运用数据结构,可以更高效地解决实际问题,提高程序的运行效率和性能。

1.2 线性表在数据结构中的地位

线性表是一种典型的线性结构,它具有简单直观的特点。许多其他复杂的数据结构,如栈、队列等,实际上可以看作是线性表在特定应用场景下的特殊形式,或者说是对线性表进行了一定的限制和改造而来的。线性表的操作和实现方式也是理解后续更复杂数据结构的基础,掌握好了线性表相关知识,就能更容易地去学习和掌握其他数据结构,进而更好地应对各种编程任务中涉及的数据组织和处理需求。

2. 线性表的基本概念

2.1 定义

线性表是由n(n≥0)个具有相同类型的数据元素组成的有限序列。这里的数据元素可以是简单的数据类型,如整数、字符等,也可以是复杂的自定义结构体类型等。例如,一个班级学生的学号列表、一个购物清单中的商品名称列表等都可以看作是线性表的实际例子。线性表中的元素之间存在着线性关系,即除了第一个元素外,每个元素都有且仅有一个直接前驱;除了最后一个元素外,每个元素都有且仅有一个直接后继。

2.2 线性表的抽象数据类型(ADT)

抽象数据类型是指一个数学模型以及定义在该模型上的一组操作。对于线性表来说,其常见的操作包括:

2.2.1 InitList(&L)

初始化操作,用于创建一个空的线性表L,并为其分配必要的存储空间等初始设置。

2.2.2 DestroyList(&L)

销毁操作,释放线性表L所占用的存储空间,将其从内存中清除,一般在不再使用该线性表时执行此操作。

2.2.3 ListInsert(&L, i, e)

插入操作,在线性表L的第i个位置插入元素e。这里要注意位置的合法性,i的取值范围一般是1到线性表长度加1(假设线性表长度用len表示,即1 ≤ i ≤ len + 1),如果i值超出这个范围,则插入操作是非法的。

2.2.4 ListDelete(&L, i, &e)

删除操作,删除线性表L中第i个位置的元素,并用e返回被删除元素的值。同样,i的取值需要在合法范围内,即1 ≤ i ≤ len。

2.2.5 GetElem(L, i, &e)

取值操作,获取线性表L中第i个位置的元素,并将其赋值给e,i需要满足1 ≤ i ≤ len。

2.2.6 LocateElem(L, e, compare())

查找操作,按照给定的比较函数compare(),在线性表L中查找与元素e满足比较条件的元素位置,如果找到返回其在表中的位置序号,若没找到则返回一个表示未找到的特定值(比如0等)。

2.2.7 ListLength(L)

求长度操作,返回线性表L的长度,也就是线性表中所含元素的个数。

这些操作构成了线性表抽象数据类型的基本接口,不同的实现方式(如顺序表和链表)都需要去实现这些基本操作,以满足对线性表的各种使用需求。

3. 线性表的顺序存储结构(顺序表)

3.1 顺序表的概念

顺序表是线性表的一种存储结构,它用一组地址连续的存储单元依次存储线性表中的数据元素,使得线性表中逻辑上相邻的元素在物理存储位置上也是相邻的。可以把顺序表想象成一个数组,数组的下标对应着线性表中元素的位置序号。例如,若定义一个整型顺序表,它在内存中就是一段连续的用于存放整数的存储空间,第一个元素存放在起始位置,后续元素依次紧挨着存放。

3.2 顺序表的存储结构描述(C++代码表示)

在C++中,可以通过结构体来定义顺序表的存储结构,以下是一个简单的示例代码:

#define MAXSIZE 100  // 定义顺序表的最大长度
typedef int ElemType;  // 假设元素类型为整数,可根据实际情况替换

// 顺序表的结构体定义
struct SqList {
    ElemType data[MAXSIZE];  // 存放数据元素的数组
    int length;  // 顺序表的当前长度
};

上述代码中,SqList结构体表示顺序表,其中data数组用于存储线性表中的元素,MAXSIZE限制了顺序表最多能存放的元素个数,而length则记录了当前顺序表中实际存放的元素个数。

3.3 顺序表的基本操作实现

3.3.1 初始化操作(InitList)

// 初始化顺序表
void InitList(SqList &L) {
    L.length = 0;  // 将顺序表长度初始化为0,表示空表
}

这个函数很简单,就是将顺序表的length成员变量设置为0,意味着创建了一个空的顺序表,等待后续插入元素等操作。

3.3.2 插入操作(ListInsert)

// 顺序表的插入操作,在第i个位置插入元素e
bool ListInsert(SqList &L, int i, ElemType e) {
    if (i < 1 || i > L.length + 1) {  // 判断插入位置是否合法
        return false;
    }
    if (L.length >= MAXSIZE) {  // 判断顺序表是否已满
        return false;
    }
    for (int j = L.length; j >= i; j--) {  // 移动元素,腾出插入位置
        L.data[j] = L.data[j - 1];
    }
    L.data[i - 1] = e;  // 将元素插入到指定位置
    L.length++;  // 顺序表长度加1
    return true;
}

该插入操作首先要检查插入位置i是否合法以及顺序表是否还有空间可插入元素。如果条件满足,就通过循环将插入位置后面的元素依次往后移动一位,腾出位置插入新元素e,最后更新顺序表的长度。

3.3.3 删除操作(ListDelete)

// 顺序表的删除操作,删除第i个位置的元素,并用e返回被删除元素的值
bool ListDelete(SqList &L, int i, ElemType &e) {
    if (i < 1 || i > L.length) {  // 判断删除位置是否合法
        return false;
    }
    e = L.data[i - 1];  // 保存要删除的元素值
    for (int j = i; j < L.length; j++) {  // 移动元素,填补删除元素后的空位
        L.data[j - 1] = L.data[j];
    }
    L.length--;  // 顺序表长度减1
    return true;
}

删除操作先验证删除位置的合法性,然后取出要删除的元素值赋给e,接着通过循环将删除位置后面的元素依次往前移动一位来填补空位,最后减少顺序表的长度。

3.3.4 取值操作(GetElem)

// 获取顺序表中第i个位置的元素,赋值给e
bool GetElem(SqList L, int i, ElemType &e) {
    if (i < 1 || i > L.length) {  // 判断位置是否合法
        return false;
    }
    e = L.data[i - 1];  // 将对应位置元素赋值给e
    return true;
}

该操作就是简单地检查位置合法性后,将顺序表中指定位置的元素赋值给e

3.3.5 查找操作(LocateElem)

// 在线性表中查找与元素e满足比较条件的元素位置
int LocateElem(SqList L, ElemType e) {
    for (int i = 0; i < L.length; i++) {
        if (L.data[i] == e) {  // 找到满足条件的元素
            return i + 1;  // 返回其位置序号(注意这里返回的是从1开始的序号)
        }
    }
    return 0;  // 没找到返回0
}

查找操作通过循环遍历顺序表中的元素,当找到与给定元素e相等的元素时,返回其位置序号(从1开始计数),如果遍历完整个顺序表都没找到则返回0。

3.3.6 求长度操作(ListLength)

// 返回顺序表的长度
int ListLength(SqList L) {
    return L.length;
}

直接返回顺序表结构体中记录长度的length成员变量的值即可。

3.4 顺序表的优缺点

3.4.1 优点

  • 随机访问性强:由于顺序表中的元素在物理存储上是连续的,只要知道元素的下标(也就是线性表中的位置序号),就可以直接通过数组的下标访问方式快速获取到对应元素,时间复杂度为O(1)。例如,要获取顺序表中第5个元素,无需遍历其他元素,直接就能访问到。
  • 存储密度高:顺序表中每个存储单元都存放的是有效的数据元素(除了可能有部分预留空间,如数组定义了较大的长度但还没存满元素的情况),不存在像链式存储那样额外的指针等空间开销,所以存储密度相对较高。

3.4.2 缺点

  • 插入和删除操作效率低(非末尾位置):当需要在顺序表的中间位置插入或删除一个元素时,需要移动大量的后续元素来腾出空间或填补空位,平均时间复杂度为O(n),特别是当顺序表元素个数较多且频繁进行插入删除操作时,效率问题会比较突出。例如,在一个有1000个元素的顺序表中,要在第500个位置插入一个元素,就需要移动后面的500个元素。
  • 需要预先分配固定大小的存储空间:在定义顺序表时,一般要指定一个最大长度(如前面代码中的MAXSIZE),如果预估不准确,可能导致空间浪费(实际存储元素个数远小于最大长度)或者空间不足(元素个数超过最大长度时无法继续插入)的情况。

4. 线性表的链式存储结构(链表)

4.1 链表的概念

链表是线性表的另一种重要存储结构,它通过一组任意的存储单元(这些存储单元在物理上可以是不连续的)来存储线性表中的元素。每个存储单元称为一个节点,节点中除了存放数据元素本身外,还包含一个指针,用于指向下一个节点,这样通过指针的链接就形成了一个线性的逻辑结构。链表有多种类型,常见的有单链表、双链表、循环链表等,下面主要以单链表为例进行介绍。

4.2 单链表的存储结构描述(C++代码表示)

// 定义链表节点结构体
typedef int ElemType;  // 假设元素类型为整数,可根据实际情况替换
struct LNode {
    ElemType data;  // 存放数据元素
    struct LNode *next;  // 指向下一个节点的指针
};
typedef struct LNode *LinkList;  // 定义LinkList为指向链表节点的指针类型

上述代码中,LNode结构体表示链表的节点,它包含了数据域data用于存放元素内容,以及指针域next用于指向链表中的下一个节点。LinkList则是定义为指向LNode结构体的指针类型,通过它可以方便地操作整个链表,比如用LinkList类型的变量来指向链表的头节点等。

4.3 单链表的基本操作实现

4.3.1 初始化操作(InitList)

// 初始化单链表(创建一个空链表)
void InitList(LinkList &L) {
    L = new LNode;  // 创建头节点
    L->next = NULL;  // 头节点的next指针初始化为空,表示空链表
}

初始化单链表就是创建一个头节点,并将头节点的指针域设置为空,意味着链表目前没有实际的数据节点,为空链表。

4.3.2 插入操作(ListInsert)

// 在单链表的第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e) {
    LinkList p = L;
    int j = 0;
    while (p && j < i - 1) {  // 找到第i - 1个节点
        p = p->next;
        j++;
    }
    if (!p || j > i - 1) {  // 判断位置是否合法
        return false;
    }
    LinkList s = new LNode;  // 创建新节点
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
}

插入操作首先要通过循环找到单链表中第i - 1个节点,然后创建一个新节点,将新节点插入到第i - 1个节点和原来第i个节点之间,调整指针的指向来完成插入操作。

4.3.2 删除操作(ListDelete)

// 删除单链表中第i个位置的元素,并用e返回被删除元素的值
bool ListDelete(LinkList &L, int i, ElemType &e) {
    LinkList p = L;
    int j = 0;
    while (p->next && j < i - 1) {  // 找到第i - 1个节点
        p = p->next;
        j++;
    }
    if (!p->next || j > i - 1) {  // 判断位置是否合法
        return false;
    }
    LinkList q = p->next;  // q指向要删除的节点
    e = q->data;
    p->next = q->next;
    delete q;  // 释放被删除节点的空间
    return true;
}

删除操作类似插入操作,先找到第i - 1个节点,然后将要删除的第i个节点从链表中断开,保存其数据值赋给e,并释放该节点所占用的内存空间。

4.3.3 取值操作(GetElem)

// 获取单链表中第i个位置的元素,赋值给e
bool GetElem(LinkList L, int i, ElemType &e) {
    LinkList p = L->next;
    int j = 1;
    while (p && j < i) {  // 找到第i个节点
        p = p->next;
        j++;
    }
    if (!p || j > i) {  // 判断位置是否合法
        return false;
    }
    e = p->data;
    return true;
}

取值操作通过循环遍历链表找到第i个节点,然后将该节点的数据元素赋值给e,同样要先验证位置的合法性。

4.3.4 查找操作(LocateElem)

// 在线性表中查找与元素e满足比较条件的元素位置
int LocateElem(LinkList L, ElemType e) {
    LinkList p = L->next;
    int i = 1;
    while (p) {
        if (p->data == e) {  // 找到满足条件的元素
            return i;
        }
        p = p->next;
        i++;
    }
    return 0;  // 没找到返回0
}

查找操作就是依次遍历链表中的节点,当找到与给定元素e相等的节点时,返回其在链表中的位置序号(从1开始计数),如果遍历完整个链表都没找到则返回0。

4.3.5 求长度操作(ListLength)

// 返回单链表的长度
int ListLength(LinkList L) {
    LinkList p = L->next;
    int len = 0;
    while (p) {
        len++;
        p = p->next;
    }
    return len;
}

通过循环遍历链表,每经过一个节点长度计数器就加1,最后返回链表的长度。

4.4 链表的优缺点

4.4.1 优点
  • 动态分配存储空间:链表不需要像顺序表那样预先分配固定大小的存储空间,它可以根据实际需要动态地申请和释放节点空间。在插入新元素时,只要有足够的内存空间,就可以创建新节点并插入到链表中,不用担心空间不足(只要内存整体没耗尽)的问题。例如,要构建一个存储学生信息的链表,随着学生数量的不断增加,随时可以新增节点来存放新学生的信息,而不用提前预估最大学生数量来分配固定空间。
  • 插入和删除操作相对灵活(非首位节点时):在链表中插入或删除节点,只需修改相关节点的指针指向即可,不需要像顺序表那样移动大量的元素。特别是对于频繁进行插入和删除操作且操作位置不固定的情况,链表的效率优势较为明显。比如在一个存储文章段落的链表中,如果要在中间某个段落位置插入或删除一段内容(对应一个节点),只需要调整前后节点的指针,时间复杂度通常为 O(1)(找到操作位置的时间复杂度不算在内,找到操作位置的时间复杂度与链表长度有关)。
4.4.2 缺点
  • 随机访问效率低:由于链表中的节点在物理存储上是不连续的,要访问链表中某个特定位置的元素,需要从链表头开始依次遍历节点,通过指针逐个查找,平均时间复杂度为 O(n)。不像顺序表可以直接通过下标快速定位元素,例如要获取链表中第100个节点的数据,必须依次经过前面的99个节点才能找到,效率远低于顺序表的随机访问。
  • 额外的空间开销:链表的每个节点除了存储数据元素本身外,还需要额外的指针空间来存储指向下一个节点(单链表情况)或前后节点(双链表情况)的指针,这使得链表的存储密度相对顺序表来说较低,一定程度上浪费了存储空间,特别是当数据元素本身占用空间较小时,指针空间开销所占比例就会更明显。

5. 顺序表与链表的比较及应用场景选择

5.1 比较总结

比较项目顺序表链表
存储结构地址连续的存储单元通过指针链接的任意存储单元
随机访问性能时间复杂度 O(1),可直接通过下标访问时间复杂度 O(n),需从头遍历查找
插入删除操作(非末尾/非首位)效率时间复杂度 O(n),需移动元素时间复杂度 O(1)(找到位置后),只需修改指针
存储空间分配需预先分配固定大小空间动态分配,按需申请释放
存储密度较高,基本都是数据元素存储较低,有指针等额外开销

从上述比较可以看出,顺序表和链表各有其优势和劣势,在不同的应用场景下有着不同的适用性。

5.2 应用场景选择

5.2.1 顺序表适用场景

  • 频繁进行随机访问操作的情况:如果在程序中需要经常根据元素的位置快速获取元素内容,比如在一个数组形式的成绩表中,经常要查询某个学生的具体成绩(通过学号对应位置来查),顺序表就是比较好的选择,因为其随机访问速度快。
  • 元素个数相对固定且已知的情况:当能提前预估数据元素的大致数量,并且后续不会有大量频繁的插入和删除操作时,使用顺序表可以充分利用其存储密度高的优点,避免空间浪费。例如,一个月份天数的列表,每个月天数基本固定,使用顺序表来存储就很合适。

5.2.2 链表适用场景

  • 需要频繁进行插入和删除操作的情况:比如在一个文本编辑器中,对文档内容的编辑往往涉及到段落(可看作节点)的频繁插入、删除操作,此时链表结构能更好地应对这种动态变化,提高操作效率,因为其插入删除只需修改指针而不用大量移动元素。
  • 数据元素数量不确定且动态变化大的情况:像一个网络聊天系统中,接收和发送的消息数量随时在变化,使用链表来存储消息记录就可以灵活地根据消息的增减来动态分配和释放节点空间,不用担心一开始分配空间不足或空间浪费的问题。

6. 线性表的扩展与应用

6.1 线性表在其他数据结构中的应用

许多其他重要的数据结构都是基于线性表衍生或者与线性表有着密切联系的。

6.1.1 栈

栈是一种只能在一端进行插入和删除操作的特殊线性表,它遵循后进先出(LIFO)的原则。可以把栈看作是对线性表插入和删除操作进行了限制的一种结构,其实现既可以采用顺序存储(顺序栈,类似顺序表的实现方式,用一个数组和一个记录栈顶位置的变量来实现),也可以采用链式存储(链栈,基于链表实现,一般以链表头作为栈顶进行操作)。栈在函数调用、表达式求值、括号匹配等众多领域都有着广泛的应用。

6.1.2 队列

队列则是一种只能在一端进行插入操作,在另一端进行删除操作的特殊线性表,遵循先进先出(FIFO)的原则。同样,队列的实现有顺序存储(顺序队列,需要考虑队头队尾指针的变化以及可能出现的假溢出等问题,通常采用循环队列的改进形式来解决相关问题)和链式存储(链队列,基于链表构建,有头指针和尾指针分别指向队列的头和尾)。队列常用于操作系统中的进程调度、消息排队等待处理等场景。

6.1.3 字符串

字符串从某种意义上来说也可以看作是一种特殊的线性表,其元素是字符类型的数据元素,线性表的各种操作在字符串处理中也有相应的体现,比如字符串的插入(在某个位置插入一段字符)、删除(删除部分字符)、查找(查找特定字符或子串的位置)等操作。在文本处理、信息检索、编程语言中的字符串操作等方面都离不开对字符串这种特殊线性表的处理。

6.2 实际编程中的线性表应用案例

6.2.1 学生成绩管理系统

在构建一个学生成绩管理系统时,可以使用线性表来存储学生的信息(包括学号、姓名、各科成绩等)。如果选择顺序表,可以提前预估学校学生的最大数量来定义合适大小的顺序表结构,方便通过学号(对应顺序表中的位置序号)快速查询某个学生的成绩情况;如果采用链表,则可以更灵活地处理学生的入学、转学、退学等导致学生数量动态变化的情况,方便进行学生信息的插入和删除操作。

6.2.2 图书管理系统

对于图书管理系统,用线性表存储图书的相关信息(如图书编号、书名、作者、馆藏数量等)也是很常见的做法。顺序表可以适用于馆藏图书数量相对稳定,且经常需要快速查找某本图书信息(比如根据图书编号快速定位)的情况;而链表则更适合在频繁进行图书的购入、借阅归还、报废等导致图书记录动态增减的场景下使用,便于灵活地更新图书信息列表。

7. 总结

线性表作为一种基础且重要的数据结构,无论是其顺序存储结构(顺序表)还是链式存储结构(链表),都有着各自独特的特点和适用场景。通过对线性表的深入学习,理解其抽象数据类型、基本操作的实现以及在不同场景下的优缺点,能够为进一步学习更复杂的数据结构,如树、图等打下坚实的基础,同时也能在实际的编程应用中根据具体需求合理地选择和运用线性表来高效地组织和处理数据,提高程序的性能和可维护性。希望本文对线性表相关知识的详细介绍以及所给出的C++代码示例能够帮助读者更好地掌握线性表这一关键的数据结构内容。

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Guiat

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

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

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

打赏作者

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

抵扣说明:

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

余额充值