初识链表
目录
1.链表的概念及结构
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
定义这样一个结构体,就可以完成链表的连接:
从定义和例子中间我们不难总结出:
1、链式结构在逻辑上是连续的,但是在物理上是不连续的,所谓的链,不过是我们构建的理论模型
2、现实中的结点一般都是从堆上申请出来的
3、从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
2.单链表
链表的种类是很多的,我们先来实现最不复杂的单链表的这几个功能吧
//打印
void SListPrint(SLTNode* phead);
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SListPushFront(SLTNode* pphead, SLTDataType x);
//动态申请一个节点
SLTNode* BuySListNode(SLTDataType x);
//尾删
void SListPopBack(SLTNode** pphead);
//头删
void SListPopFront(SLTNode** pphead);
//查找
void SListFind(SLTNode* phead, SLTDataType x);
//任意位置的插入和删除
void SListInsert(SLTNode** pphead,SLTNode * pos, SLTDataType x);
void SListErase(SLTNode** pphead, SLTNode* pos);
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
首先我们来个简单的:
打印
我们知道,打印数组是从0下表打印到size下标,那么打印链表呢?我们怎么找到它的各个节点呢?
void SListPrint(SLTNode* phead)
{
SLTNode* cur =phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;//相当于数组里面的i++
}
printf("NULL\n");
}
思路:先找到第一个结构体的地址,进而找到首结构体中的next指针,进而找到next指针指向的结构体中的next指针...每次进入一个结构体都将其中的data打印出来,这就是链表的遍历过程的实现
我们再看个难度高的:
后插
我们知道在数组中后插是要考虑扩容和越界的,但是链表和数组不同,他要考虑的是指针所带来的的问题
思路:我们先把要插入的数据x放到一个动态申请的节点里,然后让这个节点的next指向NULL(因为是在最后插入)随后我们找到原链表中的最后一个节点,把这个节点的next指向我们新申请的节点就OK了。
注意:如果这个链表为空,那么首个节点的地址就是个空指针,这个时候我们找不到所谓的最后一个节点,所以这种情况时我们直接把我们申请的节点作为首元素(*phead=STLNode *newnode)
于是,我们有:
void SListPushBack(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{ //找尾结点
SLTNode* tail = phead;
while (tail != NULL)
{
tail = tail->next;
}
tail->next = newnode;//让原来的NULL指向newnode
}
}
但是很遗憾,上面这串代码是纯小丑代码🤡
为什么小丑呢?
因为形参的改变无法让实参改变。而我们尾插到最后有可能要把第一个结构体的地址改变的(若首个结构体的地址为空,那直接吧newnode赋给它)
所以上面这串代码终归只是在自己的函数体内自导自演,没有意义。
我们知道,形参的改变不会影响实参,如果想要影响实参,就得传实参的地址!在这里,在链表为空时,改变的是首个节点的地址,因此我们得传:首个节点的地址的地址!
即:
void SListPushBack(SLTNode**pphead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//这个函数结束了newnode确实是会销毁的,但是malloc所创建的空间是不会销毁的
assert(newnode);
newnode->data = x;
newnode->next = NULL;
if (*pphead == NULL)//注意这个特殊情况
{
*pphead = newnode;
}
else
{ //找尾结点
SLTNode* tail = *pphead;
while (tail != NULL)
{
tail = tail->next;
}
tail->next = newnode;//让原来的NULL指向newnode
}
}//大功告成!
克服了上面的这一难关,那接下来的实现,就没那么可怖了,比如:
前插
思路:我们直接申请个节点,然后让这个节点指向原来链表的首个节点,然后再让这个节点的地址成为链表的地址就好了
注意:因为这里我们同样要修改链表的地址,所以还是得传二级指针
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
//SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//这个函数结束了newnode确实是会销毁的,但是malloc所创建的空间是不会销毁的
//assert(newnode);
//newnode->data = x;
//newnode->next = NULL;
//由于只要插入基本都要上面这几步,所以我们直接把这一流程写成一个函数;
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
意外收获:
动态申请节点
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
return newnode;
}
那删除呢?看看
前删
void SListPopFront(SLTNode** pphead)
{
assert(*pphead);
/*if (*pphead == NULL)
return;*///温柔类型的检查
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
思路:吧第一个节点释放掉,然后让第二个节点成为第一个节点,简单
那下面这如何呢?
后删
后删时我们要考虑的特殊情况有了:
1.删除了最后一个节点,倒数第二节点的next变成了野指针
2.删完了这个链表就变成了空链表
void SListPopBack(SLTNode** pphead)
{
assert(*pphead);
//分两种情况:1.只有一个节点2.多个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//SLTNode* tailPrev = NULL;//定义这个指针是为了防止最后一个结构体被free之后上一个next指针成为了野指针
//SLTNode* tail = *pphead;
//while (tail->next != NULL)
//{
// tailPrev = tail;
// tail = tail->next;
//}
//free(tail);
//tailPrev ->next = NULL;
SLTNode* tail = *pphead;
while (tail->next->next != NULL)//这一套写法的思路是:找倒数第二个
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
思路:我们把最后一个节点释放,在这一过程中,我们找到倒数第二个节点的next指针,使其指向NULL即可(或者直接找倒数第二个节点,反正这个节点有指向最后一个节点的指针)另外当删完之后这个链表啥都不剩了的话,我们释放完了它最后一个节点之后,一定要记得吧*phead置为NULL;
最后是
查找
void SListFind(SLTNode* phead, SLTDataType x)//从前面我们可知:查找、打印这种函数不会改变链表就传一级指针
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}//把找到的指针->data修改一下就是修改功能的实现了,即查找的附带功能就是修改。
思路:从前往后找,找到了所要找的data就停。
另外,这个查找函数也可以附带实现修改:把找到的的指针->data修改一下