我们要完成一个顺序链表
顺序表
概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
1.创建项目
先创建两个源文件,一个头文件
只有头文件SeqList.h是#include<...>
其他两个源文件直接引用有文件,也就是#include"SeqList.h"
静态顺序表
SeqList.h
首先写一个结构体
有点问题因为a写死了,以后想修改的时候不好修改,所以最好加个define。
还有点问题这个数组默认存储的是int型,以后想换很多地方都要跟着换,所以加个typedef方便以后进行更换
问:有人问了为什么不是DataType而是SLDataType?
答:因为后边链表也要从定义那这个,那DataType是谁的?所以我们取首字母的缩写也就是SLDataType。
数据结构是对数据进行管理也就是增删查改。
一般我们不喜欢写静态的顺序表因为N到底给多少比较好?小了不够用,大了浪费。
静态顺序表在实际中非常不实用,因此我们还是会选择用动态顺序表
动态顺序表
逻辑图:
先写两个简单的
初始化
SeqList.h
SeqList.c
Test.c
建议编写边测,例如写两个函数就测两个函数。
这种写法是有问题的,原因很好理解
所以三个文件都要改
SeqList.h (改进)
SeqList.c(改进)
Test.c(改进)
运行结果:
在SeqList.c中
初始化函数也不是很好,应该稍微开上那么一点点空间。
应该写成这样,但是还是不完整虽然在OJ中没问题,但是这不是一个独立的程序在其他项目也可能在用,可能内存耗尽。所以最好检查一下
exit(-1) 为什么这么写
exit(0) 代表正常结束,如果是负数那就是异常结束的
可以测试一下
打断点
F11进入函数
鼠标把小箭头拖进判断程序中
然后F11继续往下走直到结束程序看结果:
总结:正常终止是0,负数是异常终止是自己定义的。给0就不知道是正常结束还是异常结束了。
初始化写好了也要写销毁
销毁
SeqList.h
SeqList.c
后边是四个核心操作
头插头删,尾插尾删
问为什么不取insertback intend...都可以。就像给你娃取名字一样。
但是这里是因为后边学C++会有个库,这里习惯了后边会容易一些。
但是千万不要用拼音,这就像给你娃取名狗蛋一样。
尾插
SeqList.h
思路:
SeqList.c
一般我们都是扩两倍
记得判断一下是否扩容成功。
问: 这里用不用free一下a?
答:不用,realloc 的本质有两种方式原地扩容和异地扩容
这里加上个新的函数SPrint 方便测试查看。
SeqList.h
SeqList.c
Test.c
运行结果:
尾删
SeqList.h
SeqList.c
先这么写
思路:
然后测试一下看行不行
Test.c
测试结果:
自己可以多测试一下。
如果后边加上几个0呢?
运行结果:
发现这个尾删并不是很有意义,0~size-1才是有效数据。
所以SeqList.c要改
SeqList.c(改进)
思路:
有人问用不用free一下?
答:不能 ,因为C++有个规定malloc一次就要free一次,要free就全部free,不能局部释放。
SeqList.c
还有什么问题?
看一下Test.c的测试
运行结果:
删完了,竟然没有报错。为什么呢?有问题不一定报错。
再看一组测试:
运行结果:
为什么1,2没有出现因为PopBack有问题没有解决。在监视中查看到size已经减到负数了。
越界不一定报错。是抽查有时候查得出来有时候查不出来。
如果我们少删几个
运行结果:
发现报错了
free没问题,哪问题在哪里呢?
越界不一定被查出来,因为VS编译器在前后检查。-10的时候在检查外,但是size在检查内就会被查出来。
free报错不一定是这个地方,有可能在其地方。
所以
SeqList要改
Test.c
运行结果:
在测试对的:
运行结果:
头插
SeqList.h
SeqList.c
思路:
还是需要扩容所以我们干脆提取一个公共的函数出来。
检查
SeqList.h
SeqList.c
同时头插也要改
头插思路:
头插代码:
Test.c
这时候我们发现测试代码太多了,所以我们要学会分组测试
运行结果:
原来测试的可以放进TestSeqList1中。
头删
SeqList.h
SeqList.c
如果我们这样写对吗?
思路:
Test.c
运行结果:
原因和之前一样,也是越界了,越界了不一定报错。
最好的方式还是断言一下。暴力检查和温柔检查都可以。
SeqList.c(改进)
运行结果:
这也和Destroy有关(释放的时候会检查)。最好是双管齐下。
SEqList.c改进
思路:
为什么不a++?要顾头顾尾。有free和size变成随机值两个问题
a是指针,C语言数组的本质是只能定义成静态的,静态的就有一个问题写死了。动态的申请一个数组把这个空间第一个位置的地址给你。
在pos位置插入x
SeqList.h
SeqList.c
初始条件(迭代的开始),循环结束条件(迭代的终止),中间走的过程(迭代的过程)
思路以及代码:
前期多画图
Test.c
从0开始数。
运行结果:
查找
SeqList.h
SeqList.c
Test.c
运行结果:
输入一个数,找到那个数并且在前边输出他的10倍 。
到这里头插尾插是不是可以复用这个插入函数?
尾插
SeqList.c
头插
SeqList.c
Test.c
运行结果:
删除pos位置的值
SeqList.h
SeqList.c
Test.c
运行结果:
在测试一组数据
运行结果:
同时SLErase也能进行复用 。
SeqList.c
尾删
头删
Test.c
不用改,直接运行结果是一样的。
修改pos位置的值
SeqList.h
SeqList.c
Test.c
运行结果:
在测试一组数据:
运行结果:
有人问这样子该不是也可以吗?为什么要用Modify这个函数呢,不直接访问这个结构体不好吗?
数据结构这里有个原则,不要去轻易的访问数据,会带来恶果。
比如:
输入的是错误的数据它竟然不会报错。
如果用的是Modify就会报错
数据结构有个原则,哪怕再简单的操作都要去调用我们提供的函数去解决 ,因为每个函数里都有检查,函数用的不对会有更好的检查。这也是C语言的缺陷。
这里还有个小问题,还有个检查一直都没有做。
如果数据结构写出来给别人用。
你舍友用你这个程序的时候这么用
夸夸夸写了几百上千行代码运行时发现不对调试的时候看到是空指针
我们写这个设计这个代码的逻辑是整体而言,我们认为传了结构体指针给你但是我认为结构体的空间是外面开好的,不管是malloc还是直接定义一个对象结构体变量,我认为结构体是开好的,而不是只传一个指针给我,我是不帮你开空间的。所以你无论如何都不能传个空指针给我,我们可以用暴力检查把这种方式规避一下,这样就不用去调试阶段找错误,错了一定不能传这个空指针。所以暴力检查都加上,一出错就告诉你你传错了。加入更多的检查是更好的。
这是运行的时候就会报错。
这样修改就没有问题了
Destroy也不要忘记,不然越界了都检查不出来,一般越界在Destroy中 free 的时候才会检查。
总代码:
SeqList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//静态顺序表
//#define N 100
//typedef int SLDataType;//1.加了这里
//struct SeqList
//{
// SLDataType a [N];//2.这里也要跟着改
// int size;
//};
//动态顺序表
typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType *a; //指向动态开辟的数组
int size; //存储有效数据个数
int capacity; //容量空间大小
}SL;
//管理数据 -- 增删查改
// 顺序表初始化
void SLInit(SL *ps);
// 检查空间,如果满了,进行增容
void SLCheckCapacity(SL* ps);
//销毁
void SLDestroy(SL *ps);
//输出
void SLPrint(SL* ps);
//头插头删 尾插尾删
// 顺序表尾插
void SLPushBack(SL* ps, SLDataType x);
// 顺序表尾删
void SLPopBack(SL* ps);
// 顺序表头插
void SLPushFront(SL* ps, SLDataType x);
// 顺序表头删
void SLPopFront(SL* ps);
// 顺序表查找
int SLFind(SL* ps, SLDataType x);
//在pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x);
//删除pos位置的值
void SLErase(SL* ps, int pos);
//修改pos位置的值
void SLModify(SL* ps, int pos, SLDataType x);
SeqList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"
void SLInit(SL* ps)
{
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
if (ps->a == NULL)//这里
{
perror("malloc failed");
exit(-1);
}
ps->size = 0;
ps->capacity = 4;
}
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
void SLPrint(SL* ps)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* ps)
{
assert(ps);
//满了要扩容
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, ps->capacity * 2 * (sizeof(SLDataType)));
if (tmp == NULL)
{
perror("realloc failed");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
}
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//SLCheckCapacity(ps);
//ps->a[ps->size] = x;
//ps->size++;
SLInsert(ps, ps->size, x);
}
void SLPopBack(SL* ps)
{
assert(ps);
//温柔的检查
//if (ps->size == 0)
//{
// return;
//}
//暴力检查
//assert(ps->size>0);//犯了错误直接报错
ps->a[ps->size - 1] = 0;
//ps->size--;
SLErase(ps, ps->size-1);
}
void SLPushFront(SL* ps, SLDataType 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->size>0);
从前往后挪动
//int begin = 1;
//while (begin < ps->size)
//{
// ps->a[begin - 1] = ps->a[begin];
// begin++;
//}
//ps->size--;
SLErase(ps, 0);
}
int SLFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
//在pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
//删除pos位置的值
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
void SLModify(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
ps->a[pos] = x;
Test.c
void TestSeqList1()
{
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPushBack(&s1, 3);
SLPushBack(&s1, 4);
SLPushBack(&s1, 5);
SLPushBack(&s1, 0);
SLPushBack(&s1, 0);
SLPrint(&s1);
SLPopBack(&s1);
SLPopBack(&s1);
SLPrint(&s1);
//SLPopBack(&s1);
//SLPopBack(&s1);
//SLPopBack(&s1);
//SLPopBack(&s1);
//SLPopBack(&s1);
//SLPopBack(&s1);
//SLPopBack(&s1);
//SLPopBack(&s1);
//SLPrint(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPrint(&s1);
SLDestroy(&s1);
}
void TestSeqList2()
{
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPushBack(&s1, 3);
SLPushBack(&s1, 4);
SLPrint(&s1);
SLPushFront(&s1, 10);
SLPushFront(&s1, 20);
SLPushFront(&s1, 30);
SLPushFront(&s1, 40);
SLPrint(&s1);
SLDestroy(&s1);
}
void TestSeqList3()
{
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPushBack(&s1, 3);
SLPushBack(&s1, 4);
SLPushBack(&s1, 5);
SLPrint(&s1);
SLPopFront(&s1);
SLPopFront(&s1);
SLPrint(&s1);
SLPopFront(&s1);
SLPopFront(&s1);
SLPopFront(&s1);
SLPopFront(&s1);
SLPrint(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPrint(&s1);
SLDestroy(&s1);
}
void TestSeqList4()
{
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPushBack(&s1, 3);
SLPushBack(&s1, 4);
SLPushBack(&s1, 5);
SLPushFront(&s1, -1);
SLPushFront(&s1, -2);
SLPrint(&s1);
SLInsert(&s1, 3, 40);
SLPrint(&s1);
int x;
scanf("%d", &x);
int pos = SLFind(&s1, x);
if (pos != -1)
{
SLInsert(&s1, pos, x*10);
}
SLPrint(&s1);
SLDestroy(&s1);
}
void TestSeqList5()
{
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPushBack(&s1, 3);
SLPushBack(&s1, 4);
SLPushBack(&s1, 5);
SLPrint(&s1);
SLErase(&s1, 2);
SLPrint(&s1);
int x;
scanf("%d", &x);
int pos = SLFind(&s1, x);
if (pos != -1)
{
SLErase(&s1, pos);
}
SLPrint(&s1);
SLDestroy(&s1);
}
void TestSeqList6()
{
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPushBack(&s1, 3);
SLPushBack(&s1, 4);
SLPrint(&s1);
SLModify(&s1, 2, 20);
s1.a[2] = 20;
SLPrint(&s1);
//int x;
//scanf("%d", &x);
//int pos = SLFind(&s1, x);
//if (pos != -1)
//{
// //SLModify(&s1, pos, x * 10);
// s1.a[pos] = x * 10;
//}
int pos;
int x;
scanf("%d%d", &pos, &x);
//s1.a[pos] = x;
SLModify(&s1, pos, x * 10);
SLPrint(&s1);
SLDestroy(&s1);
}
void TestSeqList7()
{
//SL *s1 = NULL;
//SLInit(s1);
//SLPushBack(s1, 1);
//SLPushBack(s1, 2);
//SLPushBack(s1, 3);
//SLPushBack(s1, 4);
//SLPushBack(s1, 5);
//SLPrint(s1);
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1);
SLPushBack(&s1, 2);
SLPushBack(&s1, 3);
SLPushBack(&s1, 4);
SLPrint(&s1);
SLDestroy(&s1);
}
int main()
{
TestSeqList7();
return 0;
}
//int main()
//{
// SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
// n1->data = 10;
//
// SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
// n2->data = 20;
//
// SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
// n3->data = 30;
//
// n1->next = n2;
// n2->next = n3;
// n3->next = NULL;
//
// PrintSList(n1);
//
// return 0;
//}
有时候我们觉得写个菜单形式nb一点
菜单形式
也就是有什么方式能帮助我们输入-1?
这个样子写能在逻辑上控制。
可以加在Test.c中
void menu()
{
printf("******************************\n");
printf("*******1.头插 2.头删*******\n");
printf("*******3.尾插 4.尾删*******\n");
//......
printf("*******7.打印 -1.退出*******\n");
printf("******************************\n");
printf("******************************\n");
}
int main()
{
SL s1;
SLInit(&s1);
int input = 0;
do
{
menu();
scanf("%d", &input);
if (input == 1)
{
//printf("请依次输入你要插入的数据, 以-1结尾\n");
printf("请依次输入你要插入的数据个数和数据\n");
int n = 0;
scanf("%d", &n);
int x = 0;
int i = 0;
for (i = 0; i < n; i++)
{
scanf("%d", &x);
SLPushBack(&s1, x);
}
}
else if (input == 2)
{
SLPopFront(&s1);
}
else if (input == 7)
{
SLPrint(&s1);
}
} while (input != -1);
SLDestroy(&s1);
return 0;
}
//数据结构不太需要菜单,
//先单个测,写好了再写菜单
后边可以自己去完善。记得先单个测试,写好了再写菜单,不然一运行一片红都不知道从哪里下手。
一定要多练,厉害的神枪手都是子弹喂出来的,程序员也是如此。