线性表:
线性表是n个具有相同特性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表,链表,栈,队列,字符串....
线性表在逻辑上是线性结构,也就是说是连续的一条直线,但是在物理结构上并不一定是连续的,线性表在屋里上存储时,通常以数组和链式结构的形式存储。
首先,用“一根线儿”把它们按照顺序“串”起来,
左侧是“串”起来的数据,右侧是空闲的物理空间。把这“一串儿”数据放置到物理空间,我们可以选择以下两种方式
3a) 是多数人想到的存储方式,而3b) 却少有人想到。数据存储的成功与否,取决于是否能将数据完整地复原成它本来的样子。如果把3a) 和3b) 线的一头扯起,你会发现数据的位置依旧没有发生改变。因此可以认定,这两种存储方式都是正确的。
使用线性表存储的数据,如同向数组中存储数据那样,要求数据类型必须一致,线性表存储的数据,要么全不都是整形,要么全部都是字符串。一半是整形,另一半是字符串的一组数据无法使用线性表存储。
线性表存储数据可细分为以下 2 种:
- 将数据依次存储在连续的整块物理空间中,这种存储结构称为顺序存储结构(简称顺序表);
- 数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构(简称链表);
也就是说,线性表存储结构可细分为顺序存储结构和链式存储结构。
数据结构中,一组数据中的每个个体被称为“数据元素”(简称“元素”)。对于具有“一对一”逻辑关系的数据,线性表中的术语:
- 某一元素的左侧相邻元素称为“直接前驱”,位于此元素左侧的所有元素都统称为“前驱元素”;
- 某一元素的右侧相邻元素称为“直接后继”,位于此元素右侧的所有元素都统称为“后继元素”;
数据中的元素 3 来说,它的直接前驱是 2 ,此元素的前驱元素有 2 个,分别是 1 和 2;同理,此元素的直接后继是 4 ,后继元素也有 2 个,分别是 4 和 5。
顺序表:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删检查。
顺序表就是数组,但是在数组的基础上,它还要求数据是连续存储的,不能有跳跃间隔。
顺序表的接口函数:
//SeqList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
/*
#define N 100
typedef int SLDataType
typedef struct SeqList
{
SLDataType a[N];
int size;//表示数组中存储了多少个数据
}SL;
//接口函数
void seqListInit(SL* ps);//初始化
//静态特点:如果满了就不让插入 缺点:无法确定给的空间大小
//n给小了不够用,n给大了就浪费
void seqListPushBack(SL* ps,SLDataType x);
void seqListPopBack(SL* ps);
void seqListPushFront(SL* ps,SLDataType x);
void seqListPopFront(SL* ps);
//...
*/
//更改:
typedef int SLDataType
//动态顺序表
typedef struct SeqList
{
SLDataType *a;
int size; //表示数组中存储了多少个数据
int capacity; //数组实际能存数据的空间容量是多大
}SL;
//接口函数
void seqListInit(SL* ps);//初始化
void seqListDestory(SL* ps);//销毁数据表
//静态特点:如果满了就不让插入 缺点:无法确定给的空间大小
//n给小了不够用,n给大了就浪费
void seqListPushBack(SL* ps,SLDataType x);//尾插数据
void seqListPopBack(SL* ps);//顺序表尾删
void seqListPushFront(SL* ps,SLDataType x);//头插数据
void seqListPopFront(SL* ps);//顺序表头删
void seqListPrint(SL* ps);//打印顺序表
void seqListCheckCapacity(SL* ps);//检查容量
int seqListFind(SL* ps,SLDataType x);//找到了返回x位置下标,没有找到返回-1
void seqListInsert(SL* ps,int pos,SLDataType x);//在指定的pos位置插入
void seqListErase(SL* ps,int pos)://删除pos位置的数据
//SeqList.c
#include <SeqList.h>
#include <assert.h>
void seqListInit(SL* ps)//初始化
{
ps->a=NULL;//函数传参,形参是实参的拷贝,形参的改变不影响实参,所以void seqListInit(SL* ps)传的是指针
ps->size=ps->cacapacity=0;
}
void seqListPrint(SL* ps)//打印顺序表
{
for(int i=0;i<ps->size;++i)
{
printf("%d",ps->a[1]);
}
printf("\n");
}
void seqListPushBack(SL* ps,SLDataType x);//尾插一个数组
{
seqListCheckCapacity(SL* ps);
ps->a[ps->size]=x;//空间足够的情况下
ps->size++;
}
void seqListDestory(SL* ps)//销毁初始化的数据表
{
free(ps->a);
ps->a=NULL;
ps->capacity=pa->size=0;
}
void seqListPopBack(SL* ps)//尾删
{
//处理后面问题的两种方式:
/*
if(ps->size>0)//判断是否执行
{
ps->size--;
}
*/
/*
assert(ps->size>0);//断言
//assert 将通过检查表达式 expression 的值来决定是否需要终止执行程序。
//如果表达式 expression 的值为假(即为 0),那么它将首先向标准错误流 stderr 打印一条出错信息,
//然后再通过调用 abort 函数终止程序运行;
//否则,assert 无任何作用。
//ps->a[ps->size-1]=0;//把数组的最后一个元素置0;可以删除
ps->size--;
*/
seqListErase(ps,ps->size-1);
}
void seqListPushFront(SL* ps,SLDataType x)//头插数据
{
seqListCheckCapacity(SL* ps);
//挪动数据,空出最开始的位置
int end=ps->size-1;//数组从0开始
while(end>=0)
{
ps->a[end+1]=ps->a[end];
--end;
}
ps->a[o]=x;
ps->size++;
}
void seqListPopFront(SL* ps);//头删
{
assert(ps->size>0);
//挪动数据
int begin =1;
while(begin<ps->size)
{
ps->a[begin-1]=ps->a[begin];
++begin;
}
ps->size--;
}
void seqListCheckCapacity(SL* ps)//检查容量
{
//如果没有空间或者空间不足,那么我们就扩容
if(ps->size==ps->cacapacity)
{
int newcacapacity=ps->cacapacity==0?4:ps->cacapacity*2;//空间扩容为原来的2倍(几倍都可以)
SLDataType *temp=(SLDataType *)realloc(ps->a,newcacapacity*sizeof(SLDataType));
if(tmp=NULL)
{
printf("realloc fail\n");
exit(-1);//退出函数
}
ps->a=tmp;
ps->capacity=newcacapacity;
}
}
int seqListFind(SL* ps,SLDataType x)//找到了返回x位置下标,没有找到返回-1
{
for(int i; i<po->size; ++i)
{
if(ps->a[i]==x)
{
return 1;
}
}
return -1;
}
void seqListInsert(SL* ps,int pos,SLDataType x) //在指定的pos位置插入
{
/*
if(pos>ps->size ||pos<0)
{
printf("pos inwalid!\n");
return;
}
*/
assert(pos>=0&&pos<=ps->size);
seqListCheckCapacity(SL* ps);
int end =ps->size-1;
while(end>=pos)
{
ps->a[end+1]=ps->a[end];
--end;
}
ps->a[pos]=x;
ps->size++;
}
void seqListErase(SL* ps,int pos)//删除pos位置的数据
{
assert(pos>=0&&pos<ps->size);
int begin=pos+1;
while(begin<ps->size)
{
ps->a[begin-1]=ps->a[begin];
++begin;
}
}
//test.c
#include <SeqList.h>
#include <stdio.h>
void TestSeqList1()
{
SL s1;//函数传参,形参是实参的拷贝,形参的改变不影响实参,所以void seqListInit(SL* ps)传的是指针
SeqListInit(&s1);
seqListPushBack(&s1,1);
seqListPushBack(&s1,2);
seqListPushBack(&s1,3);
seqListPushBack(&s1,4);
seqListPushBack(&s1,5);
seqListPopBack(&s1);
seqListPopBack(&s1);
seqListPopBack(&s1);
seqListPopBack(&s1);
seqListPopBack(&s1);
seqListPopBack(&s1);
//这一步执行后载再插入数据就会报错;
//报错原因分析: 数据删完后size变成了负数;ps->a[ps->size]=x;指针指向了数组前面的地址;
//ps->size++;将会引用错误的数据
seqListInsert(&s1,2,30)
seqListPrint(&s1);
seqListDestory(&s1);
}
void TestSeqList2()
{
SL s1;//函数传参,形参是实参的拷贝,形参的改变不影响实参,所以void seqListInit(SL* ps)传的是指针
SeqListInit(&s1);
seqListPushBack(&s1,1);//尾插
seqListPushBack(&s1,2);
seqListPushBack(&s1,3);
seqListPushBack(&s1,4);
seqListPrint(&s1);
seqListPushFront(&s1,10);//头插
seqListPushFront(&s1,20);
seqListPushFront(&s1,30);
seqListPushFront(&s1,40);
seqListErase(&s1,4)
seqListPrint(&s1);
seqListPopFront(&s1);
seqListPopFront(&s1);
}
//写一个通讯录的菜单
void Menu()
{
printf("************************\n");
printf("请选择你的操作:>\n");
printf("1.头插 2.头删\n");
printf("3.尾插 4.尾删\n");
printf("5.打印 -1.退出\n");
//.....
printf("************************\n");
}
void Menutest()
{
Sl s1;
seqListInit(&s1);
int put=0;
int x;
while(input!=-1)
{
Menu();
scanf("%d",&input);
switch(input)
{
case 1:
printf("请输入你要头插的数据,以-1结束:");
scanf("%d",&x);
while(x!=-1)
{
seqListPushFront(&s1, x);
scanf("%d",&x);
}
break;
case 2:
seqListPopFront(&s1);
break;
case 3:
printf("请输入你要尾插的数据,以-1结束:");
scanf("%d",&x);
while(x!=-1)
{
seqListPushBack(&s1, x);
scanf("%d",&x);
}
break;
case 4:
seqListPopBack(&s1);
break;
case 5:
seqListPrint(&s1);
break;
default:
printf("无此项选择,请重新输入:")
break;
}
}
int main()
{
Menutest();
menu();
TestSeqList1();
TestSeqList2();
return 0;
}
尾插法:
会遇到的情况:
- 整个顺序表没有空间
- 空间不够(扩容)
- 空间足够
程序拆分:
SeqList结构体:
动态的顺序表,当空间满了应该增容,要判断存储的元素是否满了,应该要记录下当前顺序表可以存储多少个元素,也就是记录下当前顺序表容量。
为了顺序表还能存储其他类型的数据,最好是将数组的类型重命名,方便之后想存储其他类型数据时,能够方便修改。
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
size_t size;//已经存储的元素个数
size_t capacity;//记录容量
}SeqList;
SeqListInit(顺序表初始化):
初始初始化没有什么讲究,唯一要注意的是capacity在初始化时,可以给他初始化成0,也可以一开始就给一些空间。
void SeqListInit(SeqList* ps)
{
//检查空指针
assert(ps);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
SeqListCheckCapacity(容量检查):
在插入数据前都要判断下容量是否为满,顺序表有三种插入方式,那么为了避免代码的冗余,可以把它独立出来封装出一个函数。
当数组还是空时,realloc是相当于一次malloc。
realloc扩容有两种模式:
- 第一种是原地扩容,也就是在原来数组上增加一些空间,这是比较好的一种情况。
- 另一种情况是异地扩容,当数组后面的空间不够扩容的新空间大小时,数组会去堆区找一块能够容纳新空间大小的地方,将原来数组的数据拷贝到新空间位置,并释放掉原数组的空间,异地扩容会有一定的时间开销。
为了减少异地扩容带来的时间开销,我们应该尽量的避免频繁扩容。数组可能开大了用不完,开小了空间可能会不够用,折中考虑建议每次开二倍的空间,扩容后空间大小等于新空间大小。
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
//检查容量
if (ps->size == ps->capacity)
{
//容量不能是0
size_t newcapcity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(ps->a,sizeof(SLDateType) * newcapcity);
if (tmp == NULL)
{
//扩容失败
printf("%s\n", strerror(errno));
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapcity;
}
}
SeqListPushBack(尾插数据):
有了SeqListCheckCapacity的支撑顺序表尾插就非常方便了。顺序表的ps->size就是新空间的位置,每次尾插一个数据让size向后走一步,顺序表的队尾元素永远都在size-1位置。
void SeqListPushBack(SeqList* ps, SLDateType x)
{
assert(ps);
SeqListCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
SeqListPopBack(顺序表尾删) :
顺序表尾删也是十分方便的,先保证顺序表要有数据,然后直接让size--,最后一个元素就被删除掉了。当在插入一个元素会直接把那个位置的数据给覆盖掉。那要问那个被删除的位置还在那吗?答案是肯定的,它还在那个位置,但是他已经不是一个有效数据的范围了。被删除的位置无需置成某一个值,虽然可以,但没必要。假如将它置成0,如果我再插入一个新的元素,插入的元素就是0呢?那置0是不是就毫无意义,还不如就一个size--省事。
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
ps->size--;
}
SeqListPushFront(顺序表头插):
因为是插入数据,所以判断数据是否要扩容也是必要的。顺序表的头插是比较麻烦的,因为数组是连续的(成也萧何败萧何),要头插数据就要把所有元素都先后移一位,为头插的数据腾出位置之后,才能头插,那么它挪动数据的时间复杂度是O(N)。挪动数据必须是从后往前挪,否则前面的数据会将顺序表的数据全部覆盖。
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
SeqListCheckCapacity(ps);
size_t end = ps->size;
while (end > 0)
{
ps->a[end] = ps->a[end-1];
end--;
}
ps->a[0] = x;
ps->size++;
}
SeqListPopFront(顺序表头删)
删除数据首先数据不能为空,与头插一样,头删依旧需要移动数据。头删时只要让后一个数据覆盖到前一个数据的位置就完成头删了,挪动的次数除去第一个数据不需要挪动,也就是要挪动N-1次,那么除去常数项,顺序表头删的时间复杂度就是O(N)。与头插不同的是,如果还是从后往前挪数据,后面的数据会将前面的数据全部覆盖。
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
size_t end = 0;
while (end < ps->size)
{
ps->a[end] = ps->a[end + 1];
end++;
}
ps->size--;
}
SeqListFind(查找数据):
查找一个数据找他的下标,要找一个数据就要一个一个数据的去比较。最坏的情况就是找不到这个数据,也有可能这个数据在最后一个位置,因为在遍历顺序表,所以Find的时间复杂度就是O(N)。
int SeqListFind(SeqList* ps, SLDateType x)
{
for (int i = 0;i < ps->size;i++)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
SeqListInsert(pos的位置插入元素):
insert通常配合find使用,但是不排除直接使用的,所以最好还是判断一下pos是否在顺序表数据的合法范围内。插入数据与头插时一样,需要将在pos位置之后的数据都后移一位。
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDateType x)
{
assert(ps);
if (pos > ps->size)
{
printf("pos 越界\n");
return;
}
SeqListCheckCapacity(ps);
size_t end = ps->size;
while (end > pos)
{
ps->a[end] = ps->a[end - 1];
end--;
}
ps->a[end] = x;
ps->size++;
}
Insert也可以被头插和尾插复用,省时省力~~
void SeqListPushBack(SeqList* ps, SLDateType x)
{
//assert(ps);
//SeqListCheckCapacity(ps);
//ps->a[ps->size] = x;
//ps->size++;
SeqListInsert(ps, ps->size, x);
}
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
//SeqListCheckCapacity(ps);
//size_t end = ps->size;
//while (end > 0)
//{
// ps->a[end] = ps->a[end-1];
// end--;
//}
//ps->a[0] = x;
//ps->size++;
SeqListInsert(ps, 0, x);
}
SeqListErase(删除pos位置):
为了防止pos越界应当判断下pos是否合法。当要删除pos位置同样需要挪动数据,让pos位置开始,依次将后面的数据依次覆盖,Erase同样可以被尾头删复用。
void SeqListErase(SeqList* ps, size_t pos)
{
assert(ps);
assert(ps->size > 0);
if (pos > ps->size)
{
printf("pos 越界\n");
return;
}
if (ps->size > 0)
{
for (size_t i = pos;i < ps->size;i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
}
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
//size_t end = 0;
//while (end < ps->size)
//{
// ps->a[end] = ps->a[end + 1];
// end++;
//}
//ps->size--;
SeqListErase(ps, 0);
}
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
//ps->size--;
SeqListErase(ps, ps->size-1);
}
SeqListDestory(释放空间):
在堆区开辟的空间不用了,记得还给操作系统。
void SeqListDestory(SeqList* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
顺序表优点和缺点:
优点:
顺序表优点是地址连续,方便排序,很多排序算法都是基于数组地址的连续。因为地址连续,所以顺序表支持下标的随机访问,可以在任意合法下标内随机访问数据。
缺点:
一、因为顺序表地址连续,在进行头插头删和pos位置插入数据时都要挪动数据。头插头删时间复杂度是O(N),pos位置删除插入要挪动N - (pos+1)次,但pos也可能是0的位置,也就是头删,所以pos位置删除插入的时间复杂度也是O(N)。
二、顺序表都会有空间浪费,无论是静态顺序表还是动态顺序表都无法避免空间浪费的问题,只不过动态顺序表比静态顺序表要更加灵活一点。