1、链表的种类:
总共有8种,以带不带头,单向或者双向,循环或者不循环来组合形成。
单向或者双向
带头或者不带头
循环或者非循环
主要学习下面两种链表的功能实现
- 2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
2、带头双向循环链表的实现
2.1 对带头、双向、循环的理解
- 带头:存在一个哨兵位的头节点,该节点是个无效节点,不存储任何有效信息,但使用它可以方便我们头尾插和头尾删时不用判断头节点指向NULL的情况,同时也不需要改变头指针的指向,也就不需要传二级指针了。同时将头节点指向的下一个结点叫第一个元素结点。
-
双向:每个结点有两个指针,分别指向前一个结点(prev或者per)和后一个结点(next)。
-
循环:最后一个尾结点的指针不再指向NULL,而是指向第一个头结点。(单向)
第一个结点的前指针指向最后一个尾结点,最后一个尾结点的后指针指向第一个头结点(双向)。
2.2定义结点
typedef int LTDadaType;
typedef struct ListNode //类型重定义
{
struct ListNode* per; //指向前一个的指针
struct ListNode* next; //指向后一个的指针
LTDadaType data; //存储当前结点的数据
}LTNode;
2.3 链表的功能
与单链表相比,实现的功能大致一样,相对的因为结构复杂,每一个结点有两个分别指向前一个结点和后一个结点的结构体指针,所以省去了找尾的麻烦,代码实现更加简单,同时增加了双向循环链表的初始化(生成一个头节点)这个功能,使得以后的插入删除等一系列操作能够统一操控减少对头节点的判断。
//哨兵位头结点
// 尾next指向哨兵位
//哨兵位per直线尾结点
//双向循环链表的初始化
LTNode* LTInit();
//双向循环链表的销毁
void LTDstroy(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead,LTDadaType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDadaType x);
// 头删
void LTPopFront(LTNode* phead);
//申请一个内存空间
LTNode* BuyListNode(LTNode* phead);
void LTPrint(LTNode* phead);
//在pos位置之前插入
void LTnsert(LTNode* pos, LTDadaType x);
//在pos位置之前删除
void LTErease(LTNode* pos);
//修改pos位置的数据
void LTChange(LTNode* pos, LTDadaType x);
2.3.1双向循环链表的初始化
通过返回头指针解决这个需要传二级指针的问题,使得代码具有统一性。
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->per = phead;
return phead;
}
2.3.2双向循环链表的销毁
找到链表遍历完的结束条件,从后往前遍历当尾指针等于头结点时跳出循环,整个双向循环链表就剩下一个头结点没有被删除了。
void LTDstroy(LTNode* phead)
{
assert(phead);
LTNode* tail = phead->per;
while (tail != phead)
{
free(tail);
tail = tail->per;
}
}
2.3.3申请一个结点
malloc 申请一个结点返回一个指向该内存的指针,然后判断(程序健壮性)。再将指针置空和赋值,返回值该指针。
LTNode* BuyListNode(LTDadaType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
//防止野指针
newnode->data = x;
newnode->next = NULL;
newnode->per = NULL;
return newnode;
}
2.3.4尾插
//尾插
void LTPushBack(LTNode* phead, LTDadaType x) //不需要传二级指针 因为有个哨兵位还有结构特点不需要找尾
{
//需要断言 防止传错了
assert(phead);
LTNode* newnode = BuyListNode(x);
//尾
LTNode* tail = phead->per;
tail->next = newnode;
newnode->per = tail;
newnode->next = phead;
phead->per = newnode;
}
2.3.5尾删
断言防止phead传过来一个空指针, 指针变换的操作比较简单,然后一定要注意释放被删除的内存空间。
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//assert(!LTEpty(phead));
LTNode* tail = phead->per;
LTNode* pertail = tail->per;
pertail->next = phead;
phead->per= pertail;
free(tail);
// LTnsert(phead, x)
}
2.3.6头插
头插有两个方法:
- 按着一定的操作顺序,先让生成的新结点的next指向第一个元素结点,这样防止了先接头结点找不到后面第一个元素结点。
- 其次就是弄一个指针,将指针指向头节点后的第一个元素结点,这样就不需要考虑链接顺序的问题了。
我这是第一种方法 按照先接后再断前再接前的顺序
//头插
void LTPushFront(LTNode* phead, LTDadaType x)
{
assert(phead);
//按一定的顺序才行 先后接再接前 如果不按一定顺序就要再开一个指针
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->per = newnode;
newnode->per = phead;
phead->next = newnode;
// LTnsert(phead->next,x);
}
2.3.7头删
注意点和尾删一样,千万一定要注意要释放被删除的结点内存空间。
//头删
void LTPopFront(LTNode* phead)
{
//删除要断言不为空
assert(phead);
/*assert(!LTEpty(phead));*/
LTNode* cur = phead->next;
phead->next = cur->next;
cur->per = phead;
free(cur);
}
2.3.8在pos位置之前插入
//在pos位置之前插入
void LTnsert(LTNode* pos, LTDadaType x)
{
assert(pos);
LTNode* per = pos;
LTNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->per = newnode;
per->next = newnode;
newnode->per = per;
}
2.3.9在pos位置之前删除
//在pos位置之前删除
void LTErease(LTNode* pos)
{
assert(pos);
pos->per->per->next = pos;
pos->per = pos->per->per;
}
2.3.10 打印整个链表
void LTPrint(LTNode* phead)
{
assert(phead);
printf("<=head=>");
LTNode* cur = phead->next;
while (cur!=phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
}
2.3.11 修改
//修改pos位置的数据
void LTChange(LTNode* pos, LTDadaType x)
{
assert(pos);
pos->data = x;
}
3、整体代码
List.c
#include"List.h"
LTNode* BuyListNode(LTDadaType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
//防止野指针
newnode->data = x;
newnode->next = NULL;
newnode->per = NULL;
return newnode;
}
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->per = phead;
return phead;
}
void LTDstroy(LTNode* phead)
{
assert(phead);
LTNode* tail = phead->per;
while (tail != phead)
{
free(tail);
tail = tail->per;
}
}
//尾插
void LTPushBack(LTNode* phead, LTDadaType x) //不需要传二级指针 因为有个哨兵位还有结构特点不需要找尾
{
//需要断言 防止传错了
assert(phead);
LTNode* newnode = BuyListNode(x);
//尾
LTNode* tail = phead->per;
tail->next = newnode;
newnode->per = tail;
newnode->next = phead;
phead->per = newnode;
}
void LTPrint(LTNode* phead)
{
assert(phead);
printf("<=head=>");
LTNode* cur = phead->next;
while (cur!=phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//assert(!LTEpty(phead));
LTNode* tail = phead->per;
LTNode* pertail = tail->per;
pertail->next = phead;
phead->per= pertail;
free(tail);
// LTnsert(phead, x)
}
//bool LTEpty(LTNode* phead)
//{
// assert(phead);
//
//
// return phead->next == phead; //真为空 假为不空
//
//}
//头插
void LTPushFront(LTNode* phead, LTDadaType x)
{
assert(phead);
//按一定的顺序才行 先后接再接前 如果不按一定顺序就要再开一个指针
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->per = newnode;
newnode->per = phead;
phead->next = newnode;
// LTnsert(phead->next,x);
}
//头删
void LTPopFront(LTNode* phead)
{
//删除要断言不为空
assert(phead);
/*assert(!LTEpty(phead));*/
LTNode* cur = phead->next;
phead->next = cur->next;
cur->per = phead;
free(cur);
}
//在pos位置之前插入
void LTnsert(LTNode* pos, LTDadaType x)
{
assert(pos);
LTNode* per = pos;
LTNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->per = newnode;
per->next = newnode;
newnode->per = per;
}
//在pos位置之前删除
void LTErease(LTNode* pos)
{
assert(pos);
pos->per->per->next = pos;
pos->per = pos->per->per;
}
//修改pos位置的数据
void LTChange(LTNode* pos, LTDadaType x)
{
assert(pos);
pos->data = x;
}
List.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<stdbool.h>
typedef int LTDadaType;
typedef struct ListNode
{
struct ListNode* per;
struct ListNode* next;
LTDadaType data;
}LTNode;
//哨兵位头结点
// 尾next指向哨兵位
//哨兵位per直线尾结点
//双向循环链表的初始化
LTNode* LTInit();
//双向循环链表的销毁
void LTDstroy(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead,LTDadaType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDadaType x);
// 头删
void LTPopFront(LTNode* phead);
//申请一个内存空间
LTNode* BuyListNode(LTNode* phead);
void LTPrint(LTNode* phead);
//在pos位置之前插入
void LTnsert(LTNode* pos, LTDadaType x);
//在pos位置之前删除
void LTErease(LTNode* pos);
//修改pos位置的数据
void LTChange(LTNode* pos, LTDadaType x);
test.c
#include"List.h"
void testList1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
printf("\n");
LTPopBack(plist);
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
printf("\n");
LTPushFront(plist, 10);
LTPushFront(plist, 20);
LTPushFront(plist, 30);
LTPrint(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
printf("\n");
LTPrint(plist);
LTNode* find = LTFind(plist, 2);
LTnsert(find, 2);
printf("\n");
LTPrint(plist);
}
int main()
{
testList1();
return 0;
}
4.1顺序表和链表的区别
不同点 |
顺序表 |
链表 |
存储空间上 |
物理上一定连续 |
逻辑上连续,但物理上不一定连续 |
随机访问 |
支持O(1) |
不支持:O(N) |
任意位置插入或者删除元素 |
可能需要搬移元素,效率低O(N) |
只需修改指针指向 |
插入 |
动态顺序表,空间不够时需要扩容 |
没有容量的概念 |
应用场景 |
元素高效存储+频繁访问
|
任意位置插入和删除频繁 |
缓存利用率 |
高 |
低 |