链表的分类
组合起来共有八大结构。无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
- 之前已经实现过单链表了,今天主要学习实现双向带头循环链表
双向带头循环链表的实现
结构体的定义
与单链表区别在于,双向体现在多了一个prev指向上一个节点的指针,循环体现在尾节点的next指针指向哨兵位头节点,哨兵位头节点的prev指针指向尾节点。
特别说明
- 由于增删查改的操作都是改变前后指针指向,相当于改变结构体内容,所以传参时只需传结构体指针也就是一级指针,只有在初始化操作时需要传递二级指针(改变参数指针为哨兵位头节点),不过可以用返回一级指针的方式来代替。
- 各含参数指针接口均需断言不为空,因为该双向带头循环链表中至少有一个哨兵位头节点。
- 下文简称为双向链表
创建节点BuyListNode
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
定义一个node结构体指针,用malloc动态开辟一个为LTNode结构体大小的指针,将前后指针都置空,方便后续进行链接同时明确状态,然后存储数据返回该指针即可。
初始化LTInit
LTNode* LTInit()
{
LTNode* phead = BuyListNode(1);
phead->next = phead;
phead->prev = phead;
return phead;
}
其中创建节点中的数据是什么无所谓,因为哨兵位头节点主要是辅助链表操作,标记链表边界,不参与数据存储和常规数据处理逻辑。
这里在初始化一个只有哨兵位的双向循环链表,哨兵位的前后指针都指向自己,明确初始状态。
判断链表是否为空LTEmpty
bool LTEmpty(LTNode* phead)
{
assert(phead);
/*if (phead->next == phead)
{
return true;
}
else
{
return false;
}*/
return phead->next == phead;
}
用于判断双向链表是否为空链表,即是否只包含哨兵位头节点,在删除功能中需判断。return一句中如果phead的next指向自己,那么返回1即真为空链表,反之。
打印节点LTPrint
void LTPrint(LTNode* phead)
{
assert(phead);
printf("<=head=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
从phead的下一个节点开始打印,直到遍历循环到phead停止。
尾插LTPushBack
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
//可用LTInsert代替
//LTInsert(phead,x);
}
利用phead的前指针找到尾,然后改变phead的前、tail的后指针来链接newnode。
尾删PopBack
void LTPopBack(LTNode* phead)
{
assert(phead);
//判断链表不为空,否则尾删造成非法访问
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);
tail = NULL;
//可用LTErase代替
//LTErase(phead->prev);
}
删除操作要判断链表是否为空,避免非法访问,用phead前指针找到尾,改变指针链接然后置空。
头插LTPushFront
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//可以换链接顺序
//LTNode* newnode = BuyListNode(x);
提前保存节点
//LTNode* first = phead->next;
//newnode->next = first;
//first->prev = newnode;
//newnode->prev = phead;
//phead->next = newnode;
//不能交换顺序
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
newnode->prev = phead;
phead->next = newnode;
//可用LTInsert代替
//LTInsert(phead->next,x);
}
需注意链接顺序,若提前保存phead的next节点,那么先改前还是后都行
反之只能先改与后节点的链接,要不然先改与头节点的链接就找不到原phead的next节点也就是后节点了。
头删LTPopFront
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
phead->next = phead->next->next;
phead->next->next->prev = phead;
free(phead->next);
phead->next = NULL;
//可直接用LTErase代替
//LTErase(phead->next);
}
注意判断链表不为空,可以提前保存节点,也可以像这样连续访问。
具体节点前插入LTInsert
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
利用参数指针找到前一个节点保存下来,改变四个指针指向即可。
查找具体节点LTFind
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
从哨兵位下一个节点开始遍历直到等于哨兵位停止,比较各节点中存储的数据与目标值,相等即返回该节点,否则遍历完后返回空。
pos位置删除LTErase
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
//pos = NULL;这里是形参
}
一般与查找功能共同使用,不能删除哨兵位头节点,会影响该数据结构的完整性,同时在使用特定接口时还会产生非法访问的错误。
销毁链表LTDestroy
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur!=phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
从哨兵位节点下一个节点开始free,提前保存下一个节点,free后改变指针指向下一个,直到遍历到哨兵位节点,free后,由于参数指针是形参,调用完该接口后记得外部手动置空。
整体代码
- 头文件
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
//void LTInit(LTNode** pphead);
LTNode* LTInit();
//void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
// 在pos位置之前插入一个值
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);
void LTDestroy(LTNode* phead);
- 头文件
#include"List.h"
void TestList1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
LTPushFront(plist, 5);
LTPushFront(plist, 6);
LTPrint(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPrint(plist);
LTInsert(plist, 3);
LTInsert(plist, 4);
LTPrint(plist);
//LTErase(plist);不能删哨兵位头节点
}
void TestList2()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
LTNode*pos=LTFind(plist, 3);
if (pos)
{
LTErase(pos);
}
//LTErase(plist);
//LTDestroy(plist);
LTPrint(plist);
}
int main()
{
TestList1();
return 0;
}
- 各函数的定义
#include"List.h"
//创建节点,后续各接口参数指针一定不为空
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
//初始化节点
LTNode* LTInit()
{
LTNode* phead = BuyListNode(1);
phead->next = phead;
phead->prev = phead;
return phead;
}
//
bool LTEmpty(LTNode* phead)
{
assert(phead);
/*if (phead->next == phead)
{
return true;
}
else
{
return false;
}*/
return phead->next == phead;
}
//打印节点
void LTPrint(LTNode* phead)
{
assert(phead);
printf("<=head=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
//可用LTInsert代替
//LTInsert(phead,x);
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//判断链表不为空,否则尾删造成非法访问
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);
tail = NULL;
//可用LTErase代替
//LTErase(phead->prev);
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//可以换链接顺序
//LTNode* newnode = BuyListNode(x);
提前保存节点
//LTNode* first = phead->next;
//newnode->next = first;
//first->prev = newnode;
//newnode->prev = phead;
//phead->next = newnode;
//不能交换顺序
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
newnode->prev = phead;
phead->next = newnode;
//可用LTInsert代替
//LTInsert(phead->next,x);
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
phead->next = phead->next->next;
phead->next->next->prev = phead;
free(phead->next);
phead->next = NULL;
//可直接用LTErase代替
//LTErase(phead->next);
}
//具体节点前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
//pos位置删除,不能删头节点
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
//pos = NULL;这里是形参
}
//销毁链表
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur!=phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
//查找具体节点
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
一道有意思的链表oj(接口型)
思路:1.拷贝节点链接在原节点后面2.拷贝节点的random指向是原节点random的next指向3.将拷贝节点解下来链接成新链表,并恢复原链表。该方法时间复杂度为O(N)对比,用指针数组来接收拷贝链表中的每一个节点,控制遍历随机指针去找对应的random值,时间复杂度为O(N^2),低效。
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
struct Node* copyRandomList(struct Node* head) {
struct Node*cur=head;
while(cur)
{
//1.插入拷贝节点在原节点后
struct Node*copy=(struct Node*)malloc(sizeof(struct Node));
copy->val=cur->val;
struct Node*next=cur->next;
//链接
cur->next=copy;
copy->next=next;
cur=next;
}
//2.拷贝节点的random是原节点random的next
cur=head;
while(cur)
{
//上一步已经链接好copy,不需要额外开辟空间
struct Node*copy=cur->next;
if(cur->random==NULL)
{
copy->random=NULL;
}
else
{
copy->random=cur->random->next;
}
cur=copy->next;
//cur=cur->next->next;
}
//3.将拷贝节点解下来链接成新链表,恢复原链表
struct Node*copyhead,*copytail;
//用的哨兵位节点,也可以不用通过if-else语句判断头节点为空情况
copyhead=copytail=(struct Node*)malloc(sizeof(struct Node));
copytail->next=NULL;
cur=head;
while(cur)
{
struct Node*copy=cur->next;
struct Node*next=copy->next;
//copy尾插
copytail->next=copy;
copytail=copytail->next;
//恢复原链表
cur->next=next;
cur=next;
}
//注意哨兵位节点的返回值是next
return copyhead->next;
}
结语
- 链表的学习告一段落啦,希望都有所收获和进步,感谢支持