1.在了解顺序表之前,我们首先要说一下线性表,顺序表也是线性表的一部分。
1.线性表 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
了解完了线性表的概念,我们下面就开始今天的主题:顺序表
2.1顺序表的概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存 储。在数组上完成数据的增删查改。
顺序表一般可以分为:
1. 静态顺序表:使用定长数组存储元素。
?2. 动态顺序表:使用动态开辟的数组存储。
?2.2 接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空 间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间 大小,所以下面我们实现动态顺序表。
顺序表的动态存储实现:
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
int size;//记录存储了多少有效数据
int capacity;//记录容量的大小
}SL;
有没有同学认为只建立一个动态数组就够了?
答案当然是否定的!
如果只建立一个动态数组,存储data的空间肯定是够了,但是你知道你存储了多少个data吗?你知道你数组目前总共是多大吗?是不是蚌埠住了?所以,我们在实现顺序表时,一共需要三种数据结构:数组a[],当前数值个数size,数组总容量capacity。?
此外,为什么要typedef一下数组类型呢?
因为今后我们在开辟数组内容的时候并不一定是int型的,如果不typedef,那么今后改的时候就会特别麻烦,要把每一个int都改为你想要的数据类型。而重定义之后则不一样,我们只需要改一下SLDateTpye就够了。
2.3顺序表的函数包括什么,即功能有哪些?
1.顺序表的初始化
2.顺序表的销毁
3.顺序表的打印
4.顺序表是否需要扩容的检查
5.头部插入:头插
6.头部删除:头删
7.尾部插入:尾插
8.尾部删除:尾删
9.在任意pos位置插入
10.在任意pos位置删除
11.查找某个值
函数声明部分:
void SLInit(SL* s);//初始化
void SLDestroy(SL* s);//销毁
void SLPrint(SL* s);//打印数据
void SLCheckCapacity(SL* ps);//判断是否需要加空间
void SLPushFront(SL* ps, SLDateType x);//头插
void SLPopFront(SL* ps);//头删
void SLpushBack(SL* ps, SLDateType x);//尾插,即在尾部插入数据
void SLPopBack(SL* ps);//尾删。
void SLInsert(SL* ps, int pos, SLDateType x);//在pos位置插入某个值
void SLErase(SL* ps, int pos);//在pos位置删除某个值
int SLFind(SL* ps, SLDateType x);//顺序表查找
2.3.1顺序表的初始化:
我们只创建了顺序表,但是里面的值都是随机的,因此初始化顺序表是很有必要的。我们把数组a的首地址先赋值为NULL即空指针,作为尾,方便后续的操作。因为此时我们有效数据size和数组容量capacity都为空,所以我们将其赋值为0;此外,我们还要对传来的s进行判断,防止再次对空指针进行操作。(注:assert()的头文件为assert.h。点击进入网站对assert了解)
void SLInit(SL* s)//初始化
{
assert(s);
s->a = NULL;
s->capacity = 0;
s->size = 0;
}
2.3.2顺序表的销毁
当我们程序结束的时候,我们要释放掉我们所开辟的空间,即销毁他们,不然会造成内存泄漏问题。
void SLDestroy(SL* s)//销毁
{
if (s->a!=NULL)//避免释放空指针
{
free(s->a);
s->a = NULL;
s->capacity = 0;
s->size = 0;
}
}
2.3.3顺序表数据的打印
我们存入了数据,但是我们能看到数据存储的样子吗?所以,我们要让数据可视化,即写一个打印数据的函数:1.打印我们的当前数组数据 2.打印当前有效数据size 3.打印当前数组容量capacity。当然,也要断言一下传来的指针,防止打印空指针。
void SLPrint(SL* s)//打印
{
assert(s);
assert(s->size != 0);
for (int i = 0; i < s->size; i++)
{
printf("%d ", s->a[i]);
}
printf("\n");
printf("size = %d\n", s->size);
printf("capacity = %d\n", s->capacity);
}
2.3.4顺序表是否需要扩容的检查
在写入数据时,顺序表是否已经开辟好了空间?还是说顺序表刚刚初始化?还是说顺序表已经放满了数据,内存不够用了?因此,我们在放入数据时,需要检查一下数组a的空间是否够用,如果不够,需要开辟。
void SLCheckCapacity(SL* ps)//判断是否需要加空间
{
if (ps->capacity == ps->size)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * newcapacity);
//判断tmp是否开辟成功
if (!tmp)//如果开辟失败
{
perror("realloc fail:");//打印错误信息
exit(-1);//返回-1,即异常退出
}
else//开辟成功,更新数据
{
ps->a = tmp;
ps->capacity = newcapacity;
}
}
}
2.3.5头部插入:头插
有时,我们需要在头部插入数据,实现数据的前置,这时候就需要用到头插。函数实现部分也很简单,在检查传来的指针和检查空间容量后,我们只需要把所有数据都向后移动一位,然后空出来第一个位置,插入数据即可。
void SLPushFront(SL* ps, SLDateType x)//头插
{
assert(ps);
SLCheckCapacity(ps);
// 挪动数据
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
2.3.6头部删除:头删
头删的实现很简单:检查指针->检查有效数字大于0->后一个数据直接覆盖前一个数据
void SLPopFront(SL* ps)//头删
{
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
2.3.7尾部插入:尾插
尾插莫过于最简单的一个了,断言->检查空间->直接在尾部size下标处插入(因为size是元素个数,size-1是当前最后一个元素的下标,那么size就是即将插入元素的下标)
void SLpushBack(SL* ps, SLDateType x)//尾插
{
assert(ps);
//检查空间是否够用
SLCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
2.3.8尾部删除:尾删
尾删,即删除最后一个元素,然后使size--,步骤依然为先断言,再操作.
void SLPopBack(SL* ps)//尾删
{
//暴力检查,防止数组a的越界访问
assert(ps->size != 0);
ps->size--;//使得有效数据位size减1
}
2.3.9在任意pos位置插入
哈哈,终于到这个了。
?
首先我们如果实现了这个函数,那么头插和尾插是不是直接可以调用我们这个pos插呢?我们只需要把头和尾的下标传给pos就行了?这样是不是更省事了?ok,我们如何实现pos插呢?首先,断言是必须的哈哈,上面已经断言无数次了。然后,判断空间是否需要扩容对不对?除此之外,我们还要判断一下传过来的pos是不是符合要求吧?如果我们数组a目前就开辟了8个空间,你传个pos=100那还得了?判断完之后,就是函数得实现了,实现部分也很简单,就是把pos位置之后得数据全部向后移动一位,再把pos处想要插入得值插入就行了!ok,下面我们上代码!
void SLInsert(SL* ps, int pos, SLDateType x)//在pos位置插入
{
assert(ps);
assert(pos >= 0);
assert(pos <= ps->size);//等于size的时候相当于尾插
//判断是否需要加空间
SLCheckCapacity(ps);
//如果为空
int end = ps->size - 1;
while (pos <= end)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
写完这个,再来看看尾插和头插:
尾插:
void SLpushBack(SL* ps, SLDateType x)//尾插
{
SLInsert(ps, ps->size, x);
}
?头插:
void SLPushFront(SL* ps, SLDateType x)//头插
{
SLInsert(ps, 0, x);
}
此时你得意的表情应该是这样的吧:
?
2.3.10在任意pos位置删除
落霞与孤鹜齐飞,秋水共长天一色。此代码也是码中极品。能直接通过传递特定的pos值来实现头删和尾删。如何实现pos删呢?同样,首先是什么?对,断言!其次呢?检查pos的范围对不对?再然后?对,不是检查容量,哈哈,因为我们要删除某个数据,检查容量干嘛呢?哎呦你干嘛~
?
后面代码就像前面的头删一样,只不过把头下边0改成了pos,即把pos后的每一个位置都向前移动一位,实现数据的覆盖,当然,别忘了更新顺序表里的size。
void SLErase(SL* ps, int pos)//删除pos位置的数据
{
assert(ps);
assert(pos >= 0);
assert(pos < ps->size);//不能带等于号,因为此时下标最大的为size-1;
//直接覆盖
int end = ps->size-1;
while (pos < end)
{
ps->a[pos] = ps->a[pos + 1];
pos += 1;
}
ps->size--;
}
2.3.11查找某个值
最后一个了,我们有时候想要查找顺序表里面有没有这个数,这时候怎么办呢?总不能打印出来一个一个看吧哈哈,因此就有了查找数据函数的必要性。原理很简单,先断言,防止传入空指针NULL,然后用循环去查找数值x,并返回其下标,如果没找到,返回-1。
int SLFind(SL* ps, SLDateType x)//顺序表查找
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
ok,代码的实现到这里就全部实现了,是代码的汇总,避免大家翻来翻去的麻烦。
Seqlist.c
#include "Seqlist.h"
void SLCheckCapacity(SL* ps)//判断是否需要加空间
{
if (ps->capacity == ps->size)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * newcapacity);
//判断tmp是否开辟成功
if (!tmp)
{
perror("realloc fail:");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity = newcapacity;
}
}
}
void SLPrint(SL* s)//打印
{
assert(s);
assert(s->size != 0);
for (int i = 0; i < s->size; i++)
{
printf("%d ", s->a[i]);
}
printf("\n");
printf("size = %d\n", s->size);
printf("capacity = %d\n", s->capacity);
}
void SLInit(SL* s)//初始化
{
assert(s);
s->a = NULL;
s->capacity = 0;
s->size = 0;
}
void SLDestroy(SL* s)//销毁
{
if (s->a!=NULL)//避免释放空指针
{
free(s->a);
s->a = NULL;
s->capacity = 0;
s->size = 0;
}
}
void SLpushBack(SL* ps, SLDateType x)//尾插
{
//assert(ps);
检查空间是否够用
//SLCheckCapacity(ps);
//ps->a[ps->size] = x;
//ps->size++;
SLInsert(ps, ps->size, x);
}
void SLPopBack(SL* ps)//尾删
{
暴力检查,防止数组a的越界访问
//assert(ps->size != 0);
//ps->size--;//使得有效数据位size减一
SLErase(ps, ps->size-1);
}
void SLPushFront(SL* ps, SLDateType x)//头插
{
//assert(ps);
//SLCheckCapacity(ps);
挪动数据
//int end = ps->size - 1;
//while (end >= 0)
//{
// ps->a[end + 1] = ps->a[end];
// end--;
//}
//ps->a[0] = x;
//ps->size++;
SLInsert(ps, 0, x);
}
void SLPopFront(SL* ps)//头删
{
/*assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;*/
SLErase(ps, 0);
}
void SLInsert(SL* ps, int pos, SLDateType x)//在pos位置插入
{
assert(ps);
assert(pos >= 0);
assert(pos <= ps->size);//等于size的时候相当于尾插
//判断是否需要加空间
SLCheckCapacity(ps);
//如果为空
int end = ps->size - 1;
while (pos <= end)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)//删除pos位置的数据
{
assert(ps);
assert(pos >= 0);
assert(pos < ps->size);//不能带等于号,因为此时下标最大的为size-1;
//直接覆盖
int end = ps->size-1;
while (pos < end)
{
ps->a[pos] = ps->a[pos + 1];
pos += 1;
}
ps->size--;
}
int SLFind(SL* ps, SLDateType x)//顺序表查找
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
Seqlist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
静态顺序表(不太实用,N小了不够用,大了浪费)
//#define N 10
//typedef int SLDateType;
//typedef struct SeqList
//{
// SLDateType a[N];
// SLDateType size;//记录存储了多少有效数据
//
//}SL;
//
//void SLInit(SL s);//初始化
//void SLpushBack(SL s, SLDateType x);//尾插,即在尾部插入数据
//动态链表
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
int size;//记录存储了多少有效数据
int capacity;//记录容量的大小
}SL;
void SLInit(SL* s);//初始化
void SLDestroy(SL* s);//销毁
void SLPrint(SL* s);//打印数据
void SLCheckCapacity(SL* ps);//判断是否需要加空间
void SLPushFront(SL* ps, SLDateType x);//头插
void SLPopFront(SL* ps);//头删
void SLpushBack(SL* ps, SLDateType x);//尾插,即在尾部插入数据
void SLPopBack(SL* ps);//尾删。
void SLInsert(SL* ps, int pos, SLDateType x);//在pos位置插入某个值
void SLErase(SL* ps, int pos);//在pos位置删除某个值
int SLFind(SL* ps, SLDateType x);//顺序表查找
最后,我想你也不想你的博客被白嫖吧?
?
|