数据结构───链表

花费一个周时间学完了链表(的一部分),简单总结一下。

 

链表的学习离不开画图,将其抽象成一种逻辑模型,可以减少思考时间,方便理解。

链表大致分为8种结构,自己学习并实现了两种结构,也是两种最经典的结构。一种是单向不带头非循环链表,另一种是双向带头循环链表

 

 无头单向非循环链表

无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

以下就是该链表的实现:

1.链表的创建

定义一个结构体,包含存储的数据和指向后继节点的指针。

typedef int MyType;

//单向不带头非循环链表
typedef struct SingleLinklist{
	MyType data;
	struct SingleLinklist* next;
}SLNode;

2.链表功能实现

由于是不带头链表,增删改功能需要修改链表的内容,所以需要传头节点的地址,功能函数用二级指针来接收,亦或者选择用返回值的方式。下面是采取传地址的方式。

先封装一个创建新节点的函数,方便以后多次使用:

SLNode* BuyNewNode(MyType x) {
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

链表的尾插:

要实现单向链表的尾插,需要先判断是否头节点为空,然后遍历链表找到链表的最后一个结点。

 

//尾插
void SLinkListPushBack(SLNode** pphead, MyType x) {
	SLNode* newnode = BuyNewNode(x);
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {
		SLNode* tail = *pphead;
		while (tail->next!=NULL) {
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

 链表的头插:

链表的头插也大相径庭,也先判断头节点是否为空,头插完成后将新节点置成头

 

//头插
void SLinkListPushPront(SLNode** pphead, MyType x) {
	SLNode* newnode = BuyNewNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}

链表的尾删:

单链表的麻烦之处可能就是,尾插之时,不好找上一个位置。这样就需要另外一个变量来保存上有个节点。

//尾删
void SLinkListPopBack(SLNode** pphead) {
	//当链表为空,直接报错
	assert(pphead);
	//只有一个结点
	if((*pphead)->next==NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	//两个结点及以上
	else {
		SLNode* tail = *pphead;
		while (tail->next->next) {
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

链表的头删:

头删就相对容易了,可以不用考虑一个还是多个情况,因为即使一个,它的下一个空节点为新的头节点也不受影响。

//头删
void SLinkListPopFront(SLNode** pphead) {
	//链表为空
	assert(*pphead);
	SLNode* newphead = (*pphead)->next;
	free(*pphead);
	*pphead = newphead;
}

链表的查找、插入:

查找的话遍历一遍链表就好啦。插入分为在前插入和在后插入。在前插入相对麻烦,因为单向链表的前一个节点需要再找一遍,所以需要重新定义一个变量,如果插入的位置是头节点之前的话,又就变成头插了(可以直接调用头插函数)。

 

 

//查找
SLNode* SLinkListFind(SLNode* phead, MyType x) {
	SLNode* cur = phead;
	while (cur) {
		if (cur->data == x) {
			return cur;
		}else{
			cur = cur->next;
		}
	}
	return NULL;
}
//在前插入
void SLinkListInterFormer(SLNode** pphead, SLNode*pos,MyType x) {
	assert(pphead);
	assert(pos);
	SLNode* newnode = BuyNewNode(x);

	if (*pphead ==pos) {
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else {
		SLNode* Prev = *pphead;
		while (Prev->next != pos) {
			Prev = Prev->next;
		}
		Prev->next = newnode;
		newnode->next = pos;
	}

}
//在后一个位置插入
void SLinkListInterAfter(SLNode* pos, MyType x) {
	assert(pos);
	SLNode* newnode = BuyNewNode(x);
	
	newnode->next = pos->next;
	pos->next = newnode;

}

链表销毁:

由于传的的地址,直接一个函数就可以销毁了。

//摧毁链表
void SLinkListDestory(SLNode** pphead) {
	assert(pphead);

	SLNode* cur = *pphead;
	while (cur) {
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

 双向带头循环链表

 单向链表实现了,下面看一下双向带头循环链表,这种结构可以说是非常牛逼的一种链表结构。

 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

1.链表的创建

由于是双向,所以一个前继指针,一个后继指针。

typedef int MyType;

//双向循环带头链表
typedef struct DoubleLinklist {
	MyType val;
	struct DoubleLinklist* next;
	struct DoubleLinklist* prev;
}DLNode;

2.链表功能实现

上面的单向链表不带头所以不需要初始化,直接phead=NULL;就可以开始创建链表。而这种结构的好处就是不用传地值了,因为它修改的是结构体里的内容,不过要先创建一个哨兵位节点—站岗用的。

链表初始化:

//初始化链表
DLNode* DLNodeInit() {
	DLNode* phead = (DLNode*)malloc(sizeof(DLNode));

	//哨兵位 不存储有效数据
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

malloc新节点:

 

//创建新节点
DLNode* CeateNewnode(MyType x) {
	DLNode* newhead = (DLNode*)malloc(sizeof(DLNode));
	
	newhead->val = x;
	newhead->next = NULL;
	newhead->prev = NULL;

	return newhead;
}

链表的尾插:

 尾插只需要找到head->prev就行。没有节点时,head->prev就是它自己。

所以尾插就可以轻松实现。

 

//尾插
void DLNodePushBack(DLNode* phead, MyType x) {
	//因为只改变phead指向的结构体的东西,并不改变Phead,所以只传一级指针
	assert(phead);
	
	//malloc新节点
	DLNode* newnode = CeateNewnode(x);
	//pehad->prev==tail
	DLNode* tail = phead->prev;

	//连接新节点
	tail->next = newnode;
	newnode->prev = tail;
	//首尾相连,形成循环
	phead->prev = newnode;
	newnode->next = phead;

}

 链表的头插:

链表的头插注意的是插入到head->next也就是head的下一个节点。因为链表遍历是从head的下一个节点开始。这也是我一开始打错的原因。

//打印链表
void DLNodePrint(DLNode* phead) {
	assert(phead);
	//phead里没有存有效数据,所以从phead的下一个开始
	DLNode* cur = phead->next;
	while (cur != phead) {
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("\n");
}

 

//头插
void DLNodePushFront(DLNode* phead, MyType x) {
	assert(phead);
	DLNode* newnode = CeateNewnode(x);

	DLNode* next = phead->next;
	newnode->prev = phead;
	phead->next = newnode;
	next->prev = newnode;
	newnode->next = next;

	//可以调用插入函数,因为头插就是phead后结点插入
	/*DLNodeInterBack(phead, x);*/
}

链表的尾删: 

尾节点不需要遍历就能找到,而且它的前一个节点也可以找到,这样就减少了消耗。不过需要注意的是,得先判断是不是没有节点,准确来说是不是只有一个哨兵位节点。

 

//尾删
void DLNodePopBack(DLNode* phead) {
	assert(phead);
	if(phead->next != phead) {
		DLNode* tail = phead->prev;
		tail->prev->next = phead;
		phead->prev = tail->prev;
		free(tail);
		tail = NULL;
	}
}

链表的头删:

头删也大相径庭,跟尾删一样要先判断是不是只有一个头节点。

 

//头删
void DLNodePopFront(DLNode* phead) {
	assert(phead);
	if (phead->next != phead) {
		DLNode* del = phead->next;
		phead->next = del->next;
		del->next->prev = phead;
		free(del);
		del = NULL;
	}
}

链表的查找、插入和删除:

为什么上个链表没有删除,因为忘记实现了。除了删除,只要查找到位置,修改,插入,删除其实都是很容易的事情。当然在这个完美的链表结构就更容易实现啦。

 

因为无论前插后插还是删除,直接就可以找前一个结点和后一个节点。

 

//查找
DLNode* DLNodeFind(DLNode* phead, MyType x) {
	assert(phead);

	DLNode* cur = phead->next;
	while (cur != phead) {
		if (cur->val == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//结点前插入
void DLNodeInterFront(DLNode* pos, MyType x) {
	assert(pos);
	DLNode* newnode = CeateNewnode(x);

	DLNode* former = pos->prev;
	former->next = newnode;
	newnode->prev = former;
	newnode->next = pos;
	pos->prev = newnode;

}
//结点后插入
void DLNodeInterBack(DLNode* pos, MyType x) {
	assert(pos);
	DLNode* newnode = CeateNewnode(x);

	DLNode* latter = pos->next;
	pos->next = newnode;
	newnode->prev = pos;
	newnode->next = latter;
	latter->prev = newnode;
}
//删除节点
void DLNodeErase(DLNode* pos) {
	assert(pos);

	DLNode* former = pos->prev, * latter = pos->next;
	former->next = latter;
	latter->prev = former;
	free(pos);
	pos = NULL;

}

链表销毁: 

需要注意的是,这里传的是一级指针,所传指针需要在函数外置空

//摧毁链表
void DLNodeDestroy(DLNode* phead) {
	assert(phead);
	DLNode* cur = phead->next;
	while (cur!=phead) {
		DLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

 链表基础题

 链表的实现完成了,学了知识就需要接下来不断巩固。

刷题就是最好的方式,下面是几道关于链表的简单题目。

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

题解:可以看到,移除所给值的节点,直接遍历就好啦,不过需要注意的是是否为头节点,如果为头节点就需要将头指针转移,就是头删。单向链表删除节点,需要遍历出上一个节点,如果每次删除都要遍历一遍,不如只遍历一遍将节点保存一下,再继续往下走,删除时就只需要将保存的前继节点连接到后一个节点就行。也是双指针问题。

struct ListNode* removeElements(struct ListNode* head, int val){
    if(head==NULL){
        return NULL;
    }
    struct ListNode* cur=head;
    struct ListNode* prev=NULL;
    while(cur){
        if(cur->val==val){
            //头删
            if(cur==head){
                head=head->next;
                free(cur);
                cur=head;
            }else{
            prev->next=cur->next;
            free(cur);
            cur=prev->next; 
            } 
        }else{
             prev=cur;
            cur=cur->next;
        }
    }
    return head;
}

面试题 02.04. 分割链表 - 力扣(LeetCode)

这道题的思路是遍历一遍链表,将小于目标值的节点合成一个链表,大于目标值的节点合成一个链表,然后将两个链表尾首相连即可。需要注意的是如果连接后的链表的最后一个节点的后继指针不为空,需要置空,否则他依旧指向某个节点,这样一来就形成了死循环了。

还有种情况就是没有小于目标值的节点,这样直接返回大于目标值的节点就好啦。

struct ListNode* partition(struct ListNode* head, int x){
    struct ListNode*cur=head;
    struct ListNode*samllerHead=NULL;
    struct ListNode*samallerTail=NULL;
    struct ListNode*greaterHead=NULL;
    struct ListNode*gearterTail=NULL;
 
    while(cur){
        if(cur->val<x){
            if(samllerHead==NULL){
                samllerHead=samallerTail=cur;
            }else{
                samallerTail->next=cur;
                samallerTail=samallerTail->next;
            }
        }else{
              if(greaterHead==NULL){
                greaterHead=gearterTail=cur;
            }else{
                gearterTail->next=cur;
                gearterTail=gearterTail->next;
            }
        }
        cur=cur->next;
    }
    if(samallerTail){
        samallerTail->next=greaterHead;
    }else{
        samallerTail=greaterHead;
    }
    if(gearterTail){
        gearterTail->next=NULL;
    }
    if(samllerHead==NULL){
        samllerHead=greaterHead;
    }
    return samllerHead;
}

面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)

这道题解法也是双指针,可以用到双指针中的快慢指针。快指针先走k步,然后再跟慢指针一起走,当快指针为空时,此时慢指针就是倒数第k个节点。

int kthToLast(struct ListNode* head, int k){
    struct ListNode* fast=head,*slow=head;
    //先让fast走k步
    while(k--){
        fast=fast->next;
    }
    //再一起走,当fast为NULL时,此时slow就是第k的位置
    while(fast){
        slow=slow->next;
        fast=fast->next;
    }
    return slow->val;
}

 

个人总结 

链表的操作虽然学会了,但放在题目还是不会,还是算法基础太薄弱了,已经准备买书买咖啡早起学算法了,希望终有一天我也能成为一个算法大佬。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

UE5 日记(人物连招:蒙太奇动画通知(含视频链接))

教程https://www.youtube.com/watch?vsWpENaVGj2M&listPLiSlOaRBfgkcPAhYpGps16PT_9f28amXi&index10&ppiAQB 相关蓝图 连招逻辑 动画通知类 逻辑分析 1.用户输入 已搭载战斗系统模块,可以收到输入指令 2.连击 第一次攻击&#xff1a; 第一次攻击&#xff0c;…

初探802.11协议(6)——Wi-Fi 6新特性简介

目录 前言 场景需求 Trigger Frames 一. BSS Color 1.1 机制 1.1.1 Color Collision 1.2 Frame Format 1.2.1 BSS Color Information 二. TWT 2.1 节能 2.1.1 PSM 2.1.2 从PSM到TWT 2.1.2.1 TWT模式 2.2 Frame Format REF 前言 802.11ax以前强调"高吞吐量…

基于 Appium 的 Android UI 自动化测试!

自动化测试是研发人员进行质量保障的重要一环&#xff0c;良好的自动化测试机制能够让开发者及早发现编码中的逻辑缺陷&#xff0c;将风险前置。日常研发中&#xff0c;由于快速迭代的原因&#xff0c;我们经常需要在各个业务线上进行主流程回归测试&#xff0c;目前这种测试大…

@CallSuper注解方法学习

CallSuper注解方法学习 CallSuper注解是什么&#xff1f;CallSuper注解使用CallSuper 值得一提的事总结 CallSuper注解是什么&#xff1f; CallSuper 是 Android 开发中使用的一个注解&#xff0c;它的主要用途是确保在子类重写父类的方法时&#xff0c;调用 super 方法。这在…

【Qt】绘图与绘图设备

文章目录 绘图设备QPainter绘图实例案例1案例2-高级设置案例3&#xff1a;利用画家画资源图片 点击按钮移动图片 QtPaintDevice实例Pixmap绘图设备QImage 绘图设备QPicture 绘图设备 QPainter绘图 Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系…

云安全—K8S API Server 未授权访问

0x00 前言 master节点的核心就是api服务&#xff0c;k8s通过REST API来进行控制&#xff0c;在k8s中的一切都可以抽象成api对象&#xff0c;通过api的调用来进行资源调整&#xff0c;分配和操作。 通常情况下k8s的默认api服务是开启在8080端口&#xff0c;如果此接口存在未授…

RPC与HTTP的关系

首选理清楚关系 RPC与HTTP是两个不同维度的东西 HTTP 协议&#xff08;Hyper Text Transfer Protocol&#xff09;&#xff0c;又叫做超文本传输协议&#xff0c;是一种传输协议&#xff0c;平时通过浏览器浏览网页网页&#xff0c;用到的就是 HTTP 协议。 而 RPC&#xff0…

【java】【MyBatisPlus】【三】【完】MyBatisPlus扩展

目录 一、分页查询lambdaQueryWrapper 二、自定义分页查询 1、UserMapper 2、UserMapper.xml 3、测试方法 三、MybatisX插件 1、安装 2、MybatisX代码快速生成 2.1 连接数据库 2.2 操作需要生成代码的表 3、MybatisX快速生成CRUD&#xff08;前提步骤2生成&#xff…

【算法练习Day32】 斐波那契数爬楼梯使用最小花费爬楼梯

​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;练题 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 斐波那契数爬楼梯使用最小花…

探讨下前端测试的常见场景

前端测试 场景 这边指的测试是指白盒测试&#xff0c;用代码来测试代码。 测试有利于提升代码质量。 代码功能和需求一致。根据需求&#xff0c;写测试。测试通过了&#xff0c;则表明需求实现了。保证代码重构后&#xff0c;未改坏以前的功能。代码重构后&#xff0c;能通过…

[C++入门系列]——类和对象下篇

​作者主页 &#x1f4da;lovewold少个r博客主页 ⚠️本文重点&#xff1a;C类和对象下篇知识点讲解 &#x1f449;【C-C入门系列专栏】&#xff1a;博客文章专栏传送门 &#x1f604;每日一言&#xff1a;宁静是一片强大而治愈的神奇海洋&#xff01; 目录 前言 再谈构造函数…

解决方案|法大大电子合同加速互联网家装服务升级

随着互联网的快速发展以及政策的不断推动&#xff0c;家装行业“互联网”趋势也不断凸显。行业内很多企业已经开始在全链条业务中使用电子合同&#xff0c;基于电子合同合规化、无纸化、线上化、智能化的价值赋能&#xff0c;实现家装从需求沟通、家装设计、选材、装修施工、验…

【MySQL--->内外连接】

文章目录 [TOC](文章目录) 一、内连接二、左外连接三、右外连接 一、内连接 内连接就是将两个表连接进行笛卡尔积查询 显示SMITH的名字和部门名称 二、左外连接 左外连接就是以左面的表为主&#xff0c;即便是右边的表没有而左边表项中有的&#xff0c;依然显示 查询所有学…

jenkins详细安装教程

这里写目录标题 一、Jenkins安装与部署1-1、Jenkins的简介1-2、下载需要的软件1-2-1 jekins.war1-2-2 tomcat安装方式 1-3、使用11版本的jdk1-4、开启jenkins1-5、获取密码1-5 修改镜像(可改可不改) 二、卸载Jenkins 一、Jenkins安装与部署 1-1、Jenkins的简介 Jenkins是一个…

Linux 基础入门

Linux 简介 Linux 是一种自由和开放源码的类 UNIX 操作系统。 Linux 英文解释为 Linux is not Unix。 Linux 是在 1991 由林纳斯托瓦兹在赫尔辛基大学上学时创立的&#xff0c;主要受到 Minix 和 Unix 思想的启发。 Linux 内核最初只是由芬兰人林纳斯托瓦兹&#xff08;Linus T…

基于SSM的n省出口基地公共信息服务平台设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

Spring Cloud Gateway + Knife4j 4.3 实现微服务网关聚合接口文档

目录 前言Spring Cloud 整合 Knife4jpom.xmlapplication.ymlSwaggerConfig.java访问单服务接口文档 Spring Cloud Gateway 网关聚合pom.xmlapplication.yml访问网关聚合接口文档 接口测试登录认证获取登录用户信息 结语源码 前言 youlai-mall 开源微服务商城新版本基于 Spring…

评估在线不平衡学习的PAUC

评估在线不平衡学习的PAUC 原始论文《Prequential AUC: properties of the area under the ROC curve for data streams with concept drift》 由于正常的AUC需要计算整体数据集上&#xff0c;每个数据的预测置信度的排名。那么我们首先要求我们的在线学习算法在进行预测时也返…

node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)

文章目录 ⭐前言⭐ 环境准备⭐ 实现过程⭐ mysql 配置⭐路由前的准备⭐账号注册生成token⭐账号登录生成token⭐token登录 ⭐ 自测过程截图⭐总结⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享关于node实战——后端koa项目配置jwt实现登录注册&#xff08;n…

博客模板博客模板

xservices-bpm-6.2.1.1.jar 本人详解 作者&#xff1a;王文峰&#xff0c;参加过 CSDN 2020年度博客之星&#xff0c;《Java王大师王天师》作者 公众号&#xff1a;山峯草堂&#xff0c;非技术多篇文章&#xff0c;专注于天道酬勤的 Java 开发问题、中国国学、传统文化和代码爱…