当前位置: 首页 > news >正文

线性表---顺序表概述及应用

 目录

基本认识

它具体是怎么存储的

它有何特点   

主要优点

主要缺点

顺序表的基本实现

1.建立顺序表

2.顺序表的常用算法

1. ​初始化顺序表 InitList​

2. ​销毁顺序表 DestroyList​

3. ​判断空表 ListEmpty​

 4. ​获取表长 ListLength​

 5. ​输出所有元素 DisplayList​

 6. ​获取指定位置元素 GetElem​

7. ​查找元素位置 LocateElem​

 8. ​插入元素 ListInsert​

9. ​删除元素 ListDelete​

​总结与注意事项​

简单应用实例

问题1:

        解法一:

        解法二:

小总结

问题2:

解法一:

解法二:

小总结

问题3:

解法一:

解法二:

小总结


        本文重点讲述线性表中的顺序表存储结构及基本的实现过程,包含我自身的心得及思考,愿诸位能相比书上内容更好理解。


基本认识

        什么是顺序表?按一般的官方定义来看,线性表的顺序存储结构简称为顺序表,也就是一种采用连续存储空间实现线性表的数据结构,逻辑上相邻的元素,其物理存储位置也相邻。

        说白了你可以想象为:

  • 电影院连续座位​:所有座位号是连续的,你可以凭票号(索引)直接找到自己的位置。
  • 火车车厢​:每节车厢顺序连接,乘客(数据)按票号顺序就坐。

它具体是怎么存储的

        顺序表在内存中占用一块连续的内存空间。假设我们有一个存储语文,数学,英语的顺序表,其物理存储可能如下所示:

存储空间地址

数组下标

存储元素

0x001

0

语文

0x002

1

数学

0x003

2

英语

0x004

3

0x005

4

        可以看到,元素的逻辑顺序​(语文→数学→英语)与它们在内存中的物理存储顺序完全一致。每个元素的地址是连续的,并且可以通过 起始地址 + 下标 × 元素大小的公式直接计算得出,无需遍历

它有何特点   

主要优点

  1. 1. 高效的随机访问​:由于存储空间连续,可以通过下标直接访问任何位置的元素,时间复杂度为 ​O(1)​。这是顺序表最核心的优势。
  2. 2.高存储密度​:顺序表只存储元素本身的数据,不需要像链表那样为指针额外分配存储空间,因此内存利用率较高
  3. 3. 实现简单​:基于数组构建,操作逻辑相对直观,易于理解和实现

主要缺点

  1. 插入删除效率较低​:在中间或头部进行插入或删除操作时,为了保持元素的连续性,需要移动大量元素。平均时间复杂度为 ​O(n)​
  2. 容量固定,不够灵活​:静态顺序表的大小在编译时就已确定,难以适应动态变化的数据量。动态顺序表虽然可以扩容,但扩容操作本身(申请新空间、复制数据、释放旧空间)耗时且带来额外开销
  3. 可能浪费内存​:如果预先分配的空间远大于实际需要,会造成内存闲置;如果分配不足,又可能频繁扩容影响性能

顺序表的基本实现

        前面都是些令人烦躁的又臭又长的定义及概念,说重要不重要,说不重要它又对其顺序表的理解起了特别大的作用,但学习是为了完成实现的需要,也就是实践。

1.建立顺序表

        这里我们来学习整体创建顺序表,即由数组元素a[0..n-1]创建顺序表L。说白了就是把数组a依次放进顺序表中,并将n赋给顺序表当长度域。现在我们来看看怎么实现:

        首当其冲我们应该整一个也就是声明一个顺序存储类型:

typedef struct
{int data[MaxSize];int length;
}SqList;

        这个结构体应该不难理解,定义一个data数组来存储元素,length来存储长度。

data数据内容也并非是int类型的,大家想存什么就用什么类型。

大多都是在开头搞一个“typedef int ElemType”来方便类型的改变。

        接下来就是具体算法:

void CreateList(SqList *&L,int a[],int n)
{int i=0,k=0;    //i用来存放a的逻辑地址,k来存放L的个数;L=(SqList *)malloc(sizeof(SqList)); //分配存放线性表的空间while(i<n)//依次循环存放数值a{L->data[k]=a[i];k++;i++;}L->length=k;//存入长度
}

        大致应该不难看懂,就是这么个实现过程,也就是为什么L前面要加个&可能有些人就不知道,这个&符号是C++语言中提供的一种引用运算符,说白了就是实参会随形参的变化而变化

        我们就来举个例子看看

int a=4;//声明变量
int b=8;
void swap(int &x,int &y)
{int tmp=x;x=y;y=tmp;
}

        其中的形参x,y前面都有“&”,那么我们执行swap(a,b)时,实参a=8,b=4。大致就是这么个意思。

2.顺序表的常用算法

        下面我用通俗易懂的方式逐个解释顺序表最基本的函数。


1. ​初始化顺序表 InitList

void InitList(SqList *&L) {L = (SqList *)malloc(sizeof(SqList*));L->length = 0;
}
  • 作用​:为顺序表申请一块内存空间,并设置初始长度为0,准备开始使用。

  • 通俗理解​:就像你去图书馆占一张空桌子(申请内存),并且桌子上还没有书(length=0)。

  • 注意​:这里有一个小问题。sizeof(SqList*)计算的是指针的大小,而不是整个 SqList结构体的大小。这可能会导致分配的内存空间不足。通常应该写成 sizeof(SqList)


2. ​销毁顺序表 DestroyList

void DestroyList(SqList *&L) {free(L); //释放L的空间
}
  • 作用​:释放顺序表所占用的内存空间。

  • 通俗理解​:离开图书馆,把之前占的桌子还给管理员,以便他人使用。

  • 注意​:如果顺序表内部还指向其他动态内存(在你的代码中,data是静态数组,所以没问题),需要先释放内部内存再释放表本身。


3. ​判断空表 ListEmpty

bool ListEmpty(SqList *L) {return (L->length == 0);
}
  • 作用​:检查顺序表是否为空(没有任何元素)。

  • 通俗理解​:看看桌子上是不是一本书都没有。


 4. ​获取表长 ListLength

int ListLength(SqList *L) {return (L->length);
}
  • 作用​:返回顺序表中当前元素的个数。

  • 通俗理解​:数数桌子上现在有多少本书。


 5. ​输出所有元素 DisplayList

void DisplayList(SqList *L) {for (int i = 0; i < L->length; i++) {cout << L->data[i] << endl;}
}
  • 作用​:从第一个元素到最后一个元素,逐个打印出顺序表的所有内容。

  • 通俗理解​:把桌子上所有的书的名字从头到尾念一遍。


 6. ​获取指定位置元素 GetElem

bool GetElem(SqList *L, int i, int e) {if (i < 1 || i > L->length)return false;e = L->data[i - 1];return true;
}
  • 作用​:获取顺序表中第 i个位置上的元素值,并存入变量 e

  • 通俗理解​:直接看看第i本书的书名是什么。​位置i指的是逻辑上的第几本(从1开始数),而数组下标是从0开始的,所以要用 i-1

  • 注意​:这个函数有一个问题。参数 e应该是引用传递(int &e)或者指针传递(int *e),否则对形参 e的赋值无法传递给调用处的实参。现在这样写是无效的。


7. ​查找元素位置 LocateElem

int LocateElem(SqList *L, int e) {int i = 0;while (i < L->length && e != L->data[i])i++;if (i >= L->length) // 原代码是 i>L->length,但i最大等于L->length-1,所以应改为>=return 0;elsereturn i + 1;
}
  • 作用​:在顺序表中查找某个值 e,如果找到,返回它是第几个元素(位序);如果找不到,返回0。

  • 通俗理解​:从第一本书开始,一本一本地找书名叫《e》的书,找到后就告诉它是第几本。如果所有书都翻遍了还没找到,就说没有这本书。

  • 小修改​:循环条件 i < L->length已经确保 i不会等于 L->length,所以后面的判断条件 if (i > L->length)永远不会为真。通常查找失败返回0,所以条件可以改为 if (i >= L->length)


 8. ​插入元素 ListInsert

bool ListInsert(SqList *&L, int i, int e) {int j;if (i < 1 || i > L->length + 1 || L->length == MaxSize)return false;i--;for (j = L->length; j > i; j--) {L->data[j] = L->data[j - 1];}L->data[i] = e;L->length++;return true;
}
  • 作用​:在顺序表的第 i位置之前插入一个新元素 e

  • 通俗理解​:假设桌子上有5本书排成一排,你想在第3本书前面插入一本新书。那么你需要先把第3本、第4本、第5本书都往后挪一个位置,腾出第三个位置的空位,才能把新书放进去。插入后书的总数(length)就加1了。

  • 关键点​:

    • 合法性检查​:插入位置 i必须在 1length+1之间(可以在最后面追加)。并且表不能满 (L->length == MaxSize)。

    • 移动元素​:从最后一个元素开始,到第 i个元素为止,依次向后移动一个位置,为新元素腾出空间 ``。这是顺序表插入操作开销最大的地方。

    • 位置转换​:逻辑位序 i转换成数组下标需要 i-1


9. ​删除元素 ListDelete

bool ListDelete(SqList *&L, int i, int e) {int j;if (i < 1 || i > L->length)return false;i--;e = L->data[i];for (j = i; j < L->length - 1; j++)L->data[j] = L->data[j + 1];L->length--;return true;
}
  • 作用​:删除顺序表中第 i个位置的元素,并通过 e返回被删除元素的值

  • 通俗理解​:假设你想拿走桌子上的第3本书。你先把这本书的名字记下来(e = L->data[i]),然后把第4本、第5本书都往前挪一个位置,覆盖掉第三本书的位置。这样原来第三本书就被“删除”了,书的总数(length)也减1了。

  • 关键点​:

    • 合法性检查​:删除位置 i必须在 1length之间(只能删除已有的元素)。

    • 移动元素​:从第 i+1个元素开始,到最后一个元素为止,依次向前移动一个位置,覆盖掉被删除的元素 ``。这是顺序表删除操作开销最大的地方。

    • 注意​:和 GetElem类似,这里的参数 e也应该是引用传递(int &e)或指针传递(int *e),否则无法将删除的值带回去。


总结与注意事项

  1. 核心特点​:顺序表的最大优点是随机访问快(通过下标直接定位,时间复杂度O(1)),但插入和删除效率相对较低(需要移动元素,平均时间复杂度O(n))。

  2. 常见错误​:

    • InitList中的 malloc参数应该是 sizeof(SqList)

    • GetElemListDelete中的参数 e需要改为引用或指针传递才能真正改变实参。

    • 注意数组下标从0开始,而逻辑上的位序是从1开始的,所有涉及到位置的操作基本都需要 i-1进行转换。

    • 边界条件​:时刻注意检查位置参数的合法性(是否在有效范围内)以及表是否已满或已空。

        以上就是顺序表最常见的9种用法,对于我们初学者要注意的是逻辑序号和物理序号的区别,不然容易搞懵。


简单应用实例

问题1:

        设计一个算法,删除其中所有值等于x的元素,要求时间复杂度为O(n),空间复杂度为O(1)。

        解法一:

特性

整体建表法 (delnode1)

核心思想

重建新表​:遍历原表,​只保留不等于 x的元素。

指针角色

k:​新表的当前位置(记录保留元素)

终止条件

i遍历完所有元素

最终长度

L->length = k(k​ 直接就是新长度)

通俗比喻

淘金​:把沙子(x)筛掉,只收集金子(非x)放到新桶里。

void delnode1(SqList *&L,int x)//整体建表法,删除与x相等的值
{int k=0,i;for(i=0;i<L->length;i++){if(x!=L->data[i]){L->data[k]=L->data[i];k++;}}L->length=k;
}

        解法二:

特性

元素移动法 (delnode2)

核心思想

覆盖前进​:遍历原表,​跳过等于 x的元素,并用后续元素覆盖它们。

指针角色

k:​累计删除个数​(记录需要跳过的位置偏移)

终止条件

i遍历完所有元素

最终长度

L->length = L->length - k(原长度减去删除的个数 ​k)

通俗比喻

填坑​:遇到一个坑(x)就记下来,后面的石头(非x)往前填这些坑。

void delnode2(SqList *&L,int x)
{int k=0,i=0;while(i<L->length){if(L->data[i]==x)k++;elseL->data[i-k]=L->data[i];i++;}L->length-=k;
}

小总结

  1. 整体建表法 (delnode1)​​ 更直观,容易理解和实现。它明确地“构建”了一个只包含所需元素的新表。
  2. 元素移动法 (delnode2)​​ 的思维更巧妙。它通过累计的偏移量 k动态计算每个保留元素应该存放的最终位置。
  3. 虽然在这个特定问题上两种方法效率相同,但整体建表法的逻辑通常更不容易出错,尤其是在更复杂的条件判断下。
  4. 这两种方法都体现了双指针技巧的精髓(一个指针 i用于遍历,另一个指针 k用于指示下一个有效位置或记录偏移),这是解决许多线性表问题的关键。

问题2:

        设计一个算法,以第一个元素为分界线,将所有小于或等于它的元素移到该分界线前面,反之移到后面。

解法一:

特性

Partition1 (交换法)

核心思想

双向扫描,及时交换​:从两端向中间扫描,发现不符合位置条件的元素对就交换。

基准处理

始终参与比较和可能的交换

元素移动

交换

终止条件

两指针相遇(i == j

最终操作

交换基准与相遇点元素

通俗比喻

两头分类交换​:像两个人从两头一起检查,不对的就互相交换手里的物品。

void partition1(SqList *&L)
{int i=0, j=L->length-1; // i 指向头(基准的下一个方向),j 指向尾while (i < j){// 先从右往左找第一个**不大于**基准的元素(<=基准)while (i<j && L->data[j] > L->data[0])j--;// 再从左往右找第一个**大于**基准的元素(>基准)while (i<j && L->data[i] <= L->data[0])i++;// 如果左右指针未相遇,交换这两个元素if (i < j)swap(L->data[i], L->data[j]);}// 最后将基准元素交换到中间位置swap(L->data[0], L->data[i]);
}

解法二:

特性

Partition2 (空位填充法)

核心思想

空位填充​:备份基准,先从右找小于基准的填左空位,再从左找大于基准的填右空位,最后基准归位。

基准处理

先备份,原位置视为空位,最后放入正确位置

元素移动

覆盖

终止条件

两指针相遇(i == j

最终操作

将基准值放入相遇空位

通俗比喻

挖坑填数​:先挖走基准(坑),从另一边找合适的土(数)来填,最后把基准填到最后的坑里。

void partition1(SqList *&L)
{int i=0, j=L->length-1; // i 指向头(基准的下一个方向),j 指向尾while (i < j){// 先从右往左找第一个**不大于**基准的元素(<=基准)while (i<j && L->data[j] > L->data[0])j--;// 再从左往右找第一个**大于**基准的元素(>基准)while (i<j && L->data[i] <= L->data[0])i++;// 如果左右指针未相遇,交换这两个元素if (i < j)swap(L->data[i], L->data[j]);}// 最后将基准元素交换到中间位置swap(L->data[0], L->data[i]);
}

小总结

  • 共同点​:两种算法的时间复杂度都是 ​O(n)​,空间复杂度都是 ​O(1)​,且都非常高效。
  • 主要区别​:
    • partition1在扫描过程中通过交换元素来调整位置。
    • partition2通过覆盖来填充空位,最后才将基准归位,​交换次数更少​(理论上比 partition1少一半),通常性能稍优一些
  • 注意事项​:
    • 循环条件 i < j必须严格,否则会越界或错误。
    • 内层循环的条件 L->data[j] > pivotL->data[i] <= pivot要注意包含等号的情况,以确保划分的正确性。
    • 必须先进行 ​j​ 的扫描,再进行 ​i​ 的扫描,以确保最后 ij相遇的位置是一个小于等于基准的元素,这样才能正确安置基准。
  • 为何是快速排序的核心​:这个操作一次能将一个元素(基准)放到其最终位置,并将数据集分成两个子集,然后再递归地对子集进行同样的操作,这正是快速排序“分治”思想的体现

问题3:

        将一个顺序表中所有的奇数移到偶数前面。

解法一:

特性

Move1 (两端扫描交换法)

核心思想

两端逼近,交换无效元素​:从两端向中间扫描,左指针找偶数,右指针找奇数,交换它们。

指针含义

i: 从左向右扫描,寻找偶数​ (不该在左边的)
j: 从右向左扫描,寻找奇数​ (不该在右边的)

终止条件

两指针相遇 (i >= j)

交换条件

左指针找到偶数且右指针找到奇数时 (i < j)

通俗比喻

两头分类交换​:像两个人从两头一起检查,不对的就互相交换手里的物品。

void move1(SqList *&L) //将所有奇数项移到偶数项前面
{int i = 0, j = L->length - 1; // i从头部开始,j从尾部开始while (i < j) // 当i和j未相遇时循环{while (i < j && L->data[j] % 2 == 0) // 从右往左找奇数j--;while (i < j && L->data[i] % 2 != 0) // 从左往右找偶数i++;if (i < j) // 如果找到了且i仍在j左边swap(L->data[i], L->data[j]); // 交换这两个元素}
}

具体过程如下:

解法二:

特性

Move2 (奇数分区法)

核心思想

单遍扫描,扩展奇数区​:从左到右扫描,维护一个“奇数区”,将遇到的奇数交换到该区域的末尾。

指针含义

i: ​奇数区的最后一个位置
j: 当前扫描指针,遍历整个数组

终止条件

遍历完所有元素 (j到达 length)

交换条件

j找到奇数,且不在奇数区时 (j != i)

通俗比喻

收集奇数​:像玩扑克时,把所有奇数牌一张张抽出来放到左手(奇数区),右手继续翻剩下的牌。

void move2(SqList *&L) //此乃奇数分域法
{int i = -1, j = 0; // i是奇数区的边界,初始为-1;j用于遍历for (j; j < L->length; j++) // 遍历整个顺序表{if (L->data[j] % 2 == 1) // 如果当前元素是奇数{i++; // 奇数区的边界向右扩大一位if (j != i) // 如果当前奇数不在它该在的位置上swap(L->data[i], L->data[j]); // 就把它交换到奇数区的末尾}}
}

具体实现如下:

小总结

方面

Move1 (两端扫描交换法)

Move2 (奇数分区法)

时间复杂度

O(n)

O(n)

空间复杂度

O(1)

O(1)

交换次数

相对较多(每次找到一对交换一次)

相对较少(每个奇数最多交换一次)

特点

思路直观,类似快速排序的分区操作

代码简洁,一次遍历,类似插入排序的思想

适用场景

通用

尤其适用于奇数较少的情况,交换次数少

        总的来说我感觉跟问题二有异曲同工之处,大差不差,搞懂问题二的应用也就水到渠成了。


        总的来说顺序表的基本用法就是这么多了,当然还用更多的用法就需要大家自己去刷题学习了。

请大家点点关注和点赞,后面我一定会分享更多实用的项目的


文章转载自:

http://xBvFYV3o.Lsssx.cn
http://dfBQNEd4.Lsssx.cn
http://M3unrlHX.Lsssx.cn
http://M3BiGkYt.Lsssx.cn
http://xKLyYTO1.Lsssx.cn
http://Kormlswl.Lsssx.cn
http://ItIIBCq2.Lsssx.cn
http://QIN7SfrE.Lsssx.cn
http://2bxSYc0H.Lsssx.cn
http://UDjchyHm.Lsssx.cn
http://3gjQoZny.Lsssx.cn
http://qRCLzxGE.Lsssx.cn
http://LrjBtmX5.Lsssx.cn
http://hpvbRbbF.Lsssx.cn
http://qVR1qfUE.Lsssx.cn
http://GbxBn7Wu.Lsssx.cn
http://oPvdWNHB.Lsssx.cn
http://PQWrfFRs.Lsssx.cn
http://UoZY3wT3.Lsssx.cn
http://Bz8ZiEli.Lsssx.cn
http://Si0B7D1d.Lsssx.cn
http://ctKFH8aP.Lsssx.cn
http://obn2UC48.Lsssx.cn
http://voW1tBIT.Lsssx.cn
http://J7E8rYea.Lsssx.cn
http://rxFnGapn.Lsssx.cn
http://U96uxL5W.Lsssx.cn
http://CzZz5gWt.Lsssx.cn
http://1LQgYyXf.Lsssx.cn
http://f9qOYfAx.Lsssx.cn
http://www.dtcms.com/a/382350.html

相关文章:

  • Custom SRP - Point and Spot Lights
  • 狂雨小说CMS内容管理系统 v1.5.5 pc+h5自适应网站
  • DeepSeek实战--自定义工具
  • 同位素分离
  • PID算法:从理论到实践的全面解析
  • 0x03-g a+b ib
  • 【Linux】初识Linux
  • Tomcat介绍与核心操作讲解(以Rhel9.3为例)
  • @RequiredArgsConstructor使用
  • 脉冲串函数在数字信号处理中的核心应用与价值
  • AI助力HTML5基础快速入门:从零开始理解网页结构
  • 大数据与财务管理专业如何转型做金融科技?
  • 【开题答辩全过程】以 高校实习信息管理系统为例,包含答辩的问题和答案
  • 贪心算法应用:推荐冷启动问题详解
  • “单标签/多标签” vs “二分类/多分类”
  • 多商户异次元发卡网是啥啊?
  • 使用 Anaconda Distribution 安装 Python + GDAL并在vscode配置开发环境(完整版)
  • 先进电机拓扑及控制算法介绍(3)——以“数据”驱动电机实现真正的无模型
  • 进程卡顿怎么办?Process Lasso 免费功能实测解析
  • Grafana配置连接时候证书与mongosqld启动证书的关系
  • XWiki Platform 路径遍历漏洞分析 | CVE-2025-55747CVE-2025-55748
  • Python快速入门专业版(二十九):函数返回值:多返回值、None与函数嵌套调用
  • DBSCAN 聚类:以“热闹”划界,任意形状成团,孤立点全当噪声
  • 设计模式:从Collections.synchronizedCollection()出发了解【装饰器模式】
  • CSS3的新特性
  • Python的包管理工具uv下载python版本慢问题解决
  • K8s学习笔记(二):Pod
  • 贪心算法应用:异常检测阈值调整问题详解
  • C++ stack和queue的使用及模拟实现
  • 【面试题】RAG核心痛点