《数据结构》--链表【包含跳表概念】

news2024/11/28 12:31:13

不知道大家对链表熟悉还是陌生,我们秉着基础不牢,地动山摇的原则,会一点点的介绍链表的,毕竟链表涉及的链式存储也很重要的。在这之前,我们认识过顺序存储的顺序表,它其实就是一个特殊的数组。那链表到底是什么?又有什么用呢?与顺序结构有什么不一样呢?

一、认识链表

第一点:链表是什么?

链表是一种数据结构,用于以线性的方式存储一组数据。它与数组不同,链表的大小可以动态调整,且它的元素(称为节点),在内存中不必是连续存储的。每个节点包括两部分:数据域与指针域。

链表的基本结构:

头节点->节点1->节点2->节点3->尾节点

链表的分类:

根据不同的分类标准分为:单向和双向循环与不循环带不带头节点(又称哨兵节点)

常见的链表是:不带头节点的单向非循环链表带头结点的双向循环链表

链表的优点:

动态大小:方便插入和删除节点,适合频繁的插入和删除操作。

链表的缺点:

随机访问效率低,必须从头节点开始逐一遍历

每个节点需要额外的存储指针,空间开销较大

第二点:链表有什么用?

一方面,由于是动态大小,内存不必连续,这就可以充分利用碎片内存,对内存的利用率高。

另一方面,链式存储的概念在实现其它数据结构(栈和队列)以及相关算法方面有着很大的用途。

第三点:与顺序结构有什么不同?

1.存储方式:

顺序存储(数组):内存连续,通过下标直接访问元素,效率高。

链式存储(链表):内存不连续,每个节点包含数据域和指针域,节点间由指针相互链接。

2.动态大小:

顺序:大小在创建时确定,扩展和缩减不方便,需要重新分配内存进行扩容。

链式:大小可以动态变化,插入和删除节点时不需要移动其它元素,只需要改变指针域的指针指向

3.访问效率:

顺序:支持随机访问,时间复杂度O(1)

链式:只能从头遍历,时间复杂度O(n)

4.修改操作:

顺序:插入和删除需要移动大量数据,时间复杂度O(n)

链式:插入和操作只要调整指针指向,时间复杂度O(1)

5.内存占用:

顺序:由于是连续存储,内存发呢哦欸相对简单,但可能造成内存浪费(预留空间)。

链式:每个节点需要额外的存储空间来保存指针,整体的开销相对较大。

6.适用场景:

顺序存储:适合频繁访问的场景:查找、索引

链式存储:适合频繁插入和删除的场景:例如:栈、队列


 二、链表的API

基础操作:(数据结构)

增、删、查、(改)

增--插入:在链表的指定位置添加一个节点

删--删除:删除指定位置的一个节点

查--搜索:查找链表中特定位置的值、查找节点中是否中存在某值

遍--遍历:按顺序访问链表的所有节点、获取链表的长度

进阶操作:(算法提高)

合并两个有序链表:给出两个有序链表,现在要求能够合并两个链表,新的链表仍然有序

原地反转单向链表:给出一个单向链表,现在要求能够将链表反过来,要求不增加新链表

判断链表是否有环:给出一个单向链表,判断这个链表中是否存在环,没有什么特殊要求


 三、C语言实现链表

 这里不使用C++是后面我们还会用C++模拟实现STL中的list容器(有点恼火,估计要磨几天)。

单链表实现

定义链表节点

typedef int T;//T是int的别名,方便之后使用其它类型数据,增加可维护性
typedef struct ListNode {
	T data;
	struct ListNode* next;
}Node;

 创建新节点

Node* CreateNode(T _val)
{
    //使用malloc在堆区申请一个节点空间
    Node* new_node = (Node*)malloc(sizeof(Node));
    assert(new_node);
    
    new_node->data=_val;
    new_node->next=NULL;
    return new_node;
}

注意1:如果不在堆区申请空间,而是直接定义一个指针,那么当函数返回时,指针就会被自动释放,因为栈区的变量的生命周期就是定义开始到作用域结束。这样就导致后面访问时虽然还是那个地址,不过属于非法访问了已经。

注意2:assert()是断言函数,用来判断表达式是否为真,如果不为真,就强制结束程序。在这进行判断是为了保证申请空间成功。如果失败就不要后续的非法访问了。

尾插

void insert_tail(Node** pphead, T _val)
{
    assert(pphead);
    if(*pphead==NULL){//如果是空链表
        *pphead=CreateNode(_val);
        return;
    }
    //如果还没返回,那就是非空链表,在后面插入
    Node* tail=*pphead;
    while(tail->next){
        tail=tail->next;
    }
    //找到了最后一个节点,在最后一个节点后面插入一个节点
    tail->next=CreateNode(_val);
}

注意1:尾插是在链表的最后一个节点后面进行插入。需要考虑这个链表是空链表还是非空链表。如果是空链表,那么我将新节点当成头节点就成了。如果是非空链表,我需要遍历到尾节点处,尾节点的特点是next指针是空,那么我不知道具体循环多少次就可以使用while循环。当找到了尾节点,我让尾节点的next指针指向新的节点那么我的插入操作就完成了。

注意2:传入的参数是二级指针。why?首先链表的头本身是一个指针,然后我想要在这个链表上进行修改的操作,那就需要传入这个指针变量的指针。

例如:phead是一个指向链表头节点的指针,只不过此时存储的值是NULL。右面是新建的节点。

假设红色的是函数的一级指针参数, 我们想要插入一个节点,我们其实是想让phead指向这个新创建的节点(0x0001),也就是让phead的值改为0x0001。但我们的函数与phead没有任何的关系,只有_phead的值与phead的值一样,假如我让_phead的值为0x0001,那么_phead指向了新节点,一旦函数结束,形参的生命周期结束,这个指向关系将会终止,连新建的节点都不知道怎么去访问了。造成了内存泄露。

总结:头节点传二级指针,判断链表是否为空,寻找尾节点是看尾节点的next是否指向NULL

头插

void insert_head(Node** pphead,T _val)
{
    assert(pphead);

    Node* newnode = CreateNode(_val);
    newnode->next=*pphead;
    *pphead=newnode;
}

头插比较简单,不需要考虑是否为空,不管是不是空都是在头部插入。

注意1:创建完节点后,不要让头指针直接指向新节点,要先让新节点的next指针指向头指针,然后再让头指针指向新节点。不然原来的链表的地址将会丢失。 

 尾删

void del_tail(Node** pphead)
{
    assert(pphead && (*pphead));

    Node* tail=*pphead;
    if(tail->next==NULL){//如果只有一个节点
        free(*pphead);
        *pphead=NULL;
        return;
    }

    while(tail->next->next){
        tail=tail->next;
    }
    free(tail->next);
    tail->=NULL;
}

注意1:头指针不能为空,而且头指针指向的节点也不能为空。

注意2:我们要删除尾节点,那么就是将我们尾节点释放并将前一个节点的next指针置空。找到尾节点时,我们尾节点的前一个节点的状态是什么样子的呢?cur->next就指向tail,而tail->next就指向NULL,所以不妨直接将循环截至条件设置为cur->next->next==NULL时截至,然后释放cur->next也就是tail,再将cur->next置空NULL。

注意3:要完成注意2的前提下,我们需要判断尾节点是否有前一个节点,也就是是否只有一个节点。如果只有一个节点,我们将头节点释放置空就完事了。 

 头删

void del_head(Node** pphead)
{
    assert(pphead && (*pphead));

    Node* tmp_node=(*pphead)->next;
    free(*pphead);
    *pphead=tmp_node;
}

注意1:解引用符*与访问符->的优先级的问题。需要我们将*pphead整体括起来。

注意2:先存储后释放,再重设头节点。 

遍历与查找

Node* find(Node* phead,T _val)
{
    Node* cur=phead;
    while(cur){
        if(cur->data == _val){
            return cur;
        }
        cur=cur->next;
    }
    return NULL;
}

 直接循环完事了,对于不用修改操作的函数,用二级指针的必要都没有,循环遍历,如果头指针为空,那么循环都进不去,返回NULL。

在第i个位置插入节点

void insert(Node** pphead,int i,T _val){
    assert(pphead);
    assert(i>=0&&i<length(*pphead));//int length(Node* phead),遍历计数实现
    if(i==0){
        insert_head(pphead,_val);    
        return;
    }
    Node* new_node =CreateNode(_val);//新建一个待插入节点
    Node* cur=*pphead;i--;
    while(i--){//cur遍历到第i-1个位置
        cur = cur->next;
    }
    new_node->next=cur->next;//将第i个节点放在后面,
    cur->next=new_node;//连接上前面的节点,加入节点  
}

删除第i个位置节点

void del(Node* pphead,int i)
{
    assert(pphead&&(*pphead));
    assert(i>=0&&i<length(*phead));
    if(i==0){
        del_head(pphead);
        return;
    }
    if(i==length(*phead)){
        del_tail(pphead);
        return;
    }
    
    Node* cur=*pphead;i--;
    while(i--){//找到第i-1个节点
        cur=cur->next;
    }
    Node* tmp_node=cur->next;
    cur->next=cur->next->next;//如果上面不判断i==length,那么就不会有cur->next的存在
    free(tmp_node);
}

双链表实现

双链表节点定义:

typedef int T;

typedef struct TwoListNode{
    T data;
    struct TwoListNode* prev;//前驱节点指针
    struct TwoListNode* next;//后继节点指针
}TNode;

其他的操作类似,此处不再赘述。 

算法题练习

//Definition for singly-linked list.
struct ListNode {
    int val;
     ListNode *next;
     ListNode(int x) : val(x), next(NULL) {}
};

合并链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

(双指针算法:基础算法--双指针【概念+图解+题解+解释】-CSDN博客)

class Solution {
public:
	ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
		ListNode* tmp = new ListNode;
		ListNode* ans = tmp;
		while (list1 != nullptr && list2 != nullptr) {
			tmp->next = new ListNode;
			tmp = tmp->next;
			if (list1->val < list2->val) {
				tmp->val = list1->val;
				list1 = list1->next;
			}
			else{
				tmp->val = list2->val;
				list2 = list2->next;
			}
		}
		while (list1 != nullptr) {
			tmp->next = new ListNode;
			tmp = tmp->next;
			tmp->val = list1->val;
			list1 = list1->next;
		}
		while (list2 != nullptr) {
			tmp->next = new ListNode;
			tmp = tmp->next;
			tmp->val = list2->val;
			list2 = list2->next;
		}
		return ans->next;
	}
};

反转链表

给定单链表的头节点 head ,请反转链表,并返回反转后的链表的头节点。

 

(递归算法:基础算法--递归算法【难点、重点】-CSDN博客) 

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head || !head->next) {
            return head;
        }
        ListNode* second = head->next;
        ListNode* newHead = reverseList(second);
        second->next = head;
        head->next = nullptr;
        return newHead;
    }
};

环形链表

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

(双指针算法:基础算法--双指针【概念+图解+题解+解释】-CSDN博客)

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head==nullptr)return nullptr;
        ListNode* fast = head->next;
        ListNode* slow = head;
        while (fast != nullptr && fast->next != nullptr) {//确保快指针能走两步
            if (fast == slow) {
                //如果快慢指针相等了,说明是环,进行环的首节点锁定操作
                while (head != slow->next) {
                    slow = slow->next;
                    head = head->next;
                }
                return head;
            }
            
            fast = fast->next->next;//快指针走两步
            slow = slow->next;//慢指针走一步
        }
        return nullptr;
    }
};

*跳表

从第一节的对比中可以看出,链表虽然通过增加指针域提升了自由度,但是却导致数据的查询效率恶化。特别是当链表长度很长时,对数据的查询还得从头依次查询,这样效率会很低。跳表的产生就是为了解决链表过长的问题,通过增加链表的多级索引来加快原始链表的查询效率。这样的方式可以让查询的时间复杂度从O(n)提升到O(logn)

跳表通过增加的多级索引能够实现高效的动态插入和删除,其效率和红黑树平衡二叉树不相上下。目前redislevelDB都有用到跳表。

从上图可以看出,索引级的指针域除了指向下一个索引的next指针,还有一个down指针指向低一级的链表位置,这样才能实现跳跃查询的目的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2186739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

树莓派 AI 摄像头(Raspberry Pi AI Camera)教程

系列文章目录 前言 人们使用 Raspberry Pi 产品构建人工智能项目的时间几乎与我们生产 Raspberry Pi 的时间一样长。随着我们发布功能越来越强大的设备&#xff0c;我们能够支持的原生应用范围也在不断扩大&#xff1b;但无论哪一代产品&#xff0c;总会有一些工作负载需要外部…

SpringBoot介绍及整合Mybatis Plus

目录 SpringBoot背景及特点 SpringBoot整合Mybatis Plus SpringBoot背景及特点 SpringBoot的设计目是抛弃之前Spring、SpringMVC繁杂的配置过程&#xff0c;简化开发过程。之前的Spring框架需要大量的手动配置&#xff0c;包括XML配置文件或Java配置类&#xff0c;配置过程繁…

深入理解 Git 一个开发者的必备工具

深入理解 Git 一个开发者的必备工具 演示地址 演示地址 获取更多 获取更多 在现代软件开发中&#xff0c;版本控制系统扮演着至关重要的角色。其中&#xff0c;Git 是最流行的选择之一。无论你是新手还是有经验的开发者&#xff0c;了解 Git 的基本概念和使用方法都能大大提…

YOLO v11实时目标检测3:训练数据集格式说明

一、Yolov11简介 YOLOv11 是 YOLO 系列的最新版本&#xff0c;它不仅在目标检测方面表现出色&#xff0c;还引入了对象分割和多目标跟踪的功能。本文将介绍如何使用 YOLOv11 进行人流统计、车流统计以及跟踪的实际应用。 二、Yolo v11训练数据集格式说明 2.1 数据组织&#…

SAT分离轴定理的c++/python实现

分离轴定理的c/python实现 现在要对BEV模型检查出来的车辆做NMS&#xff0c;把3d框的平面属性获取到后&#xff0c;配合旋转角度投影到地面就是2D图形。 开始碰撞检测&#xff0c;判断是否重叠&#xff0c;保留置信度高的框就行。 原理 分离轴定理&#xff08;Separating A…

(C语言贪吃蛇)11.贪吃蛇方向移动和刷新界面一起实现面临的问题

目录 前言 实现效果 支持方向变换 修改默认效果 如何修改 总结 前言 我们上节实现了不需要按下右键就可以是贪吃蛇自发的向右移动&#xff0c;本节我们主要来解决贪吃蛇方向移动和刷新界面所遇到的问题。 实现效果 上图是我们希望实现的效果&#xff0c;我们可以自发地控…

Go 进阶:Go + gin 极速搭建 EcommerceSys 电商系统

Go 进阶:Go + gin 极速搭建 EcommerceSys 电商系统 前言 本章节适合有一定基础的 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。 具体请联系作者 项目结构 项目流程图 技术栈 项目结构 项目路由 4. 项目模型 项目初始化 初始化项目文…

归并排序【C语言版-笔记】

目录 一、概念二、排序流程理解三、代码实现3.1主调函数3.2 merge函数 四、性能分析 一、概念 归并是一种算法思想&#xff0c;是将两个或两个一上的有序表合并成一个长度较大的有序表。若一开始无序表中有n个元素&#xff0c;可以把n个元素看作n个有序表&#xff0c;把它们两…

Java中数据转换以及字符串的“+”操作

隐式转换&#xff08;自动类型转换&#xff09; 较小范围的数据类型转成较大范围的数据类型 强制转换&#xff08;显式转换&#xff09; 将数据范围大的数据类型转换为数据范围小的数据类型 基本数据类型之间的转换 当需要将一个较大的数据类型&#xff08;如float或double…

Linux:进程控制(一)

目录 一、写时拷贝 1.创建子进程 2.写时拷贝 二、进程终止 1.函数返回值 2.错误码 3.异常退出 4.exit 5._exit 一、写时拷贝 父子进程&#xff0c;代码共享&#xff0c;不作写入操作时&#xff0c;数据也是共享的&#xff0c;当任意一方试图写入&#xff0c;便通过写时拷…

影刀RPA实战:excel相关图片操作指令解

1.实战目标 excel是工作中必不缺少的工具&#xff0c;今天我们继续使用影刀RPA来实现excel操作的便利性&#xff0c;让影刀自动化来帮我们完成工作。 2.单元格填充图片 2.1 指令说明 功能&#xff1a;向 Excel 单元格插入本地图片或网络图片&#xff0c;支持Office和WPS&…

波阻抗,是电场矢量的模值/磁场矢量的模值

波阻抗是电场复振幅除以磁场复振幅&#xff0c;最后只与介质με有关 所以磁场可用电场强度表示&#xff08;利用波阻抗&#xff09; 问题&#xff0c;复振幅是矢量&#xff0c;波阻抗的定义是复振幅的比值&#xff1f;答案&#xff1a;不是&#xff0c;很明显&#xff0c;波阻…

Web3 游戏周报(9.22 - 9.28)

回顾上周的区块链游戏概况&#xff0c;查看 Footprint Analytics 与 ABGA 最新发布的数据报告。 【9.22-9.28】Web3 游戏行业动态&#xff1a; Axie Infinity 将 Fortune Slips 的冷却时间缩短至 24 小时&#xff0c;从而提高玩家的收入。 Web3 游戏开发商 Darkbright Studios…

Pikachu-Sql Inject-搜索型注入

MySQL的搜索语句&#xff1a; select * from table where column like %text%&#xff1b; 如&#xff1a;使用引号闭合左边的引号&#xff0c; or 11 把所有数据查询出来&#xff1b; # 注释掉后面的 引号等&#xff1b; test or 11# 查询出结果&#xff1a; 注入的核心点…

Cloneable接口(浅拷贝和深拷贝的区别)

前言 Object类中存在这一个clone方法&#xff0c;调用这个方法可以创建一个对象的“拷贝”。但是想要合法调用clone方法&#xff0c;必须要先实现Clonable接口&#xff0c;否则就会抛出CloneNotSupportedException异常。 1 Cloneable接口 //Cloneable接口声明 public interf…

CentOS 7文件系统

从centos7开始&#xff0c;默认的文件系统从ext4变成了XFS。随着虚拟化的应用越来越广泛&#xff0c;作为虚拟化磁盘来源的大文件&#xff08;单个文件几GB级别&#xff09;越来越常见。 1.XFS组成部分&#xff1a; XFS文件系统在数据的分布上主要划分为三部分&#xff1a;数据…

QT篇:QT介绍

一.QT概述 Qt 是一个跨平台的应用程序和用户界面框架&#xff0c;用于开发图形用户界面&#xff08;GUI&#xff09;应用程序以及命令行工 具。它最初由挪威的 Trolltech &#xff08;奇趣科技&#xff09;公司开发&#xff0c;现在由 Qt Company 维护&#xff0c;2020年12月8…

如何在网格中模拟腐烂扩散:如何使用广度优先搜索(BFS)解题

问题描述 你需要在一个二维的网格中处理橘子的腐烂扩散过程&#xff0c;网格中的每个单元格可以有三种状态&#xff1a; 0&#xff1a;表示空格&#xff0c;没有橘子。1&#xff1a;表示一个新鲜的橘子。2&#xff1a;表示一个腐烂的橘子&#xff0c;它可以在 1 分钟内让上下…

模拟算法(1)_替换所有的问号

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 模拟算法(1)_替换所有的问号 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1. …

MHA携手Atlas:打造高效读写分离解决方案,引领数据库性能飞跃

作者简介&#xff1a;我是团团儿&#xff0c;是一名专注于云计算领域的专业创作者&#xff0c;感谢大家的关注 座右铭&#xff1a; 云端筑梦&#xff0c;数据为翼&#xff0c;探索无限可能&#xff0c;引领云计算新纪元 个人主页&#xff1a;团儿.-CSDN博客 目录 前言&#…