双向链表及链表总结

链表的分类

在这里插入图片描述
组合起来共有八大结构。

无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。


带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

  • 之前已经实现过单链表了,今天主要学习实现双向带头循环链表

双向带头循环链表的实现

结构体的定义

在这里插入图片描述

在这里插入图片描述

与单链表区别在于,双向体现在多了一个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;
}

结语

  • 链表的学习告一段落啦,希望都有所收获和进步,感谢支持
    请添加图片描述