顺序表和链表
- 1.线性表
- 2.顺序表
- 2.1 概念和结构
- 2.2 接口实现
- 2.3 顺序表的问题及思考
- 3.链表
- 3.1 链表的概念和结构
- 3.2 链表的分类
- 3.3 链表的实现
- 3.4 双向链表的实现
- 4. 顺序表和链表的区别和联系
1.线性表
线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构
线性表在逻辑上是线性结构,连续的一条线。
物理结构上并不一定是连续的。
线性表在物理上存储时,一般以数组和链式结构的形式存储
常见线性表:顺序表,链表
顺序表
无头链表
2.顺序表
2.1 概念和结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构。
一般情况下采用数组存储元素,便于完成数据的增删查改。
顺序表分为两种:静态顺序表,动态顺序表
1.静态顺序表:使用规定长度的数组存储元素
数组长度不能进行改变
2.动态顺序表:使用动态开辟的数组存储元素
数组空间不够,可以进行扩容
默认扩容2倍,扩容一次扩多,浪费;扩少,频繁扩容,影响效率
2.2 接口实现
接口就是规定要程序做什么,但不在其中实现
动态顺序表的实现
定义数据类型和结构体
typedef int LSdatatype;
typedef struct List
{
LSdatatype* a;
int count;
int capacity;
}LS;
初始化顺序表
必须通过指针才能改变结构体的内容,所有接口都需要传址,而不是传值
//初始化顺序表
void LSinit(LS* ps);
void LSinit(LS* ps)
{
//需要进行判断,如果是空指针直接结束程序
assert(ps);
ps->a = NULL;
ps->count = 0;
ps->capacity = 0;
}
尾插
在进行尾插时,需要考虑内存是否充足,否则就会出现问题。由于在整个程序中,还有其他功能需要判断内存是否充足,所有便将其独立为函数。
void Checkcapacity(LS* ps)
{
//检查容量
if (ps->count == ps->capacity)
{
int newcapacity = ps->capacity;
newcapacity == 0 ? 4 : 2 * ps->capacity;
//为了避免内存开辟失败而将指针置为空,便创建临时变量tmp
LSdatatype* tmp = (LSdatatype*)realloc(ps->a, newcapacity * sizeof(int));
if (tmp == NULL)
{
perror("realloc");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapacity;
}
}
对于realloc
函数一般的理解是扩容,但这里就直接拿来开辟内存,是否由问题呢
答案是:没有问题
再一次地仔细地观察realloc
函数的定义
如果返回值为空指针,则 realloc
函数与 malloc
函数类似
所有以后如果遇到类似的情况也不妨使用 realloc
函数,更加的高效。
//尾插
void LSpushback(LS* ps, LSdatatype x);
void LSpushback(LS* ps, LSdatatype x)
{
assert(ps);
//判断容量
Checkcapacity(ps);
ps->a[ps->count] = x;
ps->count++;
}
头插
//头插
void LSpushfront(LS* ps, LSdatatype x);
void LSpushfront(LS* ps, LSdatatype x)
{
assert(ps);
//检查容量
Checkcapacity(ps);
int end = ps->count - 1;
//挪动数据,从后往前挪
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->count++;
}
尾删
//尾删 --最简单直接将指针ps->a向前移动
void LSpopback(LS* ps);
void LSpopback(LS* ps, LSdatatype x)
{
assert(ps);
//温柔的检查
if (ps->count == 0)
{
return;
}
暴力的检查
//assert(ps->count > 0);
ps->count--;
}
如果数据都已经被删完,还继续删除数据的话,便会使内存崩溃,所有需要进行检查,有两个检查方式:温柔和暴力。
查找顺序表中的数据
//查找顺序表中的数据 找不到返回-1
int LSfind(LS* ps, LSdatatype* x);
int LSfind(LS* ps, LSdatatype* x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->count; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
插入指定数据
//插入指定数据
void LSinsert(LS* ps, size_t pos, LSdatatype* x);
void LSinsert(LS* ps, size_t pos, LSdatatype* x)
{
assert(ps);
//相等时表示尾插
assert(pos<=ps->count)
Checkcapacity(ps);
size_t end = ps->count;
while (pos < end)
{
ps->a[end] = ps->a[end-1];
end--;
}
ps->a[pos] = x;
ps->count++;
}
由于pos
和end
的类型不同,循环条件的不同,可能会造成程序死循环
循环条件为 pos <= end
时
这里虽然end 的 数值是负数,但在与pos 进行比较时,会转化为无符号整形,一个相当大的数值,程序便会进入死循环。
删除数据
//删除数据
void LSerase(LS* ps, size_t pos);
void LSerase(LS* ps, size_t pos)
{
assert(ps);
assert(pos < ps->count - 1);
size_t begin = pos;
while (begin < ps->count - 1)
{
ps->a[begin] = ps->a[begin + 1];
begin++;
}
ps->count--;
}
//修改数据
void LSmodify(LS* ps, size_t pos, LSdatatype* x);
void LSmodify(LS* ps, size_t pos, LSdatatype* x)
{
assert(ps);
assert(pos < ps->count);
ps->a[pos] = x;
}
//销毁顺序表
//既然申请空间,在程序结束时便需要销毁空间
void LSdestory(LS* ps);
void LSdestory(LS* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->count = 0;
}
2.3 顺序表的问题及思考
- 头部,中间的数据插入或删除,需要挪动数据,时间复杂度为O(N)
- 增容需要开辟空间,有可能是原地增容,也有可能是异地增容。如果是异地增容需要拷贝数据,释放旧空间,消耗时间
- 即使是2倍扩容,也会存在一定的空间浪费
解决以上问题,就需要引出下面的链表
3.链表
3.1 链表的概念和结构
概念:链表是一种物理连续存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
逻辑结构
物理结构
- 链式结构在逻辑上是连续的,在物理上却不是
- 结点一般是从堆上申请的
- 从堆上申请的空间可能会连续
3.2 链表的分类
实际中链表的分类的有很多,这里只介绍两类:单向不带头,双向带头
单向不带头:结构简单,一般不会单独用来存储数据。更多的是作为其他数据结构的子结构,例如哈希桶
双向带头:结构最复杂,一般用来单独存储数据。
3.3 链表的实现
单向不带头
定义类型和结构体
typedef int LSdatatype;
typedef struct Slist
{
LSdatatype data;
struct Slist* next;
}SL;
//打印单链表
void SLprint(SL* phead);
void SLprint(SL* phead)
{
SL* tmp = phead;
while (tmp != NULL)
{
printf("%d->", tmp->data);
tmp = tmp->next;
}
printf("NULL\n");
}
//销毁单链表
void SLdestory(SL* phead);
void SLdestory(SL** pphead)
{
assert(pphead);
SL* cur = *pphead;
while (cur != NULL)
{
SL* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
由于每个节点中都保存下一个节点的地址,不能直接释放*pphead
,需要创建临时变量进行替换。
插入数据,便需要创建一个新的节点,由于新节点的创建不止出现一次,为了方便,将其独立为函数
SL* CreateSLnode(LSdatatype x)
{
SL* newnode = (SL*)malloc(sizeof(SL));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//头插
void SLpushfront(SL** pphead, LSdatatype x);
void SLpushfront(SL** pphead, LSdatatype x)
{
SL* newnode = CreateSLnode(x);
newnode->next = *pphead;
*pphead = newnode;
}
注意
改变数据,需要通过指针;改变指针,需要通过指针的指针。
由于上面是需要改变的指针,所有需要通过二级指针进行修改
在之后的学习中可以通过两种方式代替二级指针
- 返回新的链表头
- 设计为带哨兵位的链表
//尾插
void SLpushback(SL** pphead, LSdatatype x);
void SLpushback(SL** pphead, LSdatatype x)
{
assert(pphead);
SL* newnode = CreateSLnode(x);
//1.plist 为空 改变结构体指针
if (*pphead == NULL)
{
*pphead = newnode;
}
//2.plist 不为空 改变结构体内容
else
{
//找尾
SL* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
链表不为空,插入第一个节点,改变SL*
,通过结构体指针的指针SL*pphead
链表为空,插入第一个节点,改变SL
,通过结构体指针SL*tail
//头删
void SLpopfront(SL** pphead);
void SLpopfront(SL** pphead)
{
assert(pphead);
SL* del = *pphead;
//检查,避免数据删除完之后,继续删除数据,导致内存崩溃
//1 温柔的检查
while (*pphead == NULL)
{
return;
}
//暴力检查
/*assert(*pphead != NULL);*/
*pphead = (*pphead)->next;
free(del);
del = NULL;
}
如果直接将第一个节点删去,就不能找到第二个节点,所以创建临时变量del
保存第一个节点,之后再将其删去
//尾删
void SLpopback(SL** pphead);
void SLpopback(SL** pphead)
{
assert(pphead);
//1 温柔的检查
while (*pphead == NULL)
{
return;
}
//暴力检查
/*assert(*pphead != NULL);*/
//1 一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//2 多个节点
else
{
方法1
//SL* tail = *pphead;
//while (tail->next->next != NULL)
//{
// tail = tail->next;
//}
//free(tail->next);
//tail->next = NULL;
//方法2
//找尾
SL* tail = *pphead;
SL* pre = NULL;
while (tail->next != NULL)
{
pre = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
pre->next = NULL;
}
}
//查找节点
SL* SLfind(SL* phead, LSdatatype x);
SL* SLfind(SL* phead, LSdatatype x)
{
assert(phead);
SL* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos之前插入新节点
void SLinsert(SL** pphead, SL* pos, LSdatatype x);
void SListInsert(SL** pphead, SL* pos, LSdatatype x)
{
assert(pphead);
assert(pos);
//pos在第一个节点
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SL* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
// 暴力检查,pos不在链表中,或者pos的值是错误的
assert(prev);
}
SL* newnode = CreateSLnode(x);
prev->next = newnode;
newnode->next = pos;
}
}
//在pos后面插入新节点
void SLinsertafter(SL* pos, LSdatatype x);
void SLinsertafter(SL* pos, LSdatatype x)
{
assert(pos);
SL* newnode = CreateSLnode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos位置
void SLerase(SL* pphead, SL* pos);
void SLerase(SL** pphead, SL* pos)
{
assert(pphead);
assert(pos);
//pos是第一个节点
if (*pphead == pos)
{
SLpopfront(pphead);
}
else
{
SL* tmp = *pphead;
while (tmp->next != pos)
{
tmp = tmp->next;
}
tmp->next = pos->next;
free(pos);
//不需要将pos置空,改变pos不会改变链表
//pos=NULL
}
}
//删除pos后面的位置
void SLeraseafter(SL* pos);
void SLeraseafter(SL* pos)
{
assert(pos);
if(pos->next != NULL)
{
SL* next = pos->next;
next->next = pos->next;
free(pos);
}
else
{
return;
}
}
3.4 双向链表的实现
带头双向循环链表增删查改实现
定义类型和结构体
typedef int LTdatatype;
typedef struct LTlistnode
{
struct LTlistnode* prev;
struct LTlistnode* next;
LTdatatype data;
}LTnode;
链表初始化
//链表初始化
LTnode* LTnodeinit();
LTnode* LTnodeinit()
{
LTnode* guard = (LTnode*)malloc(sizeof(LTnode));
if (guard == NULL)
{
perror("LTnodeinit fail");
return;
}
guard->next = guard;
guard->prev = guard;
return guard;
}
链表尾插
与单链表类似,插入数据,创建一个新的节点,由于新节点的创建不止出现一次,为了方便,将其独立为函数
LTnode* Buynewnode(LTdatatype x)
{
LTnode* newnode = (LTnode*)malloc(sizeof(LTnode));
if (newnode == NULL)
{
perror("Buynewnode fail");
return;
}
newnode->prev = NULL;
newnode->next = NULL;
newnode->data = x;
return newnode;
}
//尾插
void LTnodepushback(LTnode* phead,LTdatatype x);
void LTnodepushback(LTnode* phead,LTdatatype x)
{
assert(phead);
LTnode* newnode = Buynewnode(x);
LTnode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
头插
//头插
void LTnodepushfront(LTnode* phead, LTdatatype x);
void LTnodepushfront(LTnode* phead, LTdatatype x)
{
assert(phead);
LTnode* newnode = Buynewnode(x);
LTnode* next = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;
}
计算链表长度
这里使用size_t
较为合适,如果使用char
类型来记录链表长度,当链表长度超过128时,便会出错
//计算链表长度
size_t LTsize(LTnode* phead);
size_t LTsize(LTnode* phead)
{
assert(phead);
LTnode* cur = phead->next;
size_t n = 0;
while (cur != phead)
{
n++;
cur = cur->next;
}
return n;
}
判断链表是否为空
//判断链表是否为空
bool LTnodeempty(LTnode* phead);
bool LTnodeempty(LTnode* phead)
{
assert(phead);
//链表为空返回1,不为空返回0
return phead->next == phead;
}
尾删
//尾删
void LTnodepopback(LTnode* phead);
void LTnodepopback(LTnode* phead)
{
assert(phead);
//链表不为空返回值为零,取反为真
assert(!LTnodeempty(phead));
LTnode* tail = phead->prev;
LTnode* prev = tail->prev;
phead->prev = prev;
prev->next = phead;
}
头删
//头删
void LTnodepopfront(LTnode* phead);
void LTnodepopfront(LTnode* phead)
{
assert(phead);
//链表不为空返回值为零,取反为真
assert(!LTnodeempty(phead));
LTnode* prev = phead->next;
LTnode* next = prev->next;
phead->next = next;
next->prev = phead;
}
链表查找
//链表查找
LTnode* LTnodefind(LTnode* phead,LTdatatype x);
LTnode* LTnodefind(LTnode* phead,LTdatatype x)
{
assert(phead);
LTnode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
链表插入,在pos前插入节点
//在pos前插入节点
void LTnodeinsert(LTnode* pos, LTdatatype x);
void LTnodeinsert(LTnode* pos, LTdatatype x)
{
assert(pos);
LTnode* prev = pos->prev;
LTnode* newnode = Buynewnode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
删除pos位置的节点
//删除节点
void LTnodeerase(LTnode* pos);
void LTnodeerase(LTnode* pos)
{
assert(pos);
LTnode* prev = pos->prev;
LTnode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
打印链表
//打印链表
void LTnodeprint(LTnode* phead);
void LTnodeprint(LTnode* phead)
{
assert(phead);
printf("phead<=>");
LTnode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
销毁链表
//销毁链表
void LTnodedestory(LTnode* phead);
void LTnodedestory(LTnode* phead)
{
assert(phead);
LTnode* cur = phead->next;
while (cur != NULL)
{
LTnode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
4. 顺序表和链表的区别和联系
不同的 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理上一定连续 | 逻辑上连续,物理上不一定连续 |
随机访问 | 支持O(1) | 不支持O(N) |
任意位置插入或删除元素 | 可能需要挪动数据,效率低 | 只需修改指针指向 |
插入 | 动态顺序表,空间不够进行扩容 | 没有容量的概念 |
应用 | 元素高效存储+频繁访问 | 任意位置插入或删除 |