1.1链表的概念和结构
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。链式结构在逻辑上的存储的连续的,而在物理上不一定是连续的,不同于顺序表,链表没有所谓的容量大小,也不需要扩容的操作,链表的使用是存储一个数据就开辟一块内存空间,链表间通过指针来寻找访问。
1.2链表的种类
链表的种类比较复杂,详细分类的可以分为8种结构,通过链表:单向或者双向、带哨兵卫或不带、循环或者不环,其中哨兵卫为一个指向链表头结点的一个不存储数据的结点,而这些结构总共可以组成8种之多结构的链表,但实际上我们使用最多的仅仅只有两种:不带哨兵卫的单向非循环链表 和 带哨兵卫的双向循环链表,我们只要学会了这两种结构的链表,其他结构的链表自然就没有太大问题。下面要将的单、双向链表也都是这两种最常见的。
2.1单链表的实现(不带哨兵卫的单向非循环链表)
单链表的结构是由一个包含存储数据和指针两部分的结构体类型组成的,其中存储数据的我们通常称之为数据域,存放指针的称之为指针域,其中数据域中所存储的数据类型可以是任意的,这个由具体需求决定,而指针域中存放的指针是我们所自定义的结构体指针,它指向的是下个链表,存放下个链表的地址。
2.11定义链表的基本结构
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
2.12实现链表的增删查改
生成链表结点:
通过传递链表要存储的数据,动态开辟一块内存用于存储数据。
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
if (NewNode == NULL)
{
perror("malloc");
exit(-1);
}
NewNode->data = x;
NewNode->next = NULL;
}
头插法/尾插法储存数据:
由于头插法是需要更改单链表的头结点的地址,所以需要使用二级指针,如果使用一级指针来传递参数,接收头结点的存储地址,通过形参的改变则无法对实参产生影响。而尾插的数据为第一个元素时,此时要更改头结点,任然需要传参二级指针。而尾插中还需要注意单链表要找到尾结点,只能通过头结点依次往下查找,这里要注意找到尾结点时的终止条件。
void SLTPushFront(SLTNode** phead, SLTDataType x)//头插
{
assert(phead);
SLTNode* newNode = BuySListNode(x);
if (*phead == NULL)//空表
{
*phead = newNode;
}
else
{
newNode->next = *phead;
*phead = NewNode;
}
}
void SLTPushBack(SLTNode** phead, SLTDataType x)//尾插
{
assert(phead);
SLTNode* newnode = BuySListNode(x);
SLTNode* tail = *phead;
if (*phead == NULL)//空链表
{
*phead = newnode;
}
else
{
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
头删除/尾删除:
利用assert断言能很好的知道知道程序产生的错误从而终止程序。头删除中,存储头结点的地址phead,以及存放头结点地址的地址这两者都不能为空。
void SLTPopFront(SLTNode** phead)//头删
{
assert(*phead && phead);
SLTNode* newhead = (*phead)->next;
free(*phead);
*phead = newhead;
}
void SLTPopBack(SLTNode** phead)//尾删
{
assert(*phead && phead);
SLTNode* tail = *phead;
if ((*phead)->next == NULL)//一个结点的链表
{
free(*phead);
*phead = NULL;
}
else
{
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);//释放最后一个结点
tail->next = NULL;
}
}
查找链表元素:
查找链表元素本质是通过遍历列表依次查找是否有对应元素,找到后返回结点的地址,找不到则返回NULL。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
if (phead == NULL)
{
return NULL;
}
else
{
SLTNode* Pos = phead;
while (Pos)
{
if (Pos->data == x)
return Pos;
else
Pos = Pos->next;
}
return NULL;
}
}
定点插入/删除:
同过传递要插入元素的位置,将新的元素插入到当前位置的前面或者删除当前结点。
//定点插入
void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
assert(*phead != pos);
assert(pos);
SLTNode* tail = *phead;
SLTNode* newnode = BuySListNode(x);
if (tail==pos)//插入到第一个位置前
{
newnode->next = tail;
*phead = newnode;
}
else
{
while (tail->next != pos)
{
tail = tail->next;
}
newnode->next = tail->next;
tail->next = newnode;
}
}
//定点删除
void SLTErase(SLTNode** phead, SLTNode* pos)
{
assert(pos);
SLTNode* tail = *phead;
if ((*phead)->next == pos)//删除第一个
{
*phead = tail->next;
free(tail);
tail->next = NULL;
}
else
{
while (tail->next->next != pos)
{
tail = tail -> next;
}
SLTNode* temp = tail->next;
tail->next = tail->next->next;
free(temp);
temp->next = NULL;
}
}
删除链表:
在使用完链表后我们应当需要对结点依次删除,防止造成内存泄露。
void SLTDestroy(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
SLTNode* Next = cur->next;//保存下一结点
free(cur);
cur = Next;
}
}
2.2双链表的实现(带哨兵卫的双向循环链表)
与单链表相比,双链表中多存放了一个指向上一结点的指针,避免了单链表只能从头往下找某一元素的缺点,双链表的结点可以同时找到上一结点和下一结点,与单链表相比,更加具有实用性。此外在结构上,循环链表的尾指针的所指向的后继结点为头结点,而头结点所指向的前驱结点为尾结点。
2.11定义双链表的基本结构
typedef int LTDataType;
typedef struct LTNode
{
struct LTNode* prev;//前驱
struct LTNode* next;//后继
LTDataType data;
}LTNode;
2.21实现双向链表的增删查改
初始化哨兵卫结点:
带有哨兵卫的链表在插入元素时首先需要对哨兵卫结点进行初始化
LTNode* LTInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->next = phead;
phead->prev = phead;
return phead;
}
生成链表结点:
通过传递链表要存储的数据,动态开辟一块内存用于存储数据,与单链表一致。
LTNode* BuyLTNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
头插法/尾插法储存数据:
与单链表相比,双向链表的头插、尾插更为方便,不需要考虑对头结点做出的改变,只需要更改结点中前驱和后继所指向的结点,所以在传递参数上也只需要传递一级指针。
void LTPushFront(LTNode* head, LTDataType x)//头插
{
assert(head);
LTNode* newnode = BuyLTNode(x);
LTNode* first = head->next;
newnode->next = first;
first->prev = newnode;
newnode->prev = head;
head->next = newnode;
}
void LTPushBack(LTNode* head, LTDataType x)//尾插
{
assert(head);
LTNode* newnode = BuyLTNode(x);
LTNode* tail = head->prev;
tail->next = newnode;
newnode->prev = tail;
head->prev = newnode;
newnode->next = head;
}
头删除/尾删除:
头删尾删时要注意哨兵卫的后继结点指向向的不是自己,而是其他结点,这是判断链表是否为空的依据。
void LTPopFront(LTNode* head)//头删
{
assert(head);
assert(head->next!=head);
LTNode* first = head->next;
LTNode* second = first->next;
head->next = second;
second->prev = head;
free(first);
first = NULL;
}
void LTPopBack(LTNode* head)//尾删
{
assert(head);
assert(head->next!=head);
LTNode* tail = head->prev;
head->prev = tail->prev;
tail->prev->next = head;
free(tail);
tail==NULL;
}
定点插入/删除:
定点的插入删除可以复用于头插头删、尾插尾删中,为预防前驱后继指针指向错误,我们可以多定义几个指针保存要删除位置的前驱后继结点。
void LTInsert(LTNode* pos, LTDataType x)//定点插入
{
assert(pos);
LTNode* newnode = BuyLTNode(x);
LTNode* posPrev = pos->prev;
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
void LTErase(LTNode* pos)//定点删除
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
查找链表元素:
与单链表一样,同样是遍历链表,不同的是,作为循环链表,遍历的终止条件有所不同,循环链表结束遍历的条件为指针指向哨兵卫结点,而非循环链表结束遍历的条件为指针指向空。
LTNode* LTFind(LTNode* head, LTDataType x)
{
LTNode* cur = head->next;
while (cur != head)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
删除链表:
同样的,在使用完链表后我们也应当需要对结点依次删除,防止造成内存泄露。通过遍历链表依次删除每个结点,最后的哨兵卫结点也要删除
void LTDestory(LTNode* head)
{
assert(head);
LTNode* cur = head->next;
while (cur!=head)
{
LTNode* curNext = cur->next;
free(cur);
cur = curNext;
}
free(head);
}
结语:
本期关于链表的相关知识到这就介绍完了,如果感觉对你有帮助的话还请点个赞支持一下!有不对或者需要改正的地方还请指正,感谢各位的观看。