双向链表的实现
- 一,双向链表的特点
- 二,双向链表的结构
- 三,双向链表的内容实现
- 3.1创建node节点
- 3.2初始化
- 3.3打印
- 3.4插入
- 3.4.1尾插
- 3.4.2头插
- 3.4.3在pos位置上插入
- 3.5删除
- 3.5.1尾删
- 3.5.2头删
- 3.5.3删除pos位置上的数据
- 四,调试技巧(具体示例)
- 五,总结
一,双向链表的特点
这里的双向链表就是单链表的升级版,链表有的共性他也有感兴趣的可以先看看单链表的内容链接: link
二,双向链表的结构
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
LTDataType date;
struct ListNode* next;
}ListNode;
这里还是老生常谈,在写比较大一些的代码的时候我们还是要重新定义一下数据类型这样方便我们以后可以更方便的使用各种数据类型。
至于双向链表,就是它有两个指针既可以指向前面的数据也可以指向后面的数据,所以这里定义的时候就分成了前指针prev和后指针next,这里贴一张图方便理解双向链表。
三,双向链表的内容实现
// 创建返回链表的头结点.
ListNode* BuyLTNode(LTDataType x);
//初始化
ListNode* LTInit();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
这里还要提醒一下,我们在写代码对我们的函数命名的时候尽量是按照它对应的英文单词取进行命名,这样有助于提升我们的代码质量和可读性。
3.1创建node节点
//创建节点
ListNode* BuyLTNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL)
{
perror("malloc failed");
exit(-1);
}
node->prev = NULL;
node->date = x;
node->next = NULL;
return node;
}
3.2初始化
//初始化
ListNode* LTInit()
{
ListNode* phead = BuyLTNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
初始化是所有的开始,我们前面的顺序表,单链表都有进行初始化,后边我们要学习的栈和队列也是要有初始化的。
3.3打印
//打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
printf("pHead<->");
while (cur != pHead)
{
printf("%d<->", cur->date);
cur = cur->next;
}
printf("\n");
}
打印这里是为了方便我们进行调试代码的。
3.4插入
这里我们的双向链表的插入一共分为了三种插入分为是尾插,头插,以及在特定位置下插入
3.4.1尾插
//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
ListNode* newnode = BuyLTNode(x);
ListNode* tail = pHead->prev;
newnode->prev = tail;
tail->next = newnode;
newnode->next = pHead;
pHead->prev = newnode;
}
这里我们需要理解原本d3作为尾它的next是指向d1的,而d1的prev是指向d3的所以我们先记录下d1的prev的数据然后再把newnode的prev指向d3也就是tail,然后tail的next就指向的newnode,接着newnode的next就指向的头,头的prev就指向了newnode的尾。
3.4.2头插
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = BuyLTNode(x);
newnode->next = pHead->next;
pHead->next->prev = newnode;
pHead->next = newnode;
newnode->prev = pHead;
}
我们还是画图分析,因为是头插,我们newnode的next就指向了phead的下面一个位置也就是d2,接着d2的prev就要指向newnode,头的next的位置就指向了newnode,newnode的prev就指向了头。
3.4.3在pos位置上插入
//在pos前面插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prevpos = pos->prev;
ListNode* newnode = BuyLTNode(x);
newnode->next = pos;
pos->prev = newnode;
newnode->prev = prevpos;
prevpos->next = newnode;
}
这里我们就不具体分析了,我们重点说一下这里的技巧,在双向链表的插入中我们可以看到其实头插和尾插都是在指定位置上插入的特例,所以我们在写双向链表的插入的时候完全可以先写这个函数然后头插和尾插也就完成了,这里我们展示一下改造后的头尾插。
//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
ListInsert(pHead->prev, x);
}
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
ListInsert(pHead->next, x);
}
怎么样是不是非常的方便,那么举一反三我们在后面的删除中叶有这样的技巧。
3.5删除
3.5.1尾删
//尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(pHead->next);
ListNode* tail = pHead->prev;
ListNode* prevtail = tail->prev;
prevtail->next = pHead;
pHead->prev = prevtail;
free(tail);
}
看图分析,我们删除了tail,记录前一个的位置也就是prevtail,让prevtail和phead再次变成循环就可以了。
3.5.2头删
//头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(pHead->next);
ListNode* first = pHead->next;
ListNode* second = first->next;
free(first);
second->prev = pHead;
pHead->next = second;
}
头删也时同样的记录下一个的数据在跟前面链接free掉第一个数据。
3.5.3删除pos位置上的数据
//删除pos的数据
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* nextpos = pos->next;
ListNode* prevpos = pos->prev;
free(pos);
prevpos->next = nextpos;
nextpos->prev = prevpos;
}
同样的,尾删和头删也是包括在这一种删除中的,所以我们也可以进一步简化我们的代码。
//尾删
void ListPopBack(ListNode* pHead)
{
ListErase(pHead->prev);
}
//头删
void ListPopFront(ListNode* pHead)
{
ListErase(pHead->next);
}
四,调试技巧(具体示例)
我们在写大型的代码的时候有一种调试方法可以极大的简便的让我们去检查代码就是写一查一
例如我们写了尾插的代码。
void Text1()
{
ListNode* plist = LTInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPushBack(plist, 5);
ListPrint(plist);
}
int main()
{
Text1();
return 0;
}
我们就可以用Text1去检查我们的尾插有没有错误。
同样的我们写完头插之后再写一个Text2去检查我们的函数有没有错误。把一个复杂的大型代码去分解为一个个小的单元会让我们的效率提升很多。
五,总结
双向链表虽然是单链表之后的内容,但是我们会发现因为有两个指针的原因他的所有的操作比单链表比起来更加的方便,更易于理解。总之,还是要自己上手操作才能加深自己的印象。数据结构重点就是:画图!!画图!!画图!!