本篇的内容是对链表的一个介绍,并且会使用C语言实现链表的几个主要接口。
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
一个链表由n个节点组成,每一个节点都是一个结构体,这个结构体里的一个成员是该节点存储的数据,另一个成员是指向下一个节点的指针。
其结构如下
上图只是一种结构的链表,并不是所有的链表都是这样的结构,在实际中链表的结构非常多样,有以下几种情况:是否带头,单向还是双向,是否循环。共23=8种。
我们接下来来研究其中最典型的两种:无头单向不循环链表与带头双向循环链表。
1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结
构,如哈希桶、图的邻接表等等。其结构如图:
2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。其结构如图:
观察上图我们就可以发现,其实
双向链表与单向链表的区别就是双向链表比单向链表多了一个指针用来指向前一个节点。
循环链表的意思就是链表的最后一个节点不是指向空指针,而是指向链表的第一个节点,构成循环。
带头的意思就是在链表的第一个节点之前再加一个节点,这里不存放任何值,方便我们找到链表的头。
我们先申明我们链表节点的结构体类型
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SListNode;
然后我们要有函数来创建节点方面我们对链表进行操作
SListNode* BuySLTNode(SLTDataType x)
{
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
node->data = x;
node->next = NULL;
return node;
}
这是创建一个值为x,指向NULL的节点的函数。
实现对链表的打印功能
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
拿到链表的指针,然后赋给一个临时指针,用这个指针来遍历链表的每一个节点,然后逐个打印出来。
实现在链表的尾部插入数据的功能
void SListPushBack(SListNode** pplist, SLTDataType x)
{
SListNode* newnode = BuySLTNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
//先遍历找尾
SListNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
1.首先先创建出一个新的节点用来插入到链表中。
2.如果链表里面没有节点,那么我们创建的节点就会是这个链表的第一个节点,这时我们就需要改变链表的地址,那么参数传链表的地址就不能实现修改地址的作用,所以我们应该传链表地址的地址,二级指针,然后对传来的地址进行判断,如果为空,就把新建的节点地址当作链表的地址。
3.如果链表不是空,那么我们尾插首先要遍历链表找到链表的尾,然后把新的节点插入进去。
实现在链表头部插入数据的功能
void SListPushFront(SListNode** pplist, SLTDataType x)
{
SListNode* newnode = BuySLTNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
1.与尾插类似,先创建新节点。
2.因为我们知道头节点的地址,所以可以直接把新的节点链接到头节点上。
3.在头节点之前链接节点后,新的节点成为了头节点,改变了链表的地址,所以传二级指针。
实现删除单链表最后一个节点的功能
void SListPopBack(SListNode** pplist)
{
if (*pplist == NULL)
{
return;
}
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* prev = NULL;
SListNode* tail = *pplist;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
1.首先判断链表是否还有节点可删,如果链表已经为空,就直接返回。
2.如果链表里只剩一个节点,删除后链表为空,链表的地址就应该置为空,也有可能改变链表的地址,所以传二级指针。
3.一般情况时,删除最后一个节点的同时我们还要找到倒数第二个节点,然后把它变成尾节点,即让它指向NULL,所以我们需要两个指针同时寻找这两个位置,然后释放掉最后一个节点,修改倒数第二个节点。
实现删除链表第一个节点的功能
void SListPopFront(SListNode** pplist)
{
if (*pplist == NULL)
{
return;
}
else
{
SListNode* tmp=( * pplist)->next;
free(*pplist);
*pplist = tmp;
}
}
1.头删直接释放头节点后让第二个节点做头,肯定会改变链表的地址,所以传二级指针。
2.判断链表是否还有节点可以删。
3.直接释放第一个节点,然后把第一个节点指向的位置置为链表的头,如果链表只有一个元素,头删过后链表为空,而这时唯一一个节点指向的也正好是NULL,所以当前代码可以符合这种情况。
实现在链表中查找某个元素的功能
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
SListNode* cur = plist;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
1.查找节点不需要对链表的地址进行改变,所以传一级指针。
2.遍历链表,寻找数据为指定值的节点,如果能找到,就返回该节点的地址,找不到就返回空。
已知链表的一个节点的地址,实现在这个节点之后插入一个节点的功能
一般用于在查找接口找到地址之后,利用查找到的地址来插入节点。
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
1.判断地址是否为空。
2.创建新的节点。
3.把新节点链接到pos的后面,然后新节点自己指向原本是pos后边的节点。
实现在一个已知节点地址之前插入节点的功能
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySLTNode(x);
if (pos == *pplist)
{
newnode->next = *pplist;
*pplist = newnode;
}
else
{
SListNode* prev = NULL;
SListNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
1.判断地址是否为空。
2.创建新节点。
3.如果已知的地址是链表的第一个节点的地址,那么就是对这个链表进行头插,会改变链表的地址,所以传二级指针,然后让新节点做链表的头。
4.一般情况,如果要在pos之前插入节点,那么我们还要改变pos之前的节点的指针,所以我们需要遍历链表来找到pos之前的节点,然后让之前的节点的指针指向新节点,新节点指向pos,实现链接。
实现删除指定位置节点的后一个节点
void SListEraseAfter(SListNode* pos)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
SListNode* next = pos->next;
pos->next = next->next;
free(next);
}
}
1.判断指定位置是否为空。
2.判断指定位置后面是否还有节点。
3.一般情况,让pos节点指向后一个节点指向的节点,然后时候pos后面的节点。
实现删除指定位置的节点的功能
void SListEraseCur(SListNode** pplist, SListNode* pos)
{
assert(pos);
if (pos == *pplist)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* prev = NULL;
SListNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = cur->next;
free(cur);
}
}
1.判断指定位置是否为空。
2.如果指定位置是头节点,就释放头节点,然后让头节点指向的节点做头,如果只有一个节点,那正好链表被置空。
3.一般情况下,与在指定位置之前插入节点类似,我们需要对指定位置之前的节点进行操作,所以需要遍历链表寻找之前的节点,然后释放pos位置的节点,链接pos之前和pos之后的节点。
头文件:SList.h
#pragma once
#include
#include
#include
typedef int SLTDataType;
typedef struct SListNodep
{
int data;
struct SListNode* next;
}SListNode;
//单项 不带头 不循环
//尾插
void SListPushBack(SListNode** pplist, SLTDataType x);
//头插
void SListPushFront(SListNode** pplist, SLTDataType x);
//尾删
void SListPopBack(SListNode** pplist);
//头删
void SListPopFront(SListNode** pplist);
//寻找
SListNode* SListFind(SListNode* plist, SLTDataType x);
//在pos之后插入
void SListInsertAfter(SListNode* pos, SLTDataType x);
//在pos之前插入
void SListInsertBefore(SListNode* plist, SListNode* pos, SLTDataType x);
//删除pos之后
void SListEraseAfter(SListNode* pos);
//删除pos自己
void SListEraseCur(SListNode** pplist, SListNode* pos);
源文件:SList.c
#define _CRT_SECURE_NO_WARNINGS
#include "SList.h"
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SListNode* BuySLTNode(SLTDataType x)
{
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
node->data = x;
node->next = NULL;
return node;
}
void SListPushBack(SListNode** pplist, SLTDataType x)
{
SListNode* newnode = BuySLTNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
//先遍历找尾
SListNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPushFront(SListNode** pplist, SLTDataType x)
{
SListNode* newnode = BuySLTNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
void SListPopBack(SListNode** pplist)
{
if (*pplist == NULL)
{
return;
}
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* prev = NULL;
SListNode* tail = *pplist;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
void SListPopFront(SListNode** pplist)
{
if (*pplist == NULL)
{
return;
}
else
{
SListNode* tmp=( * pplist)->next;
free(*pplist);
*pplist = tmp;
}
}
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
SListNode* cur = plist;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySLTNode(x);
if (pos == *pplist)
{
newnode->next = *pplist;
*pplist = newnode;
}
else
{
SListNode* prev = NULL;
SListNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
void SListEraseAfter(SListNode* pos)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
SListNode* next = pos->next;
pos->next = next->next;
free(next);
}
}
void SListEraseCur(SListNode** pplist, SListNode* pos)
{
assert(pos);
if (pos == *pplist)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* prev = NULL;
SListNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = cur->next;
free(cur);
}
}
以上就是无头单项非循环链表主要的接口实现,在实现时要注意以下几点:
1.判断该接口是否有可能改变链表的地址,如果可能,那传参时就需要传二级指针。
2.在对指定的位置进行操作时,如果我们同时也要对指定位置之前的节点进行操作,那么我们只能遍历链表来找到之前的节点。
3.要对指定的地址进行判断,判断是否为空。
该链表的结构前面已经说明过了,那我们就可以声明对于的结构来实现节点。
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType val;
}ListNode;
两个指针,prev指向前一个,next指向后一个。
先写一个创建新节点的函数
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->next = NULL;
node->prev = NULL;
node->val = x;
return node;
}
在我们创建链表时,我们要先创建一个头节点,然后对它完成链接,形成一个元素个数为0,只有头节点的双向循环链表。
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
完成了上述内容,我们就要对该链表的接口进行实现了
实现对该链表的打印功能
void ListPrint(ListNode* phead)
{
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
通过头指针找到第一个节点,然后遍历该链表,挨个打印节点的数据,直到回到头节点。
实现在双向循环链表尾部插入节点的功能
void ListPushBack(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* tail = phead->prev;
ListNode* newnode = BuyListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
1.判断传入的头指针是否为空。
2.创建一个新节点。
3.因为是循环链表,所以可以通过头节点的prev指针直接找到链表的最后一个节点,然后把新节点链接上去即可,如果链表为空,那该代码也可以实现功能。
实现在链表头部插入节点的功能
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
1.判断头节点是否为空。
2.创建新节点。
3.把新节点链接在头节点后面即可,链表里原本有没有节点都可以实现。
删除最后一个节点
void ListPopBack(ListNode* phead)
{
assert(phead);
assert(phead->next != phead);
ListNode* tail = phead->prev;
tail->prev->next = phead;
phead->prev = tail->prev;
free(tail);
}
1.判断头节点。
2.还要判断链表是否还有节点。
3.把最后一个节点链接出去,然后释放该节点即可,也不用考虑非一般情况。
删除第一个节点
void ListPopFront(ListNode* phead)
{
assert(phead);
assert(phead->next != phead);
ListNode* first = phead->next;
first->next->prev = phead;
phead->next = first->next;
free(first);
}
1.判断头节点。
2.判断链表是否为空。
3.把第一个节点链接出去,然后释放,任然可以适用与非一般情况。
在链表中查找一个元素
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
{
return cur;
}
cur=cur->next;
}
return NULL;
}
1.判断头节点。
2.遍历链表,寻找指定的元素,找到了返回地址,找不到返回NULL。
在指定位置插入一个节点,一般配合查找接口使用
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->prev = newnode;
prev->next = newnode;
newnode->prev = prev;
}
1.判断指定位置是否为空
2.创建新节点。
3.找到该位置之前的节点,然后把新节点连接到该位置之前。
删除指定位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
1.判断指定位置是否为空。
2.把该位置的节点链接出去。
2.释放该节点的空间。
判断一个链表是否为空
int ListEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead ? 1 : 0;
}
1.判断头节点。
2.使用三目表达式,链表为空返回1,不为空返回0。
计算一个链表的大小
int ListSize(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
int size = 0;
while (cur != phead)
{
cur = cur->next;
size++;
}
return size;
}
1.判断头节点。
2.遍历链表,统计链表节点的个数。
销毁该链表
void ListDestory(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
int size = 0;
while (cur != phead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
//调用结束后在外面吧指针置空
}
1.判断头节点。
2.遍历链表,把链表的每一个节点挨个释放,最后再释放头节点,当我们释放完头节点,为了防止野指针应该把头节点的指针置空,但是我们不能在接口里面把头节点的指针置空,因为我们传的是一级指针,不能改变参数的地址,它只是头节点地址的一个临时拷贝,置空它不会起作用,只有当我们传二级指针时,才可以在接口里面把头节点的地址置空,但是为了接口的一致性,我们还是使用一级指针,但是要标注使用者需要在接口外面把头节点地址置空。
头文件:List.h
#pragma once
#include
#include
#include
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType val;
}ListNode;
void ListPrint(ListNode* phead);
ListNode* BuyListNode(LTDataType x);
ListNode* ListCreate();
void ListPushBack(ListNode* phead, LTDataType x);
void ListPushFront(ListNode* phead, LTDataType x);
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);
// 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
//空返回1,非空返回0
int ListEmpty(ListNode* phead);
int ListSize(ListNode* phead);
void ListDestory(ListNode* phead);
因为尾插头插,尾删头删可以当作是指定位置插入和指定位置删除的情况之一,所以我们又可以对代码进行简化。
源文件:List.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->next = NULL;
node->prev = NULL;
node->val = x;
return node;
}
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
void ListPushBack(ListNode* phead, LTDataType x)
{
ListInsert(phead, x);
}
void ListPrint(ListNode* phead)
{
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
void ListPushFront(ListNode* phead, LTDataType x)
{
ListInsert(phead->next, x);
}
void ListPopBack(ListNode* phead)
{
ListErase(phead->prev);
}
void ListPopFront(ListNode* phead)
{
ListErase(phead->next);
}
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
{
return cur;
}
cur=cur->next;
}
return NULL;
}
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->prev = newnode;
prev->next = newnode;
newnode->prev = prev;
}
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
int ListEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead ? 1 : 0;
}
int ListSize(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
int size = 0;
while (cur != phead)
{
cur = cur->next;
size++;
}
return size;
}
void ListDestory(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
int size = 0;
while (cur != phead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
//调用结束后在外面吧指针置空
}
在我们实现这两种链表时,尤其是带头双向循环链表,虽然它的结构比较复杂,但是在我们实现接口时非常简单,不需要考虑特殊的情况,这是带头双向循环链表的结构优势。
而与顺序表相比,链表的优势在于没有空间的浪费,我们需要一个元素就创建一个空间,删除一个元素就释放一个空间,不存在空间的浪费和性能消耗,并且很适合在任意位置插删数据,尤其是双向链表,时间复杂度只有O(1)。
但是链表的缺点就是不支持下标的随机访问。
我们发现链表的优点其实就是顺序表的缺点,链表的缺点就是顺序表的优点,这两个数据结构是相辅相成的,互相弥补对方的缺点,所以对他们的使用要结合具体的场景。