顺序表是数据结构学习所接触的第一个数据存储结构,对顺序表的结构有清楚的了解,将对后面的学习大有帮助。(本文章默认读者c语言有一定了解)还需要注意的是:数据结构的学习,我们亲自画图(理解逻辑的过程)十分重要,如果顺序表不好好画图,相信链表的实现时会让你头疼一阵。
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
int size;
int capacity;
}SeqList;
顺序表的载体是一个结构体,它实际的数据将储存在指针a所指向的一片空间中
(我们用malloc开辟)。而size和capacity同样作为顺序表的属性成为顺序表结构体中的成员,这样可以方便我们随时获取顺序表的状态。
// SeqList.h
#pragma once
#include
#include
#include
#include
#define INIT_CAPACITY 3
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
int size;
int capacity;
}SeqList;
// 对数据的管理:增删查改
//初始化
void SeqListInit(SeqList* ps);
//销毁
void SeqListDestroy(SeqList* ps);
//打印查看
void SeqListPrint(SeqList* ps);
//尾插
void SeqListPushBack(SeqList* ps, SLDateType x);
//头插
void SeqListPushFront(SeqList* ps, SLDateType x);
//头删
void SeqListPopFront(SeqList* ps);
//尾删
void SeqListPopBack(SeqList* ps);
//检查容量大小/并扩容
void SeqListCheckCapacity(SeqList* ps);
// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDateType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);
顺序表结构体中的成员a指针在结构体变量刚被创建时是一个野指针(我们没有给予初始化,它是随机的,也无法得知该指针指向哪里),如果这时对该指针进行解引用,就会非法访问内存空间。
所以我们用malloc函数对成员指针a进行空间开辟(在堆上),malloc开辟的空间将在内存中具有连续性(和数组一样),也可解引用找到顺序表的各个数据。
错误代码示例
void SeqListInit(SeqList* ps)
{
ps->capacity = INIT_CAPACITY;
for (int i = 0; i < ps->capacity; i++)
{
*(ps->a + i)=0;
}
ps->size = 0;
}
这是新手常见的初始化错误,对野指针解引用!
正确的初始化(如下)
void SeqListInit(SeqList* ps)
{
assert(ps);
//开辟空间
ps->a = (SLDateType*)malloc(sizeof(SLDateType) * INIT_CAPACITY);
//开辟失败报错
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
这里注意的是:1.对参数ps进行断言(我们无法对空的结构体指针进行成员访问)
2.当malloc开辟失败时我们打印错误信息并结束该函数。(一般不会失败)
(后面的数据结构学习我将逐渐减少强调)
尾插时结构体中size的大小刚好是a指向的下一个位置下标的位置,且尾插不需要移动数组中其他值的位置,所以尾插是顺序表中比较推荐的数据插入方式。
值得注意的是:当size的大小等于capacity时,我们需要对顺序表进行扩容操作!!(我们选择给顺序表扩容2倍)
void SeqListPushBack(SeqList* ps, SLDateType x)
{
assert(ps);
//扩容
if (ps->size == ps->capacity)
{
SLDateType* tmp = (SLDateType*)realloc(ps->a,sizeof(SLDateType) * 2 * ps->capacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
else
{
ps->a = tmp;
}
ps->capacity *= 2;
}
//SeqListCheckCapacity(ps);
//开始尾插
ps->a[ps->size] = x;
ps->size++;
对已经申请的内存空间扩容时,要使用realloc函数,它两个参数分别是原地址和希望扩容的内存大小(单位是也是字节), 注意realloc也有可能开辟失败(当需要的内存太大时),所以要做好报错准备,因为它需要保持像数组那样的顺序连贯的结构,所以当其指针所指空间的后面内存空间不足时,它会另寻足够空间的位置开辟,也就是所谓的异地扩容,此时realloc返回的指向开辟空间的指针的位置改变,需要另外一个临时指针变量做过渡。
尾删就更简单了,我们只需要对size进行减一,无需担心没有删除已存的值,新的值插入时会将它替换。
不过空的顺序表无法删除,我们最好断言它,同时不要忘记对函数的参数指针进行断言。
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
// if (ps->size == 0)
// {
// return;
// }
ps->a[ps->size - 1] = 0;
ps->size--;
}
//虽然注释里的报错也可以,但是最好使用暴力一点的报错assert。
在顺序表里我们头插头删时,最差需要调整表中每个值的位置,所以时间复杂度是O(N),不高效。
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
//扩容
SeqListCheckCapacity(ps);
int end=ps->size-1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
注意上动图的size已经等于capacity了,所以在下次进入插入相关的函数时,我们需要对顺序表进行扩容,既然已经或者将要扩容多次,那么不如将扩容操作封装成一个函数 SeqListCheckCapacity。
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * 2 * ps->capacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
printf("扩容成功!");
}
}
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
for (int i = 0; i < ps->size-1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
其实对于一个程序员而言,写一个程序,自己代码占3成,另外7成靠copy,,,,,,,,
开玩笑的O(∩_∩)O~,另外7成是调试,有时候虽然我们的代码能跑(没有报错),但是储存效果却不尽人意,所以数据结构学习中,我们最方便的调试方法就是在各函数正常运作的情况下尽快的、正确的写出一个打印函数(如果你不会调试的话),以便我们检查结构中的储存情况。
void SeqListPrint(SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
->操作符和[]的优先级一致,所以遵循从左到右的计算顺序。
注意:这里的参数结构体指针ps不能为空(不能对空的结构体指针进行成员访问),但是ps成员中的指针a可以为空(空顺序表也可以打印),这里看着简单,可是请你格外记忆理解,不要对它嗤之以鼻,如果不想被链表的断言扇巴掌的话。
int SeqListFind(SeqList* ps, SLDateType x)
{
assert(ps);
int pos=-1;
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
pos = i;
return pos;
}
}
return pos;
}
这里的指定位置的插入删除和头插头删逻辑差不多,而且不知道你们有没有发现 ,其实只要完成了对指定位置pos位置的插入和删除的函数,我们的所有插入删除函数都可以用这两个函数实现(头插不就是指定下标为0的位置插入吗?尾删不就是指定下标为size-1的位置删除吗?)这样我们不就可以轻松解锁成就:15分钟写完一个顺序表~~~了吗。O(∩_∩)O
void SeqListInsert(SeqList* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size - 1);
SeqListCheckCapacity(ps);
for (int i = ps->size - 1; i >= pos; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->size++;
ps->a[pos] = x;
}
void SeqListErase(SeqList* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size - 1);
for (int i = pos; i <= ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
销毁也是代码新手容易常错的地方。
错误代码如下
void SeqListDestroy(SeqList* ps)
{
free(ps);
free(ps->a);
ps->size = 0;
ps->capacity = 0;
ps=NULL;
}
错误:1.释放了结构体指针所指向的位置却又访问它的内存(非法访问)。
2.释放了成员a的内存却不置空(容易再次访问a,可是内存已经释放,所以依旧是非法访问)
(虽然有了错误1错误2就不会发生)
3.对函数中的形式参数进行改值(我们无法在函数中改变一个形式参数的值,除非传它的指针给函数,解引用访问,这里就是传指针的指针<二级指针>)。
正确代码如下
void SeqListDestroy(SeqList* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
正如错误3中所言,我们只能将结构体指针的置空交给函数的使用者!(也就是进行对实参的置空)
注意:1.断言ps不为空指针
2.断言pos的位置为顺序表中有效位置
#define _CRT_SECURE_NO_WARNINGS 1
// SeqList.h
#pragma once
#include
#include
#include
#include
#define INIT_CAPACITY 3
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
int size;
int capacity;
}SeqList;
// 对数据的管理:增删查改
void SeqListInit(SeqList* ps);
void SeqListDestroy(SeqList* ps);
void SeqListPrint(SeqList* ps);
void SeqListPushBack(SeqList* ps, SLDateType x);
void SeqListPushFront(SeqList* ps, SLDateType x);
void SeqListPopFront(SeqList* ps);
void SeqListPopBack(SeqList* ps);
void SeqListCheckCapacity(SeqList* ps);
// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDateType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);
//标准初始化
void SeqListInit(SeqList* ps)
{
ps->a = (SLDateType*)malloc(sizeof(SLDateType) * INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
//顺序表结构体里,SLDateType*只是一个指针,为它在内存堆区里自定义开辟内存区域(使其自定义指向想要的内存大小)。
//标准顺序表销毁
void SeqListDestroy(SeqList* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//对malloc开辟(借用)的内存空间用完记得释放(还给操作系统)
//而对指向该空间的指针记得置空,以防意外非法访问已经释放的空间。
//标准顺序表打印
void SeqListPrint(SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
//->操作符和[]的优先级一致,所以遵循从左到右的计算顺序。
//标准顺序表尾插
void SeqListPushBack(SeqList* ps, SLDateType x)
{
assert(ps);
扩容
//if (ps->size == ps->capacity)
//{
// SLDateType* tmp = (SLDateType*)realloc(ps->a,sizeof(SLDateType) * 2 * ps->capacity);
// if (tmp == NULL)
// {
// perror("realloc fail");
// return;
// }
// else
// {
// ps->a = tmp;
// }
//}
SeqListCheckCapacity(ps);
//开始尾插
ps->a[ps->size] = x;
ps->size++;
}
//对已经申请的内存空间扩容时,要使用realloc函数,它两个参数分别是原地址和希望扩容的内存大小(单位是字节),
// 注意realloc也有可能开辟失败(当需要的内存太大时),所以要做好报错准备
//因为它需要保持像数组那样的顺序连贯的结构,所以当其指针所指空间的后面内存空间不足时,它会另寻
//足够空间的位置开辟,也就是所谓的异地扩容,此时指针的位置改变,需要另外一个临时指针变量做过渡。
//标准顺序表尾删
void SeqListPopBack(SeqList* ps)
{
assert(ps->size > 0);
// if (ps->size == 0)
// {
// return;
// }
ps->a[ps->size - 1] = 0;
ps->size--;
}
//虽然注释里的报错也可以,但是最好使用暴力一点的报错assert,
//它会使程序直接弹出报错窗口,更容易找到程序的错误所在。
// 标准的顺序表头插
//头插头删在顺序表里时间复杂度是O(N),不高效。
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
//扩容
SeqListCheckCapacity(ps);
int end=ps->size-1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
//头插也需要扩容,既然已经或者将要扩容多次,那么不如将扩容操作封装成一个函数SeqListCheckCapacity。
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * 2 * ps->capacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
printf("扩容成功!");
}
}
//标准的顺序表头删
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
for (int i = 0; i < ps->size-1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
//标准顺序表查找
int SeqListFind(SeqList* ps, SLDateType x)
{
assert(ps);
int pos=-1;
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
pos = i;
return pos;
}
}
return pos;
}
//标准顺序表指定位置插入
void SeqListInsert(SeqList* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size - 1);
SeqListCheckCapacity(ps);
for (int i = ps->size - 1; i >= pos; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->size++;
ps->a[pos] = x;
}
//标准的顺序表指定位置删除
void SeqListErase(SeqList* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size - 1);
for (int i = pos; i <= ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。