【数据结构】(6) LinkedList 链表

一、什么是链表

1、链表与顺序表对比

不同点LinkedListArrayList
物理存储上不连续连续
随机访问效率O(N)O(1)
插入、删除效率O(1)O(N)

3、链表的分类

        链表根据结构分类,可分为单向/双向无头结点/有头节点非循环/循环链表,这三组每组各取一个就构成一种结构链表。其中,单向、不带头、非循环链表学习的重点双向、不带头、非循环链表在实际开发中常用。

        以单向、不带头、非循环链表为例:

        双向、不带头、非循环链表:

        单向、带头、非循环链表(好处在于,不用特别处理头节点,比如增删时):

        单向、不带头、循环链表:

二、单向、不带头、非循环链表的实现

1、IList 接口

        定义 IList 接口,声明线性表需要实现的方法:

package listinterface;public interface IList {//头插法void addFirst(int data);//尾插法void addLast(int data);//任意位置插入,第一个数据节点为0号下标void addIndex(int index,int data);//查找是否包含关键字key是否在单链表当中boolean contains(int key);//删除第一次出现关键字为key的节点void remove(int key);//删除所有值为key的节点void removeAllKey(int key);//得到单链表的长度int size();void clear();void display();
}

2、结点内部类

        结点仅由链表内部使用,且每个结点的产生不依赖于某个链表,可定义为链表的私有静态内部类:

public class MySingleList implements IList {// 结点内部类private static class Node {int data;Node next;public Node(int data) {this.data = data;}}// 头结点private Node head;// 链表长度private int size;............
}

3、打印链表

    @Overridepublic void display() {Node cur = head;while (cur != null) {System.out.print(cur.data + " ");cur = cur.next;}System.out.println(" size: " + size);}

4、清空链表、获取链表长度

    @Overridepublic int size() {return size;}@Overridepublic void clear() {head = null;size = 0;}

        head 为空,没有引用指向 node1,自动回收 node1;node1 回收,没有引用指向 node2,自动回收 node2……故 clear 只需 head 设为 null 即可。

5、头插法

    @Overridepublic void addFirst(int data) {Node newNode = new Node(data);newNode.next = head;head = newNode;
//        if (head != null) {
//            newNode.next = head;
//        }
//        head = newNode;size++;}

6、尾插法

    @Overridepublic void addLast(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;size++;return;}Node cur = head;while (cur.next != null) { // 找到尾节点cur = cur.next;}cur.next = newNode;size++;}

7、任意位置插入

受查异常,父类方法没抛出异常,子类方法不能抛出异常;父类抛出了,子类不做要求。非受查异常,不做要求。)为了不改动 IList,且让 addIndex 抛出异常,以便在 main 处对异常做处理,链表下标越界异常选择继承非受查异常 RuntimeExcption。

package myexception;public class ListIndexOutOfBoundsException extends RuntimeException {public ListIndexOutOfBoundsException(String message) {super(message);}
}
    private void checkIndexOutOfBounds(int index) throws ListIndexOutOfBoundsException {if (index < 0 || index > size) {throw new ListIndexOutOfBoundsException("插入位置不合法。" + "Index: " + index + ", Size: " + size);}}private Node findIndex(int index) {Node cur = head;int cnt = 0;while (cnt != index) {cur = cur.next;cnt++;}return cur;}@Overridepublic void addIndex(int index, int data) throws ListIndexOutOfBoundsException {// 先检查索引是否越界checkIndexOutOfBounds(index);if (index == 0) { // 在 0 处插入,没有前驱节点addFirst(data);return;}if (index == size) { // 在末尾插入addLast(data);return;}Node newNode = new Node(data);// 找到插入位置前的节点Node pre = findIndex(index - 1);// 插入新节点newNode.next = pre.next;pre.next = newNode;size++;}

8、删除第一次出现关键字为key的节点

    private Node findKey(int key) {Node cur = head;while (cur.next != null) {if (cur.next.data == key) {return cur;}cur = cur.next;}return null;}@Overridepublic void remove(int key) {if (head == null) { // 空链表return;}if (head.data == key) { // 头节点就是要删除的节点head = head.next;size--;return;}Node pre = findKey(key);if (pre != null) { // 找到了要删除的节点pre.next = pre.next.next;size--;} else { // 要删除的节点不存在System.out.println("要删除的节点不存在。");}}

9、删除所有关键字为 key 的节点

    @Overridepublic void removeAllKey(int key) {if (head == null) { // 空链表return;}Node pre = head;Node cur = head.next;while (cur != null) {if(cur.data == key) {pre.next = cur.next;// cur = cur.next;}else {pre = cur;// cur = cur.next;}cur = cur.next;}if (head.data == key) {head = head.next;size--;}}

三、链表面试题练习

1、反转链表

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

思路:从头开始,边遍历边反转,最后尾结点为 head。

class Solution {public ListNode reverseList(ListNode head) {// 特殊处理 空链表、只有一个结点的链表if(head == null || head.next == null) { // 两者顺序不能反,因为 null 没有nextreturn head;}ListNode cur = head.next;// 头结点,其 next 为 null head.next = null;// 边反转边遍历,直到反转完尾结点while(cur != null) {ListNode curN = cur.next; cur.next = head;head = cur;cur = curN;}return head;}
}

2、链表的中间结点

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

思路

1、计算链表长度的一半,再走一半长。时间复杂度 O(N) + O(N/2)。

2、设置 slow 和 fast 从 head 开始走,slow 每次走 1 步,fast 每次走 2 步。那么 fast 走完全程(fast = null 或者 fast.next = null),必是 slow 的两倍长度,故 slow 就是中间节点的位置。时间复杂度 O(N/2) ,更优。

class Solution {public ListNode middleNode(ListNode head) { // 根据题目,链表非空// 从头节点开始ListNode slow = head;ListNode fast = head;// fast 走完整个链表while(fast != null && fast.next != null) {// slow 每次走 1 步,fast 每次走 2 步slow = slow.next;fast = fast.next.next;}// slow 就是中间结点return slow;}
}

3、返回倒数第 k 个结点

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

思路:slow 从 head 开始,fast 比 slow 先多走 k-1 步,然后 slow、fast 每次只走一步,直到 fast 走到尾结点。

class Solution {public int kthToLast(ListNode head, int k) {// 根据题目,k 保证有效,因此不用判断 k<=0 和 k > len 的情况ListNode slow = head;ListNode fast = head;// 当 fast 先走 k-1 步int cnt = k-1;// k保证有效,fast 不会走过头while(cnt != 0) { fast = fast.next;cnt--;}// 让 fast 走到尾结点,slow 就是倒数第 k 个结点while(fast.next != null) {slow = slow.next;fast = fast.next;}return slow.val;}
}

 4、合并两个有序列表

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

思路:分别同时遍历两个链表,更小的插入新链表,插入的链表插入后更新为 next,直到 l1 或者 l2 遍历完了。没遍历完的那个链表,把剩的接在新链表尾。为了不单独处理头指针,新链表带头节点。

class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {ListNode head = new ListNode(); // 创建头节点ListNode cur = head;ListNode cur1 = list1;ListNode cur2 = list2;// 其中一个遍历完就退出while(cur1 != null && cur2 != null) {if(cur1.val < cur2.val) {cur.next = cur1;// cur = cur.next;cur1 = cur1.next;}else {cur.next = cur2;// cur = cur.next;cur2 = cur2.next;}cur = cur.next;}// 没遍历完的,接在后面if(cur1 != null) {cur.next = cur1;}if(cur2 != null) {cur.next = cur2;}return head.next; // 不带头节点}
}

5、链表分割

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

思路:先创建两个链表,依次遍历原链表,小于和大于等于 x 的分开尾插入两个链表,再合并。

public class Partition {public ListNode partition(ListNode pHead, int x) {// 创建两个头节点ListNode head1 = new ListNode(-1); ListNode head2 = new ListNode(-1);// 遍历原链表,每个结点与 x 比大小,分类放在两个新链表中ListNode cur1 = head1;ListNode cur2 = head2; while(pHead != null) {if(pHead.val < x) {cur1.next = pHead;cur1 = cur1.next;}else {cur2.next = pHead;cur2 = cur2.next;}pHead = pHead.next;}// 现在 cur1、cur2 分别指向两个链表的尾结点。cur1.next = head2.next; // 链表1的尾接上链表2的头cur2.next = null; // 链表2的尾接上 null// 如果最后链表1为空,cur1就是head1,没问题// 如果最后链表2为空,cur2就是head2,没问题return head1.next;}
}

6、链表的回文结构

链表的回文结构_牛客题霸_牛客网

思路

1、求尾结点,头、尾同时前进并比较,到 head = tail(奇数长度) 或者 head.next = tail (偶数长度)为止。tail 往前走行不通,因为是单链表。

2、把原链表的后一半反转,再将反转链表与原链表对比,直到反转链表遍历完。时间复杂度O(N/2)+O(N/2)+O(N/2)=O(N)。找到一半处、将一半反转、对比一半。

public class PalindromeList {private ListNode findHalf(ListNode head) {ListNode slow = head;ListNode fast = head;while(fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}private ListNode reverse(ListNode head) {if(head == null || head.next == null) {return head;}ListNode cur = head.next;head.next = null;while(cur != null) {ListNode curN = cur.next;cur.next = head;head = cur;cur = curN;}return head;}public boolean chkPalindrome(ListNode A) {// 找到中间结点ListNode halfNode = findHalf(A);// 将后一半反转ListNode B = reverse(halfNode);// 将后半段与前半段对比,后半段遍历完退出while(B != null) {if(A.val != B.val) {return false;}A = A.next;B = B.next;}return true;}
}

7、相交链表

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

思路:可能分叉的地方,前边或后边。但是对于链表不可能后边分叉,因为如果后边分叉了,当你遍历一条单链时,遍历到后边分叉的地方,就不知道继续走哪条路了,这样就不是单链表了。因此只有可能如上图的Y型,或者在一条直线上。

        如果同时各自出发,每次走一步,直到有一方到达尾结点,那么短的链表总是先到达,并且它们的最终距离是两链表的长度差。长度差来自分叉的部分,而不是相交的部分。那么如果让长的先走长度差步,再同时走,它们碰面的地方将是相交处。

public class Solution {private int size(ListNode head) {ListNode cur = head;int cnt = 0;while(cur != null) {cnt++;cur = cur.next;}return cnt;}public ListNode getIntersectionNode(ListNode headA, ListNode headB) {// 求两条链的长度int size1 = size(headA);int size2 = size(headB);// 让长的作为 headA,求两链表长度差(正),int len = size1 - size2;if(size1 < size2) {len = size2 - size1;ListNode tmp = headA;headA = headB;headB = tmp;}// 让长的先走 len 步while(len != 0) {headA = headA.next;len--;}// 再同时走,相等的地方就是相交结点while(headA != headB) {headA = headA.next;headB = headB.next;}return headA;}
}

8、环形链表

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

思路:慢指针和快指针同时走,如果有环,那么快、慢指针总会在环里相遇(快指针多跑 k 圈,然后追上慢指针);如果没环,快指针先走完全程结束。我们设慢指针每次走1步,快指针每次走2步。

        为什么快指针不能是 3、4、……、n 步?如果是3步,存在以下情况,无论走多久都不会相遇:

        快指针如果走 4 步,存在以下情况,无论走多久都不会相遇:

        以此类推……

public class Solution {public boolean hasCycle(ListNode head) {// 设置快、慢指针ListNode slow = head;ListNode fast = head;// slow 每次走1步,fast 每次走2步// 直到 fast = null(偶数个) 或者 fast.next = null(奇数个) 返回 flase// 或者 slow = fast 返回 truewhile(fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if(slow == fast) {return true;}}return false;}
}

9、环形链表Ⅱ

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

思路

        若一指针 head1 从相遇点开始,一指针 head2 从头指针开始,同时走,每次走一步。当 head1 走 X 步到达入口;同时,head2 从相遇点开始走了 (k-1) 圈回到相遇点,再走 N 步到入口。即两指针相遇处,就是入口结点。

public class Solution {private ListNode getMeetNode(ListNode head) {ListNode slow = head;ListNode fast = head;while(fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if(slow == fast) {return slow;}}return null;}public ListNode detectCycle(ListNode head) {// 获得 slow 与 fast 的相遇结点ListNode meetNode = getMeetNode(head);// 链表无环,返回 nullif(meetNode == null) {return null;}// 分别从头指针,相遇结点开始走,两指针相遇处就是入口while(head != meetNode) {head = head.next;meetNode = meetNode.next;}return head;}
}

四、双向、不带头、非循环链表的实现

1、Node 内部类和属性

public class MyLinkedList  implements IList {private static class Node {int val;Node next; // 后继指针Node prev; // 前驱指针public Node(int val) {this.val = val;}}private Node head; // 头指针private Node tail; // 尾指针private int size; // 链表大小............
}

2、清空链表

        与单向不同直接 head = null,双向需要遍历结点并释放 node,原因如下:head 为空,还有 node2 指向 node1,所以手动置 node2 的 pre 为空,node1释放;还有 node3 指向 node2,手动置 node3 的 pre 未空……

    @Overridepublic void clear() {Node cur = head;while (cur!= null) {Node curN = cur.next;cur.prev = null;cur.next = null;cur = curN;}head = null;tail = null;size = 0;}

3、头插法

    @Overridepublic void addFirst(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;tail = newNode;} else {newNode.next = head;head.prev = newNode;head = newNode;}size++;}

4、尾插法

    @Overridepublic void addLast(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;tail = newNode;} else {tail.next = newNode;newNode.prev = tail;tail = newNode;}size++;}

5、任意位置插入

    @Overridepublic void addIndex(int index, int data) {// 先检查索引是否越界checkIndexOutOfBounds(index);if (index == 0) { // 在 0 处插入,没有前驱节点addFirst(data);return;}if (index == size) { // 在末尾插入addLast(data);return;}Node newNode = new Node(data);// 找到插入位置Node cur = findIndex(index);// 插入新节点newNode.next = cur;newNode.prev = cur.prev;cur.prev.next = newNode;cur.prev = newNode;size++;}

6、删除第一次出现的 key

    private Node findKey(int key) {Node cur = head;while (cur != null) {if (cur.val == key) {return cur;}cur = cur.next;}return null;}@Overridepublic void remove(int key) {Node deleteNode = findKey(key); // 找到待删除节点,如果不存在,返回 nullif (deleteNode == null) { // 包含空链表的情况System.out.println("要删除的节点不存在。");return;}if (deleteNode == head) { // 待删除节点是头节点,包含了链表只有一个结点的情况head = deleteNode.next;if (head == null) { // 链表只有一个节点tail = null;} else {head.prev = null;}} else if (deleteNode == tail) { // 待删除节点是尾节点tail = deleteNode.prev;tail.next = null;} else { // 待删除节点是中间节点deleteNode.prev.next = deleteNode.next;deleteNode.next.prev = deleteNode.prev;}size--;}

7、删除所有 key

    @Overridepublic void removeAllKey(int key) {Node cur = head;while (cur != null) {if (cur.val == key) {if (cur == head) { // 待删除节点是头节点head = cur.next;if (head == null) { // 链表中只有一个节点tail = null;}else {head.prev = null;}} else if (cur == tail) { // 待删除节点是尾节点tail = cur.prev;tail.next = null;} else { // 待删除节点是中间节点cur.prev.next = cur.next;cur.next.prev = cur.prev;}size--;
//                cur = cur.next; // 跳过已删除节点,继续遍历} /*else {cur = cur.next;}*/cur = cur.next;}}

五、LinkedList 的使用

        集合类中,LinkedList 的底层是双向链表

1、常用方法的使用

2、迭代器

        Iterator<E> 是集合类通用的迭代器,线性表专用的迭代器 ListIterator<E> 功能更强,可以反向迭代:

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

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

相关文章

使用Pygame制作“俄罗斯方块”游戏

1. 前言 俄罗斯方块&#xff08;Tetris&#xff09; 是一款由方块下落、行消除等核心规则构成的经典益智游戏&#xff1a; 每次从屏幕顶部出现一个随机的方块&#xff08;由若干小方格组成&#xff09;&#xff0c;玩家可以左右移动或旋转该方块&#xff0c;让它合适地堆叠在…

(苍穹外卖)项目结构

苍穹外卖项目结构 后端工程基于 maven 进行项目构建&#xff0c;并且进行分模块开发。 1). 用 IDEA 打开初始工程&#xff0c;了解项目的整体结构&#xff1a; 对工程的每个模块作用说明&#xff1a; 序号名称说明1sky-take-outmaven父工程&#xff0c;统一管理依赖版本&…

【漫画机器学习】082.岭回归(或脊回归)中的α值(alpha in ridge regression)

岭回归&#xff08;Ridge Regression&#xff09;中的 α 值 岭回归&#xff08;Ridge Regression&#xff09;是一种 带有 L2​ 正则化 的线性回归方法&#xff0c;用于处理多重共线性&#xff08;Multicollinearity&#xff09;问题&#xff0c;提高模型的泛化能力。其中&am…

websocket自动重连封装

websocket自动重连封装 前端代码封装 import { ref, onUnmounted } from vue;interface WebSocketOptions {url: string;protocols?: string | string[];reconnectTimeout?: number; }class WebSocketService {private ws: WebSocket | null null;private callbacks: { [k…

【华为OD-E卷 - 115 数组组成的最小数字 100分(python、java、c++、js、c)】

【华为OD-E卷 - 数组组成的最小数字 100分&#xff08;python、java、c、js、c&#xff09;】 题目 给定一个整型数组&#xff0c;请从该数组中选择3个元素组成最小数字并输出 &#xff08;如果数组长度小于3&#xff0c;则选择数组中所有元素来组成最小数字&#xff09; 输…

在Vue3 + Vite 项目中使用 Tailwind CSS 4.0

文章目录 首先是我的package.json根据官网步骤VS Code安装插件验证是否引入成功参考资料 首先是我的package.json {"name": "aplumweb","private": true,"version": "0.0.0","type": "module","s…

AURIX TC275学习笔记4 官方GTM例程 GTM_TOM_PWM_1

文章目录 概述其他例程 代码分析IfxGtm_enable()IfxGtm_Cmu_enableClocks&#xff08;&#xff09;IfxGtm_Tom_Pwm_initConfig&#xff08;&#xff09;IfxGtm_Tom_Pwm_init&#xff08;&#xff09;IfxGtm_Tom_Pwm_start&#xff08;&#xff09;fadeLED() 概述 目的&#xf…

ASP.NET Core中间件的概念及基本使用

什么是中间件 中间件是ASP.NET Core的核心组件&#xff0c;MVC框架、响应缓存、身份验证、CORS、Swagger等都是内置中间件。 广义上来讲&#xff1a;Tomcat、WebLogic、Redis、IIS&#xff1b;狭义上来讲&#xff0c;ASP.NET Core中的中间件指ASP.NET Core中的一个组件。中间件…

app专项测试(网络测试流程)

一、网络测试的一般流程 step1&#xff1a;首先要考虑网络正常的情况 ① 各个模块的功能正常可用 ② 页面元素/数据显示正常 step2&#xff1a;其次要考虑无网络的情况 ① APP各个功能在无网络情况下是否可用 ② APP各个页面之间切换是否正常 ③ 发送网络请求时是…

Vue el-input密码输入框 按住显示密码,松开显示*;阻止浏览器密码回填,自写密码输入框;校验输入非汉字内容;文本框聚焦到内容末尾;

输入框功能集合 <template><div style"padding: 10px"><!-- 密码输入框 --><el-input:type"inputType"v-model"password"placeholder"请输入密码"auto-complete"new-password"id"pwd"style…

【数据结构】_复杂度

目录 1. 算法效率 2. 时间复杂度 2.1 时间复杂度概念 2.2 准确的时间复杂度函数式 2.3 大O渐进表示法 2.4 时间复杂度的常见量级 2.5 时间复杂度示例 3. 空间复杂度 3.1 空间复杂度概念 3.2 空间复杂度示例 1. 算法效率 一般情况下&#xff0c;衡量一个算法的好坏是…

Day48_20250130【回校继续打卡】_单调栈part1_739.每日温度|496.下一个更大元素I|503.下一个更大元素II

Day48_20250130_单调栈part1_739.每日温度|496.下一个更大元素I|503.下一个更大元素II 20250130补完 739.每日温度 题目 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0…

ASP.NET Core中间件Markdown转换器

目录 需求 文本编码检测 Markdown→HTML 注意 实现 需求 Markdown是一种文本格式&#xff1b;不被浏览器支持&#xff1b;编写一个在服务器端把Markdown转换为HTML的中间件。我们开发的中间件是构建在ASP.NET Core内置的StaticFiles中间件之上&#xff0c;并且在它之前运…

Text2Sql:开启自然语言与数据库交互新时代(3030)

一、Text2Sql 简介 在当今数字化时代&#xff0c;数据处理和分析的需求日益增长。对于众多非技术专业人员而言&#xff0c;数据库操作的复杂性常常成为他们获取所需信息的障碍。而 Text2Sql 技术的出现&#xff0c;为这一问题提供了有效的解决方案。 Text2Sql&#xff0c;即文…

将Deepseek接入pycharm 进行AI编程

目录 专栏导读1、进入Deepseek开放平台创建 API key 2、调用 API代码 3、成功4、补充说明多轮对话 总结 专栏导读 &#x1f338; 欢迎来到Python办公自动化专栏—Python处理办公问题&#xff0c;解放您的双手 &#x1f3f3;️‍&#x1f308; 博客主页&#xff1a;请点击——…

【人工智能】使用Python实现图像风格迁移:理论、算法与实践

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 图像风格迁移是一种利用深度学习技术将一张图像的内容与另一张图像的风格相结合的技术。本文将深入探讨图像风格迁移的基本理论和实现方法,…

ASP.NET Core筛选器Filter

目录 什么是Filter&#xff1f; Exception Filter 实现 注意 ActionFilter 注意 案例&#xff1a;自动启用事务的筛选器 事务的使用 TransactionScopeFilter的使用 什么是Filter&#xff1f; 切面编程机制&#xff0c;在ASP.NET Core特定的位置执行我们自定义的代码。…

缓存类为啥使用 unordered_map 而不是 map

性能考虑&#xff1a; std::unordered_map 是基于哈希表实现的&#xff0c;而 std::map 是基于红黑树实现的。对于查找操作&#xff0c;std::unordered_map 的平均查找时间复杂度是 O ( 1 ) O(1) O(1)&#xff0c;而 std::map 的查找时间复杂度是 O ( l o g n ) O(log n) O(l…

113,【5】 功防世界 web unseping

进入靶场 代码审计 <?php // 高亮显示当前 PHP 文件的源代码&#xff0c;方便开发者查看代码结构和内容 highlight_file(__FILE__);// 定义一个名为 ease 的类 class ease {// 私有属性 $method&#xff0c;用于存储要调用的方法名private $method;// 私有属性 $args&…

Android记事本App设计开发项目实战教程2025最新版Android Studio

平时上课录了个视频&#xff0c;从新建工程到打包Apk&#xff0c;从头做到尾&#xff0c;没有遗漏任何实现细节&#xff0c;欢迎学过Android基础的同学参加&#xff0c;如果你做过其他终端软件开发&#xff0c;也可以学习&#xff0c;快速上手Android基础开发。 Android记事本课…