【数据结构】线性表--链表
一.前情回顾
上篇文章讲述了动态顺序表及其实现,我们知道了动态顺序表在物理结构上是连续的,因此我们也认识到它的缺点:
①如果空间不足需进行增容,付出一定的性能消耗,并且可能存在一定的空间浪费。
②在进行某些插入或删除操作时,需要大量移动数据。这是因为相邻数据元素在物理存储结构上也是连续存储的,中间没有空隙。
因此本篇文章将讲述线性表的另一种表示方法:链表。
二.链表的概念
链表,即线性表的链式实现,指用一组任意连续或者不连续的存储单元存储数据,通过指针像链条一样链结各个元素的一种存储结构。
如图所示:
因此,对于每个数据元素,除了要存储自身信息,也要存储后继(下一个)数据元素的信息。这两部分合起来被称为结点。
每个结点包含两个域,一个是数据域:存储数据元素的信息;另一个是指针域:存储后继元素的位置信息。n个结点链结成一个链表。如图所示:
对于线性表,总要有头有尾,我们把链表中第一个结点的存储位置叫做头指针,最后一个结点置为NULL。由于每个结点的指针域只包含一个指向后继位置的指针,因此该链表又称单链表(单向链表)。
三.链表的实现
1.链表结点的结构:
在C语言中用结构体指针来存储后继结点的信息。
cpp
typedef int SLDataType;
//结点的结构体
typedef struct SListNode
{
SLDataType data;//数据域
struct SListNode* next;//指向下一个结点的指针域,所以指针类型应该为struct SListNode*
}SLTNode;//起别名,将struct SListNode简写成SLTNode
2.申请新结点函数:
因为在插入操作中需要频繁申请结点,因此可以将申请结点的操作封装成一个函数。
cpp
//申请新结点
SLTNode* BuySListNode(SLDataType x)
{
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
NewNode->data = x;
NewNode->next = NULL;
return NewNode;
}
3.尾插函数:
需要特别注意的是,凡是涉及修改链表,必须传二级指针,因为链表本身是用每个结点的指针链结而成的,作为参数传递时是一级指针,再将每个结点的地址作为实参传递,这是二级指针。
cpp
//尾插函数
//需要传二级指针,否则形参的改变不影响实参
void SListPushBack(SLTNode** pphead, SLDataType x)
{
assert(pphead);//不能传空地址,否则解引用找链表头结点会报错
//创建新结点
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
NewNode->data = x;
NewNode->next = NULL;
//链表为空,直接插入
if (*pphead == NULL)
{
*pphead = NewNode;
}
else
{
//需要找到最后一个结点才能尾插,因此先用一个cur结点标记当前所在位置
SLTNode* cur = *pphead;
while (cur->next != NULL)//循环结束走到最后一个结点
{
cur = cur->next;//让cur遍历到最后一个结点
}
if (NewNode == NULL)
{
perror("malloc fail!");
exit(1);
}
cur->next = NewNode;
}
}
4.头插函数:
cpp
//头插函数
void SListPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead);
//创建新结点
SLTNode* NewNode = BuySListNode(x);
NewNode->next = *pphead;
*pphead = NewNode;
}
5.尾删函数:
cpp
//尾删函数
void SListPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//如果只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* cur = *pphead;
while (cur->next->next != NULL)//需要找到倒数第二个结点才能删除最后一个结点
{
cur = cur->next;
}
SLTNode* tmp = cur->next;
free(tmp);
tmp = NULL;
cur->next = NULL;
}
}
6.头删函数:
cpp
//头删函数
void SListPopFront(SLTNode** pphead)
{
assert(pphead&&*pphead);//链表为空时不能删除
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
7.在指定结点之前插入:
cpp
//在指定结点之前插入函数
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pphead && *pphead);
assert(pos);
//需要找到指定结点的前一个结点
SLTNode* prev = *pphead;
//可能第一个结点就是指定结点,此时相当于头插
if (prev == pos)
{
//直接调用头插函数
SListPushFront(pphead, x);
}
else
{
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* NewNode = BuySListNode(x);
NewNode->next = prev->next;
prev->next = NewNode;
}
}
8.在指定结点之后插入:
cpp
//在指定结点之后插入函数
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* NewNode = BuySListNode(x);
NewNode->next = pos->next;
pos->next = NewNode;
}
8.删除指定结点:
cpp
//删除指定结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
SLTNode* prev = *pphead;
//如果第一个结点就是要删除的结点
if (prev == pos)
{
//直接调用头删
SListPopFront(pphead);
}
else
{
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* tmp = prev->next;//tmp即要删除的结点
prev->next = tmp->next;
free(tmp);
tmp = NULL;
}
}
9.查找函数:
cpp
//查找
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
SLTNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
//没找到或链表为空时,返回空指针
return NULL;
}
10.销毁链表:
cpp
//销毁链表函数
void SListDestory(SLTNode** pphead)
{
assert(pphead && *pphead);
while (*pphead != NULL)
{
SLTNode* tmp = *pphead;
*pphead = (*pphead)->next;
free(tmp);
tmp = NULL;
}
}
四.全部源代码实现
1.头文件(声明动态顺序表的结构,操作等,起到目录作用):
SList.h
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
//结点的结构体
typedef struct SListNode
{
SLDataType data;//数据域
struct SListNode* next;//指向下一个结点的指针域,所以指针类型应该为struct SListNode*
}SLTNode;//起别名,将struct SListNode简写成SLTNode
//打印函数(方便调试)
void SListPrint(SLTNode* phead);
//申请新结点
SLTNode* BuySListNode(SLDataType x);
//尾插函数
void SListPushBack(SLTNode** pphead, SLDataType x);//需要传二级指针,否则形参的改变不影响实参
//头插函数
void SListPushFront(SLTNode** pphead, SLDataType x);
//尾删函数
void SListPopBack(SLTNode** pphead);
//头删函数
void SListPopFront(SLTNode** pphead);
//在指定位置之前插入函数
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
//在指定位置之后插入函数
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLDataType x);
//删除指定结点
void SListErase(SLTNode** pphead, SLTNode* pos);
//查找
SLTNode* SListFind(SLTNode* phead, SLDataType x);
//销毁链表函数
void SListDestory(SLTNode** pphead);
2.源文件(具体实现各种操作):
SList.c
cpp
#include"SList.h"
//打印函数(方便调试)
void SListPrint(SLTNode* phead)
{
assert(phead);
SLTNode* cur = phead;
while (cur != NULL)//循环结束走到空结点
{
printf(" %d ->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//申请新结点
SLTNode* BuySListNode(SLDataType x)
{
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
NewNode->data = x;
NewNode->next = NULL;
return NewNode;
}
//尾插函数
//需要传二级指针,否则形参的改变不影响实参
void SListPushBack(SLTNode** pphead, SLDataType x)
{
assert(pphead);//不能传空地址,否则解引用找链表头结点会报错
//创建新结点
SLTNode* NewNode = BuySListNode(x);
//链表为空,直接插入
if (*pphead == NULL)
{
*pphead = NewNode;
}
else
{
//需要找到最后一个结点才能尾插,因此先用一个cur结点标记当前所在位置
SLTNode* cur = *pphead;
while (cur->next != NULL)//循环结束走到最后一个结点
{
cur = cur->next;//让cur遍历到最后一个结点
}
if (NewNode == NULL)
{
perror("malloc fail!");
exit(1);
}
cur->next = NewNode;
}
}
//头插函数
void SListPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead);
//创建新结点
SLTNode* NewNode = BuySListNode(x);
NewNode->next = *pphead;
*pphead = NewNode;
}
//尾删函数
void SListPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//如果只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* cur = *pphead;
while (cur->next->next != NULL)//需要找到倒数第二个结点才能删除最后一个结点
{
cur = cur->next;
}
SLTNode* tmp = cur->next;
free(tmp);
tmp = NULL;
cur->next = NULL;
}
}
//头删函数
void SListPopFront(SLTNode** pphead)
{
assert(pphead&&*pphead);//链表为空时不能删除
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//在指定结点之前插入函数
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pphead && *pphead);
assert(pos);
//需要找到指定结点的前一个结点
SLTNode* prev = *pphead;
//可能第一个结点就是指定结点,此时相当于头插
if (prev == pos)
{
//直接调用头插函数
SListPushFront(pphead, x);
}
else
{
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* NewNode = BuySListNode(x);
NewNode->next = prev->next;
prev->next = NewNode;
}
}
//在指定结点之后插入函数
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* NewNode = BuySListNode(x);
NewNode->next = pos->next;
pos->next = NewNode;
}
//删除指定结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
SLTNode* prev = *pphead;
//如果第一个结点就是要删除的结点
if (prev == pos)
{
//直接调用头删
SListPopFront(pphead);
}
else
{
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* tmp = prev->next;//tmp即要删除的结点
prev->next = tmp->next;
free(tmp);
tmp = NULL;
}
}
//查找
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
SLTNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
//没找到或链表为空时,返回空指针
return NULL;
}
//销毁链表函数
void SListDestory(SLTNode** pphead)
{
assert(pphead && *pphead);
while (*pphead != NULL)
{
SLTNode* tmp = *pphead;
*pphead = (*pphead)->next;
free(tmp);
tmp = NULL;
}
}
3.测试文件(测试各个函数的功能)
test.c
cpp
#include"SList.h"
//测试尾插函数
void test01()
{
SLTNode* phead = NULL;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPrint(phead);
}
//测试头插函数
void test02()
{
SLTNode* phead = NULL;
SListPushFront(&phead, 1);
SListPushFront(&phead, 2);
SListPushFront(&phead, 3);
SListPushFront(&phead, 4);
SListPrint(phead);
}
//测试尾删函数
void test03()
{
SLTNode* phead = NULL;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPrint(phead);
SListPopBack(&phead);
SListPrint(phead);
SListPopBack(&phead);
SListPrint(phead);
SListPopBack(&phead);
SListPrint(phead);
}
//测试头删函数
void test04()
{
SLTNode* phead = NULL;
SListPushFront(&phead, 1);
SListPushFront(&phead, 2);
SListPushFront(&phead, 3);
SListPushFront(&phead, 4);
SListPrint(phead);
SListPopFront(&phead);
SListPrint(phead);
SListPopFront(&phead);
SListPrint(phead);
SListPopFront(&phead);
SListPrint(phead);
SListPopFront(&phead);
SListPrint(phead);
SListPopFront(&phead);
SListPrint(phead);
}
//测试查找函数
void test05()
{
SLTNode* phead = NULL;
SListPushFront(&phead, 1);
SListPushFront(&phead, 2);
SListPushFront(&phead, 3);
SListPushFront(&phead, 4);
SListPrint(phead);
SLTNode* ret1 = SListFind(phead, 2);
if (ret1 != NULL)
printf("找到了\n");
else
printf("未找到\n");
SLTNode* ret2 = SListFind(phead, 57);
if (ret2 != NULL)
printf("找到了\n");
else
printf("未找到\n");
}
//测试在指定结点之前插入
void test06()
{
SLTNode* phead = NULL;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPrint(phead);
//在第三个结点前插入57
//先查找到第三个结点
SLTNode* pos1 = SListFind(phead, 3);
SListInsert(&phead, pos1, 57);
SListPrint(phead);
//在第一个结点前插入79
//先查找到第一个结点
SLTNode* pos2 = SListFind(phead, 1);
SListInsert(&phead, pos2, 79);
SListPrint(phead);
//在最后一个结点前插入36
//先查找到最后一个结点
SLTNode* pos3 = SListFind(phead, 4);
SListInsert(&phead, pos3, 36);
SListPrint(phead);
}
//测试在指定结点之后插入
void test07()
{
SLTNode* phead = NULL;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPrint(phead);
//在第三个结点后插入57
//先查找到第三个结点
SLTNode* pos1 = SListFind(phead, 3);
SListInsertAfter(&phead, pos1, 57);
SListPrint(phead);
//在第一个结点后插入79
//先查找到第一个结点
SLTNode* pos2 = SListFind(phead, 1);
SListInsertAfter(&phead, pos2, 79);
SListPrint(phead);
//在最后一个结点后插入36
//先查找到最后一个结点
SLTNode* pos3 = SListFind(phead, 4);
SListInsertAfter(&phead, pos3, 36);
SListPrint(phead);
}
//测试删除结点函数
void test08()
{
SLTNode* phead = NULL;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPrint(phead);
//删除第一个结点
SLTNode* pos1 = SListFind(phead, 1);
SListErase(&phead, pos1);
SListPrint(phead);
//删除第三个结点
SLTNode* pos2 = SListFind(phead, 3);
SListErase(&phead, pos2);
SListPrint(phead);
//删除最后一个结点
SLTNode* pos3 = SListFind(phead, 4);
SListErase(&phead, pos3);
SListPrint(phead);
}
//测试销毁函数
void test09()
{
SLTNode* phead = NULL;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPrint(phead);
SListDestory(&phead);
SListPrint(phead);
}
int main()
{
//test01();
//test02();
//test03();
//test04();
//test05();
//test06();
//test07();
//test08();
test09();
return 0;
}
五.单链表和顺序表的对比
1.存储分配方式
顺序表采用一段连续的存储单元存储数据元素。
单链表采用一组任意的存储单元存储元素。
2.时间性能
查找:
顺序表按值查找O(n),按索引查找O(1)。
单链表O(n)。
插入和删除:
顺序表O(n)。
单链表O(1)。
3.空间性能
顺序表需要预分配空间,小了需再次分配,大了造成空间浪费。
单链表需要时申请结点空间。
4.总结
若线性表需要频繁查找,宜采用顺序存储结构。若频繁插入和删除,宜采用链式存储结构。比如说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构 。而游戏中的玩 家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不大合适了,链表结构就可以大展拳脚。当然,这只是简单的类比,现实中的软件开发,要考虑的问题会复杂得多。
总之,线性表的顺序存储和链式存储各有优缺点,不能简单说哪个好,哪个不好,需根据实际情况做出选择。