目录
前言
练会铁砂掌,手撕顺序表
知识点:需要基本上对于C语言有一点的了解
顺序表分为静态顺序表和动态顺序表,重点说明动态顺序表。
如铁砂掌招数有问题,或可以优化,望各位大侠进行斧正。(ง •̀_•́)ง(ง •̀_•́)ง(ง •̀_•́)ง
提示:以下是本篇文章正文内容,下面案例可供参考
一、手撕顺序表
1.铁砂掌总纲
我们知道顺序表属于数据结构,而数据结构的作用无非就四个字(增删查改);在不同的场景使用不同的数据结构类型,如果数据结构有等级的话,那么顺序表是进入数据结构的大门阶梯,拿下顺序表才能触摸到数据结构的大门,也是筑起数据结构大厦的地基。
数据结构分为:线性数据结构、非线性数据结构、特殊数据结构。而今天要手撕的就是线性数据结构下面的小喽啰。干掉小喽啰让我们逐步升级。
顺序表简单来说就是一个数组,它通过与结构体相结合进行实现。一下就是顺序表的两种不同的状态和定义,如果C语言结构体学的还可以的话那么静态顺序表就不用多说了就是结构体。如果静态看不懂的话,需要对结构体进行补一下,因为在数据结构的时间里,不会结构体和指针,就相当于修仙没有灵气。
// 静态顺序表 -- 非常不好用,开多了浪费,开少了不够
//struct seq_list
//{
// int a[10]; //
// int size;
//};
// 动态顺序表 -- 按需申请,不够了扩容
typedef struct seq_list
{
int size; // 有效数据个数
int capacity; // 现在空间容量的大小
int* p; //一个int类型的指针,指向一个空间
}sl;
2.动态数据结构
顺序表就相当于一个数组,动态顺序表相当于一个可以变大的数组;而所要实现的无法就是增删查改。所以我们先声明一下所需要的东西。 对于相应的函数也有注释
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// 顺序表
// 用一段物理地址连续的依次性的储存数据元素的线性结构
typedef int sl_data_type; //对顺序表的类型名称进行定义,好处是想存字节换成 char就可以了
#define INIT 4 //初始化的容量
// typedef通常是对类型进行重定义, #define 中的 I 通常是定义常量或者宏函数
// 动态顺序表 -- 按需申请,不够了扩容
typedef struct seq_list
{
int size; // 有效数据个数
int capacity; // 现在空间容量的大小
sl_data_type* p; //一个int类型的指针,指向一个空间
}sl;
// 数据结构无非是 增删查改
void seq_init(sl* ps); //初始化
void seq_destroy(sl* ps); //删除
void seq_push_back(sl* ps, sl_data_type x); //尾插
void seq_pop_back(sl* ps); //尾删
void seq_push_front(sl* ps, sl_data_type x); //头插
void seq_pop_front(sl* ps); //头删
void seq_push_insert(sl* ps, int pos, sl_data_type x);//某个位置插入
void seq_pop_insert(sl* ps, int pos);//某个位置删除
void seq_printf(sl* ps); //打印
void seq_check(sl* ps);//扩容
3.实现
我们先定义一个结构体,定义好之后一个结构体我们要传入结构体的地址,当我们传s的话我们需要进行拷贝结构体的数据,拷贝完之后进行压栈,当一个结构体数据过大,压栈所使用的空间和时间也会变大,但如果我们传&s,它可以通过指针直接指向结构体,所以当我们进行结构体传参的时候首选地址传参。
test_seqlist()
{
sl s; //开辟了结构体变量 s
seq_init(&s); // 为什么传地址
}
int main()
{
test_seqlist(); // 顺序表
return 0;
}
首先我们需要对它进行初始化,我们把指针所指向的空间想象成一个数组,这个数组刚开始什么都没有,我们在根据malloc函数对p指向的类型空间开辟内存,当开辟完内存之后我们还有判断一下内存是否开辟成功。而 size 我们可以看作数组的下标。 capacity看作数组空间的大小。
#define _CRT_SECURE_NO_WARNINGS
#include "seq_list.h" //声明自己创建的头文件
void seq_init(sl* ps) //初始化
{
ps->p = NULL; //将指针初始化为空
ps->p= (sl_data_type*)malloc(sizeof(sl_data_type) * INIT); //单个对象的大小*所要对象的个数
if (ps->p == NULL)
{
perror("malloc fail"); // 若为空指针开辟失败
return;
}
ps->size = 0; //存了多少数据
ps->capacity = INIT; // 先开辟一点空间
}
古语有云:有借有还,再借不难。当我们不用顺序表的时候就要把它还回去。这里的还回去是将整个顺序表全部给换回去,因为free释放的是整块空间。
void seq_destroy(sl* ps) //空间不用了要归还
{
free(ps->p);
ps->p = NULL;
ps->capacity = ps->size = 0;
}
初始化之后我们放进去了四个类型大小的空间,但是如果我们要放进去五个数据,这时候只有四个数据是不行的,怎么办? 扩容,没有空间好说给它空间不就完事了。但是问题又开了,怎么扩容呢?需要使用C语言中的realloc函数进行扩容。
void seq_check(sl* ps) //扩容
{
assert(ps);
if (ps->size == ps->capacity)
{
sl_data_type* tmp = (sl_data_type*)realloc(ps->p, ps->capacity * 2);
if (tmp == NULL)
{
perror("reallpc fail");
return;
}
ps->capacity *= 2; // 扩容成功,可用容量数*2
}
}
练铁砂掌所需要的洗手丹已经配置好,所需要的材料也配置完成。我们已经能够进行开辟空间,空间不够给它空间,如果不需要将空间还给操作系统。让我们开始练铁砂掌。
铁砂掌共有三式,分别为:劈天,劈地,劈神。
1.1:劈天
下面让我们开始练铁砂掌的第一式:劈天。
对于顺序表数据的插入,我们先来搞定头插,什么是头插呢?顾名思义,从首位插入,如果我们插入1,2,3,4,5。那么顺序表里面的顺序就是 5,4,3,2,1。实现头插就要定义一个函数,依靠这个函数实现头插,那么实现头插,我们要有一个要插入数据的参数,和指向顺序表地址的参数。通过这个参数进行实现。断言和扩容这个就不用说了。插入第一个不就是将数据依次向后面放,放完之后再将要插入的数据放进第一个位置空间。放到第一个空间,后面数据该怎么移动呢?我们将最后一个数据向后放,不断的对数据进行覆盖。
3 | 2 | 1 | ||
4 | 3 | 2 | 1 | |
5 | 4 | 3 | 2 | 1 |
void seq_push_front(sl* ps, sl_data_type x)
{
assert(ps);
void seq_check(sl * ps);//扩容
int end = ps->size - 1; //要插入对象下标
while (end >= 0) //循环停止代表找到了最前面的地址了
{
ps->p[end + 1] = ps->p[end];
--end;
}
ps->p[0] = x;
ps->size++; //插入完成之后数组元素多了一位要加进去
}
// 第二种方式
front(sl* ps, sl_data_type x)
{
assert(ps);
void seq_check(sl * ps);//扩容
int cmp = ps->size + 1;
while (cmp)
{
ps->p[cmp] = ps->p[cmp-1];
cmp--;
}
ps->p[0] = x;
ps->size++;
}
头插我们学会了,不就是向里面放数据吗?下面我们学习它相反的一面,从头删数据。万物有阴有阳,我们的招数也不例外,插入数据是阳,删除数据就是阴。
当我们进行头删的时候,第一个点是什么?里面要有数据,如果没有数据,我们可以不提醒直接退出,也可以提醒告诉他没有数据不用删除了。根据不同场景进行使用。删除就是将前面的数据进行覆盖,size-- 使得访问不了最后一个数据。这样就完成了头删。
注意:向前覆盖后面的数据还再,只不过当我们size进行-1的时候访问不到数据了。当我们再插入数据。后面的数据会被覆盖,不会有任何的印象
5 | 4 | 3 | 2 | 1 |
4 | 3 | 2 | 1 | size--(1) |
3 | 2 | 1 | size--(1) | (1) |
void seq_pop_front(sl* ps) //头删
{
assert(ps);
assert(ps->size > 0);//当表为空就不用再删了
int begin = 1; //当这个数组下标为0的时候就不能在删了
while (begin < ps->size)
{
ps->p[begin - 1] = ps->p[begin]; // 每次向前一位
begin++;
}
ps->size--; //删去一位下标也要-1
}
1.2:劈地
铁砂掌已经登堂入室了。下面我们就来学习第二式,劈地。
劈天是进行首位的插入和删除,劈地正好相反,是尾部的插入和删除。学会劈天,劈地真是轻轻松松,size在输入数据的时候会++所以,空间永远会多一个。我们只需要在指针size位置上进行放入数据即可。
1 | ||||
1 | 2 | |||
1 | 2 | 3 |
void seq_push_back(sl* ps, sl_data_type x)
{
assert(ps);
void seq_check(sl * ps);//扩容
ps->p[ps->size] = x;//我们将p指向的空间看作下标为0的数组,size为下标,又因为size在结构体里面
//所以ps->size代表size的数字
ps-> size++;
}
下面就是尾删 ,这个更简单,看图,尾删将size--不就行了。要注意这是一整块空间,但删除一个数据不能free。
1 | 2 | 3 | 4 | 5 |
1 | 2 | 3 | 4 | |
1 | 2 | 3 |
void seq_pop_back(sl* ps)
{
assert(ps->p > 0); //大于0说明还能继续走,等于0 结束了
// ps->p[ps->size - 1] = 0; //赋值成0是没有意义的
ps->size--;
// 这里不能free空间
}
1.3:劈神
各位,我们现在学会了两招,下面就让我们学会最后一招,然后撕碎顺序表。
我们学会了,头插,头删。那么如果我要在某一个位置插入或者删除怎么办。
比如
1 | 2 | |||
1 | 3 | 2 | ||
1 | 3 | 4 | 2 |
我们坑定需要构造一个函数,那么这个函数该怎么构造呢?首先我们要传插入的数据,传指针这都是上面的东西 。那么我们要在第二个位置插入,是不是要告诉函数,我在什么位置插入,插入X位置,传进入X。这包含了头插。首先判断传进来的不是个空指针。然后不能是头插和尾插我们将要插入位置向后移动,在将数据插入到这个位置中
注意向后覆盖数据,例子中2这个数据没有被删除,只不过我们再后面将这个位置的数据覆盖了
int end = ps->size - 1; 这里注意,size位置是没有数据的,所以我们要将它-1获取有效数据。当移动到pos指定位置就不用再向前覆盖了。我们给一个end接收这个位置是防止size变动。
1 2 3 起始地址 1 2 2 3 向后覆盖数据 1 4 2 3 在指定位置插入
void seq_push_insert(sl* ps, int pos, sl_data_type x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size); //不能插入不存在的位置
void seq_check(sl * ps);//是否需要扩容
int end = ps->size - 1; //上一个数组的元素
while (end >= pos) //pos占据了end的位置,所以不能 >=
{
ps->p[end + 1] = ps->p[end];//将数据向后移动
--end;
}
ps->p[pos] = x;
ps->size++; //插入完成之后数组元素多了一位要加进去
}
再指定位置插入数据完成了。那么是不是还要学会再指定位置删除数据。
我相信这个时候有一些小伙伴就不想看了。怎么还有啊。坚持住,这是最后一式的下半式。学会了,铁砂掌就算练成了。这个前面断言和插入一样
1 | 2 | 3 | 4 | 起始地址 |
1 | 3 | 4 | size-- | |
1 | 4 | size-- |
这个代码应该不用多说了。注释说的很清楚了。
void seq_pop_insert(sl* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size); //不能删除不存在的位置
int begin = pos+1;//删除pos下标的数据,将这个数据向前覆盖即可
while (begin < ps->size)//向前覆盖数据
{
ps->p[begin - 1] = ps->p[begin];
begin++;
}
ps->size--;
总结
截止到现在我们的铁砂掌算是成功练成了。这是一个成功,迈入数据结构大门的第一步,还没进行。需要我们举行努力。总结就是,顺序表是以一个结构体来创建一个类似数组的空间,只不过这个空间可以变大。铁砂掌是基础,为了日后降伏数据结构这条龙,学会终极掌法,降龙十八掌打下的基础。