目录
一、单链表的定义和表示
线性表的链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
用单链表(Single Linked List)表示线性表时,为了表示每个数据元素与其直接后继数据元素之间的逻辑关系,对每个数据元素来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。
根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表(每个结点只包含一个指针域)、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。其中单链表、循环链表和双向链表用于实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。
单链表可由头指针唯一确定,在 C 语言中可用"结构指针"来描述:
typedef struct SLLNode
{
SLLDataType data; // 结点的数据域
struct SLLNode* next; // 结点的指针域
}SLLNode, *SLinkList;
SLinkList
与SLLNode*
,两者本质上是等价的。通常习惯上用SLinkList
定义单链表,强调定义的是某个单链表的头指针;用SLLNode*
定义指向单链表中的任意结点的指针变量。单链表是由表头指针唯一确定的,因此单链表可用用头指针的名字来命名。若头指针名是 L,则简称该单链表为表 L。
一般情况下,为了处理方便,在单链表的第一个结点之前附设一个结点,称之为头结点。
下面对首元结点(新概念)、头结点、头指针三个容易混淆的概念加以说明:
-
首元结点是指链表中存储第一个数据元素的结点。
-
头结点是在首元结点之前附设的一个结点。
-
头指针是指向链表中第一个结点的指针。
-
若链表设有头结点,则头指针所指结点为线性表的头结点,即:
-
若链表不设头结点,则头指针所指结点为该线性表的首元结点,即:
-
单链表是非随机存取的存储结构,要取得第 i
个数据元素必须从头指针出发顺链进行寻找,也称为顺序存取的存储结构。因此,其基本操作的实现不同于顺序表。
二、单链表基本操作的实现
2.1 - 不设头结点的单链表
SLinkList.h:
#pragma once
// 不设头结点的单链表
typedef int SLLDataType;
typedef struct SLLNode
{
SLLDataType data; // 结点的数据域
struct SLLNode* next; // 结点的指针域
}SLLNode, *SLinkList;
// 基本操作
void SLinkListInit(SLinkList* pphead); // 初始化
SLLNode* BuySLLNode(SLLDataType e); // 动态申请一个新结点
void SLinkListPushBack(SLinkList* pphead, SLLDataType e); // 尾插
void SLinkListPopBack(SLinkList* pphead); // 尾删
void SLinkListPushFront(SLinkList* pphead, SLLDataType e); // 头插
void SLinkListPopFront(SLinkList* pphead); // 头删
void SLinkListInsertAfter(SLLNode* pos, SLLDataType e); // 在 pos 之后插入
void SLinkListEraseAfter(SLLNode* pos); // 删除 pos 之后的结点
void SLinkListInsertBefore(SLLNode* pos, SLLDataType e); // 在 pos 之前插入
void SLinkListErase(SLinkList* pphead, SLLNode* pos); // 删除 pos 结点
void SLinkListEraseNonTail(SLLNode* pos); // 删除 pos 非尾结点
SLLNode* SLinkListFind(const SLinkList phead, SLLDataType e); // 查找
void SLinkListPrint(const SLinkList phead); // 打印
SLinkList.c:
-
初始化:
void SLinkListInit(SLinkList phead) { phead = NULL; }
单链表的初始化操作就是构造一个空表,即将头指针置为空。
但是该函数无法初始化单链表,因为传值调用时,修改形参并不会影响实参,即如果要将头指针置为空,那么传给函数的实参就应该是头指针的地址,形参则应该是一个二级指针。
修正:
void SLinkListInit(SLinkList* pphead) { assert(pphead); *pphead = NULL; }
pphead
是指向头指针的指针,它不能为空,对空指针解引用会导致运行时的错误。由于有传入 NULL 的风险,所以先进行断言,提高代码的健壮性。
-
动态申请一个新结点:
SLLNode* BuySLLNode(SLLDataType e) { SLLNode* newnode = (SLLNode*)malloc(sizeof(SLLNode)); if (NULL == newnode) { perror("malloc failed!"); return NULL; } newnode->data = e; newnode->next = NULL; // 将结点的指针域置为空 return newnode; }
-
尾插:
void SLinkListPushBack(SLinkList* pphead, SLLDataType e) { assert(pphead); SLLNode* newnode = BuySLLNode(e); if (*pphead == NULL) // 如果表为空 { *pphead = newnode; } else { SLLNode* tail = *pphead; while (tail->next != NULL) { tail = tail->next; } tail->next = newnode; } }
尾插的重点是找到尾结点,尾结点的特点是其 next 成员的值为 NULL,即
tail->next == NULL
。因为尾插时单链表可能为空,需要修改头指针,所以形参仍需要用一个二级指针
SLinkList* pphead
来接受头指针的地址&phead
。 -
尾删:
法一:
void SLinkListPopBack(SLinkList* pphead) { assert(pphead); // 判断是否为空表 if (*pphead == NULL) { return; } // 尾删 if ((*pphead)->next == NULL) // 如果表中只包含一个结点 { free(*pphead); *pphead = NULL; } else { SLLNode* pre_tail = *pphead; SLLNode* tail = (*pphead)->next; while (tail->next != NULL) { pre_tail = tail; tail = tail->next; } free(tail); tail = NULL; pre_tail->next = NULL; } }
删除操作首先要判断表是否为空,其次该尾删操作是通过找到尾结点和倒数第二个结点实现的,因此就需要先考虑表中只含一个结点的情况,在这种情况下需要删除唯一的结点,并将头指针置为空,即涉及到修改了头指针。
法二:
void SLinkListPopBack(SLinkList* pphead) { assert(pphead); // 判断是否为空表 if (*pphead == NULL) { return; } // 尾删 if ((*pphead)->next == NULL) // 如果表中只包含一个结点 { free(*pphead); *pphead = NULL; } else { SLLNode* pre_tail = *pphead; while (pre_tail->next->next != NULL) { pre_tail = pre_tail->next; } free(pre_tail->next); pre_tail->next = NULL; } }
该尾删操作是通过直接找到倒数第二个结点实现的,也需要考虑到表中只包含一个结点的情况。
-
头插:
void SLinkListPushFront(SLinkList* pphead, SLLDataType e) { assert(pphead); SLLNode* newnode = BuySLLNode(e); newnode->next = *pphead; *pphead = newnode; }
头插必然涉及到修改头指针。
注意:
newnode->next = *pphead;
和*pphead = newnode
的顺序不能发生改变。 -
头删:
void SLinkListPopFront(SLinkList* pphead) { assert(pphead); // 判断是否为空表 if (*pphead == NULL) { return; } // 头删 SLLNode* first = *pphead; *pphead = first->next; free(first); first = NULL; }
头删也必然涉及到修改头指针。
-
在
pos
之后插入:void SLinkListInsertAfter(SLLNode* pos, SLLDataType e) { assert(pos); SLLNode* newnode = BuySLLNode(e); newnode->next = pos->next; pos->next = newnode; }
-
删除
pos
之后的结点:void SLinkListEraseAfter(SLLNode* pos) { assert(pos && pos->next); // pos 不能为空且不能指向尾结点 SLLNode* tmp = pos->next; // tmp 指向 pos 之后的结点 pos->next = pos->next->next; // 或者 pos->next = tmp->next; free(tmp); tmp = NULL; }
-
在
pos
之前插入:void SLinkListInsertBefore(SLLNode* pos, SLLDataType e) { assert(pos); SLLNode* newnode = BuySLLNode(e); // 先在 pos 之后插入 newnode->next = pos->next; pos->next = newnode; // 再交换两个结点的数据 SLLDataType tmp = newnode->data; newnode->data = pos->data; pos->data = tmp; }
-
删除
pos
结点:void SLinkListErase(SLinkList* pphead, SLLNode* pos) { assert(pphead); assert(pos); // 若 pos != NULL,则意味着 phead != NULL if (pos == *pphead) { SLinkListPopFront(pphead); } else { SLLNode* pre = *pphead; while (pre->next != pos) { pre = pre->next; } pre->next = pos->next; free(pos); pos = NULL; } }
-
删除
pos
非尾结点:void SLinkListEraseNonTail(SLLNode* pos) { assert(pos && pos->next); // pos 不能为空且不能指向尾结点 SLLNode* tmp = pos->next; // tmp 指向 pos 之后的结点 pos->data = tmp->data; pos->next = tmp->next; free(tmp); tmp = NULL; }
-
查找:
SLLNode* SLinkListFind(const SLinkList phead, SLLDataType e) { const SLLNode* cur = phead; while (cur != NULL) { if (cur->data == e) { return (SLLNode*)cur; } cur = cur->next; } return NULL; }
-
打印(不通用):
void SLinkListPrint(const SLinkList phead) { const SLLNode* cur = phead; while (cur != NULL) { printf("%d->", cur->data); cur = cur->next; } printf("NULL\n"); }
2.2 - 设头结点的单链表
链表增加头结点的作用如下:
-
便于首元结点的处理。
增加了头结点后,首元结点的地址保存在头结点(即其"前驱"结点)的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。
-
便于空表和非空表的统一处理。
当链表不设头结点时,假设
phead
为单链表的头指针,它应该指向首元结点,则当单链表为长度 n 为 0 的空表时,phead
指针为空(判定空表的条件可记为:phead == NULL
)。增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。若为空表,则头结点的指针域为空(判定空表的条件可记为:
phead->next == NULL
)。
设头结点的单链表与不设头结点的单链表在基本操作的实现上逻辑是相似的,需要注意的是:
-
初始化:
void SLinkListInit(SLinkList* pphead) { assert(pphead); SLLNode* newnode = BuySLLNode(0); // 生成一个新结点作为头结点 if (NULL == newnode) { printf("initialization failed!"); return; } *pphead = newnode; // 让头指针指向头结点 }
-
在实现插入、删除等操作时,不涉及头指针的修改,函数形参可以是一个一级指针。
-
由于单链表带表头结点,那么首先应该判定头指针是否为空,即
assert(phead)
。
欲知后事如何,且听下回分解~