JS 数据结构:链表

news2025/8/6 6:25:08

单链表

每个节点中只包含一个指针域的链表称为单链表

头结点—其指针域指向表中第一个结点的指针(头结点不是必须的,只是习惯上加上头结点,而头结点的数据域一般记录的是该链表的相关数据,如:链表长度)。

单链表由头指针唯一确定,因此单链表可以用头指针的名字来命名。

例如:若头指针为head,则可把链表称为“表head”。
在这里插入图片描述
话不多说,直接来看看链表的相关操作吧!!!

定义结点结构体

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}
class Head {
  constructor() {
    this.count = 0;
    this.next = null;
  }
}

插入

这一部分可以说是链表操作的精华部分,理解这一部分代码很关键哦!

尾插

尾插法就是在链表尾部插入新节点,那么要做的第一步当然是找到链表的尾结点了,找到后直接尾结点指针指向新节点就好啦,是不是很简单!!!

function insert(head, value) {
  const node = new Node(value);
  if (head.next == null) {//若链表为空尾结点即为头结点
    head.next = node;
  } else {
    let tem = head.next;
    while (tem.next != NULL) {//链表不为空则不断往后遍历
      tem = tem.next;
    }
    tem.next = node;
  }
}

在这里插入图片描述

头插

头插法就是在链表的头部插入新节点,而头结点的指针就是指向链表第一个节点的指针,所以将新节点指针域指向头结点原来指向的节点,再将头结点指向新节点即可。啊这,晕了?没关系,来,上图。

function head_insert(head, value) {
  const node = new Node(value);
  node.next = head.next;
  head.next = node;
}

在这里插入图片描述
如图所示,头插法需要两步完成:1.将新节点指针域指针指向图中首元结点。2.将头结点指针域指针指向新节点(完成这一步的时候图中打×的地方就断开了)。想一想这两步的顺序能否颠倒?答案是不能,至于为什么好好想一想吧!

前插

即为在指定元素前插入新节点

function pre_insert(head, value, element) {
  const node = new Node(value);
  if (head.next == null) {//如果链表为空
    head.next = node;
  } else {//链表不为空
    let pre = head, tem = head.next;
    while (tem != null) {
      if (tem.value == element) {//找到目标节点
        node.next = tem;
        pre.next = node;
        return;
      }
      tem = tem.next;
      pre = pre.next;
    }
  }
  console.log("指定元素不存在,插入失败!!!");
}

在这里插入图片描述

后插

在指定元素后插入新节点。

function back_insert(head, value, element) {
  const node = new Node(value);
  if (head.next == null) {
    head.next = node;
  } else {
    let tem = head.next;
    while (tem.next != null) {//遍历查找指定元素
      if (tem.value == element) {
        break;
      }
      tem = tem.next;
    }
    node.next = tem.next;
    tem.next = node;
    return;
  }
  console.log("指定元素不存在,插入失败!!!");
}

在这里插入图片描述
链表的插入只需要改变指针域指向的节点即可,单链表的后插要比前插简单一点。

删除

找到目标节点,使其前驱结点指向其指向的下一个节点即可。

function del(head, element) {//删除
  let tem = head.next;//临时节点
  if (head.next == null) {//链表为空
    console.log("链表为空!!!");
    return;
  } else if (head.next.data == element) {//第一个节点为目标节点
    head.next = tem.next;
    return;
  } else {//第一个节点不是目标节点
    let pre = head;//tem前驱节点
    while (tem != null) {
      if (tem.value == element) {
        pre.next = tem.next;
        return;
      }
      pre = pre.next;
      tem = tem.next;
    }
    console.log("链表中没有此元素!!!");
  }
}

在这里插入图片描述
单链表的其它操作比较简单且容易理解,具体看完整代码和注释。

分解 & 合并

将两个链表合成一个,先找到第一个链表的尾节点,将第二个链表的第一个节点作为尾节点的后续节点插入就好了。分解则是逆过程。

//合并
function combine(head1, head2) {
  let tem = head1.next;
  if (tem == null) {//head1链表为空head1直接指向head2指向的节点
    head1.next = head2.next;
    return;
  }
  while (tem.next != null) {//若head1不为空,找head1尾节点,使其指向head2的首节点
    tem = tem.next;
  }
  tem.next = head2.next;
}
//分解
function resolve(head, element) {
  if (head.next == null) {
    console.log("链表为空,分解失败!!!");
    return null;
  }
  let head1 = new Head();//为新头节点分配内存空间
  head1.next = null;
  let tem = head.next;
  while (tem != null) {//寻找目标节点
    if (tem.value == element) {//将新头节点指向目标节点指向的节点,将目标节点的指针域置为空
      head1.next = tem.next;
      tem.next = null;
      return head1;
    }
    tem = tem.next;
  }
  console.log("未找到标记点,分解失败!!!");
  return null;
}

在这里插入图片描述

其它操作

//查询
function search(head, element) {
  let number = 1;
  let tem = head.next;
  if (tem == null) {
    console.log("链表为空!!!");
    return;
  }
  while (tem != null) {//遍历
    if (tem.value == element) {
      console.log("所查找的元素为链表第" + number + "个节点!",);
      return;
    }
    number++;
    tem = tem.next;
  }
  console.log("目标元素不存在!!!");
}
//从大到小排序
function sorted(head) {
  if (head.next == null) {
    console.log("链表为空,排序失败!!!");
    return;
  }
  let tem1 = head.next;
  while (tem1 != null) {
    let tem2 = tem1.next;
    while (tem2 != null) {
      if (tem1.value < tem2.value) {
        let p = tem1.value;
        tem1.value = tem2.value;
        tem2.value = p;
      }
      tem2 = tem2.next;
    }
    tem1 = tem1.next;
  }
}
// 返回链表的长度
function length(head) {
  let len = 0;
  let tem = head.next;
  while (tem != null) {
    len++;
    tem = tem.next;
  }
  return len;
}
//输出链表
function print(head) {
  if (head.next == null) {
    console.log("链表为空,无法输出!!!");
    return;
  } else {
    let tem = head.next;
    while (tem != null) {
      console.log(tem.value);
      tem = tem.next;
    }
    return;
  }
}

双向链表(Double linked list)

单链表的每个结点再增加一个指向其前趋的指针域 pre,这样形成的链表有两条不同方向的链,称之为双向链表。

特点:

  1. 双链表一般也由头指针head唯一确定。
  2. 每一结点均有:
    数据域(value)
    左链域(pre)指向前趋结点.
    右链域(next)指向后继。
    是一种对称结构(既有前趋势,又有后继)
  3. 双链表的前插和后插难度是一样的。

缺点

指针数的增加会导致存储空间需求增加;二是添加和删除数据时需要改变更多指针的指向。

节点

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.pre = null;
  }
}

双向链表的大部分操作与单链表非常类似,只是在操作的时候改变指针稍稍不同,这里只重点说明一下变化较大的操作。

插入

插入空链表或在链表尾部插入 这种情况相对来说比较简单。
在这里插入图片描述
在链表中间插入需要移动的指针较多具体看图。
在这里插入图片描述

// 在指定位置插入
function insert(head, value, index = 0) {
	if (index >= 0 && index <= head.count) {//指定位置是否合法
		const node = new Node(value);
		let num = 0, tem = head;
		while (num != index && tem != null) {//找指定位置的前驱节点
			tem = tem.next;
			num++;
		}
		if (tem.next == null) {
			node.pre = tem;
			tem.next = node;
		} else {
			node.pre = tem;
			node.next = tem.next;
			tem.next.pre = node;
			tem.next = node;
		}
		head.count++;
		return node;
	} else {
		console.log('插入位置错误');
	}
}

查询

单链表只能从头结点往后遍历查找,但双向链表可从链表任意位置开始查找。而我给出的示例中是同时从头尾开始向中间遍历查找,这样会加快便利的速度。我这里是在求链表长度的时候记录下尾结点。

//查询 
function search(head, element, tail = null) {
	let number = 1;
	if (head.next == null) {
		console.log("链表为空!!!");
		return;
	}
	head = head.next;
	if (tail) { // 如果传入尾节点就行二分查找
		while (head != tail && tail.next != head) {
			if (head.value == element) {
				console.log("所查找的元素为链表第" + number + "个节点!");
				return head;
			} else if (tail.value == element) {
				console.log("所查找的元素为链表倒数第" + number + "个节点!");
				return tail;
			}
			number++;
			head = head.next;
			tail = tail.pre;
		}
	} else {// 从头开始遍历
		while (head != null) {
			if (head.value == element) {
				console.log("所查找的元素为链表第" + number + "个节点!");
				return head;
			}
			number++;
			head = head.next;
		}
	}
	console.log("目标元素不存在!!!");
}

在这里插入图片描述
如图:tem利用节点next指针从前往后遍历,temp利用节点pre指针从后往前遍历,遍历结束条件为:
若有奇数个节点,则结束时tem一定等于temp;节点为偶数个时,结束时tem->next一定等于temp或者temp->pre一定等于tem。

删除

双链表删除要比单链表简单一些,因为它不需要额外的寻找指定节点的前驱结点。如上图:若要删除p节点,只需将head节点next指针指向rear节点,而rear节点pre指针指向head节点,最后再释放掉p节点所占内存就完成了删除操作。

//删除
function del(head, element = undefined, node = null) {
	let tem = head.next;//临时节点
	if (head.next == null) {//链表为空
		console.log("链表为空!!!");
		return;
	}
	if (element && !node) {// 传入的是一个值
		while (tem != null) {// 循环找值
			if (tem.value == element) {
				tem.pre.next = tem.next;
				if (tem.next) {
					tem.next.pre = tem.pre;
				}
				head.count--;
				return;
			}
			tem = tem.next;
		}
	} else if (!element && node) {// 传入的是要删除的节点对象
		node.pre.next = node.next;
		if (node.next) {
			node.next.pre = node.pre;
		}
		head.count--;
		return;
	}
	console.log("链表中没有此元素!!!");
}

其它操作

//输出函数
function print(head) {
	if (head.next == null) {
		console.log("链表为空,无法输出!!!");
		return;
	} else {
		tem = head.next;
		while (tem != null) {
			console.log(tem.value);
			tem = tem.next;
		}
		return;
	}
}
//合并
function combine(head1, head2, tail = null) {
	if (tail) {// 如果传入 head1 链表的尾节点
		tail.next = head2.next;
		head2.next.pre = tail;
		head1.count += head2.count;
	} else {// 如果未传入 head1 链表的尾节点
		let tem = head1;
		while (tem.next != null) {// 循环遍历找 head1 尾节点
			tem = tem.next;
		}
		tem.next = head2.next;
		head2.next.pre = tem;
		head1.count += head2.count;
	}
}

循环链表(Circular linked list)

整个链表形成一个环,从表中任一结点出发均可找到表中其它结点。
特点:

  1. 表中最后一个结点的指针指向第一个结点或表头结点(如有表头结点的话)
  2. 循环链表的运算与单链表基本一致。但两者判断是否到表尾的条件不同: 单链表:判断某结点的链域是否为空。循环链表:判断某结点的链域值是否等于头指针。

插入 & 删除

循环链表的插入与单链表极为相似,唯独在尾插、头插、删除‘尾节点‘和删除第一个节点的时候有点区别,因为需要移动’尾节点‘的指针。如下图是’尾插‘,删除’尾节点‘是逆过程。

其实本质上循环链表并没有严格意义上的尾节点,因为该链表就相当于一个环,所以‘尾插’就是严格意义上的在链表中间插入。
在这里插入图片描述
删除头结点如下图所示,头插是逆过程。
在这里插入图片描述

// 指定位置插入
function insert(head, value, index = 0) {
  if (index >= 0 && index <= head.count) {//指定位置是否合法
    const node = new Node(value);
    let num = 0;
    if (index === 0) {//处理头插
      if (head.next == null) {// 链表为空的情况
        head.next = node;
        node.next = node;
      } else {// 链表不为空的情况
        let tem = head.next;
        node.next = head.next;
        while (tem.next != head.next) {// 找到尾节点
          tem = tem.next;
        }
        head.next = node;
        tem.next = node;
      }
    } else {//处理尾插和在链表中间插入
      let tem = head;
      while (num != index && tem != null) {//找指定位置的前驱节点
        tem = tem.next;
        num++;
      }
      node.next = tem.next;
      tem.next = node;
    }
    head.count++;
    return node;
  } else {
    console.log('插入位置错误');
  }
}
// 删除某一元素
function del(head, element = undefined) {
  let tem = head.next;//临时节点
  if (head.next == null) {//链表为空
    console.log("链表为空!!!");
    return;
  }
  if (head.count === 1) {// 处理链表只有一个节点的情乱搞
    if (head.next.value === element) {
      head.next = null;
      return;
    }
  } else {
    while (tem.next != head.next) {// 循环找目标节点的前驱
      if (tem.next.value === element) {
        tem.next = tem.next.next;
        while (tem.next != head.next) {// 循环找尾节点
          tem = tem.next;
        }
        tem.next = head.next;// 使尾节点指针指向新的第一个节点
        head.count--;
        return;
      }
      tem = tem.next;
    }
  }
  console.log("链表中没有此元素!!!");
}

合并

两循环链表的合并只需要将第一个链表的尾节点指向第二个链表的头节点,第二个链表的尾节点指向第一个链表的头节点即可。其实就是交换两链表的尾节点的指针值。如图所示:
在这里插入图片描述

// 合并链表
function combin(head, head1) {
  let tem = head.next, tem1 = head1.next;;//临时节点
  while (tem.next && tem.next != head.next) {// 找链表1的尾节点
    tem = tem.next;
  }
  while (tem1.next && tem1.next != head1.next) {// 找链表2的尾节点
    tem1 = tem1.next;
  }
  // 交换两尾节点的指针值
  const temp = tem.next;
  tem.next = tem1.next;
  tem1.next = temp;
}

循环链表的其它操作与单链表和双向链表极为相似,就不在赘述其操作了。

链表的内容基本上就这么多了,希望能够就对你有所帮助。欢迎各位小伙伴在下方留言区留言评论或提问!我是孤城浪人,一名正在前端路上摸爬滚打的菜鸟,欢迎你的关注。

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

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

相关文章

Redis-Linux中安装Redis、命令操作Redis

目录 一、Redis简介 NoSQL与SQL的区别 二、Linux上安装redis 上传并解压redis.gz 进入 redis的解压目录&#xff0c;执行命令 make ​编辑 修改redis为守护进程 们测试一下能否远程连接RedisDesktopManager客户端 开放6379端口 授权&#xff0c;允许远程连接 三、redis命…

小程序上新(2022.10.13~11.14)

20221101 【官方公告】境外主体小程序补充信息存储地区通知20221103 小程序基础库 2.27.1 更新 新增 框架 新增 xr-fame 能力&#xff0c;kanata 更新 详情新增 组件 map 组件新增 bindrendersuccess 属性 详情 (官方文档还查不到这个)新增 API 新增 wx.getRendererUserAgen…

tep时隔8个月迎来重大全新升级

tep此次更新&#xff0c;旨在从“工具”升级为“框架”&#xff0c;为此做了大量的代码整洁工作&#xff0c;重新设计了部分功能&#xff0c;项目脚手架也焕然一新。 功能展示 conftest.py 脚手架生成的conftest.py只有一行代码&#xff1a; fixture自动加载等操作都隐藏到了te…

【学习笔记22】JavaScript数组的练习题

笔记首发 一、已知一个排序好的数组 将数字按照原有顺序插入到数组内 var arr [10, 20, 30, 40, 50];var n 11;// 1. 将n插入数组中arr.push(n);// 2. 冒泡排序for (var k 0; k < arr.length - 1; k) {for (var i 0; i < arr.length - 1 - k; i) {if (arr[i] > …

antd——使用a-tree组件实现 检索+自动展开+自定义增删改查功能——技能提升

之前写后台管理系统时&#xff0c;遇到一个下面的需求&#xff0c;下面是最终完成的效果图。 实现的功能有&#xff1a; 1. 下拉 选择不同的类型——就是一个普通的select组件&#xff0c;下面并不做介绍 2. 通过关键字可以进行tree树形结构的筛选&#xff0c;然后将筛选后的…

数据结构学习笔记——查找算法

目录前言一、查找的相关概念&#xff08;一&#xff09;内查找和外查找&#xff08;二&#xff09;静态查找和动态查找&#xff08;三&#xff09;平均查找长度二、线性查找&#xff08;一&#xff09;顺序查找1、查找思想2、算法分析3、有序表的顺序查找&#xff08;二&#x…

gitlab-runner 的安装使用(含 .gitlab-ci.yml 的简单使用)

简介 GitLab Runner 是一个开源项目&#xff0c;用于运行您的作业并将结果发送回 GitLab。它与 GitLab CI 一起使用&#xff0c;GitLab CI 是 GitLab 随附的开源持续集成服务&#xff0c;用于协调作业。 简单理解就是一个服务放在那儿&#xff0c;当你提交代码时&#xff0c;…

[附源码]java毕业设计在线课程网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

腾讯安全SOC+能力图谱正式发布,助力政企构建闭环安全运营体系

随着云计算、人工智能、5G等新兴技术的融合发展&#xff0c;数字化转型正成为企业数字经济时代的重要发展路径。然而&#xff0c;数字化转型过程中IT架构的重塑、安全产品体系化能力不足带来的安全运营挑战&#xff0c;使得企业在业务突破上面临安全瓶颈。 面对网络安全事件频…

做一个微信小程序需要多少钱?

做一个微信小程序需要多少钱&#xff1f; 如果是选择套用小程序模板&#xff0c;自建的方式的话&#xff0c;价格是在698-3498元一年的。 如果是代建小程序的话&#xff0c;需要在自建的费用上&#xff0c;再加上1500-12000元的代建费用。 下面主要给大家讲讲698-3498元这个…

Spring依赖注入源码解析(上)

文章目录前言一、Spring中到底有几种依赖注入的方式&#xff1f;1、手动注入1.1、set方法进行注入1.2、通过构造方法进行注入2、自动注入2.1、XML的autowire自动注入2.2、Autowired注解的自动注入二、autowireByName && autowireByType核心源码分析2.1、autowireByName…

最新最全的JavaScript入门视频,包含小程序和uniapp相关的JavaScript知识学习

写在前面 我们学习JavaScript不仅可以用于web网站开发&#xff0c;也可以用于小程序&#xff0c;uniapp项目的开发&#xff0c;所以我们学习JavaScript很重要。 准备工作 我们这里学习JavaScript用的是小程序开发者工具或者uniapp开发者工具&#xff0c;所以需要你先装好这两…

基于Servlet+jsp+mysql开发javaWeb学生管理系统(学生信息、学生选课、学生成绩、学生签到考勤)

你知道的越多&#xff0c;你不知道的越多 点赞再看&#xff0c;养成习惯 如果您有疑问或者见解&#xff0c;或者没有积分想获取项目&#xff0c;欢迎指教&#xff1a; 企鹅&#xff1a;869192208 文章目录一、开发背景二、 需求分析三、开发环境四、运行效果五、开发流程工程目…

【web前端开发】HTML知识点超详细总结

文章目录什么是网页常用的浏览器及内核VScode和WebStrom使用HTML常用标签文档类型<!DOCTYPE>网页语言lang字符集title标签标题标签段落和换行标签文本格式化标签div和span标签图像标签路径相对路径同一级路径上一级路径:下一级路径绝对路径链接标签超链接标签外部链接:内…

为什么心脏长在左边?

人体各项生命活动的正常维持&#xff0c;都离不开血液循环系统输送营养和代谢废物&#xff0c;而给全身输送血液的动力器官就是心脏。可以说&#xff0c;心脏是人体的发动机。不过&#xff0c;你有没有思考过&#xff0c;为什么心脏会长在我们身体的左边呢&#xff1f; 为了解释…

css ppt操作面板 预览时其中标签定位问题

最近用网页写了一个类似PPT页面板操作功能&#xff0c;就是把文本框和图片放入操作面板后&#xff0c;手动拖动到自定义位置&#xff0c;并可以控制文本框和图片大小&#xff0c;但是在预览时位置怎么都放不对&#xff0c;可能跟我css知识不扎实有关&#xff0c;两天没解决&…

从一道题到贪心入门

今天,我们将从一道题引入贪心算法的认识. 题目 题目描述 又是一年秋季时&#xff0c;陶陶家的苹果树结了 n 个果子。陶陶又跑去摘苹果&#xff0c;这次他有一个 a 公分的椅子。当他手够不着时&#xff0c;他会站到椅子上再试试。 这次与 NOIp2005 普及组第一题不同的是&#x…

搭建ESP8266开发环境

获取工具 安信可一体化开发环境 Source insight (本菜鸟不太喜欢用Eclipse) 安装 安装ESP8266开发环境(Windows) 1)双击运行解压到文件 2)安装方式一:eclipse 双击运行ConfigTool.exe点击 Default 可以检测当前所在路径&#

图神经网络关系抽取论文阅读笔记(三)

1 用于关系提取的注意引导图卷积网络&#xff08;Attention Guided Graph Convolutional Networks for Relation Extraction&#xff0c;2020&#xff09; 论文&#xff1a;Attention Guided Graph Convolutional Networks for Relation Extraction&#xff0c;2020 1.1 引言 依…

笔试强训第一天

选择题&#xff1a; 题目1&#xff1a; 我们对这道题目进行分析&#xff1a;第一个打印的是computer没有什么问题&#xff0c;但是第二个%5.3s我们需要进行分析&#xff1a; %m.ns m表示输出字符串的宽度&#xff0c;这里输出字符串的宽度为5 n表示左起截取目标字符串的n个…