引言
在链表中,我们可以分成三张情况
- 单向或双向
- 带头或不带### H3头
- 循环或非循环 将他们排列组合,我们可以得到八种情况 虽然有这么多的链表结构,但是我们实际中最常用的还是两种结构:
无头单向肺循环链表(无头单链表)
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
带头双向循环链表
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会++带来很多优势++,实现反而简单了,后面我们代码实现了就知道了。
1.初始化链表
定义一个结构体
typedef struct ListNode
{
LTDateType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
声明初始化结构体
void ListInit(LTNode* phead);
在test.c中测试
void TestList1()
{
LTNode* plist = NULL;
ListInit(plist);
}
当我们把plist赋值为NULL,实参改变,但是我们写的ListInit函数中,形参的改变不会影响实参的改变,这里如果想改变实参,就需要传二级指针。如果不传二级指针,我们可以加返回值。
LTNode* ListInit();
LTNode* ListInit()
{
//哨兵位头结点
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->next = phead;
phead->prev = phead;
return phead;
}
test.c中
LTNode* plist = ListInit();//将实参初始化,这时plist就可以接收
这里再强调一下,单链表中的二级指针是因为形参的改变不会影响实参, 因为他没有哨兵位的头节点,我们在test.c中将头节点初始化为NULL,整个链表为空,需要将数据插入到头节点,改变头结点的值,一级指针无法将数据返回给实参,实参得不到改变,所以我们要传二级指针,取到形参的地址,它里面的值改变,才会传到test.c中。其实单链表也可以用 返回值来接收,但是他在test.c中测试的时候,会变成
SLTNode* plist = NULL;
plist = SListPusthFront(plist, 1);
plist = SListPusthFront(plist, 2);
plist = SListPusthFront(plist, 3);
每次都要用plist接收一下。 而带哨兵位的双向循环链表,我们将值传进去,改变了一次哨兵位头节点的值,将地址传进去,后面的值将其链接即可。带哨兵位的双向循环链表最好用带返回值的来接收。
尾插
尾插的思路跟单链表一样,找到为节点并插入,但是这次我们不需要遍历整个链表找到为节点,因为头结点的前一个就是尾节点,直接就能找到尾节点。
List.h声明
void ListPushBack(LTNode* phead, LTDateType x);
void ListPushBack(LTNode* phead, LTDateType x)
{
//phead因为指向malloc出来的哨兵位,一定不为空,所以我们断言一下
assert(phead);
//定义尾节点来尾插
//给新节点开辟新的空间
LTNode* tail = phead->prev;
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
//尾插新节点
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
双向循环链表不需要考虑头结点是否为空,因为我们malloc出来的哨兵位,在初始化的时候,头指向phead,尾也指向phead,都不为空,存在prev和next.在单链表中如果为空,tail为空,它不存在next,还要考虑为空的情况。 这就是双向循环链表的好处,看起来复杂,用起来简单。 有的人说为什么双向循环链表这么方便还要有单链表的存在。单链表很大意义上是用来考察能力的,在面试的时候也很喜欢考察这类。
打印链表、
声明
void ListPrint(LTNode* phead);
打印的时候我们是从哨兵位的后面开始打印的,结束循环的条件是当cur走到哨兵位头节点phead的时候结束循环,所以循环条件是,cur!=phead 时进入循环。
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur!=phead)
{
printf("%d ", cur->data);//打印节点的data数据
cur = cur->next;//让cur继续向下走
}
printf("\n");
}
尾删
定义指针tail找到尾,并记录tail的前一个成为新的尾,我们可以定义指针tailPrev来记录tail的前一个,方便找到。记录好后释放尾节点,头节点的prev指向新尾节点,新尾节点的next指向哨兵位头。
void ListPopBack(LTNode* phead)
{
assert(phead);//判断链表存不存在
assert(phead->next != phead);//链表为空就不能删
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
}
测试尾插和尾删 test.c
void TestList1()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPrint(plist);
}
int main()
{
TestList1();
return 0;
}
头插
malloc开辟一个新节点newnode,定义一个指针next记录phead的next,因为我们要头插,所以要在哨兵位phead和pehad的下一个next中插入数据。
oid ListPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* next = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;
}
头删
与尾删思路大同小异。定义一个节点cur记录要删除的节点,定义一个节点记录cur的next方便哨兵位链接。这样可以不用管顺序,直接free要删除的节点,再链接。非常奈斯
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next);
LTNode* cur = phead->next;
LTNode* curNext = cur->next;
phead->next = curNext;
curNext->prev = phead;
free(cur);
}
测试一下头插和头删 test.c
void TestList2()
{
LTNode* plist = ListInit();
ListPushFront(plist, 1);
ListPushFront(plist, 2);
ListPushFront(plist, 3);
ListPushFront(plist, 4);
ListPrint(plist);
ListPopFront(plist);
ListPopFront(plist);
ListPopFront(plist);
ListPrint(plist);
}
int main()
{
//TestList1();
TestList2();
return 0;
}
找到所在位置
ListFind的逻辑跟ListPrint非常相似,定义一个指针遍历链表,如果没有找到所在数字,继续向下遍历,如果找到,返回指针。
LTNode* ListFind(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data != x)
{
cur = cur->next;
}
else
{
return cur;
}
}
printf("\n");
}
在任意位置插入,在pos位置前插入
声明
void ListInsert(LTNode* pos, LTDateType x);
这里有的人会定义的是下标位置,都是可以的,这里我们定义pos的节点指针,这样可以通过指针链接。思路跟头插一样,因为在pos之前插入,所以定义一个指针存放pos的前一个位置,再将其连接起来
void ListInsert(LTNode* pos, LTDateType x)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* newnode = BuyListNode(x);
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
写完插入,他有很多好处,比如前面写的头插和尾插我们可以复用,头插的时候pos就是phead->next的位置,尾插时,因为我们是在pos之前插入,所以pos的位置就是phead 这样尾插就变成
void ListPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
ListInsert(phead, x);//将phead给到pos的位置
}
头插就变成
void ListPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
ListInsert(phead->next, x);
}
删除pos位置
同样定义两个指针,记录pos的前一个位置和后一个位置,删除pos时,将前后链接起来
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* curNext=pos->next;
LTNode* curPrev = pos->prev;
curPrev->next = curNext;
curPrev->prev = curPrev;
free(pos);
pos = NULL;
}
测试一下 test.c
void TestList3()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPushFront(plist, 1);
ListPushFront(plist, 2);
ListPushFront(plist, 3);
ListPushFront(plist, 4);
ListPrint(plist);
LTNode* pos = ListFind(plist, 2);
if (pos)
{
ListErase(pos);
}
ListPrint(plist);
}
int main()
{
TestList3();
return 0;
}
本次代码的链接 Thanks for watching :)