前言
今天学习一门新的招式,八卦掌又称为游身八卦掌、八卦连环掌,是一种以内功和外功相结合,以掌法变换和行步走转为主要特点的中国传统拳术。
本次主要学习掌法变换
我们学的是无头单向非循环链表
如八卦掌招数有问题,或可以优化,望各位大侠进行斧正。(ง •̀_•́)ง(ง •̀_•́)ง(ง •̀_•́)ง
提示:以下是本篇文章正文内容,下面案例可供参考
一、链表是什么?
概念:链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
我们可以把链表想象成一列火车,这列火车有一个火车头和n个车厢,乘客的数量决定了车厢的数量。车厢有挂钩将车厢之间连接起来。我们把车厢想象成物理空间,指针指向的地址表示挂钩,链表的结构体表示车厢的形状。
二、实现单链表
1.结构体的定义
根据上面的图,我们应该知道。单链表是一个结构体,这个结构体有两个空间。一个是链表的数据,它放在结构体里面,一个是指向下一个结构体的类型,它也放在结构体之中。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int stl_type; // 对变量名进行重命名
typedef struct s_list_node (车厢)
{
stl_type data; // 存放数据的类型 (乘客)
struct s_list_node* next; 指针的类型是这个结构体的类型 (车钩)
}s_l_node;
2.八大掌
2.1 起手(扩容)
在学习八大掌之前我们要打基础,又称起手式
当我们要放进n个数据,怎么定义,这就和动态顺序表一样,对结构体进行扩容,需要放进去几个就扩容几个。扩容的大小为一个结构体的大小。同时对其进行判断,是否扩容成功。
这里的扩容是指,我们向内存要一块空间(车厢)并且放入数据(乘客)。所以,我们传进来的是数据,同时地址指向空,这是防止地址出现野指针。
s_l_node* add_capacity(stl_type x)
{
s_l_node* new_node = (s_l_node*)malloc(sizeof(s_l_node)); //扩容一个整形的地址
if (new_node == NULL)
{
perror("malloc fail");
return NULL;
}
new_node->data = x;
new_node->next = NULL;
return new_node;
}
2.2 八大掌
在八卦掌中 八大掌分别对应 天、地、雷、风、水、火、山、泽。下面就让我们开始练功。
链表无非增删查改所以我们要实现 尾插,头插,尾删,头删,out之前插入,out之后插入,out位置删除,out后面删除
2.2.1:尾插
尾插,将数据放在最后的位置上,比如依此尾插 1,2,3,4,5 它们的顺序应该是1->2->3->4->5->NULL。那么它该如何写呢?我们定义一个函数,对于函数的类型为什么要选择void (void是因为我们是链表不需要进行返回类型。并且由于是链表,我们需要修改指针pp所以我们要用**pp二级指针进行接收。这是由于形参是实参的一份临时拷贝。如果传入的是*pp对函数外的指针不会做任何改变)
void s_l_push_back(s_l_node** pp, stl_type x) // 尾插
{
s_l_node* new_node = add_capacity(x);
if (*pp == NULL) // 假如传进来的是一个空指针,代表这个链表是空的,是没有数据的
{
*pp = new_node;
}
else // 代表链表里面至少有一个数据,那么就要实现在最后一个数据后面添加新数据
{
s_l_node* tem = *pp; // 定义一临时结构体来接收链表地址。
while (tem->next != NULL)
{
tem = tem->next;
}
tem->next = new_node; // 连接放入数据的车厢
}
}
在写完一个函数之后要干嘛?肯定是要进行验证。如果我们把整个链表都写完再去验证那么,如果出现错误在排查的过程中将会非常麻烦,如果是大型项目那么会令人非常恶心。
为了能更直观的感受,在进行打印数据的时候,数据后面加入 “->”代表这个数据所指向的下一个数据。
void slt_printf(s_l_node* p)
{
s_l_node* cur = p;
while (cur != NULL) // 满足条件继续循环
{
printf("%d->", cur->data); //打印数据类型,
cur = cur->next; //当结构体指向结构体里面的结构体地址的时候就到了下一个位置
}
printf("NULL\n");
}
void slt_test()
{
s_l_node* pl = NULL; // 对链表初始化
s_l_push_back(&pl, 1);
s_l_push_back(&pl, 2);
s_l_push_back(&pl, 3);
s_l_push_back(&pl, 4);
slt_printf(pl); //打印链表
}
int main()
{
slt_test();
return 0;
}
2.2.2:头插
我们下面进行头插,将新数据放在第一个位置上,也就是说,首先拿过来一个存放乘客的新车厢,和尾插不同的是,当数据为空的时候我们不像尾插那样需要判断。只需要考虑,如果将新车厢,和现有的车厢相连接。将新车厢的挂钩挂在(指针指向)现有车厢即可。
void s_l_push_fornt(s_l_node** pp, stl_type x) // 头插
{
s_l_node* new_node = add_capacity(x);
s_l_node* tem = *pp;
new_node->next = tem;
*pp = new_node;
}
2.2.3:指定位置前插入
下面就是在out之前插入。这是什么意思呢。如果我们的链表存放的是,1,3,4,5。我们想让链表存放1,2,3,4,5那么就需要在3这个位置前插入一个链表。
我们看一张简易的结构体的表,数字代表地址,我们在out车厢前进行插入,可以很明显的看到插入的过程是什么样子的。找到out车厢前的车厢,将这个车厢的挂钩指向插入的新车厢,将新车厢的挂钩指向out车厢的位置即可。同时我们还需要考虑第二章种情况,在a车厢的位置进行插入,我们无法在N前插入,因为那是空指针,所以在out前插入只有两种情况。而第二种情况不就是头插吗!所以当为第二种情况的时候,我们调用头插的函数即可。
void s_l_insert(s_l_node** pp, s_l_node* out, stl_type x) //out之前插入
{
assert(out);
if (out == *pp) // 第一种情况
{
s_l_push_fornt(pp, x);
} // d
else // a->b->c->e->f->NULL
{
s_l_node* new_node = add_capacity(x);
s_l_node* text = *pp;
while (text->next != out && text->next != NULL)
{
text = text->next;
}
if (text->next == out)
{
text->next = new_node;
new_node->next = out;
}
else
{
printf("没有查找到out");
free(new_node);
}
}
}
2.2.4:指定位置后插入
有out位置前插入就有out位置后插入。 可以看下面的图片,其插入方法就是,将新车厢指向d在将out车厢指向新车厢即可。
我们还需要考虑其他情况吗?比如在e后面插入?自己思考就知道,不需要了。在后面插入,第三方存放的地址是NULL,新车厢指向第三方所存的地址,是可以的。
void s_l_insert_after(s_l_node* out, stl_type x) //out之后插入
{
assert(out);
s_l_node* new_node = add_capacity(x);
new_node->next = out->next;
out->next = new_node;
}
到现在我们学会了四大掌增添数据,后面就来学习后四大掌删除数据。
2.2.5:尾删
尾删顾名思义,删除最后一个。学会了上面四个删除还是比较简单的。我们找到链表最后一个删除即可
void s_l_out_back(s_l_node** pp)//尾删
{
assert(*pp);
s_l_node* del = *pp;
if (del->next->next == NULL)
{
free(del->next);
del->next = NULL;
}
else
{
while (del->next->next != NULL)
{
del = del->next;
}
free(del->next);
del->next = NULL;
}
}
2.2.6:头删
头删稍微复杂一点,但也是很简单的。删除最前面的数据。
void s_l_out_fornt(s_l_node** pp)//头删
{
assert(*pp);
s_l_node* del = *pp; //给它起始地址
*pp = del->next; //跳过首地址
free(del);
del->next = NULL;
}
2.2.7:在指定位置删除
有了上面的功底,看下面的图片,便很容易便可以写出来。我们需要考虑两种情况,那就是当只有a这一个数据的时候怎么删除。 我要删除F但链表里面没有F。
void s_l_erase(s_l_node** pp, s_l_node* out) // out位置删除
{
assert(*pp);
assert(out);
if (*pp == out) // 当只有一个数据的时候
{
s_l_out_fornt(pp);
}
s_l_node* del = *pp;
while (del->next != out && del->next != NULL) // 如何链表里面没有out
{
del = del->next;
}
if (del->next == out)
{
del->next = out->next;
free(out);
del->next = NULL;
}
else
{
printf("链表内没有这个数,无法删除它");
}
}
2.2.8:在指定位置后面删除
这个更加简单,只需要考虑链表不为空的情况即可。断言一下不就行了。
void s_l_del_after(s_l_node* out) //out后面删除
{
assert(out);
s_l_node* del = out->next;
out->next = del->next;
free(del);
del = NULL;
}
总结
至此八卦掌上部正式学完。要掌握为什么有的函数要用二级指针。