双向链表:
每个结点存在两个指针域,分别存储该结点的前驱结点引用和后继结点引用,从任意一个结点出发,都能通过前驱引用以及后继引用完成整个链表结点的访问。所以不难看出,单向链表能干的事,双向链表也能干!但是正是因为这一特性,相比于单链表,双向链表在访问其他结点上带来方便的同时,将占用更多的资源,因此在使用的时候可以根据自己的场景来决定使用何种数据结构。
和单链表改造的循环链表比起来,共同点是这两个结构都能够从任意结点通过不同的操作访问到链表的所有结点。但是除此之外,二者无论是结构还是访问方式等都不尽相同,何处不同,阅读完本文后自见分晓。
应用场景:单链表适用于解决一条道走到黑的访问场景,比如给正在排队测温的人测量,只需要测试一次就下一位,无需重复测试;循环链表适用于具有环状数据结构的场景,比如丢手绢游戏;双向链表则适合需要提供多个方向访问功能的数据结构,比如老师在考场监考,可以来回走动(假设这个考场座位是顺序的)
#pragma once
#include
#include
#include
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}ListNode;
//链表初始化
ListNode* ListInit();
// 创建返回链表的头结点.
ListNode* ListCreate(int x);
// 双向链表销毁
void ListDestory(ListNode** plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
#include"DSLTNode.h"
// 创建返回链表的头结点.
ListNode* ListCreate(int x)
{
ListNode *tmp = (ListNode *)malloc(sizeof(ListNode));
tmp->data = x;
tmp->next = tmp;
tmp->prev = tmp;
return tmp;
}
//链表初始化
ListNode * ListInit()
{
ListNode *head = ListCreate(0);
head->next = head;
head->prev = head;
return head;
}
// 双向链表销毁
void ListDestory(ListNode** plist)
{
ListNode *cur = *plist;
while (cur != *plist)
{
ListNode* next = cur->next;
cur = next;
free(next);
}
free(*plist);
*plist = NULL;
}
// 双向链表打印
void ListPrint(ListNode* plist)
{
assert(plist);
ListNode *cur = plist->next;
while (cur != plist)
{
printf("%d->",cur->data);
cur = cur->next;
}
printf("NULL\n");
}
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x)
{
assert(plist);
ListNode *newNode = ListCreate(x);
newNode->data = x;
ListNode *listtali = plist->prev;
listtali->next = newNode;
newNode->prev = listtali;
newNode->next = plist;
plist->prev = newNode;
}
// 双向链表尾删
void ListPopBack(ListNode* plist)
{
assert(plist);
assert(plist->next != plist);
ListNode *tail = plist->prev;
ListNode *prevtail = tail->prev;
free(tail);
prevtail->next = plist;
plist->prev = prevtail;
}
// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode *newNode = ListCreate(x);
ListNode *first = phead->next;
newNode->next = first;
first->prev = newNode;
newNode->prev = phead;
phead->next = newNode;
}
// 双向链表头删
void ListPopFront(ListNode* plist)
{
assert(plist);
assert(plist->next != plist);
ListNode *first = plist->next;
ListNode *second = first->next;
plist->next = second;
second->prev = plist;
free(first);
}
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x)
{
assert(plist);
ListNode *cur = plist->next;
while (cur != plist)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
ListNode *newNode = ListCreate(x);
ListNode *prev = pos->prev;
prev->next = newNode;
newNode->prev = prev;
newNode->next = pos;
pos->prev = newNode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
ListNode *prev = pos->prev;
ListNode *next = pos->next;
prev->next = next;
next->prev = prev;
}
初始化一个带两个指针的结点,需要将他的前驱指针和后继指针都指向他自己本身
// 创建返回链表的头结点.
ListNode* ListCreate(int x)
{
ListNode *tmp = (ListNode *)malloc(sizeof(ListNode));
tmp->data = x;
tmp->next = tmp;
tmp->prev = tmp;
return tmp;
}
带哨兵位的头节点,作用是为了实现一个带头的双向链表
//链表初始化
ListNode * ListInit()
{
ListNode *head = ListCreate(0);
head->next = head;
head->prev = head;
return head;
}
将每一个结点取下来,将他释放掉,由于需要将头节点给释放并且将头节点置空,所以需要用到二级指针
// 双向链表销毁
void ListDestory(ListNode** plist)
{
ListNode *cur = *plist;
while (cur != *plist)
{
ListNode* next = cur->next;
cur = next;
free(next);
}
free(*plist);
*plist = NULL;
}
从头节点的下一个结点开始遍历一次,将结点的data打印一次,当再次回到头节点的时候循环终止,链表就已经被遍历一次了
// 双向链表打印
void ListPrint(ListNode* plist)
{
assert(plist);
ListNode *cur = plist->next;
while (cur != plist)
{
printf("%d->",cur->data);
cur = cur->next;
}
printf("NULL\n");
}
找到链表的尾,这里可以通过头节点的前驱指针来找到链表的尾tail,将tail的后继指针指向newNode,newNode的前驱指针指向tail,最后让newNode成为新的尾,newNode的后继指针指向头节点,头结点的前驱指针指向newNode这样新结点就尾插到链表的后面了
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x)
{
assert(plist);
ListNode *newNode = ListCreate(x);
newNode->data = x;
ListNode *listtali = plist->prev;
listtali->next = newNode;
newNode->prev = listtali;
newNode->next = plist;
plist->prev = newNode;
}
通过头节点找到尾tail,再找到尾结点的前一个结点prev,将prev的next指向头节点,再将头节点的前驱指针指向prev,最后释放尾结点
// 双向链表尾删
void ListPopBack(ListNode* plist)
{
assert(plist);
assert(plist->next != plist);
ListNode *tail = plist->prev;
ListNode *prevtail = tail->prev;
free(tail);
prevtail->next = plist;
plist->prev = prevtail;
}
头插并不是插入在哨兵位的节点前面,而是插入在哨兵位节点的下一个位置,把他看作第一个结点,先将头节点的下一个结点first找出来,将新的结点的后继指针指向first,first的前驱指针指向新结点,再让哨兵位结点的后继指针指向新结点,新结点的前驱指针指向哨兵位结点
// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode *newNode = ListCreate(x);
ListNode *first = phead->next;
newNode->next = first;
first->prev = newNode;
newNode->prev = phead;
phead->next = newNode;
}
// 双向链表头删
void ListPopFront(ListNode* plist)
{
assert(plist);
assert(plist->next != plist);
ListNode *first = plist->next;
ListNode *second = first->next;
plist->next = second;
second->prev = plist;
free(first);
}
遍历一遍原链表,找出值为x的结点就将他返回
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x)
{
assert(plist);
ListNode *cur = plist->next;
while (cur != plist)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
ListNode *newNode = ListCreate(x);
ListNode *prev = pos->prev;
prev->next = newNode;
newNode->prev = prev;
newNode->next = pos;
pos->prev = newNode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
ListNode *prev = pos->prev;
ListNode *next = pos->next;
prev->next = next;
next->prev = prev;
}
顺序表优点:
1、按下标进行随机访问
2、cpu高速缓存命中比较高
顺序表缺点:
1、空间不够需要增容,一定程度的性能消耗,有一定的空间浪费
2、头部或者中间插入删除数据,需要挪动数据,效率比较低,挪动数据的时间复杂度O(N)
链表表优点:
1、按需申请内存,需要存一个数据,就申请一块内存,也不浪费空间
2、任意位置插入删除不需要挪动数据,时间复杂度O(1)
顺序表缺点:
1、空间不够需要增容,一定程序的性能消耗
2、头部或者中间插入删除数据,需要挪动数据,效率比较低,挪动数据的时间复杂度O(N)
总结:顺序表和链表是相辅相成的,互相弥补对方的缺点