引言
跳转文尾
1.顺序表是线性表的一种顺序存储形式
。换句话说,线性表是逻辑结构,表示元素之间一对一
的相邻关系;而顺序表是存储结构,是指用一组地址连续的存储单元,依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上地相邻。
2.设顺序表的第一个元素a0的存储地址为Loc(a0) ,每个元素占d个存储空间,则第i个元素的地址为:Loc(ai-1)= Loc(a0)+(i- 1)x d
3.顺序表在程序中通常用一维数组
实现,一维数组可以是静态分配
的,也可以是动态分配
的。
在静态分配时,由于数组的大小和空间是固定的, -但空间占满,就无法再新增数据
,否则会导致数据溢出。
而在动态分配时,存储数组的空间在程序执行过程中会动态调整大小,当空间占满时,可以另行开辟更大的存储空间
来储存数
据。
4.顺序表最主要的特点
是可以进行随机访问,即可以通过表头元素的地址和元素的编号(下标) , 在0(1)的时间复杂度内找到
指定的元素。
顺序表的不足之处
是插入和删除操作需要移动大量的元素,从而保持逻辑上和物理上的连续性。
4.一般实现顺序表的构造、插入、扩容、查找、删除、遍历这6种基本操作
。
文章目录
一、线性表的基本运算
数据结构的运算是定义在逻辑结构层次上的,而运算的具体实现是建立在存储结构上的,因此下面定义的线性表的基本运算作为逻辑结构的一部分,
每一个操作的具体实现只有在确定了线性表的存储结构之后才能完成。(以下表达是建立在位序从1开始的前提下,即书上的表达)
线性表上的基本操作如下。
1、线性表初始化:Init_ List(L)。
初始条件:表L不存在。
操作结果:构造一个空的线性表否则为一个空地址
2、求线性表的长度:Length_ List(L)。
初始条件:表L存在。
操作结果:返回线性表中所含元素的个数。
3、取表元;Get_ List(L,i).
初始条件:表L存在且1≤i≤Length_List(L)。
操作结果:返回线性表L中第i个元素的值或地址。
4、按值查找:Locate_ List(L,x)
x是给定的一个数据元素。
初始条件:线性表L存在。
操作结果:在表L中查找值为x的数据元素,其结果返回在L中首次出现的值为r的
那个元素的序号或地址,称为查找成功;否则,在L中未找到值为x的数据元素,返回一特殊值表示查找失败。
5、插入操作:Insert. List(L,i,x)。
初始条件:线性表L存在,插入位置正确(1≤i<≤n+1,n为插入前的表长)。
操作结果:在线性表L的第i个位置上插入一个值为x的新元素,这样使原序号为i,i+
1…n的数据元素的序号变为i+1.+2…n+1.插入后表长=原表长+1。
6、删除操作:Delete_ List(L,i)。
初始条件:线性表L存在,1≤i≤n。
操作结果:在线性表L中删除序号为i的数据元素,删除后使序号为i+1.i+2…n
的元素变为序号为i.i+1…n- 1,新表长=原表长一1。
提示:下面顺序表只展示其中部分运算操作
二、顺序表是结构定义
即表的性质,也是表的组成部分。有了结构定义,才有具体的结构操作
有如下三个定义,在实际使用过程,用了结构体封装
*data | 数据元素 |
---|---|
size | 容量大小 |
length | 表的长度 |
代码演示
typedef int Type; //方便更该数据类型
typedef struct Vector {
Type *data; //定义数据指针而非数组
int size, length;
}Vec;
解释
:这边指针的形式定义数组,即实现一个长度可以改变动态顺序表,而非一个固定长度的静态顺序表
其中使用了两个typedef
第一个:可以快速更改表的数据类型
第二个:使结构体类型名更改为Vec ,使用时直接用Vec定义结构体变量,不懂看着,Vec代表的就是一个顺序表,
三、顺序表是结构操作
(本篇讲解都是序号从0开始,n-1结尾,与数组相同)
1、结构体初始化(动态开辟空间)
初始条件:表L不存在。
操作结果:构造一个空的线性表或一个空指针
代码如下:
Vec *init (int n) {
Vec *v = (Vec *)malloc(sizeof(Vec));
v->data = (Type *)malloc(sizeof(Type) * n);
v->size = n;
v->length = 0;
return v;
}
解释:
Vec *init (int n):初始化具有n个元素的结构体,并返回指向该结构体的首指针
malloc使用
1、malloc开辟一个该结构体类型的空间,成功开辟 返回指向空间首地址指针
2、malloc 前面的括号部分可以理解为强调了申请空间的类型。
3、赋给该顺序表结构类型变量 v
在使用动态分配时,一定要先申请空间才能使用,因为如果没有申请空间,它仅仅是一块地址,而没用所需要的空间。
malloc用法
拓展——静态开辟空间
:
#define SIZE 100 //存储空间初始分配量
typedef int Type;
typedef struct Vector {
Type data[size]; //定义数组-存储数据
int length;
}Vec;
2、释放结构体空间
代码如下:
void clear (Vec *v) {
if(v == NULL) return ;
free (v->data);
free(v);
return ;
}
注意: 必须先释放数据,再释放内存,否则会造成内存泄露
3、插入
初始条件: 1、线性表L存在;2、插入位置正确(0≤i<≤n,n为插入前的表长即length);
3、 空间未满。
操作结果:在线性表L的第i个位置上插入一个值为x的新元素,这样使原序号为i,i+1…n-1的数据元素的序号变为i+1.+2…n
。插入后表长=原表长+1。(本篇序号从0开始,n-1结尾,与数组同)
代码如下:
int insert(Vec *v, int ind,Type val) {
if (v == NULL) return 0;
if (ind < 0 || ind > v->length) return 0;
if (v->length == v->size) return 0;
for (int i = v->length ;i > ind; i--){
v->data[i] = v->data[i-1];
}
v->data[ind] = val;
v->length += 1;
return 1;
}
解释:
v:被插入的列表 ,ind :表示插入的位序,val:表示插入的元素
v->data[i] = v->data[i-1]:防止后一位被前一位元素覆盖
4、删除
初始条件:1、线性表L存在;2、删除位置正确( 0≤i≤n-1)。
操作结果:在线性表L中删除序号为i的数据元素,删除后使序号为i+1.i+2…n-1
的元素变为序号为i.i+1…n- 2,新表长=原表长一1。(本篇序号从0开始,n-1结尾,与数组同)
代码如下:
int erase (Vec *v, int ind){
if (v == NULL) return 0;
if (ind < 0 || ind >=v->length) return 0;
for (int i = ind + 1; i< v->length ;i++ ) {
v->data[i-1] = v->data[i];
}
v->length -= 1;
return 1;
}
```c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef int Type; //方便更该数据类型
typedef struct Vector {
Type *data; //定义数据指针而非数组
int size, length;
}Vec;
Vec *init (int n) {
Vec *v = (Vec *)malloc(sizeof(Vec));
v->data = (Type *)malloc(sizeof(Type) * n);
v->size = n;
v->length = 0;
return v;
}
/*释放内存 先释放数据再释放内存*/
void clear (Vec *v) {
if(v == NULL) return ;
free (v->data);
free(v);
return ;
}
int insert(Vec *v, int ind,Type val) {
if (v == NULL) return 0;
if (ind < 0 || ind > v->length) return 0;
if (v->length == v->size) return 0;
for (int i = v->length ;i > ind; i--){
v->data[i] = v->data[i-1];
}
v->data[ind] = val;
v->length += 1;
return 1;
}
int erase (Vec *v, int ind){
if (v == NULL) return 0;
if (ind < 0 || ind >=v->length) return 0;
for (int i = ind + 1; i< v->length ;i++ ) {
v->data[i-1] = v->data[i];
}
v->length -= 1;
return 1;
}
void output(Vec *v){
if(v == NULL) return;
printf("Vetor(%d):[",v->length);
for (int i = 0; i < v->length; i++) {
i && printf(", "); //表示非常优秀
printf("%d",v->data[i]);
}
printf("]\n");
return;
}
int main() {
#define MAX_N 20
Vec *v = init(MAX_N + 5);
srand(time(0));
for (int i = 0; i < MAX_N; i++) {
int op = rand()%4;
int ind = rand()%(v->length + 1);
int val = rand()%100;
switch(op) {
case 0:
case 1:
case 2: {
printf("insert %d at %d to Vector = %d\n", val , ind , insert(v,ind,val));
}break;
case 3: {
printf("erase a iterm at %d from Vetor = %d\n",ind , erase(v,ind));
}break;
}
printf("%dst: ",i+1);
output(v), printf("\n");
}
#undef MAX_N //good code habit for muiti_file compilation
clear(v);
return 0;
}
结果展示
四、问题与思考
1、顺序表的位序是从0开始还是从1 开始,为什么?
答:绝大多数回答是从1开始,因为教科书上是这么写的,但是我们为了优化算法,减少那一步减法操作,本文从位序0开始,也不用与数组划分开来
数组 | a[k] 的内存地址公式: |
---|---|
从 0 开始 | a[k]_address = base_address + k * type_size |
从 1 开始 | a[k]_address = base_address + (k-1)*type_size |
顺序表 | a[k] 的内存地址公式: (d表示存储单元大小 ) |
从 0 开始 | Loc(ai) = Loc(a0) + d*i |
从 1 开始 | Loc(ai) = Loc(a1) + d*(i-1) |
对比表的四个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说, 就是多了一次减法指令。数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操 作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始,同理顺序表以数组的形式存储,我们也可采用同样的方式
2、顺序表元素内置与外置区别?
答:说白了,元素外置,该位置上存放的是指向该元素指针或地址,而非元素本身,有点类似线性标的链式存储,但两者有本质的不同,具体下节分析
3、扩容该如何操作
扩容操作
初始条件:1.表的长度与开辟即看空间大小相等(存不下多余元素);2、 只能是插入操作
操作结果:成功开辟一块新的空间,返回性开辟空间的指针否则返回空指针。一般采用realloc 方式,解释一下重新分配(开辟)按以下二者之一执行含义
扩张 | 在开辟的原空间接上一块地址,若不行,执行重分配,(自动执行,不用自己编程序) |
---|---|
重分配 | 原空间后无法再开辟一块新空间,即需要重新分配一块大地址,复制原空间内容并释放原空间 |
成功时,返回指向新分配内存的指针,原指针(个人代码中 data) 被非法化
失败时,返回空指针。原指针 data保持有效
本篇不展示扩容操作,展示需要改变其它相关代码
总结
-
顺序表的结构操作过程中最重要的判断:纠结顺序表位序元素是从位序0还是从位序1开始并没意义,两者都可实现,具体原因上面思考有分析;从0开始目的只是为减少其中的一步减法运算,本篇顺序表采用0位序(0 — n-1)
-
插入步骤:
1、插入位后所有数据向后移,为避免数据覆盖,赋值顺序从末位到插入位的后一位
2、将插入值付给插入的位置
3、数据长度+1
举一反三:删除就是插入的逆反,程序也是 从删除为开始:数据向前移动,为避免覆盖,赋值顺序重插入位后一位到末位 数据长度-1 -
释放内存,必须先释放数据,再释放内存,否则会造成内存泄露
-
易错点:指针指代问题
参考资料
—————————
顺序表的定义(静态与动态实现顺序表)
列表的下标为什么是从0开始而不是1?(顺序表)