线性表之链表

news/2024/12/21 22:10:01/

1、链表概述

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

顺序表的存储位置可以用一个简单直观的公式表示,它可以随机存取表中任意一个元素,但插入和删除需要移动大量元素。链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此插入和删除操作不需要移动元素,只需要修改指针,这也意味着链表失去了可随机存取的特点。

2、链表的分类

链表结构种类多样,可以按照是否带头、是否循环、单向或者双向大致分类。

(1)带头结点和不带头结点

 (2)单向链表和双向链表

 (3)循环链表和非循环链表

以上情况组合就有8中链表结构,但实际中应用最多的链表结构是无头单向非循环链表和带头双向循环链表。下面介绍两种链表的基本实现。

3、无头单向非循环链表的基本实现

为了建立数据元素之间的线性关系,链表结点除了存放数据,还需要存放一个指向其后继的指针。单链表可以解决顺序表需要大量连续存储单元的缺点。由于单链表的元素离散地分布在存储空间中,所以单链表时非随机存取的存储结构。

单链表的结点类型描述如下。

typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;

3.1单链表的打印

为了观感上更贴近单链表的定义,打印时先打印结点的值,“->”表示链表的指针,打印完所有元素后再打印“NULL”。

void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}

3.2单链表的销毁

销毁时传入的是一级指针

void SLTDestroy(SLTNode* phead)
{SLTNode* cur = phead;while (phead){cur = phead;phead = phead->next;free(cur);cur = NULL;}printf("success\n");
}

3.3头插法插入结点

使用头插法插入新结点时,不用考虑链表是否为空,因为不涉及空指针的引用,直接插入即可。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}

3.4尾插法插入结点

使用尾插法插入新结点时需要判断链表是否为空,因为插入过程中涉及到了空指针的引用。

当链表为空时,新结点即链表第一个结点;当链表不为空时,先找到链表的尾结点,然后直接将新结点插到尾结点后面。

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);if (*pphead == NULL)*pphead = newnode;else{SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}

3.5头删法删除结点

删除之前需要判断链表是否为空,链表不为空时,删除第一个结点。

void SLTPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* del = *pphead;*pphead = (*pphead)->next;free(del);del = NULL;
}

3.6尾删法删除结点

删除之前同样需要判断链表是否为空,此外还需要判断链表是否只有一个结点,因为删除过程中涉及到空指针的引用。如果链表只有一个结点,直接将该结点删除释放;如果链表有多个元素,找到倒数第二个结点,然后删除该结点的后继。

void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}
}

3.7按值查找

从头开始依次对比,如果找到值为x的结点则返回该结点的地址,如果没找到则返回空指针。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x)return cur;cur = cur->next;}return NULL;
}

3.8在指定位置之前插入结点

插入分为链表只有一个结点和有多个结点这两种情况。当链表只有一个元素或者指定位置为第一个结点时,相当于头插法;当链表有多个元素或者指定位置为其他结点时,找到指定位置的前驱,然后修改其前驱的后继以及新结点的后继。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);if (pos == *pphead)SLTPushFront(pphead, x);else{SLTNode* posPrev = *pphead;SLTNode* newnode = BuySLTNode(x);while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = newnode;newnode->next = pos;}
}

3.9在指定位置之后插入结点

在指定位置之后插入不用考虑链表有几个结点,直接插入即可。注意,要先修改新结点的后继,再修改指定位置结点的后继。

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);newnode->next = pos->next;pos->next = newnode;
}

3.10删除指定位置的结点

当链表只有一个元素或者指定位置为第一个结点时,相当于链表的头删;当链表有多个元素或者指定位置为其他结点时,找到指定位置结点的前驱,并修改其后继,再删除并释放指定位置结点。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);if (pos == *pphead)SLTPopFront(pphead);else{SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = pos->next;free(pos);pos = NULL;}
}

3.11删除指定位置之后的结点

在指定位置之后删除不用考虑链表有几个结点,直接删除即可。

void SLTEraseAfter(SLTNode* pos)
{assert(pos);SLTNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}

4、带头双向循环链表的基本实现

单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点时,只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。为了克服单链表的这些缺点,引入双链表,双链表中有两个指针prev和next,分别指向前驱结点和后继结点。

双链表中结点类型的描述如下:

typedef int LTDataType;
typedef struct ListNode
{LTDataType data;struct ListNode* prev;struct ListNode* next;
}LNode;

双链表可以很方便找到前驱结点,因此,插入、删除操作的时间复杂度为O(1)。

此外,链表带头结点,无论链表是否为空,其头指针都是指向头结点的非空指针,因此在链表的插入和删除中空链表和非空链表的处理得到了统一。

4.1初始化双向链表

为了减少二级指针的使用,在初始化双链表时,使用一级指针定义并返回头结点。因此初始化时直接建立一个头结点并返回头结点的地址。

因为后续实现链表插入时还需要建立新结点,为了简化代码以及增强代码的可读性,定义一个建立新结点的函数,后续需要建立新结点时直接调用该函数即可。建立新结点函数如下。

LTNode* BuyLTNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->next = NULL;newnode->prev = NULL;newnode->data = x;return newnode;
}

初始化双链表函数如下。

LTNode* Init()
{LTNode* head = BuyLTNode(-1);head->next = head;head->prev = head;return head;
}

4.2双向链表的打印

需要注意的是,打印时从phead的next开始打印。为了观感上更贴近双链表的定义,打印时先打印“guard”表示头结点,“<==>”表示链表的双指针,打印完所有元素后再打印一次“guard”表示尾结点的链接到头结点。

void LTPrint(LTNode* phead)
{LTNode* cur = phead->next;printf("guard<==>");while (cur != phead){printf("%d<==>", cur->data);cur = cur->next;}printf("guard\n");
}

4.3双向链表的销毁

双链表的销毁同单链表的销毁类似,这里使用的仍是一级指针,所以需要用户在调用完销毁函数后,手动置空头结点。

void LTDestory(LTNode* phead)
{LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}
}

4.4头插法插入结点

在使用头插法插入新结点时,要注意指针修改顺序,指针顺序虽然不是唯一的,但也不是任意的。新结点前驱和后继的修改必须在头结点后继的修改之前进行,否则头结点的后继结点的指针就会丢掉,导致插入失败。如图所示,1和2必须在4之前进行。

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyLTNode(x);newnode->next = phead->next;phead->next->prev = newnode;newnode->prev = phead;phead->next = newnode;
}

还有一种方法,可以不用考虑指针修改的先后顺序,就是重新定义一个结点存放头结点的后继结点,这样就不用担心头结点的后继指针丢失的问题了。

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyLTNode(x);LTNode* first = phead->next;  newnode->prev = phead;  phead->next = newnode;newnode->next = first;first->prev = newnode;
}

4.5尾插法插入结点

使用尾插法时,不用像单链表那样遍历找尾,头结点的前驱结点就是尾结点。插入时同样需要注意指针的修改顺序,1和2必须要在3之前进行。

和头插法类似,重新定义一个结点保存头结点的前驱结点,就不用考虑指针修改的顺序了。

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyLTNode(x);LTNode* tail = phead->prev;tail->next = newnode;newnode->prev = tail;phead->prev = newnode;newnode->next = phead;
}

4.6头删法删除结点

在删除之前需要将头结点的后继结点保存起来,否则无论在修改指针之前free后继结点还是在修改指针之后free后继结点都会出现错误。

 注意,当链表为空时(只有头结点)时不能继续删除,所以在删除之前需要判断链表是否为空。

bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}
void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* first = phead->next;phead->next = first->next;first->next->prev = phead;free(first);
}

4.7尾删法删除结点

在删除之前需要将尾结点保存起来,为了方便,将尾结点的前驱结点也保存一下。与头删法同样,在删除前需要判断双链表是否为空,如果为空则不能继续删除。

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);
}

4.8按值查找

需要注意的是,从头结点的后继结点开始查找,并且停止查找的条件是当前结点不等于头结点。

LTNode* LTFind(LTNode* phead, LTDataType x)
{LTNode* cur = phead->next;while (cur != phead){if (cur->data == x)return cur;cur = cur->next;}return NULL;
}

4.9在指定位置之前插入结点

在插入之前先保存pos的前驱结点,这样在插入时就不用考虑指针的修改顺序了。

void LTInsert(LTNode* pos, LTDataType x)
{LTNode* newnode = BuyLTNode(x);LTNode* posPrev = pos->prev;posPrev->next = newnode;newnode->prev = posPrev;newnode->next = pos;pos->prev = newnode;
}

 头插法和尾插法插入结点可以通过该函数的复用实现。

//头插法
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTInsert(phead->next, x);
}//尾插法
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTInsert(phead, x);
}

注意,头插法传入的pos为头结点的后继结点;尾插法传入的pos为头结点,因为头结点的前驱结点就是尾结点。

4.10删除指定位置的结点

在删除之前先保存pos的前驱结点和后继结点。

void LTErase(LTNode* pos)
{LTNode* posPrev = pos->prev;LTNode* posNext = pos->next;free(pos);posPrev->next = posNext;posNext->prev = posPrev;
}

同样地,头删法和尾删法删除结点可以通过该函数的复用实现。

//头删法
void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTErase(phead->next);
}//尾删法
void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTErase(phead->prev);
}

注意,头删法传入的pos是头结点的后继结点;尾删法传入的pos是头结点的前驱结点。

5、顺序表与链表的比较


http://www.ppmy.cn/news/971762.html

相关文章

(博客笔记)SLAM的三相性和世界观

SLAM的世界观&#xff1a; 本文是读了一篇博客之后的随笔&#xff0c;记录一下SLAM的世界观。 ​ 编辑切换为居中 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; SLAM的三相性是指开销、鲁棒性和精度&#xff0c;简单来说就是快&#xff0c;稳&#…

windows系统修改mysql8配置文件,关闭ssl验证

如何寻找配置文件 我的电脑&#xff0c;右键&#xff0c;管理&#xff0c;服务 找到MySQL8 右键&#xff0c;属性 找到配置文件位置 通常情况下的默认路径是&#xff1a; C:\ProgramData\MySQL\MySQL Server 8.0\my.ini 如何关闭SSL验证 打开 my.ini 配置内容如下&#x…

《零基础入门学习Python》第056讲:论一只爬虫的自我修养4:网络爬图

今天我们结合前面学习的知识&#xff0c;进行一个实例&#xff0c;从网络上下载图片&#xff0c;话说我们平时闲来无事会上煎蛋网看看新鲜事&#xff0c;那么&#xff0c;熟悉煎蛋网的朋友一定知道&#xff0c;这里有一个 随手拍 的栏目&#xff0c;我们今天就来写一个爬虫&…

sqlite3 插入数据

文章目录 需求&#xff0c;操作1.进入sqlite终端2.打开数据库3.执行插入语句。4.查看是否成功 最近有项目在用sqlite3&#xff0c;这个嵌入式数据库&#xff0c;不是很熟练&#xff0c;连个插入数据的语句都得百度哈哈。 记录下&#xff0c;加深记忆&#xff0c;给同样小白的人…

00_ubuntu_开发环境的搭建

ubuntu 的版本22.04 2023-07-21 1.卸载firefox dpkg --get-selections |grep firefox // 查看安装包的信息 sudo apt-get purge firefox firefox-locale-en firefox-locale-zh-hans // 卸载相应的包 2.下载google安装包并安装 wget https://dl.google.com/linux/direct/goo…

设计模式再探——状态模式

目录 一、背景介绍二、思路&方案三、过程1.状态模式简介2.状态模式的类图3.状态模式代码4.状态模式还可以优化的地方5.状态模式的项目实战&#xff0c;优化后 四、总结五、升华 一、背景介绍 最近产品中有这样的业务需求&#xff0c;不同时间(这里不是活动的执行时间&…

视频特效软件有哪些?这些软件值得一试

大家平常在制作视频时&#xff0c;经常需要将多个视频拼接&#xff0c;但是如果两个视频中间没有什么转场过渡的话&#xff0c;会显得很单调。我们可以增加一些转场、音乐、特效&#xff0c;这样整支视频看起来效果会好很多。讲到视频特效&#xff0c;可能有些小伙伴会觉得它很…

如何给视频添加特效?快速制作特效视频

如何给视频添加特效&#xff1f;现如今几乎我们人人每天都在与短视频打交道&#xff0c;有些人在日常的生活中也会剪辑一些短视频。其实剪辑短视频并没有你想象中的那么困难。只是需要找到一款合适的软件就可以很快完成。在短视频剪辑中就有需要给短视频添加特效的操作&#xf…