前言:
前篇学习了 数据结构的单链表 那么这篇深入学习单链表,即完成带头双向链表的实现。
掌握了带头双向链表之后会发现,它的巧妙之处在于两个核心的函数能嵌入其它操作的接口函数中,使得能够称为在十分钟内搞定的数据存储操作方法。
/知识点汇总/
学习了单链表之后,可以发现,单链表因为只能单一的指向后继结点,想要找到前驱就不容易,因此,引入双向链表,当用单链表来进行插入删除时,显得比较麻烦,而带头双向循环链表就可以很好的解决这个问题。
首先来看一下双向链表的结构:
定义一个头结点,当只有一个头结点时,这里注意的是,头结点的next 和prev不再指向NULL 而是指向自己,这样就构成了一个双向循环带头的链表;每一个结点都有prev和next 他们分别指向当前结点的前一个结点和后一个结点。因此只要知道一个结点就可以找到他的前一个结点(前驱)和后一个结点(后继)。
typedef int LTDataType;
//定义双向链表的结构体
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType val;
}LTNode;
各个接口函数的声明。
//创建节点
LTNode* CreatLTNode(LTDataType x);
//初始化
//void LTInit(LTNode* phead);
LTNode* LTInit();
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTpopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//任意位置插入
void LTInsert(LTNode* phead, LTDataType x);
//任意位置删除
void LTErase(LTNode* phead);
//销毁链表
void LTDistory(LTNode* phead);
由于创建结点涉及多个函数中都会使用到,所以直接封装为独立的CreatLTNode函数,以便后面使用时直接调用,如下所示:
LTNode* CreatLTNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");
//return;
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
接下来写初始化接口函数,介绍两种方法,之前两篇都是用的传入参数的方法,那么这次就用传出参数的方法进行初始化。
//初始化1
void LTInit(LTNode* phead)
{
phead = CreatLTNode(-1);
phead->next = phead;
phead->prev = phead;
}
//初始化2
LTNode* LTInit()
{
LTNode* phead = CreatLTNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
然后,为了让链表中的操作数据得以体现,就写一个打印的接口函数,主要涉及到一个哨兵位就方便定义一个工作指针遍历访问数据域输出到屏幕即可,如下所示:
void LTPrint(LTNode* phead)
{
assert(phead);
printf("哨兵位:>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d", cur->val);
cur = cur->next;
if(cur != phead)
printf("<==>");
}
printf("\n");
}
头插、尾插操作,为了体现后面的LTInsert对这两个函数的区别对比或者快捷封装对比,就以注释写法2得以体现。
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = CreatLTNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
//写法2:
//LTInsert(phead->prev, x);
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//写法1:
//LTNode* newnode = CreatLTNode(x);
//newnode->next = phead->next;
//phead->next->prev = newnode;
//phead->next = newnode;
//newnode->prev = phead;
//写法2:加一个指针,这样可以不像上面的写法注意顺序
LTNode* newnode = CreatLTNode(x);
LTNode* first =phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
//写法3;
//LTInsert(phead->next, x);
}
头删、尾删操作,为了体现后面的LTErase对这两个函数的区别对比或者快捷封装对比,就以注释写法2得以体现。
//尾删
void LTpopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);//判空
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
free(tail);
tailprev->next = phead;
phead->prev = tailprev;
//写法2:
//LTErase(phead->prev);
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);//注意判空,头节点不可删
LTNode* first = phead->next;
LTNode* second = phead->next->next;
phead->next = second;
second->prev = phead;
free(first);
first = NULL;
//写法2:
//LTErase(phead->next);
}
两个关键接口函数牢牢掌握有助于带头双向链表的一气呵成,而且简单又高效,具体如下所示:
//任意位置插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* newnode = CreatLTNode(x);
//三个指针的连接
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
//任意位置删除
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posNext = pos->next;
LTNode* posPrev = pos->prev;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
查找操作,与打印函数相似引用工作指针,找到对应数据。
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
return cur;
cur = cur->next;
}
return NULL;
}
最后一个销毁链表,与单链表类似free掉各个结点即可,另外记得头结点也得销毁。
//销毁链表
void LTDistory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);//删除头节点
phead = NULL;
}
简单的写几个测试应用,目的是检测各个接口函数是否满足需求,是否存在一些bug。
#include "DLList.h"
//测试1;
void TestList1()
{
//LTNode* plist = NULL;
//LTInit(&plist);//传地址
//方法二:
LTNode* plist = LTInit();
//尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
//打印
LTPrint(plist);
}
//测试2;
void TestList2()
{
LTNode* plist = LTInit();
//尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
//打印
LTPrint(plist);
//头插
LTPushFront(plist, 6);
//打印
LTPrint(plist);
//头插
LTPushFront(plist, 7);
//打印
LTPrint(plist);
//头插
LTPushFront(plist, 8);
//打印
LTPrint(plist);
//头删
LTPopFront(plist);
LTPrint(plist);
//头删
LTPopFront(plist);
LTPrint(plist);
//头删
LTPopFront(plist);
LTPrint(plist);
}
//测试3;
void TestList3()
{
LTNode* plist = LTInit();
//尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
//打印
LTPrint(plist);
//头插
LTPushFront(plist, 6);
//打印
LTPrint(plist);
//找到元素
LTNode* pos = LTFind(plist, 3);
//任意位置插入
if (pos)
{
pos->val *= 10;
}
LTPrint(plist);
}
int main()
{
//TestList1();
//TestList2();
TestList3();
return 0;
}
为了体现两个关键函数带来的简便操作。
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTInsert(phead->prev, x);
}
//尾删
void LTpopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);//判空
LTErase(phead->prev);
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTInsert(phead->next, x);
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
LTErase(phead->next);
}
//任意位置插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* newnode = CreatLTNode(x);
//三个指针的连接
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
//任意位置删除
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posNext = pos->next;
LTNode* posPrev = pos->prev;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
1.顺序表与链表的对比,有什么区别呢?
顺序表:只适合尾的操作,头部、中间元素涉及大量的挪动数据O(N);其次地址空间是连续的,方便排序。
空间不够需要扩容,扩容有一定的消耗,且有可能存在一定的空间浪费。支持下标随机访问O(1)
2.链表(双向)优势:
(1).任意位置插入删除都是O(1);
(2).按需申请释放,合理利用空间,不存在浪费。
(3).还有一个显著的优势,带头双向链表的接口实现关键就是掌握任意位置插入和任意位置删除两个函数就事半功倍了。
问题:下标随机访问不方便。链表不好排序