链表(c语言版)

目录

1.链表概念及结构

 2.单链表的实现

2.1头文件

2.2接口具体实现

2.2.1打印链表

2.2.2申请节点

2.2.3链表尾插

2.2.4链表头插

2.2.5链表尾删

2.2.6链表头删

2.2.7链表寻找节点

2.2.8链表指定位置之后插入数据

2.2.9链表删除指定节点

2.2.10链表删除指定位置之后的节点

2.2.11链表销毁

 3.链表例题

3.1移除链表元素

3.2反转链表

3.3寻找链表倒数第k个节点

3.4寻找中间节点

3.6链表分割

3.7链表回文结构

3.9环形链表

3.10环形链表2

3.11随机链表的复制

4.双链表

4.1双链表头文件

4.2双链表具体实现

4.2.1尾插

4.2.2申请节点

4.2.3初始化(重点!巧妙改变,不用二级指针了)

4.2.4打印

4.2.5尾删

4.2.6链表销毁

4.2.7头插

4.2.8头删

4.2.9寻找位置

4.2.10指定位置前插入

4.2.11指定位置删除

5.顺序表和链表的异同

 5.1存储

5.2访问

5.3指定位置增删

5.4插入

5.5应用

5.6缓存


1.链表概念及结构

在我顺序表的文章里说了,顺序表是逻辑连续,物理也连续的一段空间,而链表只有逻辑是连续的,物理是不连续的,是通过指针来实现逻辑连续的

在代码实现中,我们会运用动态内存函数在堆上申请空间,每次申请的空间可能恰好连续,也可能不连续。(内存空间分为堆、栈等,具体区别,我改天写篇文章(我现在还有点糊涂))

这是单链表的结构,地址是内存地址,运用指针解引用,我们就能按顺序找到不同的节点

 2.单链表的实现

2.1头文件

#pragma once

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义链表节点结构
typedef int SLDataType;
//跟顺序表类似,这也是为了方便改变存储的数据类型
typedef struct SListNode//这就是链表的节点
{
	SLDataType data;//存储数据
	struct SListNode* next;//存储下一个节点的地址
}SLNode;
//同样,加快写代码速度

//打印链表
void SLNPrint(SLNode* phead);
//申请一个链表节点
SLNode* SLBuyNode(SLDataType x);


//尾插
void SLPushBack(SLNode** pphead, SLDataType x);
//头插
void SLPushFront(SLNode** pphead, SLDataType x);

//尾删
void SLPopBack(SLNode** pphead);
//头删
void SLPopFront(SLNode** pphead);

//指定位置插入删除
// 
//指定位置之前插入
void SLInsert(SLNode** pphead, SLNode* pos,SLDataType x);

//指定位置之后插入
void SLInsertAfter(SLNode* pos, SLDataType x);

//查找节点

SLNode* SLFind(SLNode** pphead, SLDataType x);

//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos);

//删除pos之后节点
void SLEraseAfter(SLNode* pos);

//链表销毁
void SLDesTroy(SLNode** pphead);

2.2接口具体实现

2.2.1打印链表

void SLNPrint(SLNode* phead)
{
	//循环打印
	SLNode* pcur = phead;
	while (pcur)
	{
		printf("%d ->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}
链表按顺序打印,就是不停的对当前节点存储的下一节点地址解引用
所以我们用一个pcur接收第一个节点的地址
(为什么要额外创建,因为指针的改变是具有全局性的,phead我们
要求一直指向第一个节点地址,但我们如果打印的时候将phead不停的改变
我们就找不到原来的第一个节点了)
pcur==NULL的时候说明已经打印完了,最后打印一个NULL即可

2.2.2申请节点

SLNode* SLBuyNode(SLDataType x)
{
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	node->data = x;
	node->next = NULL;
    return node;
}
范围类型用链表节点的地址形式

运用动态内存函数,申请一块空间链表节点的空间

这里没有判断是否开辟失败,你们自己试的时候可以加上判断

接下来,把x赋值给data
因为只是申请,具体怎么关联别的节点在外面实现,所以这里next置NULL即可

2.2.3链表尾插

void SLPushBack(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = node;
		return;
	}
	SLNode* pcur = *pphead;
	while (pcur->next)
	{
		pcur = pcur->next;
	}
	pcur->next = node;
}
断言空指针,防止野指针引用和引用空指针

调用申请节点的函数,将新节点赋给一个节点指针,

要注意,我们这里用的是二级指针,因为我们要知道,函数
调用的时候是创建一个新的变量来接受值,那么如果我们在外面创建了一个节点指针
plist,并且赋值了NULL,然后传参进去,我们要把一个节点地址赋给他,但我们如果用的是
一级指针,我们只是改变这个函数里面的phead指向的空间,事实上,外面的plist
还是指向NULL,那么我们就没有实现我们的目标,所以用二级指针
可以把plist指针的地址传进来,这样我们就能直接修改plist指向的空间了

二级指针存的是节点指针的地址,解引用一层
就是节点地址,再解引用就是节点本身

如果第一个节点是空(这个其实跟我们外面传参究竟是传一个空指针还是一个节点指针
有关),那么我们直接把申请的节点赋给这个解引用一层的二级指针即可

不为空,接下来,我们用一个指针接收第一个节点的地址,
注意循坏条件写pcur->next,因为我们要找到最后一个节点
然后再把新的节点插入到尾



2.2.4链表头插

void SLPushFront(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	node->next = *pphead;//plist
	*pphead = node;
}
断言空指针

申请节点

因为是头插,所以新节点的next应该指向原先的第一个节点,
也就是*pphead,然后再改变*pphead指向的空间为
新节点

因为我们要修改main函数中定义的节点指针指向的空间,所以我们要用二级指针

2.2.5链表尾删

void SLPopBack(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	SLNode* prev = NULL;
	SLNode* ptail = *pphead;

	while (ptail->next != NULL)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = ptail->next;
	free(ptail);
	ptail = NULL;//防止创建链表出问题
}
断言两处,一个是传参时是否是空指针,一个是空链表

如果链表此时只有一个节点,那么我们只需要free掉这个节点即可
记得置空,以免野指针

接下来prev指向ptail的上一个节点,
ptail->next==NULL时,说明ptail指向最后一个节点
,prev指向倒数第二个节点
而我们要删除的是最后一个节点,所以把prev的next置为NULL
再free掉ptail,并且置空,防止野指针

2.2.6链表头删

void SLPopFront(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;//代码规范

}
断言两处,空指针和空链表
把第一个节点地址给一个节点指针

让*pphead指向第二个节点
也就是让外面plist指向第二个节点

然后free掉del,del置空即可

2.2.7链表指定位置前插入

void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	SLNode* node = SLBuyNode(x);
	//if ((*pphead)->next == NULL || pos == *pphead)也可以,只是如果是第一个节点
	//那么pos也就是第一个节点,所以直接写即可
	if ( pos==*pphead)
	{
		node->next = *pphead;
		*pphead = node;
		return;
	}

	SLNode * prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}

	node->next = pos;
	prev->next = node;

}
断言指定位置是否合法和空指针、空链表

申请新节点,如果是指定位置是第一个节点,那么就是头插即可

如果不是,那么定义一个节点指针,指向第一个节点,
循坏条件说明,结束的时候,正好prev是pos位置的前一个节点

让新节点next指向pos位置节点,prev位置节点的next指向node,
这样就关联起来了

2.2.7链表寻找节点

SLNode* SLFind(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}
断言空指针,

定义节点指针指向第一个节点,遍历链表,
找到返回地址,找不到返回NULL

2.2.8链表指定位置之后插入数据

void SLInsertAfter(SLNode* pos, SLDataType x)
{
	assert(pos);
	SLNode* node = SLBuyNode(x);
	node->next = pos->next;
	pos->next = node;
}
断言空指针

申请新的节点,让node的next指向pos的next,
再把pos的next指向node,形成一个逻辑顺序即可

2.2.9链表删除指定节点

void SLErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	if (pos == *pphead)
	{
		*pphead = (*pphead)->next;
		free(pos);
		return;
	}

	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}
断言空指针和空链表

如果位置是第一个节点,直接头删即可

定义节点指针,指向第一个节点
遍历链表,循环结束时,prev是pos位置的前一个节点

让prev的next指向pos位置的next,再free掉pos即可,
形成逻辑顺序

2.2.10链表删除指定位置之后的节点

void SLEraseAfter(SLNode* pos)
{
	assert(pos && pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;

}
断言两处,因为可能pos是最后一个节点

定义节点指针,指向pos的下一个节点

把pos的next指向del的next,让从逻辑上
先删除,然后再free掉del,即物理上取消这片
空间

2.2.11链表销毁

void SLDesTroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

链表的销毁,要从第一个节点开始,逐个free掉

因为*pphead也就是main函数里的plist指针还可能被其他接口调用,
所以要置空

 3.链表例题

3.1移除链表元素

203. 移除链表元素 - 力扣(LeetCode)

typedef struct ListNode ListNode;

struct ListNode* removeElements(struct ListNode* head, int val) {
	ListNode* newHead, * newTail;
	newHead = newTail = NULL;
	ListNode* pcur = head;
	while(pcur)
	{
		if (pcur->val != val)
		{
			if (newHead == NULL)
			{
				newHead = newTail = pcur;
			}
			else
			{
				newTail->next = pcur;
				newTail = newTail->next;
			}
		}
		pcur = pcur->next;
	}
    if(newTail)
    {
        newTail->next=NULL;
    }
    return newHead;
}
定义newHead和newTail节点指针,置空

定义指针pcur指向第一个节点

如果pucr的val不等于val,且newhead还是NULL的时候,把两个指针都指向pcur,
也就是第一个检查通过的节点,也是返回的时候的头结点

之后一旦pcur的val不等于val,把newtail的next指向pcur,nexttail再变成pcur
保持newtail总是在我们要返回的链表的最后一个节点

不管什么情况,pcur都进入下一个节点

循环结束后,如果newtail不是空,把newtail指向空

3.2反转链表

206. 反转链表 - 力扣(LeetCode)

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head){
    if(head==NULL)
    {
        return NULL;
    }
    ListNode*n1,*n2,*n3;
    n1=NULL;
    n2=head;
    n3=head->next;
    while(n2)
    {
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3)
        n3=n3->next;
    }
    return n1;
}

这题光靠文字还有点吃力,下面是图解

每次循坏都是进行类似的步骤,n3如果是NULL,就不用继续往下走了,此时n2=n3,n2也等于空,在此之前已经完成了整个链表的反转,所以结束循坏

3.3寻找链表倒数第k个节点

链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

#include <string.h>
#include<assert.h>
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    struct ListNode* fast=pListHead;
    struct ListNode* cur=pListHead;
    while(k--)
    {
        if(fast==NULL)
        {
            return NULL;
        }
        fast=fast->next;
    }
    while(fast)
    {
        fast=fast->next;
        cur=cur->next;
    }
    return cur;
}
首先用k--条件循环,如果fast==NULL,说明k>链表的长度
此时直接return NULL

接下来,我们简单推理下,一共10节点,求倒数第3个节点,
那么我们先循环3次,fast指向第4个节点,如果从第4个节点遍历到最后一个节点的next即NULL,
需要循坏几次呢,7次,那么如果此时同时cur指针从第一个节点开始循环,那么循坏7次
,正好就循环到了第8个节点,也就是倒数第3个节点。然后return cur即可,

如果一共10个节点,倒数第10个节点,正好是第一个节点,第一个while会循坏10次,
循坏结束,fast正好指向NULL,第二个while循坏就不会进入,此时cur还是指向第一个节点,
直接返回即可。

3.4寻找中间节点

876. 链表的中间结点 - 力扣(LeetCode)

typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head){
    if(head==NULL)
    {
        return NULL;
    }
    ListNode*slow,*fast;
    slow=fast=head;
    while(fast && fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
} 

这个我们用图解

3.5合并有序链表

21. 合并两个有序链表 - 力扣(LeetCode)

 typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    if(list1==NULL)
    {
        return list2;
    }
    if(list2==NULL)
    {
        return list1;
    }
    //断言两个链表是否是空链表
    ListNode*cur1=list1;
    ListNode*cur2=list2;
    //让cur1和cur2指向两个链表的头结点
    //带头不循环链表
    ListNode*newHead,*newTail;
    newHead=newTail=(ListNode*)malloc(sizeof(ListNode));
    //定义一个头结点指针和尾节点指针,并且直接申请一个节点
    //这个节点不存数据
    while(cur1 &&cur2)
    {
        if(cur1->val <cur2->val)
        {

            newTail->next=cur1;
            newTail=newTail->next;
            cur1=cur1->next;
        }
       //两个节点的值比对,小的放进新的链表里,直接尾插
        //newtail指向新加进来的,记得让cur1继续走
        else
        {

            newTail->next=cur2;
            newTail=newTail->next;
            cur2=cur2->next;
        }
        //同理,等于或大于,放cur2的
    }
    if(cur1)
    {
        newTail->next=cur1;
    }
    if(cur2)
    {
        newTail->next=cur2;
    }
    //每次可能有其中一个还没循环到空,如果有的话,就直接继续尾插
    ListNode*retHead=newHead->next;
    free(newHead);
    因为返回的时候要有效头结点,所以做一下处理,返回第一个有数据的节点地址
    return retHead;
}

3.6链表分割

链表分割_牛客题霸_牛客网

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        struct ListNode* head1,*tail1,*head2,*tail2;
        head1=tail1=(struct ListNode*)malloc(sizeof(struct ListNode));
        head2=tail2=(struct ListNode*)malloc(sizeof(struct ListNode));
        ListNode*cur=pHead;
        while(cur)
        {
            if(cur->val<x)
            {
                tail1->next=cur;
                tail1=tail1->next;
            }
            else {
                tail2->next=cur;
                tail2=tail2->next;
            }
            cur=cur->next;
        }
        tail1->next=head2->next;
        tail2->next=NULL;
        pHead=head1->next;
        free(head1);
        free(head2);
        return pHead;
    }
};
这个格式不要在意,是c++的,因为牛客网在这题里没有设置给c语言的

定义两个新的链表,原链表里小于x或大于等于x的各自放入两个链表,
最后把小的链表跟大的链表互相连接起来就可以

3.7链表回文结构

链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

class PalindromeList {
public:
bool chkPalindrome(ListNode* A) {
    struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
    head->next = A;
    struct ListNode* slow = head->next;
    struct ListNode* fast = head->next;
    if (A == NULL)return false;
//定义3个新的节点指针,一个是哨兵位,不存数据,剩下两个指向第一个节点A
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    //利用快慢指针,快速找到中间节点
    struct ListNode* n1 = slow;
    struct ListNode* n2 = head->next;
    struct ListNode* n3 = head->next->next;
    while (n2 != slow)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        n3 = n3->next;
    }
    head->next = n1;
     //将slow前面的节点反转,并且让第一个节点变为n1
    struct ListNode* cur = head -> next;
    struct ListNode* cur1 = NULL;
    if (fast == NULL)
    {
        cur1 = slow;
    }
    else if (fast->next == NULL)
    {
         cur1 = slow->next;
    }
    让cur指向第一个节点,根据奇数偶数节点情况,把slow或slow下一个
    节点的地址给cur1
    while (cur != slow)
    {
        if (cur->val == cur1->val)
        {
            cur = cur->next;
            cur1 = cur1->next;
        }
        else {
            return false;
        }
    }
    两边同时开始遍历,如果都相同,就返回true,如果有一个不一样,就返回false
    return true;
}
};

3.8相交链表

160. 相交链表 - 力扣(LeetCode)

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode*curA=headA,*curB=headB;
    int lenA=1,lenB=1;
    while(curA->next)
    {
        lenA++;
        curA=curA->next;
    }
    while(curB->next)
    {
        lenB++;
        curB=curB->next;
    }
    if(curA!=curB)
    {
        return NULL;
    }
    //先遍历一遍,计算两个链表有多少个节点
    //因为遍历条件是curB->next,所以按理来说此时两个
    //指针指向的是同一个空间,否则就是不相交的
    int n=abs(lenA-lenB);
    struct ListNode*longlist =headA,*shortlist=headB;
    if(lenB>lenA)
    {
        longlist=headB;
        shortlist=headA;
    }
    while(n--)
    {
        longlist=longlist->next;
    }
    //定义一个长指针和短指针,运用假设,让两个指针
    //指向正确位置
    //然后让长的指针先遍历n遍,n是两个单链表相差的节点数

    while(longlist!=shortlist)
    {
        longlist=longlist->next;
        shortlist=shortlist->next;
    }
    //然后同时开始遍历,直到地址相同,即是所求的节点
    return longlist;
}

3.9环形链表

141. 环形链表 - 力扣(LeetCode)

bool hasCycle(struct ListNode *head) {
    struct ListNode*slow=head,*fast=head;
    while(fast&& fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)return true;
    }    

    return false;
}

思路很简单,就是利用快慢指针,因为每次都是多走一步,
那么当slow和fast都在环中,fast每走一步,跟slow的距离就会缩减1,
直到相遇,如果能够相遇,那么就说明有环,
如果不是环,那么fast->next会等于NULL,那么就退出,
然后返回false,如果fast是NULL指针,那么最开始就不会进入循环,直接返回NULL。

3.10环形链表2

142. 环形链表 II - 力扣(LeetCode)

首先要证明一个公式的成立与否。

假设,一个指针从起始点到入口点的距离是L,环的长度是C,(这里直接使用slow走一步,fast走2步,可以最快相遇,因为进入环后,每次fast和slow的距离都会缩减1,这样就不会错过)快慢指针在环中相遇的点跟入口的距离是x,那么相遇点到入口点的距离就是C-X。当slow从起始点走到相遇点,就是L+X,fast从起始到相遇就是L+X+N*C(因为slow进环前,fast可能经历了很多圈)。而由于fast的路程是slow的两倍,所以2(L+X)=L+N*C+X,所以L=N*C-X,所以如果有一个指针从相遇点开始移动,一个指针从起始点开始移动,那么第一个指针完成运转后,最终会跟第二个指针在入口点相遇,那么这时,就是我们要求的入口点了

struct ListNode *detectCycle(struct ListNode *head) {
        struct ListNode*slow=head,*fast=head;
    while(fast&& fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)
        {
            struct ListNode*meet=slow;
            while(head!=meet)
            {
                head=head->next;
                meet=meet->next;
            }
            return meet;
        }
    }    
    return NULL;
}

3.11随机链表的复制

138. 随机链表的复制 - 力扣(LeetCode)

typedef struct Node nd;
struct Node* copyRandomList(struct Node* head) {
	if(head==NULL)
    {
        return NULL;
    }
    //断言空指针
    for(nd*node=head;node!=NULL;node=node->next->next)
    {
        nd *nodeNew=malloc(sizeof(nd));
        nodeNew->val=node->val;
        nodeNew->next=node->next;
        node->next=nodeNew;
    }
    for(nd*node=head;node!=NULL;node=node->next->next)
    {
        nd*nodeNew=node->next;
        nodeNew->random=(node->random!=NULL)?node->random->next:NULL;
    }
    nd*Newhead=head->next;
    for(nd*node=head;node!=NULL;node=node->next)
    {
        nd*nodeNew=node->next;
        node->next=node->next->next;
        nodeNew->next=(nodeNew->next!=NULL)?nodeNew->next->next:NULL;
    }
    return Newhead;
}

将原节点的拷贝节点跟原节点相连,第一个for循环是拷贝,同时将拷贝节点的next指向原节点的next节点,原节点的next节点指向拷贝节点。

第2个for循环是将拷贝节点的random指向原节点的random节点的next节点,如果是NULL,则给空

第3个for循环是将原节点的next指向原链表的相应的下一个节点,再讲拷贝节点的下一个节点变成下一个节点(下一个原节点)的next节点(即下一个原节点的拷贝节点),从而将两个链表分开。

4.双链表

链表根据单向、双向,带头、不带头,循环不循环,可以分很多种。

我们前面实现的单链表就是不带头,单向,不循环链表,在oj题中比较常见

双向链表则是双向、带头、循坏,在实际运用比较常见

4.1双链表头文件

#pragma once

#include<assert.h>
#include<stdlib.h>
#include<stdio.h>
typedef int LTDataType;
//方便改数据类型
typedef struct ListNode
{
	struct ListNode* next;//指向下一个节点
	struct ListNode* prev;//指向前一个节点
	LTDataType data;
}LTNode;
//链表节点

//尾插
void LTPushBack(LTNode*phead,LTDataType x);
//打印
void LTPrint(LTNode* phead);
//申请节点
LTNode* CreateLTNode(LTDataType x);
//初始化
LTNode* LTInit();
//尾删
void LTPopBack(LTNode* phead);


//销毁
void LTDestory(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//寻找指定节点
LTNode* LTFind(LTNode* phead, LTDataType x);
//指定位置插入
void LTInsert(LTNode* pos, LTDataType x);
//指定位置删除
void LTErase(LTNode* pos);

4.2双链表具体实现

4.2.1尾插

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = CreateLTNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}
首先,我们要注意,这里不用二级指针,因为我们后面
在初始化中,会直接开辟一个节点空间返回给外面的节点指针,
所以就直接引用一级指针就可以了,增删查改。


因为是带头,且循环,所以头结点的前一个节点就是尾节点

申请一个新的节点

让tail的next不指向头结点,指向新的节点
新节点的next指向头结点,prev指向tail
头节点的prev指向新的尾节点

4.2.2申请节点

LTNode* CreateLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode==NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
返回类型采用,节点指针

申请节点空间,判断开辟是否失败

给data复制,next和prev置空,返回即可。

4.2.3初始化(重点!巧妙改变,不用二级指针了)

LTNode* LTInit()
{
	LTNode *phead = CreateLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
申请节点

因为带头、循坏、双向,所以prev和next都是指向自己

注意,这个函数使用后是返回一个有效链表节点的地址

那么我们在外面使用时,节点指针会指向一个有效的空间

那么我们后续增删查改,引用一级指针即可,无需二级指针

4.2.4打印

void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
没什么好说的,就是链表遍历打印

4.2.5尾删

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTNode* tail = phead->prev;
	phead->prev = tail->prev;
	tail = tail->prev;
	tail->next = phead;
}
断言空指针和空链表情况

tail指向尾节点,让头结点的prev指向尾节点的prev

让尾节点变成倒数第二个节点

让新尾节点的next指向头结点,完成

4.2.6链表销毁

void LTDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = cur->next;
	}
	free(phead);
} 
断言空指针

cur指向存储数据的第一个节点

遍历链表,逐级free空间

因为这里是一级指针,所以如果为了防止野指针,
规范写法的话,我们要在外面手动给外面的节点指针置空

4.2.7头插

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = CreateLTNode(x);

		LTNode* next = phead->next;
		phead->next = newnode;
		newnode->prev = phead;
		newnode->next = next;
	 next->prev=newnode;
}
断言空指针

申请节点,定义节点指针next接收第一个有效节点的next节点

然后让头结点的next指向新节点,新节点的prev指向头结点

新节点的next指向next节点,next节点的prev指向新节点

4.2.8头删

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead); 
	LTNode* next = phead->next->next;
	LTNode* del = phead->next;
	phead->next = next;
	next->prev = phead;
	free(del);
	del = NULL;
	
}
断言空指针
断言空链表的情况

next指针指向第二个有效节点

del指向第一个有效节点(要被删的节点)

头节点指向next,next的prev指向头结点

free掉del指向空间,指针置空

4.2.9寻找位置

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;
}
断言空指针

cur指向第一个有效节点

遍历链表

找到值返回地址,找不到返回NULL

4.2.10指定位置前插入

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	
	LTNode* newnode = CreateLTNode(x);
	newnode->next = pos;
	newnode->prev = pos->prev;
	pos->prev->next = newnode;
	pos->prev = newnode;
	
}
断言空指针

申请新节点

新节点的next指向pos节点,prev指向pos节点的prev

pos的prev节点的next指向新节点

pos的prev节点指向新节点

4.2.11指定位置删除

void LTErase(LTNode* pos)
{
	assert(pos);
	assert(pos->next != pos);
	LTNode* posnext = pos->next;
	LTNode* posprev = pos->prev;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;

}
断言空指针和空链表

posnext指向pos节点的下一个节点

posprev指向pos节点的上一个节点

让posprev的next指向posnext
posnext的prev指向posprev

free掉pos
pos置空

5.顺序表和链表的异同

 5.1存储

顺序表物理和逻辑都连续,链表逻辑连续,物理不一定连续

5.2访问

顺序表根据下标,可以直接以O(1)的时间复杂度访问,链表不可以

必须遍历一遍,复杂度O(N)

5.3指定位置增删

顺序表要往前或往后大规模挪数据,链表只需改变指针指向

5.4插入

顺序表要考虑空间是否够用,要扩容。链表不需要考虑容量(内存够用情况下)

5.5应用

顺序表适用于频繁访问,高效存储,链表适用于增删频繁的情况

5.6缓存

数据加载的内存的高速缓存中,命中率高就意味着数据是已经放在缓存里的,命中率低,就说明事先不在,需要临时加载进缓存,而加载,每次都会加载一片空间,而链表在物理上不一定是连续的,顺序表是一定连续的,显而易见,顺序表在一次加载会放进更多的数据在缓存中。因此每次cpu寻找数据时,很多数据只需要访问缓存就可以了,对缓存里的数据利用率就高了,相反,链表一次加载大概率只会放很少的数据在缓存里,cpu找数据的时候,要频繁访问内存中其他部件,这对缓存中的数据利用率就低了

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值