链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
如图所示,链表与顺序表不同的地方在于,链表的存储并不是在一块连续的空间中,而是通过动态分配的一个个内存块链接起来进行存储的。图中的指针就是用来存储下一个内存块的地址,通常用next表示。链表的结构在逻辑上看起来是一个接一个连接起来的,但是在内存中,却是散乱排布的,只能通过指针定位。
下面我会详细说明链表是如何实现的:
单向链表的实现
单向链表的结构体定义是这样的:
typedef struct List
{
LDataType data;
struct List* next;
}List;
其中有一个数据位来存储数据,还有一个指针位指向下一个节点的位置,通过指针将该节点和后面的节点链接起来。
动态申请节点
对于链表来说,想要存储一个数据,首先需要开辟一个节点,作为存放数据的容器。我们通过malloc函数来获得一个节点。
List* BuySListNode(LDataType x)
{
//动态开辟一个节点
List* Node = (List*)malloc(sizeof(List));
if (Node == NULL)
{
printf("节点开辟失败\n");
exit(-1);
}
Node->data = x;
Node->next = NULL;
return Node;
}
单链表的打印
我们在写单向链表的时候,如果想要测试的话,可以通过调试窗口来进行查看,但是通过将链表打印出来能够更加直观的表现,同时也能够检查链表中的连接是否出现错误。
// 单链表打印
void SListPrint(List* plist)
{
assert(plist);
List* cur = plist;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
通过cur指针来维护,最开始cur指向链表的头,从前往后进行遍历,每次对其中的数据进行打印,直到cur指向的为空指针,标志着走到尾部。
单向链表的尾插
单向链表的插入有很多种,首先介绍一下尾插,尾插也就是在单向链表的尾部进行插入。
// 单链表尾插
void SListPushBack(List** pplist, LDataType x)
{
assert(pplist);
List* newNode = BuySListNode(x);
List* cur = *pplist;
//第一种情况:链表为空,直接在头插入数据
if (*pplist == NULL)
{
*pplist = newNode;
}
//第二种情况:链表不为空,在某个数据的后面插入
else
{
//如果头节点不是空,就说明链表中有元素
//要先找到尾部
while (cur->next)
{
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
}
每次进行尾插时要通过遍历找到最后一个节点,然后进行插入。但是这其中分为两种情况:第一种是如果链表是一个空链表的话,也就不存在尾部,所以要先在头节点中存入数据;第二种是链表为非空,此时是有尾节点的,这时我们通过遍历找到链表中的最后一个节点,然后进行插入。
需要注意的是,这里连接的顺序是非常有讲究的,要先将新节点的next指向尾节点的next,再让尾节点的next指向新节点。如果顺序颠倒的话,就无法找到尾节点的next了,但是非要这么做的话,可以定义一个变量保存尾节点的next。
注意:在进行尾插的时候我传的是二级指针,这是因为节点本身就是一个指针,如果想要修改的话,必须要传它的地址过去,因此需要用二级指针接收。后面功能的实现,有修改链表的地方我都会用二级指针来传递,就不再解释了
单向链表的尾删
有了尾插必定会有尾删的操作,尾删就是在尾部删除,那么应该如何做呢?
// 单链表的尾删
void SListPopBack(List** pplist)
{
assert(pplist);
assert(*pplist);
List* cur = *pplist;
List* prev = NULL;
//尾删首先要找到最后的节点
while (cur->next)
{
prev = cur;
cur = cur->next;
}
//如果prev不为空,说明链表中至少还有两个元素
if (prev)
{
prev->next = cur->next;
free(cur);
cur = NULL;
}
//如果prev为空,说明链表中只剩下一个元素,进行尾删的话要把pplist置为空指针
else
{
free(*pplist);
*pplist = NULL;
}
}
想要进行尾删的话,首先需要遍历找到尾节点,但是这里有一点要注意,尾删需要让尾节点的前面的节点连接到尾节点的next,因此我们在遍历时,需要保存每一个节点的前驱prev,直到找到尾节点,此时,prev指向的是尾节点的前驱。最后让尾节点的前驱连接到尾节点的后继即可。
尾插也有两种情况需要注意:一个是链表中至少还有两个节点的时候,这种情况是存在尾节点的,因此我们按照上面的方法操作。还有一种情况比较特殊,就是链表中只剩下一个节点的时候,此时我们就不需要找到尾节点,直接删除这个节点即可。删除之后,链表就会变成空链表,所以最后我们需要将头节点置空。下一次进行插入的时候就按照空链表进行插入。
单向链表的头插
这是单向链表的另一种插入方式,头插的方式要比尾插要简单很多,因为尾插的时候,需要遍历来找到尾节点,而头插并不需要这样子,头插只要记录下来头节点的下一个节点,然后进行插入即可。当然还要注意一点,当链表为空的时候,此时插入是把新的节点给到头节点。
// 单链表的头插
void SListPushFront(List** pplist, LDataType x)
{
assert(pplist);
List* newNode = BuySListNode(x);
List* head = *pplist;
//第一种:链表为空
if (*pplist == NULL)
{
*pplist = newNode;
}
//第二种:链表不为空
else
{
newNode->next = head;
*pplist = newNode;
}
}
单向链表的头删
对于单向链表的头删,其实也很简单,只需要记录头节点的下一个节点,然后释放掉头节点,使下一个节点作为新的头节点。
// 单链表头删
void SListPopFront(List** pplist)
{
assert(pplist);
List* head = *pplist;
*pplist = (*pplist)->next;
free(head);
}
单向链表的查找
// 单链表查找
List* SListFind(List* plist, LDataType x)
{
assert(plist);
List* cur = plist;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
单向链表在中间位置插入
在做这个操作的时候我们一般时在一个位置的后面进行插入,很多人会问这是为什么呢?因为单向链表中只有后继没有前驱,如果在前面插入的话就需要去遍历链表,效率比较低;而在后面插入可以通过next直接找到后继的节点进行插入。
void SListInsertAfter(List* pos, LDataType x)
{
assert(pos);
List* newNode = BuySListNode(x);
//在pos位置之后插入一定要注意顺序
//要先将新节点的next指向pos的next,在使pos的next指向新节点
//如果顺序颠倒,除非再用一个变量记录pos的next,否则就找不到pos的下一个节点
newNode->next = pos->next;
pos->next = newNode;
}
单向链表删除中间位置的节点
在中间位置删除节点,我们一般删除某个位置之后的节点,和中间位置的插入相同,如果删除某个位置的节点,就要找到它的前驱和后继,这就非常浪费时间,没有必要去浪费效率。
void SListEraseAfter(List* pos)
{
assert(pos);
List* next = pos->next;
pos->next = next->next;
free(next);
}