目录
一.单链表的定义
链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
采用这种存储方式要找到某一个位序的结点,只能从第一个结点开始利用指针的信息依次往后寻找直到找到我们想要的那个结点。因此单链表这种实现方式不支持随机存取。
二.单链表的分类
用代码定义一个单链表:
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode,*SLT;
//强调这是一个单链表使用STL,强调这是一个结点用SLTNode*,实际上STL和STLNode*是等价的
要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点。
SLT L;//声明一个指向单链表第一个结点的指针
2.1.不带头结点的单链表
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode, * SLT;
//初始化一个空的单链表
bool InitList(SLT* L)
{
L = NULL;//空表,暂时还没有结束,防止脏数据
return true;
}
//判空操作
bool Empty(SLT L)
{
if (L == NULL)
return true;
else
return false;
}
void test()
{
SLT L;//声明一个指向单链表的指针,注意此处并没有创建一个结点
//初始化一个空表
InitList(L);
}
2.2.带头结点的单链表
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode, * SLT;
//初始化一个空的单链表
bool InitList(SLT* L)
{
L = (SLTNode*)malloc(sizeof(SLTNode));//分配一个结点
if (L == NULL)//内存不足,分配失败
return false;
L->next = NULL;//头结点不存储数据,头结点之后暂时没有结点
return true;
}
//判空操作
bool Empty(SLT L)
{
if (L->next == NULL)
return true;
else
return false;
}
void test()
{
SLT L;//声明一个指向单链表的指针,注意此处并没有创建一个结点
//初始化一个空表
InitList(L);
}
不带头结点与带头结点对比:
三.单链表的功能实现
3.1.单链表的定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
3.2.单链表的打印
//打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;//phead指向头结点
while (cur != NULL)
{
printf("%d->",cur->data);
cur = cur->next;
}
printf("NULL\n");
}
单链表的头指针phead标识着整个单链表的开始,习惯上用头指针代表单链表。给定单链表的头指针phead,即可顺着每个结点的next指针域得到单链表中的每个元素。因此对于整个单链表的操作必须从头开始。
调试分析:
void test()
{
SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n1);
SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n2);
SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n3);
SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n4);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;
SLTNode* plist = n1;
//打印
SListPrint(n1);
}
int main()
{
test();
return 0;
}
运行结果:
3.3.单链表的结点的创建
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
return newnode;
}
单链表是由一个个结点组成,而每个结点都是通过调用malloc函数来进行开辟的,同时返回一个该类型的指针。
调试分析:
void test()
{
SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n1);
SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n2);
SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n3);
SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n4);
//创建结点
SLTNode* n5 = BuySListNode(5);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = n5;
SLTNode* plist = n1;
//打印
SListPrint(n1);
}
int main()
{
test();
return 0;
}
运行结果:
3.4.单链表的尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)//链表为空
{
*pphead = newnode;//直接插入
}
else
{
SLTNode* tail = *pphead;
//找尾结点
while (tail->next != NULL)
{
tail = tail->next;
}
//插入
tail->next = newnode;
}
}
在对单链表进行尾插操时,我们要分两种情况进行讨论:第一,当*pphead==NULL,也就是当单链表为空时,我们直接创建一个新结点newnode,并让头指针*pphead指向该结点;第二,当*pphead!=NULL,也就是当链表不为空时,我们通过while循环,查找到链表的最后一个结点tail,并将新创建的结点newnode插入到尾结点tail的后面,此时链表的尾结点变为newnode。
这里还需要特别说明的一点是,我们此时传入的是二级指针,而非一级指针。在指针变量定义语句SLTNode* pphead中,将pphead定义为指向SLTNode类型的指针变量,这里的pphead是一级指针,可以通过->来访问结构体成员变量;在指针变量定义语句SLTNode** pphead中,pphead是指向单链表的头结点的指针,用来接收主程序中待初始化单链表的的头指针变量的地址,*pphead相当于主程序中待初始化单链表的头指针变量,这里的*pphead是一级指针。我们要想通过函数传参来实现对单链表的修改,如果此时传入一级指针,也就相当于传值,是现实不了对单链表的修改的,因为形参的存储空间是函数被调用时才分配的,调用开始,系统为形参开辟一个临时的存储区,然后将各实参传递给形参,这时形参就得到了实参的值,任何的修改都是在副本形参上作用,没有作用在原来的实参上,在函数调用完毕之后,形参会立即释放所占用的内存空间,因此它并不会把修改后的值传递给实参;如果此时传入二级指针,也就相当于传地址,此时是可以实现对单链表的修改的,它把实参的存储地址传送给形参,使得形参指针和实参指针指向同一块地址。因此,被调用函数中对形参指针所指向的地址中内容的任何改变都会影响到实参。
调试分析:
void test()
{
SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n1);
SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n2);
SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n3);
SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n4);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;
SLTNode* plist = n1;
//尾插
SListPushBack(&plist, 5);
SListPushBack(&plist, 6);
SListPushBack(&plist, 7);
SListPushBack(&plist, 8);
//打印
SListPrint(n1);
}
int main()
{
test();
return 0;
}
运行结果:
3.5.单链表的头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
在进行头插之前要调用BuySListNode函数来创建一个新结点newnode,并让新结点的next指针指向头结点*pphead,最后把新结点赋值给*pphead,使其变为链表新的头结点。
调试分析:
3.6.单链表的尾删
void SListPopBack(SLTNode** pphead)
{
//地址不为空
assert(pphead);
//链表不为空
assert(*pphead != NULL);
//只有一个结点
if ((*pphead)->next == NULL)
{
//释放该结点
free(*pphead);
*pphead = NULL;
}
else
{
//有多个结点
/*
SLTNode* tailPrev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tailPrev = tail;
tail = tail->next;
}
free(tail);
tailPrev->next = NULL;
*/
SLTNode* tail = *pphead;
while (tail->next->next != NULL)//查找倒数第二个结点
{
tail = tail->next;
}
free(tail->next);//释放掉倒数第一个结点
tail->next = NULL;//tail变为新的尾结点
}
}
在进行尾删之前,需要进行一定的条件判断。其一:当链表为空时,此时无法进行尾删,强制删除会报错,这时需要进行assert断言,判断链表是否为空,避免错误的发生;其二:当链表只包含一个结点时,则直接调用free函数释放该结点,并将释放后的结点置为NULL;其三:当链表中包含两个以上结点时,这时要通过循环查找到倒数第二个结点tail,然后调用free函数释放掉尾结点tail->next,这时尾结点变为tail,直接将其next指针域置为NULL即可。
调试分析:
运行结果:
3.7.单链表的头删
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
//判断链表是否为空
/*
//温柔检查
if (*pphead == NULL)
{
return;
}
*/
//暴力检查
assert(*pphead != NULL);
SLTNode* cur = *pphead;
SLTNode* next =