1 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2 顺序表
1 概念:
顺序表是⽤⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采⽤数组 存储。
顺序表的底层结构是数组,对数组的封装,实现了常⽤的增删改查等接⼝
2 分类
顺序表的底层是数组,数组又分为两类
(1)编译时就确定的数组的大小 int arr[N]
(2)编译时不确定数组的大小,使用时才申请空间---->int* arr 动态申请空间(malloc,calloc,realloc)
<1>静态顺序表
定义:使用定长数组存储元素
定长数组:
1)主要性质:
长度固定:定长数组在初始化的时候就必须明确其大小,之后无法对这个大小进行调整。
内存连续:数组里的所有元素在内存中是连续存放的,这使得通过索引能够快速地访问元素。
类型一致:数组中的每个元素都必须是同一种数据类型,像整数、浮点数等。
2)优点:
高效访问:由于元素在内存中是连续存储的,所以可以通过索引直接访问任意元素,访问效率很高。
内存占用少:相较于动态数组,定长数组不需要额外的空间来管理数组的容量变化,内存利用率更高。
3)缺点
缺乏灵活性:一旦数组的长度确定,就不能再进行扩展或收缩操作。如果需要改变数组的长度,只能重新创建一个新的数组。
容易造成空间浪费:如果数组预留的空间过大,而实际使用的元素较少,就会导致内存空间的浪费;反之,如果预留空间不足,又会无法存储更多的元素。
静态顺序表缺陷:空间给少了不够⽤,给多了造成空间浪费。
如果想往数组里插入数据,就得知道数组里的有效数据个数 int size,以及他的空间大小,此时就能知道现在是否能往里面插入数据。
使用结构的原因:要在一个结构里保存两个不同的变量(int arr[N] int size)
size指向有效数据后的下一个位置
<2>动态顺序表
3 动态顺序表的实现
(1)项目结构
头文件:.h文件 只做结构的定义和函数的申明。能有效的管理项目(相当于目录)。
.c文件:包含头文件“include xx.h"实现对头文件函数的定义
test.c文件:测试文件(对前面写的方法的测试)
(2)定义
此处的顺序表指定的存储数据的的结构为 int*,意味着只能存储整型但是数组还能存储字符等类据,此时需要给数组取别名。
//定义动态顺序表的结构
struct SeqList
{
int* arr; //存储数据
int size; //有效数据个数
int capacity; //空间大小
};
这样做的目的:如果整个.c文件一共有三千个int ,其中有一千个为数组的int,若要将顺序表中的数据类型(数组)改成char,则不好修改。若顺序表中的数据类型用SLTDataType替换,则便于修改(将 typedef int SLTDataType直接修改为 typedef char SLTDataType),且不会影响其他地方的int。
//定义动态顺序表的结构
typedef int SLTDataType;
struct SeqList
{
SLTDataType* arr; //存储数据
int size; //有效数据个数
int capacity; //空间大小
};
两种给结构体取别名的方法:
(1)
//定义动态顺序表的结构
typedef int SLTDataType;
struct SeqList
{
SLTDataType* arr; //存储数据
int size; //有效数据个数
int capacity; //空间大小
};
typedef struct SeqList SL;//在结构体的后面取别名
(2)
//定义动态顺序表的结构
typedef int SLTDataType;
typedef struct SeqList
{
SLTDataType* arr; //存储数据
int size; //有效数据个数
int capacity; //空间大小
}SL;
(3)初始化
顺序表的三个成员需要初始化。
初始化函数:void SLInit(SL s);(在.h文件中进行初始化)
在.c文件中实现声明的方法:需要先包含头文件#include"SeqList.h",否则无法找到SL。(SL为上方对结构起的别名)。
#include"SEqList.h"
void SLInit(SL s)
{
s.arr=NULL:
s.size=s.capacity=0;
}
用test.c测试初始化是否写对:
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(sl);
}
int main()
{
test01();
return 0;
}
运行之后出错,因为此时test函数中的sl是实参,而SILnit 函数中的s是形参,形参给实参传值时,形参的改变不影响实参,所以需要传地址,此时形参的改变影响实参。
除了数组之外(数组名就是地址),要用取地址操作符 & 取地址。把实参的地址给形参,此时形参指向这个地址,要用一级指针接收变量的地址。一级指针的解引用要用箭头,此时.c文件代码变为
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
此时运行没有问题。
除了部分情况外(数组),有取地址操作符才叫变量传地址。
(4)插入数据
往顺序表里插入数据,就相当于往数组里插入数据。(注意,插入的数据类型要和顺序表的类型相统一)
往数组里插入数据,有两种情况:尾插法和头插法。
1尾插法
//尾插
void SLPushBack(SL* ps, SLTDataType x)
size指向数组有效数据的下一个位置.
尾插分为两种:1顺序表里空间足够 2顺序表内空间不够
1)空间足够
void SILPushBack(SL*ps, SILDataType x)
{
ps->arr[ps->size] = x;//size指向最后一个有效数据的下一个位置,直接将插入的数据x放到下标为size的位置
++ps->size;//插入数据之后,size++指向下一个位置
}
这里的++可以写成后置++(因为是先赋值后++)
void SLPushBack(SL* ps, SLTDataType x)
{
ps->arr[ps->size++] = x;
}
2 空间不够
当size和capacity相等时,说明空间不够此时要对数组增加容量,也就是增容。
增容:一般成倍数增加,两倍,三倍…,而不是插入一个数据,就要扩充一个空间。(插入的数据越多,则申请的空间越来越大)
增容要动态申请空间:malloc (申请连续的空间)calloc(初始化+申请连续空间) realloc(在已有空间的基础上,增加容量)。
增容的底层原理:连续空间够直接增容,如果不够:1)申请大的空间 2)拷贝旧数据 3)释放空间
//尾插
void SLPushBack(SL* ps, SLTDataType x)
{
//空间不够
if (ps->size == ps->capacity)
{
//增容
ps->arr = (SLTDataType*)realloc(ps->arr, 2*ps->Capacity);
}
//空间足够
ps->arr[ps->size++] = x;
}
但是此时的增容代码有问题 如果realloc增容失败,那么会返回NULL给arr,此时arr的内容会被覆盖。
所以需要一个中间变量tmp存储realloc返回的值,并判断是否为NULL。
若capacity初始化值为0,则需要为他赋值,否则增容之后空间大小仍为0。
//尾插
void SLPushBack(SL* ps, SLTDataType x)
{
//空间不够
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//增容
SLTDataType* tmp = (SLTDataType*)realloc(ps->arr, newCapacity*sizeof(SLTDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
//空间足够
ps->arr[ps->size++] = x;
}
2 头插法
1)空间足够
void SLPushFront(SL* ps, SLTDataType x)
{
//温柔的处理方式
if (ps == NULL)
{
return;
}
}
void SLPushFront(SL* ps, SLTDataType x)
{
//不温柔的方式
assert(ps != NULL); //等价于assert(ps)
}
2)空间不够
需要增容,并将数据整体向后挪动一位 因为还要用到增容的部分,所以写一个整容的函数节约时间
增容函数:
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//增容
SLTDataType* tmp = (SLTDataType*)realloc(ps->arr, newCapacity * sizeof(SLTDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
头插函数:
//头插
void SLPushFront(SL* ps, SLTDataType x)
{
//温柔的处理方式
//if (ps == NULL)
//{
// return;
//}
assert(ps != NULL); //等价于assert(ps)
//空间不够
SLCheckCapacity(ps);
//数据整体向后挪动一位
for (int i = ps->size; i > 0 ; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
(5)删除数据
1)尾删法
删除前提:顺序表不能为空
//尾删
void SLPopBack(SL* ps)
{
assert(ps && ps->size);
ps->size--;
}
因为size是指有效数据的个数,当size — —时,有效数据的个数也减一,此时无论后面的空间是否有数据,都当作无效的。
2)头删法
//头删
void SLPopFront(SL* ps)
{
assert(ps && ps->size);
//数据整体向前挪动一位
for (int i = 0; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
(6)查找
//查找
int SLFind(SL* ps, SLTDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
//找到了
return i;
}
}
//未找到
return -1;
}
(7)指定位置之前插入
//指定位置之前插⼊
void SLInsert(SL* ps, int pos, SLTDataType x)
{
assert(ps);
//0<= pos < ps->size
assert(pos >= 0 && pos < ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
//pos及之后数据向后挪动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
(8)删除pos位置数据
/删除pos位置的数据
void SLErase(SL* ps, int pos)
{
assert(ps);
//pos:[0,ps->size)
assert(pos >= 0 && pos < ps->size);
//pos后面的数据向前挪动一位
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
(9)销毁
//销毁
void SLDestroy(SL*ps)
{
if(ps->arr)
free(ps->arr);
ps->arr = NULL:
ps->size = ps->capacity = 0;
}
如上已实现动态顺序表,完整的代码如下
#include"SeqList.h"
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
void SLDestroy(SL* ps)
{
if(ps->arr)
free(ps->arr);
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//增容
SLTDataType* tmp = (SLTDataType*)realloc(ps->arr, newCapacity * sizeof(SLTDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
//尾插
void SLPushBack(SL* ps, SLTDataType x)
{
assert(ps);
//空间不够
SLCheckCapacity(ps);
//空间足够
ps->arr[ps->size++] = x;
}
//头插
void SLPushFront(SL* ps, SLTDataType x)
{
//温柔的处理方式
//if (ps == NULL)
//{
// return;
//}
assert(ps != NULL); //等价于assert(ps)
//空间不够
SLCheckCapacity(ps);
//数据整体向后挪动一位
for (int i = ps->size; i > 0 ; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
//尾删
void SLPopBack(SL* ps)
{
assert(ps && ps->size);
ps->size--;
}
//头删
void SLPopFront(SL* ps)
{
assert(ps && ps->size);
//数据整体向前挪动一位
for (int i = 0; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
//查找
int SLFind(SL* ps, SLTDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
//找到了
return i;
}
}
//未找到
return -1;
}
//指定位置之前插⼊
void SLInsert(SL* ps, int pos, SLTDataType x)
{
assert(ps);
//0<= pos < ps->size
assert(pos >= 0 && pos < ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
//pos及之后数据向后挪动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
//删除pos位置的数据
void SLErase(SL* ps, int pos)
{
assert(ps);
//pos:[0,ps->size)
assert(pos >= 0 && pos < ps->size);
//pos后面的数据向前挪动一位
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}