概念:链表是一种物理存储结构上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
注意:
1、从上图可看出:链式结构在逻辑上是连续的,但是在物理上不一定连续。
2、现实中的结点一般都是从堆上申请出来的。
3、从堆上申请空间,是按照一定策略来分配的,再次申请的空间可能连续,不可能不连续。
链表中的结点也是动态内存分配空间,按需分配。
假设在32位系统上,结点中值域为int类型,则一个结点的大小位8字节,则可能有如下链表:
链表结构中的结点由两部分组成:
- 数据域:存储元素数值数据。
- 指针域:存储直接后继节点的存储位置。
实际中链表的结构非常多,以下情况组合起来就有8中链表结构:
结点只有一个指针域的链表,称为单链表或线性表。
结点有两个指针域的链表,称为双链表。
首尾相接的链表称为循环链表。
头指针、头结点和首元结点:
头指针:是指向链表中第一个结点的指针。
首元结点:是指链表中存储第一个数据元素a1的结点。
头结点(哨兵位):是在链表的首元结点之前附设的一个结点,此节点不存储数据,只记录首元结点的地址。
讨论一:在链表中设置头结点有什么好处?
1、便于首元节点的处理:
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行 特殊处理。
2、便于空表和非空表的统一处理:
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
讨论二:头结点的数据域内装的是什么呢?
头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此结点不能计入链表长度值。
1、单向或者双向
2、带头或者不带头
3、循环和非循环
虽然有那么多的链表结构,但是我们实际中最常用的还是两种结构:
1、无头单向非循环链表
2、带头双向循环链表
1、无头单向非循环链表:结构简单,一般不会单独用来__存放数据__,实际中更多是作为__其它数据结构的子结构__,如:哈希桶,图的邻接表等等。另外这种结构在笔试中出现很多。
2、带头双向循环链表:结构复杂,一般用在单独存储数据。实际中使用的链表结构,都是带头双向链表,另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
链表主要有以下几个接口功能:
- 打印(SListPrint)
- 销毁链表(SListDestory)
- 扩容(BuyListNode)
- 头插(SListPushFront)
- 头删(SListPopFront)
- 尾插(SListPushBack)
- 尾删(SListPopBack)
- 查找元素(SListFind)
- 在pos位置之前插入(SListInsert)
- 在pos位置之后去插入(SListErase)
- 删除pos当前位置的数据(SeqListErase)
- 删除pos位置之后的一个结点(SListEraseAfter)
和顺序表不同的是,无头单向非循环链表是不用初始化的。所以没有初始化接口。我们只需要在main函数中申请一个结点地址即可。如下:
int main()
{
SLTNode* plist = NULL;
return 0;
}
#pragma once;
#include
#include
#include
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* Next;
}SLTNode;
data变量用于存储数据,叫做数据域。
Next指针变量用于存储下一个结点的地址,叫做指针域。
链表的好处之一就是存储一个数据就申请一块空间,所以在链表中进行任意擦汗如操作时,都需要先申请内存空间。
SLTNode* BuyListNode(SLDataType x)
{
//创建新节点(就是需要尾插的那个结点)
//注意这个newnode指针,它就是新节点的地址,后面将尾结点和新节点关联的时候,就需要用到此地址。
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->Next = NULL;
return newnode;
}
void SListPushFront(SLTNode** pphead, SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->Next = *pphead;
*pphead = newnode;
}
这里打印只需要传一级指针即可。只需打印输出,无需其它操作,所以传一级指针即可。
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d ", cur->data);
cur = cur->Next;
}
printf("\n");
}
销毁链表的错误做法,不能直接操作:free(*pphead);
如果这样,只释放了头结点的地址,后面的结点的地址,都会找不到且无法释放。所以会造成内存泄漏。
正确的操作:一个结点一个结点的释放,并且在释放一个结点之前先把此结点的地址先保存来下来,以防找不到。
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->Next;
free(cur);
cur = next;
}
*pphead = NULL;
}
头删的核心思想:先保存第二个结点的地址,然后释放头结点,最后再将头指针和第二个结点的地址链接起来。
void SListPopFront(SLTNode** pphead)
{
assert(*pphead != NULL);
//保存第二个结点
SLTNode* next = (*pphead)->Next;
//释放第一个结点
free(*pphead);
//重新链接结点
*pphead = next;
}
尾插的核心思想:找到最后一个结点,然后和扩容的newnode新节点进行链接。
需要注意的是:由于我们实现的是无头链表,所以在进行结点的插入以及删除时,需要考虑,空链表,只有一个结点的特殊情况。
void SListPushBack(SLTNode** pphead, SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//首先需要找到链表中的尾结点,尾结点有个特征:它的Next为NULL。
SLTNode* tail = *pphead;
while (tail->Next)
{
tail = tail->Next;
}
//关联尾节点和新节点
tail->Next = newnode;
}
}
尾删的核心思想:同时找到最后一个结点和倒数第二个结点。然后释放最后一个结点,并且将倒数第二个结点的Next指为NULL。
这里也需要考虑3种情况:没有结点,只有一个结点,有多个结点。
void SListPopBack(SLTNode** pphead)
{
//先处理没有结点的情况,直接断言即可。
assert(*pphead != NULL);
//其次处理只有一个结点的情况
if ((*pphead)->Next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//最后考虑的就是有多个结点的情况
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->Next)
{
prev = tail;
tail = tail->Next;
}
free(tail);
tail = NULL;
prev->Next = NULL;
}
}
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (x == cur->data)
{
return cur;
}
else
{
cur = cur->Next;
}
}
return NULL;
}
核心思想:找到pos结点前面的结点,然后依次链接起来即可。
特殊情况:需要考虑只有一个结点和多个结点的情况
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
//这个用于只有一个结点的情况
if (*pphead == pos)
{
*pphead = newnode;
newnode->Next = pos;
}
//这个用于多个结点前插入数据
else
{
SLTNode* posPrev = *pphead;
//找到pos的前一个位置
while (posPrev->Next != pos)
{
posPrev = posPrev->Next;
}
//链接
posPrev->Next = newnode;
newnode->Next = pos;
}
}
在pos之前插入的时间复杂度分析。这里分三种情况:
- 直接是头插,那时间复杂度是O(1)
- 需要插入的数据在中间
- 需要插入的数据在最后一个结点前,所以是O(N)。
所以取悲观预期,综合来说时间复杂度是O(N)。
在pos位置之后插入比在pos位置之前插入简单。就直接依次链接即可。
void SListInsertAfter(SLTNode** pphead, SLTNode* pos,SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
SLTNode* posNext = pos->Next;
pos->Next = newnode;
newnode->Next = posNext;
}
在pos之后插入的时间复杂度分析:
在pos位置之后插入,由于确定好了插入的位置,可以直接插入。所以时间复杂度为O(1)。
相比在pos位置之前插入,结点,更推荐使用pos位置之后插入结点。
核心思想:找到pos前一个结点,将pos前一个结点和pos后一个结点链接,然后释放pos。
考虑情况:
- 只有一个结点
- 有多个结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
//删除头结点
if (*pphead == pos)
{
*pphead = pos->Next;
free(pos);
}
//删除非头结点的其它结点。
else
{
SLTNode* posPrev = *pphead;
while (posPrev->Next != pos)
{
posPrev = posPrev->Next;
}
posPrev->Next = pos->Next;
free(pos);
}
}
删除pos位置当前的时间复杂度:O(N)。
很简单,直接链接。
void SListEraseAfter(SLTNode* pos)
{
assert(pos->Next);
SLTNode* next = pos->Next;
pos->Next = next->Next;
free(next);
}
在pos位置之后的时间复杂度:O(1)。
这里使用三个文件:
- seqlist.h:用于结构体、各种函数接口的声明
- seqlist.c:用于各种函数接口的定义。
- test.c:用于创建链表,实现链表。
#pragma once;
#include
#include
#include
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* Next;
}SLTNode;
//扩容
SLTNode* BuyListNode(SLDataType x);
//头插
void SListPushFront(SLTNode** pphead, SLDataType x);
//打印
void SListPrint(SLTNode* phead);
//销毁
void SListDestroy(SLTNode** pphead);
//头删
void SListPopFront(SLTNode** pphead);
//尾插
void SListPushBack(SLTNode** pphead, SLDataType x);
//尾删
void SListPopBack(SLTNode** pphead);
//查询指定元素,返回结点地址
SLTNode* SListFind(SLTNode* phead,SLDataType x);
//在pos位置之前插入结点
void SListInsert(SLTNode** phead, SLTNode* pos, SLDataType x);
//在pos位置之后插入结点
void SListInsertAfter(SLTNode** pphead, SLTNode* pos,SLDataType x);
//删除当前pos位置结点
void SListErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置之后的结点
void SListEraseAfter(SLTNode* pos);
#include "list.h"
//扩容
SLTNode* BuyListNode(SLDataType x)
{
//创建新节点(就是需要尾插的那个结点)
//注意这个newnode指针,它就是新节点的地址,后面将尾结点和新节点关联的时候,就需要用到此地址。
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->Next = NULL;
return newnode;
}
//头插
void SListPushFront(SLTNode** pphead, SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->Next = *pphead;
*pphead = newnode;
}
//打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d ", cur->data);
cur = cur->Next;
}
printf("\n");
}
//销毁
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->Next;
free(cur);
cur = next;
}
*pphead = NULL;
}
//头删
void SListPopFront(SLTNode** pphead)
{
assert(*pphead != NULL);
//保存第二个结点
SLTNode* next = (*pphead)->Next;
//释放第一个结点
free(*pphead);
//重新链接结点
*pphead = next;
}
//尾插
void SListPushBack(SLTNode** pphead, SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->Next)
{
tail = tail->Next;
}
tail->Next = newnode;
}
}
//尾删
void SListPopBack(SLTNode** pphead)
{
if ((*pphead)->Next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->Next)
{
prev = tail;
tail = tail->Next;
}
free(tail);
tail = NULL;
prev->Next = NULL;
}
}
//查询指定元素,返回结点地址
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (x == cur->data)
{
return cur;
}
else
{
cur = cur->Next;
}
}
return NULL;
}
//在pos位置之前插入结点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
if (*pphead == pos)
{
*pphead = newnode;
newnode->Next = pos;
}
else
{
SLTNode* posPrev = *pphead;
while (posPrev->Next != pos)
{
posPrev = posPrev->Next;
}
posPrev->Next = newnode;
newnode->Next = pos;
}
}
//在pos位置之后插入结点
void SListInsertAfter(SLTNode** pphead, SLTNode* pos,SLDataType x)
{
SLTNode* newnode = BuyListNode(x);
SLTNode* posNext = pos->Next;
pos->Next = newnode;
newnode->Next = posNext;
}
//删除当前pos位置结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (*pphead == pos)
{
*pphead = pos->Next;
free(pos);
}
else
{
SLTNode* posPrev = *pphead;
while (posPrev->Next != pos)
{
posPrev = posPrev->Next;
}
posPrev->Next = pos->Next;
free(pos);
}
}
//删除pos位置之后的结点
void SListEraseAfter(SLTNode* pos)
{
assert(pos->Next);
SLTNode* next = pos->Next;
pos->Next = next->Next;
free(next);
}
#include "list.h"
void TestList1()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPushFront(&plist, 6);
SLTNode* pos = SListFind(plist, 3);
SListEraseAfter(pos);
SListPrint(plist);
SListDestroy(&plist);
}
int main()
{
TestList1();
return 0;
}