链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
注意: 链表在逻辑上是连续的,但在物理上不一定连续,链表不像顺序表那样在物理上是一点连续的,我们要特别注意这一点。
实际中链表的结构非常多样,我们有以下分类:
1、单向或者双向
2、带头或者不带头
3、循环或者非循环
而根据上面的3种情况组合起来就有8种链表结构,分别是:
1.无头单向不循环链表
2.无头单向循环链表
3.无头双向不循环链表
4.无头双向循环链表
5.有头单向不循环链表
6.有头单向循环链表
7.有头双向不循环链表
8.有头双向循环链表
但是虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成,所以我们要创建一个结点型,这个可以用结构体实现,在结构体里面有指针域和数据域,数据域用来存放数据,指针域存放下一个结点的地址。
创建类型如下:
typedef int SLNDataType; //对int类型重新起个名字叫SLNDataType
struct SListNode
{
SLNDataType data; //数据域
struct SListNode* next; //指针域
};
//对链表类型struct SListNode重新起个名字叫SLNode
typedef struct SListNode SLNode;
然后有了类型之后我们就在可以创建一个链表类型的指针plist,这个指针用来维护链表的第一个结点,所以我们有一点要非常注意:
如果想要改变第一个链表的第一个结点就要传指针plist的地址,然后要用二级指针来接收plist的地址,从而解引用来改变指针plist所存储的第一个结点地址,这样才会改变指针plist维护的第一个结点。
如果传指针plist的值,那么只是对plist内容的拷贝,不会改变plist里面存储的第一个结点地址,从而第一个结点仍然没有被改变。
//新开一个结点
SLNode* BuyNewNode(SLNDataType x)
{
SLNode* tmp = (SLNode*)malloc(sizeof(SLNode));
if (tmp == NULL)
{
perror("erron ");
exit(-1);
}
tmp->data = x;
tmp->next = NULL;
//把新开结点的地址返回去
return tmp;
}
//尾插
void SListPushBack(SLNode** pphead, SLNDataType x)
{
assert(pphead);
SLNode* tmp = BuyNewNode(x); //新开一个结点
//1.链表为空的情况
if (*pphead == NULL)
{
*pphead = tmp;
}
//2.链表为多个节点的情况,要找到最后一个结点
else
{
SLNode* cur = *pphead;
while (cur->next != NULL)
{
cur = cur->next;
}
//把新结点的地址给最后一个结点的指针域
cur->next = tmp;
}
}
//头插
void SListPushFront(SLNode** pphead, SLNDataType x)
{
assert(pphead);
SLNode* tmp = BuyNewNode(x);
tmp->next = *pphead;
//新的结点成为第一个结点
//就算链表为空也能实现
*pphead = tmp;
}
尾删分为三种情况:
1、链表为空就不能再删除了,直接报错。
2、链表为多个结点,如果删除的不是第一个结点我们可以传指针plist的值即可。
3、链表只有一个结点,但是链表要删除第一个结点我们还得传二级指针来改变指针plist存的地址,所以我们统一传二级指针来实现。
4、注意:删除尾部的数据还要把尾部的前一个结点的指针域置为NULL空指针,否则我们下次删除就找不到尾了。
代码实现如下:
//尾删
void SListPopBack(SLNode** pphead)
{
assert(pphead);
//空链表就不能再删除了
assert(*pphead);
//链表为一个结点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//链表为多个结点的情况
else
{
SLNode* cur = *pphead;
SLNode* prev = NULL;
while (cur->next != NULL)
{
prev = cur;
cur = cur->next;
}
free(cur); //释放结点
prev->next = NULL; //把倒数第二个结点指针域置为空
}
}
//头删
void SListPopFront(SLNode** pphead)
{
assert(pphead);
//空链表就不能再删除了
assert(*pphead);
SLNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
显示数据即把结点里面数据域的内容打印出来,我们在这个过程中没有改变第一个结点的地址,所以我们没有不必要传二级指针,直接传指针plist的值过去即可。然后根据指针plist里面存的地址一个接着一个访问结点就可以了,直到链表结束。
代码实现如下:
//显示数据
void SListPrint(SLNode* phead)
{
SLNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
这个接口和显示数据类似,我们在查找过程中一定不会改变第一个结点的地址,所以我们
没有不必要传二级指针,直接传指针plist的值过去即可。然后根据指针plist里面存的地址一个接着一个访问结点里面的数据域,如果找到了就返回这个结点的地址,找不到就返回空指针NULL。
代码如下:
//查找数据
SLNode* SListFind(SLNode* phead,SLNDataType x)
{
SLNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
//找到了就返回结点的地址
return cur;
}
cur = cur->next;
}
//找不到就返回空指针
return NULL;
}
这个接口有两种情况:
1、在其它结点前面插入
2、在第一个结点前面插入
如果是第一种情况我们可以传指针plist的值过去即可,但是如果是第二种情况,就要改变第一个结点的地址要传指针plist的地址过去才能修改plist里面的内容;所以我们还是统一传二级指针过去。
代码如下:
//在结点前面插入数据
void SListInsertPrev(SLNode** pphead, SLNode* pos, SLNDataType x)
{
assert(pphead);
assert(pos);
SLNode* tmp = BuyNewNode(x);
//1.如果要插入的位置在头部前面就头插
if (pos == *pphead)
{
tmp->next = *pphead;
*pphead = tmp;
}
//2.在其它位置就找前一个结点
else
{
SLNode* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = tmp;
tmp->next = pos;
}
}
这个比要在结点前面插入简单很多,不必要找前面一个结点的地址了。
因为不会改变链表第一个结点的地址,所以我们可以不用传二级指针也能实现。
代码如下:
//在结点后面插入数据
void SListInsertAfter(SLNode* pos, SLNDataType x)
{
assert(pos);
SLNode* tmp = BuyNewNode(x);
tmp->next = pos->next;
pos->next = tmp;
}
这个有2种情况:
1、删除的是不是第一个结点
2、删除的是第一个结点
因为删除当前位置数据有可能是第一个结点,所以我们统一传二级指针来实现。
代码如下:
//删除当前位置数据
void SListErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(pos);
//要删除的是第一个结点
if (*pphead == pos)
{
*pphead = pos->next;
free(pos);
}
//删除的是其它结点,要找到前一个结点
else
{
SLNode* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
}
这个接口不可能删除第一个结点,所以我们不用传二级指针来改变,直接指针plist存的地址即可。
不够要注意的是不能传最后一个结点的地址过来,因为最后一个结点后面没有数据可以删除了。
代码如下:
void SListEraseAfter(SLNode* pos)
{
assert(pos);
//不能传最后一个结点的地址过来
assert(pos->next != NULL);
pos->next = pos->next->next;
free(pos->next);
}
要注意的是不能一下子就释放第一个结点,要保存好下一个结点地址,一个接一个释放。
因为要释放第一个结点所以还要传二级指针来改变plist里面的地址。
代码如下:
void SListDestroy(SLNode** pphead)
{
SLNode* cur = *pphead;
while (cur != NULL)
{
SLNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
以上就是无头单向不循环链表各种接口的实现了,总的来说实现有几点要注意:
1、要改变指针plist所存储第一个结点的地址必须要传址实现
2、要把握好结点之间存放的信息
下面是我的测试代码:
#include "SList.h"
//测试头插和尾插
void TestPush()
{
SLNode* plist = NULL;
printf("尾插4个数据:\n\n");
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
printf("头插两个数据:\n");
SListPushFront(&plist, 8);
SListPushFront(&plist, 9);
SListPushFront(&plist, 10);
SListPrint(plist);
SListDestroy(&plist);
}
//测试头删和尾删
void TestPop()
{
SLNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
printf("尾删两个数据:\n");
SListPopBack(&plist);
SListPopBack(&plist);
SListPrint(plist);
printf("头删两个数据:\n");
SListPopFront(&plist);
SListPopFront(&plist);
SListPrint(plist);
SListDestroy(&plist);
}
//测试插入
void TestInsert()
{
SLNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushBack(&plist, 5);
SListPrint(plist);
SLNode* pos = SListFind(plist, 1);
if (pos != NULL)
{
printf("在1的前面插入99:\n");
SListInsertPrev(&plist, pos, 99);
SListPrint(plist);
}
pos = SListFind(plist, 4);
if (pos != NULL)
{
printf("在4的前面插入88:\n");
SListInsertPrev(&plist, pos, 88);
SListPrint(plist);
}
pos = SListFind(plist, 5);
if (pos != NULL)
{
printf("在5的后面插入99:\n");
SListInsertAfter(pos, 55);
SListPrint(plist);
}
SListDestroy(&plist);
}
//测试删除
void TestErase()
{
SLNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushBack(&plist, 5);
SListPrint(plist);
SLNode* pos = SListFind(plist, 2);
if (pos != NULL)
{
printf("把2删除掉:\n");
SListErase(&plist, pos);
SListPrint(plist);
}
pos = SListFind(plist, 4);
if (pos != NULL)
{
printf("把4删除掉:\n");
SListErase(&plist, pos);
SListPrint(plist);
}
pos = SListFind(plist, 3);
if (pos != NULL)
{
printf("删除掉2后面的下一个数据:\n");
SListEraseAfter(pos);
SListPrint(plist);
}
SListDestroy(&plist);
}
int main()
{
TestPush();
TestPop();
TestInsert();
TestErase();
return 0;
}
以上就是我的链表实现内容了,其中前面我只有把源文件SList.c 和测试文件test.c拆开来分析了。如果想要无头单向不循环链表的全部内容,阔以移步到gitee上获取。
内容链接:https://gitee.com/fait-juyuan/data-structure/tree/master/test_10_22_%E5%8D%95%E9%93%BE%E8%A1%A8/test_10_22_%E5%8D%95%E9%93%BE%E8%A1%A8