目录
前言:继上面的顺序表实现完成后,这次我们来实现单链表。
这个单链表是不带哨兵位的头节点的。
一.单链表逐步实现
1.创建结构体
要想创建一个单链表,我们先创建3个文件分别为SList.h, SList.c, Test.c
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SListNode;
这个typedef在上次顺序表说过,这次再说一遍。
在创建结构体的时候,我们会发现如果我们这次想要存储的数据类型为int类型,而下次想要存储其它数据想要修改,就必须把后面所有的int都修改一遍,很麻烦。因此我们可以使用typedef用SLTDataType作为数据类型,这时如果想要存储其它类型,则只需要在这修改。
2.不需要初始化
为什么第二个就是打印链表,而不是初始化链表呢?
之前的顺序表之所以要初始化时因为刚开始就有一个结构体,结构体中指向数组的指针为空,这个结构体内的元素是随机值;而链表最开始的一个值都没有的时候,并没有结构体(没有节点),而只有一个指针,因为现阶段的结构只需要有一个指针就可以了。
但是我们需要创建一个结构体指针,我们可以把它置为空,用它作为头节点。
例如:
SListNode* slist = NULL;
3.打印链表
打印链表只需要收到链表头的指针即可,不需要对链表进行更改,所以用一级指针接收头指针。
通过创建一个临时变量cur,对cur进行操作。
cur = cur ->next,就使cur可以不断的找到之后的节点,使之可以读取到链表中的数据data。
这里我用的是while循环,条件为cur != NULL的原因是:当cur = cur->next直到最后一个节点的下一个时,由于没有创建空间,就不能找到那块空间,所以是NULL,因此当cur == NULL的时候就说明该链表已经没有节点了。
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
4.尾插
尾插的时间复杂度为O(N)
这个需要我们传二级指针,原因在于,这个单链表没有头节点,当我们想要插入第一个数据时,插入的是头节点中,这时我们需要改变这个头节点,传二级指针相当于传原来的slist指针的地址,而传址,在函数中改变形参就可以连带着改变实参。如果就传一级指针,相当于是传值,因为原来的slist就是一级指针。
当然了,如果不想传二级指针,那也可以像print一样传一级指针,但必须返回头节点!
例如:(返回头节点)
SListNode* SListPushBack(SListNode* phead, SLTDataType x)
或者:(传二级指针)
void SListPushBack(SListNode** pphead, SLTDataType x)
这两种方法都是可以的。
接下来说说内部实现:
1. 首先要想插入一个元素,第一步就是要创建一个新节点newnode。在这里,是使用了BuyListNode这个函数去创建新节点(原因就是单链表不止要实现尾插,还有头插,插入,都需要创建新节点,因此可以创建一个函数)。
创建节点c语言就可以使用malloc,这时如果malloc失败了,就应该报错,并直接exit终止程序。创建成功就让该节点的data的值为该值,然后让next为空,因为我们不确定这是不是链表中创建的最后一个元素。
2. 创建好节点之后,我们就要把该节点连接在链表中,因为这个是尾插,我们需要插入到最后一个节点后,所以我们可以创建一个tail变量,通过tail = tail->next,一直找到最后一个节点。最后一个节点的特点就是它的next的指针为空,所以while条件为tail->next != NULL。最后找到了原本的尾节点后,就再让最后一个节点的next指针存储newnode的地址。
3.要注意一种特殊情况,就是当链表中没有节点时,传过来的pphead是空指针,tail也就是空指针,此时tail->next是非法访问,会报错,所以可以用if else来区分这两种情况,这种情况就是当传过来的指针是NULL,就直接让*pphead = newnode。而这个地方就是为什么要传二级指针的原因,因为这对链表进行了修改(对头节点进行了修改)。
SListNode* BuyListNode(SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
else
{
newnode->data = x;
newnode->next = NULL;
}
return newnode;
}
void SListPushBack(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuyListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SListNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
5.头插
说完了尾插,我们来看看头插。
头插的时间复杂的为O(1)
头插相比于尾插,时间复杂度低,效率要高,因此头插要比尾插好,也更好实现。
头插就只需要创建一个节点,之后让节点的next存头节点的地址,最后让这个节点变成新的头节点即可,并且这个不会有特殊情况的发生。
void SListPushFront(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
6.尾删
尾删时间复杂度为O(N)
尾删需要多注意一些地方,包括像尾插那样,如果只有一个头节点怎么办,又或者链表中没有节点。这些都是需要注意的地方。
因为要删除最后一个节点,所以我们可以创建tail取变量为了找到最后一个节点,这个与尾插类似,但是因为是删除,所以我们还要找到尾节点的前一个节点,并把前一个节点的next置NULL,否则当尾节点被删除了,next还保留原本尾节点的地址,就是非法访问,所以一定要置NULL。
而两种特殊情况,一种是链表为空的时候,链表没有可以删的,所以直接return,第二种是只有一个节点的时候,就直接free掉头节点,并且把头节点置NULL。
void SListPopBack(SListNode** pphead)
{
assert(pphead);
if (*pphead == NULL)
{
return;
}
else if ((*pphead) ->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SListNode* prev = *pphead;
SListNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
7.头删
头删时间复杂度为O(1)
头删依旧比尾删好,效率高,并且实现简单。
头删与尾删的关系,类似于头插与尾插的关系,尾插和尾删都需要考虑头节点的情况,而尾删和头删需要注意链表为空的情况。
头删,我们只需要创建一个变量next去保存头节点的下一个节点的地址,之后直接free头节点,之后再让头节点为next即可。
这个也需要考虑链表为空的情况,如果为空,直接return。
void SListPopFront(SListNode** pphead)
{
assert(pphead);
if (*pphead == NULL)
{
return;
}
else
{
SListNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
8.查找
这个很简单,就是为了使用Find查找链表中的节点,需要返回该节点,所以返回类型为SListNode*。
只需要创建一个临时变量,并循环链表中的节点,如果cur->data == x为真,就说明这个是想要查找的节点,然后就返回该节点。如果到尾了,还没有查找到,就返回NULL。
SListNode* SListFind(SListNode** pphead, SLTDataType x)
{
SListNode* cur = *pphead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
9.在pos位置之前插入
时间复杂度为O(N)
这个也是插入,只是可以选择插入的位置。
这个与尾插类似,只是这个尾并不是真的尾,并且是插在这个所谓的尾的前面。
同时这个也要考虑pos为头节点这种特殊情况。
因为要插在pos位置之前,所以我们可以定义变量prev,用以通过prev = prev->next,找到pos位置的前一个节点,因此条件是prev->next != pos,这时,我们就只需要创建这个节点,并插入到pos位置之前。
如果pos是头节点,那么prev->next为非法访问,是错误的,所以我们要单独写这种条件,即当pos == *pphead的时候,直接调用头插函数即可。
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SListPushFront(pphead ,x);
}
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->next = pos;
}
10.在pos位置之后插入
时间复杂度为O(1)
这个相比于上一个,效率更高,并且c++中的STL内部的单链表实现就为这种插入方法。
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* next = pos->next;
SListNode* newnode = BuyListNode(x);
pos->next = newnode;
newnode->next = next;
}
这个很简单,我们甚至不需要传头节点,因为我们可以根据pos直接找到pos的下一个位置的地址,所以我们直接把创建的节点链接到pos节点和pos原下一个节点之间。
11.删除pos位置
时间复杂度为O(N)
这个也是需要考虑特殊情况的。
类似于在pos位置之前插入,也需要一个prev变量,去找到pos之前的节点,然后使之和pos位置之后的节点链接,再free掉pos节点。
而如果出现只剩一个头节点的情况时,也是由于prev->next会非法访问,所以要分情况,而如果只有一个头节点的时候,就之间调用头删函数即可。
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
12.删除pos位置之后的值
时间复杂度O(1)
也行我们可能觉得这个有一些奇怪,但是实际上c++的STL中就是用的这种。
因为它的时间复杂度低,效率高。
这个很简单,也是不需要传头节点地址,只需要创建一个next去存pos的下一个节点的地址,并且当这个next节点不为空时,就先让pos链接到pos的下一个节点的下一个节点,然后就free掉pos后的节点,并把next置NULL。
void SListEraseAfter(SListNode* pos)
{
assert(pos);
SListNode* next = pos->next;
if (next)
{
pos->next = next->next;
free(next);
next = NULL;
}
}
13.销毁链表
最后一步,销毁链表。
创建一个临时变量,通过该变量的while循环把节点逐个free掉。
在whle中创建一个next为了保存下一个节点的地址,否则,free掉该节点之后,就无法找到下一个节点的地址了,保存之后再free,然后再让cur = next。最后再把头节点置NULL。
void SListDestroy(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
二.代码
1.SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SListNode;
//打印链表
void SListPrint(SListNode* phead);
//尾插
void SListPushBack(SListNode** pphead, SLTDataType x);
//头插
void SListPushFront(SListNode** pphead, SLTDataType x);
//尾删
void SListPopBack(SListNode** pphead);
//头删
void SListPopFront(SListNode** pphead);
//查找
SListNode* SListFind(SListNode** pphead, SLTDataType x);
//在pos位置之前插入
void SListInser(SListNode** pphead, SListNode* pos, SLTDataType x);
//在pos位置之后插入
void SListInsertAfter(SListNode* pos, SLTDataType x);
//删除pos位置
void SListErase(SListNode** pphead, SListNode* pos);
//删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
//销毁链表
void SListDestroy(SListNode** phead);
2.SList.c
#include "SList.h"
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SListNode* BuyListNode(SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
else
{
newnode->data = x;
newnode->next = NULL;
}
return newnode;
}
void SListPushBack(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuyListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SListNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPushFront(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPopBack(SListNode** pphead)
{
assert(pphead);
if (*pphead == NULL)
{
return;
}
else if ((*pphead) ->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SListNode* prev = *pphead;
SListNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
void SListPopFront(SListNode** pphead)
{
assert(pphead);
if (*pphead == NULL)
{
return;
}
else
{
SListNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
SListNode* SListFind(SListNode** pphead, SLTDataType x)
{
SListNode* cur = *pphead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SListPushFront(pphead ,x);
}
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->next = pos;
}
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* next = pos->next;
SListNode* newnode = BuyListNode(x);
pos->next = newnode;
newnode->next = next;
}
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
void SListEraseAfter(SListNode* pos)
{
assert(pos);
SListNode* next = pos->next;
if (next)
{
pos->next = next->next;
free(next);
next = NULL;
}
}
void SListDestroy(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
3.Test.c
我的这个并不重要,但是我们在写链表的时候,一定要充分利用这个去测试自己刚刚完成的接口函数是否有错误,否则链表全部写完时,再发现错误,会比较难找。所以大家一定要好好利用这个去测试接口函数的对错。
#include "SList.h"
void TestSList1()
{
SListNode* slist = NULL;
SListPushBack(&slist, 1);
SListPushBack(&slist, 2);
SListPushBack(&slist, 3);
SListPushBack(&slist, 4);
SListPushBack(&slist, 5);
SListPushFront(&slist, 0);
SListPushFront(&slist, -1);
SListPushFront(&slist, -2);
SListPrint(slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPopBack(&slist);
SListPrint(slist);
SListPushBack(&slist, 30);
SListPushBack(&slist, 50);
SListPrint(slist);
}
void TestSList2()
{
SListNode* slist = NULL;
SListPushFront(&slist, 0);
SListPushFront(&slist, 1);
SListPushFront(&slist, 2);
SListPushFront(&slist, 3);
SListPrint(slist);
SListPopFront(&slist);
SListPopFront(&slist);
SListPopFront(&slist);
SListPrint(slist);
SListPopFront(&slist);
SListPopFront(&slist);
SListPopFront(&slist);
SListPrint(slist);
SListPushBack(&slist , 30);
SListPushBack(&slist, 40);
SListPrint(slist);
}
void TestSList3()
{
SListNode* slist = NULL;
SListPushBack(&slist, 1);
SListPushBack(&slist, 2);
SListPushBack(&slist, 3);
SListPushBack(&slist, 4);
SListNode* pos = SListFind(&slist, 3);
if (pos)
{
printf("找到了:%p\n", pos);
//修改
pos->data *= 10;
}
SListPrint(slist);
}
void TestSList4()
{
SListNode* slist = NULL;
SListPushBack(&slist, 1);
SListPushBack(&slist, 2);
SListPushBack(&slist, 3);
SListPushBack(&slist, 4);
SListPrint(slist);
SListNode* pos = SListFind(&slist, 3);
if (pos)
{
SListInsert(&slist, pos, 30);
}
SListPrint(slist);
pos = SListFind(&slist, 2);
if (pos)
{
SListInsertAfter(pos, 100);
}
SListPrint(slist);
}
void TestSList5()
{
SListNode* slist = NULL;
SListPushBack(&slist, 1);
SListPushBack(&slist, 2);
SListPushBack(&slist, 3);
SListPushBack(&slist, 4);
SListPrint(slist);
SListNode* pos = SListFind(&slist, 3);
if (pos)
{
SListErase(&slist, pos);
}
SListPrint(slist);
pos = SListFind(&slist, 1);
if (pos)
{
SListEraseAfter(pos);
}
SListPrint(slist);
}
int main()
{
//TestSList1();
//TestSList2();
//TestSList3();
//TestSList4();
TestSList5();
return 0;
}