目录
一、链表的分类
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
二、双向链表
1.双向链表内部定义
2.双向链表的初始化(void LTPrint(LTNode* phead);)
3.双向链表的销毁(void LTDataDestroy(LTNode* phead);)
4.双向链表的尾插(void LTPushBack(LTNode* phead, LTDataType x);)
5.判断链表是否为空(bool LTEmpty(LTNode* phead);)
6.双向链表的尾删(void LTPopBack(LTNode* phead);)
7.双向链表的头插( void LTPushFront(LTNode* phead, LTDataType x); )
8.双向链表的头删(void LTPopFront(LTNode* phead);)
9.双向链表的查找(LTNode* LTFind(LTNode* phead, LTDataType x);)
10.双向链表的删除(void LTErase(LTNode* pos);)
11.双向链表在pos位置之前插入一个值(void LTInsert(LTNode* pos,LTDataType x);)
三、双向链表的完整代码
List.h
List.c
test.c
四、顺序表和链表的对比
一、链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
2. 带头或者不带头
带头的为哨兵位置头结点,特点:不存储有效数据 优点:插入数据时不需要探空
3. 循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
二、双向链表
1.双向链表内部定义
#pragma once
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
2.双向链表的初始化(void LTPrint(LTNode* phead);)
//双链表的初始化
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
3.双向链表的销毁(void LTDataDestroy(LTNode* phead);)
// 双链表的销毁
void LTDataDestroy(LTNode* phead) {
assert(phead); // 确保头节点不为空
LTNode* cur = phead->next; // 从第一个节点开始
while (cur != phead) { // 遍历链表,直到回到头节点
LTNode* next = cur->next; // 保存下一个节点
free(cur); // 释放当前节点
cur = next; // 移动到下一个节点
}
free(phead); // 释放头节点
phead = NULL; // 将头节点指针置为 NULL(注意:这里只是局部修改,外部需要手动置空)
}
4.双向链表的尾插(void LTPushBack(LTNode* phead, LTDataType x);)
//双链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
//phead tail newnode
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
tail = tail->next;
}
这里不需要用二级指针,因为这个函数里面没有修改phead的值,因为有哨兵结点,那么就不需要考虑链表为空的情况,这里对比单链表的尾插:在单链表的尾插中如果链表为空那么就有一个这个操作:*pphead = newnode;这里修改了形参的值那么就必须传递二级指针
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
}
newnode->data = x;
newnode->Next = NULL;
if (*pphead == NULL)
{
*pphead = newnode;
}。
else
{
//找尾
SLTNode* tail = *pphead;
while (tail->Next != NULL)
{
tail = tail->Next;
}
tail->Next = newnode;
}
5.判断链表是否为空(bool LTEmpty(LTNode* phead);)
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
//等价于
/*if (phead->next == phead)
{
return true;
}
else
{
return false;
}*/
}
6.双向链表的尾删(void LTPopBack(LTNode* phead);)
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
//等价于
/*if (phead->next == phead)
{
return true;
}
else
{
return false;
}*/
}
7.双向链表的头插( void LTPushFront(LTNode* phead, LTDataType x); )
双链表的头插需要注意,不要一上来就phead->next=newnode newnode->pre=phead;这样就不能找到原来的第一个了
//双链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;//newnode->next要指向d1,d1就是phead->next
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
8.双向链表的头删(void LTPopFront(LTNode* phead);)
//双链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* head = phead->next;
LTNode* headnext = head->next;
phead->next = headnext;
head->prev = phead;
free(head);
head = NULL;
}
9.双向链表的查找(LTNode* LTFind(LTNode* phead, LTDataType x);)
//双链表的查找
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;
}
10.双向链表的删除(void LTErase(LTNode* pos);)
//删除
void LTErase(LTNode* pos)
{
//prev pos next
assert(pos);
LTNode* p = pos->prev;
LTNode* n = pos->next;
p->next = n;
n->prev = p;
free(pos);
pos = NULL;//这行代码其实没有特别大的意义
}
11.双向链表在pos位置之前插入一个值(void LTInsert(LTNode* pos,LTDataType x);)
//在pos位置之前插入一个值
void LTInsert(LTNode* pos,LTDataType x)
{
assert(pos);
LTNode* prev = pos->next;
LTNode* newnode = BuyListNode(x);
//prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
思考一个问题:有了Insert还需不需要头插?
答:不用 LTInsert(phead->next,x)就是头插,尾插也一样LTInsert(phead,x)
三、双向链表的完整代码
List.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<errno.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
//双链表的打印
void LTPrint(LTNode* phead);
//双链表的初始化
LTNode* LTInit();
//双链表的销毁
void LTDataDestroy(LTNode* phead);
//双链表的尾插
void LTPushBack(LTNode* phead, LTDataType x);
//双链表的尾删
void LTPopBack(LTNode* phead);
//判断链表是否为空
bool LTEmpty(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);
List.c
#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;
}
//双链表的打印
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<->", cur->data);
cur = cur->next;
}
}
//双链表的初始化
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
// 双链表的销毁
void LTDataDestroy(LTNode* phead) {
assert(phead); // 确保头节点不为空
LTNode* cur = phead->next; // 从第一个节点开始
while (cur != phead) { // 遍历链表,直到回到头节点
LTNode* next = cur->next; // 保存下一个节点
free(cur); // 释放当前节点
cur = next; // 移动到下一个节点
}
free(phead); // 释放头节点
phead = NULL; // 将头节点指针置为 NULL(注意:这里只是局部修改,外部需要手动置空)
}
//双链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
//phead tail newnode
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
tail = tail->next;
//这里不需要用二级指针
// 用二级指针是因为要改变结构体里面的指针
// 而双链表改的都是结构体里面的变量,用结构体的指针即可
//
}
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
//等价于
/*if (phead->next == phead)
{
return true;
}
else
{
return false;
}*/
}
//双链表的尾删
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;
}
//双链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
//双链表的头插需要注意,不要一上来就phead->next=newnode newnode->pre=phead;这样就不能找到原来的第一个了
assert(phead);
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;//newnode->next要指向d1,d1就是phead->next
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
//双链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* head = phead->next;
LTNode* headnext = head->next;
phead->next = headnext;
head->prev = phead;
free(head);
head = NULL;
}
//在pos位置之前插入一个值
void LTInsert(LTNode* pos,LTDataType x)
{
assert(pos);
LTNode* prev = pos->next;
LTNode* newnode = BuyListNode(x);
//prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}//思考一个问题:有了Insert还需不需要头插?答:不用 LTInsert(phead->next,x)就是头插,尾插也一样LTInsert(phead,x)
//删除
void LTErase(LTNode* pos)
{
//prev pos next
assert(pos);
LTNode* p = pos->prev;
LTNode* n = pos->next;
p->next = n;
n->prev = p;
free(pos);
pos = NULL;//这行代码其实没有特别大的意义
}
//双链表的查找
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;
}
test.c
#include"List.h"
void TestList1()
{
LTNode* plist = LTInit();
//尾插测试
LTPushBack(plist,1);
LTPushBack(plist,2);
LTPushBack(plist,3);
LTPushBack(plist,4);
LTPrint(plist);
//尾删测试
printf("\n");
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
//头插测试
printf("\n");
LTPushFront(plist,5);
LTPushFront(plist,8);
LTPrint(plist);
//头删测试
printf("\n");
LTPopFront(plist);
LTPrint(plist);
}
int main()
{
TestList1();
return 0;
}
四、顺序表和链表的对比
不同点
|
顺序表
|
链表
|
存储空间上
|
物理上一定连续
|
逻辑上连续,但物理上不一定
连续
|
随机访问
| 支持O(n) | 不支持O(n) |
任意位置插入或者删除
元素
|
可能需要搬移元素,效率低
O(N)
|
只需修改指针指向
|
插入
|
动态顺序表,空间不够时需要 扩容
|
没有容量的概念
|
应用场景
|
元素高效存储
+
频繁访问
|
任意位置插入和删除频繁
|
缓存利用率
|
高
|
低
|