链表的分类以及双向链表的实现
1.链表的分类
1.1单向链表与双向链表的区别
单向链表只能从头结点遍历到尾结点。
双向链表不仅能从头结点遍历到尾结点,还能从尾结点遍历到头结点。
1.2带头链表与不带头链表的区别
1.3循环链表与不循环链表的区别(也称为带环链表与不带环链表)
不循环链表中,尾结点的next指针的值为NULL
循环链表中,尾结点的next指针可以指向该链表中的任意一个结点,包括尾结点。
2.双向链表中需要注意的点
2.1:双向链表的全称为双向带头循环的链表,单向链表的全称为单向不带头不循环的链表。
2.2双向链表中结点的结构
typedef int DataType;
typedef struct ListNode
{
DataType data;//存储DataType类型的数据
struct ListNode* next;//存储后一个结点的地址
struct ListNode* prev;//存储前一个结点的地址
}LN;
2.3区分单链表(单向不带头不循环链表)与双向链表(双向带头循环链表)为空表的情况
2.3.1: 若单链表为空表,表示单链表中没有结点(即没有存储有效数据),即指向第一个结点的指针为空指针
2.3.2:若双向链表为空表,并不是表示双向链表中没有结点,而是只有一个头结点,且头结点中存储一个无效数据,并且头结点中的两个指针均指向头结点(要满足循环)。故双向链表为空表时,并不是指链表中一个结点也没有,而是指链表中没有存储有效数据。
2.4常使用的两种链表
虽然链表的分类有很多,但是最常用的只有单链表(单向不带头不循环链表)与双向链表(双向带头循环链表)。
1.单链表:结构简单,一般不会用来存数据,实际中更多的是作为其他数据结构的子结构,如哈希桶、图的邻接表等等,另外单链表在笔试面试中考察的较多。
2.双向链表:结构复杂,一般用于单独存储数据,实际中使用的链表数据结构,通常都是双向链表。虽然双向链表的结构复杂,但是该结构的优势明显,另外该结构也容易实现。
2.5顺序表与单链表的比较
3.双向链表的实现
a.List.h文件
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int DataType;
//定义双向链表结点的结构
typedef struct ListNode
{
DataType data;
struct ListNode* next;//存储下一个结点的地址
struct ListNode* prev;//存储上一个结点的地址
}ListNode;
//申请新结点的空间,并返回该空间的地址
ListNode* ListBuyNode(DataType x);
//双向链表的初始化(初始情况下,双向链表中只有一个头结点,不存储任何有效的数据)
void ListInit(ListNode** pphead);
//打印双向链表(打印存储的有效数据)
void ListPrint(ListNode* phead);
//向双向链表尾部插入结点
void ListPushBack(ListNode *phead, DataType x);//向双向链表尾部插入结点时,不会改变头结点的地址,因此传头结点的地址即可
//向双向链表头部插结点(注意:不是插在头结点之前,而是插在头结点后面。)
//因为头结点中存储的是无效数据,第一个存储有效数据的结点是头结点后面的结点
void ListPushFront(ListNode* phead, DataType x);//向双向链表的头部插入结点时,不会改变头结点的地址,因此传头结点的地址即可
//判断双向链表是否为空表
//若双向链表中只有一个头结点,头结点中没有存储有效数据,且头结点中的两个指针均指向头结点(要满足循环)),则双向链表为空表
bool ListEmpty(ListNode* phead);
//删除尾部的结点(删除尾部的结点时,头结点的地址不会发生变化,因此传头结点的地址即可)
void ListPopBack(ListNode* phead);
//删除第一个存储有效数据的结点(删除该结点时,头结点的地址不会变化,因此传头结点的地址即可)
//为什么不是删头结点呢?因为如果把头结点删除了,双向循环链表就不带头了,就不是双向循环链表了
void ListPopFront(ListNode* phead);
//查找存储数据为x的结点,返回该结点的地址
ListNode* ListFind(ListNode* phead, DataType x);
//删除指定的结点(pos指向的结点)
//头结点是不能删的,不然就不是双向链表了
void ListErase(ListNode * pos);
//在指定的结点(pos指向的结点)后插入结点
void ListInsert(ListNode* pos, DataType x);
//双向链表的销毁(包括头结点也要销毁)
void ListDestory(ListNode** pphead);//链表销毁后,头结点的地址就变成了NULL,因此要传指向头结点的指针的地址
/*
容易发现除了双向链表的初始化与销毁的形参是二级指针外,其余的函数的形参都是一级指针
那么能不能将初始化与销毁这两个函数的形参做一些修改呢?
答案是可以的,看下面修改后的函数
*/
//双向链表的初始化(直接申请一个头结点的空间,然后返回空间的地址)
ListNode* ListInit2();
//双向链表的销毁的第二种方法
void ListDestory2(ListNode* phead);
b.List.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//申请新结点的空间
ListNode* ListBuyNode(DataType x)
{
ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));
assert(NewNode);
NewNode->data = x;
NewNode->next = NewNode->prev = NewNode;
/*
这种写法便于创建头结点,当双向链表为空表时,头结点中的两个指针均指向自己(链表满足循环)
如果是下面这种写法,创建头结点时,链表无法循环,就不是双向链表了
NewNode->next = NewNode->prev =NULL,
*/
return NewNode;
}
//双向链表的初始化(初始情况下,双向链表中只有一个头结点,不存储任何有效的数据)
void ListInit(ListNode** pphead)
{
assert(pphead);//双向链表未初始化之前,链表中没有头结点,因此指向头结点的指针的值为NULL,但指向头结点的指针的地址不是NULL
//pphead:指向头结点的指针的地址
//*pphead:头结点的地址
*pphead = ListBuyNode(INT_MAX);//用INT_MAX表示头结点中存储的是无效数据
}
//打印双向链表(打印存储的有效数据)
void ListPrint(ListNode* phead)
{
assert(phead);//双向链表中,头结点的地址不可能是NULL
if (phead->next == phead)//若双向链表为空表(只有一个头结点,头结点中存储的是无效数据,且头结点中的两个指针均指向头结点)
{
printf("双向链表为空表\n");
return;
}
ListNode* pcur = phead->next;//pcur指向第一个存储有效数据的结点
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//向双向链表尾部插入结点
void ListPushBack(ListNode* phead, DataType x)
{
assert(phead);//双向链表中,头结点的地址不可能是NULL
//申请新结点的空间
ListNode* NewNode = ListBuyNode(x);
//先修改新结点中指针的指向
NewNode->prev = phead->prev;
NewNode->next = phead;
//再修改原链表的尾结点中next指针的指向
phead->prev->next = NewNode;
//最后修改头结点中的prev指针的指向
phead->prev = NewNode;
}
//向双向链表头部插结点(注意:不是插在头结点之前,而是插在头结点后面。)
void ListPushFront(ListNode* phead, DataType x)
{
assert(phead);//双向链表中,头结点的地址不可能是NULL
//申请新结点的空间
ListNode* NewNode = ListBuyNode(x);
//先修改新结点中指针的指向
NewNode->next = phead->next;
NewNode->prev = phead;
//再改变原链表中第一个存储有效数据的结点中prev指针的指向
phead->next->prev = NewNode;
//最后改变头结点中next指针的指向
phead->next = NewNode;
}
//判断双向链表是否为空表(若双向链表中只有一个头结点,头结点中没有存储有效数据,且头结点中的两个指针均指向头结点(要满足循环))
bool ListEmpty(ListNode* phead)
{
assert(phead);//双向链表中,头结点的地址不可能是NULL
return phead->next == phead;//若表达式为真(即双向链表是空表),返回true,否则返回false
}
//删除尾部的结点
void ListPopBack(ListNode* phead)
{
assert(phead);//双向链表中,头结点的地址不可能是NULL
assert(!ListEmpty(phead));//若双向链表为空表则不能删除结点
//删除结点时,会影响头结点的prev指针,以及尾结点的前一个结点的next指针
ListNode* ptail = phead->prev;//ptail指向的是尾结点
ListNode* prev = ptail->prev;//定义prev指针,指向尾结点的前一个结点
//等号的左边的prev是该函数中是定义的变量,它的作用域是该函数;而右边的prev是结构体中成员变量(只有访问这个结构体时,才会用到prev),它的作用域是这个结构体。两个prev的作用域不同,因此两者不冲突。
//先修改原链表中,尾结点的前一个结点的next指针
prev->next = phead;
//再修改头结点的prev指针
phead->prev = prev;
//最后释放原链表中尾结点的空间
free(ptail);
ptail = NULL;
}
//删除第一个存储有效数据的结点
void ListPopFront(ListNode* phead)
{
assert(phead);//双向链表中,头结点的地址不可能是NULL
assert(!ListEmpty(phead));//若双向链表为空表则不能删除结点
//删除结点时,会影响头结点中的next指针,以及存储第一个有效数据的结点的后一个结点中的prev指针
ListNode* first = phead->next;//first指向第一个存储有效数据的结点
ListNode* Next = first->next;//Next指向存储第一个有效数据的结点的后一个结点
//先修改存储第一个有效数据的结点的后一个结点中prev指针的指向
Next->prev = phead;
//再修改头结点中的next指针
phead->next = Next;
//最后释放原链表中存储第一个有效数据的结点的空间
free(first);
first = NULL;
}
//查找存储数据为x的结点,返回该结点的地址
ListNode* ListFind(ListNode* phead, DataType x)
{
assert(phead);//双向链表中,头结点的地址不可能是NULL
//从第一个存储有效数据的结点开始往后找
ListNode* pcur = phead->next;//pcur指向第一个存储有效数据的结点
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//若将所有存储有效数据的结点都遍历了一遍后,依旧找不到存储x的结点,则返回NULL
return NULL;
}
//删除指定的结点(pos指向的结点)
void ListErase(ListNode* pos)
{
assert(pos);//pos指向的结点的地址不可能是NULL
//删除结点时,会影响pos指向的结点的前一个结点中的next指针,
// 以及pos指向的后一个结点的prev指针
//先修改pos指向的结点的前一个结点中的next指针
pos->prev->next = pos->next;
//再修改pos指向的后一个结点的prev指针
pos->next->prev = pos->prev;
//最后释放pos指向的结点的空间
free(pos);
pos = NULL;
}
//在指定的结点(pos指向的结点)后插入结点
void ListInsert(ListNode* pos, DataType x)
{
assert(pos);//pos指向的结点的地址不可能是NULL
//申请新结点的空间
ListNode* NewNode = ListBuyNode(x);
//插入结点时,会影响pos指向的结点的next指针,以及原链表中pos指向的结点的后一个结点的prev指针
ListNode* Next = pos->next;//Next指向原链表中,pos指向的结点的下一个结点
//先修改新结点中指针的指向
NewNode->prev = pos;
NewNode->next = Next;
//再修改pos指向的结点的next指针
pos->next = NewNode;
//再修改原链表中pos指向的结点的后一个结点的prev指针
Next->prev = NewNode;
}
//双向链表的销毁(包括头结点也要销毁)
void ListDestory(ListNode** pphead)
{
//pphead表示指向头结点的指针的地址
//*pphead表示头结点的地址
//若pphead==NULL,则*pphead就无法找到头结点的地址了
//双向链表中,头结点的地址不可能为NULL
assert(pphead && *pphead);
ListNode* pcur = (*pphead)->next;//pcur指向第一个存储有效数据的结点
//先回收存储有效数据的结点的空间
while (pcur != *pphead)
{
ListNode* Next = pcur->next;//Next指向pcur指向的结点的下一个结点
free(pcur);
pcur = Next;
}
//再回收头结点的空间
free(*pphead);
*pphead = pcur = NULL;
}
//双向链表的初始化(直接申请一个头结点的空间,然后返回空间的地址)
ListNode* ListInit2()
{
ListNode* phead = ListBuyNode(INT_MAX);//INT_MAX表示头结点中存储的是无效数据
assert(phead);
return phead;
}
//双向链表的销毁的第二种方法
void ListDestory2(ListNode* phead)
{
assert(phead);//双向链表中,头结点的地址不可能为NULL
ListNode* pcur = phead->next;//pcur指向第一个存储有效数据的结点
//先回收存储有效数据的结点的空间
while (pcur != phead)
{
ListNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
//再回收头结点的空间
free(phead);
/*
头结点的空间虽然被回收了,但由于形参接收的是实参的值,不是实参的地址,形参的改变无法影响实参
因此实参(plist)依然是指向头结点,plist变成了野指针,需要手动将plist置为NULL
*/
pcur = phead = NULL;
}
test.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//测试双向链表的初始化
void test1(ListNode **pplist)
{
ListInit(pplist);
}
//测试向双向链表中尾插结点
void test2(ListNode *phead)
{
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);
}
//测试向双向链表中头插结点
void test3(ListNode *phead)
{
ListPushFront(phead, 4);
ListPushFront(phead, 3);
ListPushFront(phead, 2);
ListPushFront(phead, 1);
ListPrint(phead);
}
//测试删除尾结点
void test4(ListNode* phead)
{
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPrint(phead);//1->2->
ListPopBack(phead);
ListPrint(phead);//1->
ListPopBack(phead);
ListPrint(phead);//空表
//ListPopBack(phead);空表不能删除结点,会报错
}
//测试删除第一个存储有效数据的结点
void test5(ListNode* phead)
{
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPrint(phead);//1->2->
ListPopFront(phead);
ListPrint(phead);//2->
ListPopFront(phead);
ListPrint(phead);//此时双向链表为空表
}
//测试查找存储数据为x的结点
void test6(ListNode *phead)
{
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);//1->2->3->4->
ListNode* find = ListFind(phead, 2);
if (find != NULL)
{
printf("找到了\n");
}
else
{
printf("找不到\n");
}
}
//测试删除指定的结点(pos指向的结点)
void test7(ListNode* phead)
{
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);//1->2->3->4->
ListNode* find = ListFind(phead, 1);
ListErase(find);
ListPrint(phead);//2->3->4->
}
//测试在指定的结点(pos指向的结点)后插入结点
void test8(ListNode* phead)
{
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPrint(phead);//1->2->3->4->
ListNode* find = ListFind(phead, 4);
ListInsert(find, 5);//1->2->3->4->5->
ListPrint(phead);
}
//测试摧毁双向链表
void test9(ListNode** pphead)
{
ListPushBack(*pphead, 1);
ListPushBack(*pphead, 2);
ListPrint(*pphead);//1->2->
ListDestory(pphead);
}
int main()
{
ListNode * plist = NULL;//plist表示指向头结点的指针
//测试双向链表的初始化
test1(&plist);
//测试向双向链表中尾插结点
//test2(plist);
//测试向双向链表中头插结点
//test3(plist);
//测试删除尾结点
//test4(plist);
//测试删除第一个存储有效数据的结点
//test5(plist);
//测试查找存储数据为x的结点
//test6(plist);
//测试删除指定的结点(pos指向的结点)
//test7(plist);
//测试在指定的结点(pos指向的结点)后插入结点
//test8(plist);
//测试销毁双向链表
//test9(&plist);
//测试销毁双向链表的第二种方法
ListDestory2(plist);
//虽然链表被销毁了,但是plist依然是指向原来链表中的头结点,变成了野指针,需要手动将plist置为NULL
plist = NULL;
return 0;
}