当前位置: 首页 > news >正文

【数据结构篇】手写双向链表、单向链表(超详细)

文章目录

  • 链表
    • 1、基本介绍
    • 2、单向链表
      • 2.1 带头节点的单向链表
        • 测试类
        • 链表实现类:
      • 2.2 不带头节点的单向链表
      • 2.3 练习
        • 测试类:
        • 链表实现类:
    • 3、双向链表
        • 测试类:
        • 双向链表实现类:
    • 4、单向环形链表
        • **测试类**:
        • **实现类**:

链表

1、基本介绍

  • 什么是链表

    链表(Linked List)是用链式存储结构实现的线性表。链表示意图:

    image-20220814112509739

  • 链表的组成数据域+引用域(数据域和引用域合称结点或元素)

    • 数据域存放数据元素自身的数据
    • 引用域存放相邻结点的地址
  • 链表的特点

    • 链表中元素的联系依靠引用域

    • 具有线性结构的特点,链表所使用的逻辑结构是线性结构

    • 具有链式存储结构的特点,所使用的物理存储结构是链式存储

  • 链表的分类

    • 单向链表:单链表是一种最简的链表,只有一个引用域1next

      特点:通过next可以访问到后继结点,终端结点的引用域指向null

    • 双向链表:具有两个引用域prevnext,prev用来保存前驱结点的地址,next用来保存后继结点的地址

      特点:通过next可以访问后继结点,终端结点的next指向null;通过prev可以访问到前驱节点,起始结点的prev指向null

    • 循环链表:循环链表本质是一种特殊的单向链表,只是它的终端结点指向了开始结点(也就是next存放了开始结点的地址)

      特点:所有结点都能具有前驱节点和后继结点

  • 链表的使用场景:对查找速度要求不高,但对插入和删除速度要求高时,可以使用链表。常见的比如:

2、单向链表

单向链表(简称单链表)有带头结点的单链表,也有不带头链表的单链表。

image-20220814212948169

image-20220814213046211

  • 单链表的基本操作

    • 非空判断:判断链表中是否含有元素
    • 求表长度:获取链表中所有元素的个数
    • 插入结点:在单链表中添加一个新的结点
    • 删除结点:删除单链表中的结点
    • 取表元素:更具所给索引,确定该索引所在链表的结点
    • 定位元素:根据所给值,确定该元素所在链表的索引号
    • 修改元素:根据所给索引,修改对应的结点
    • 清空链表:清空链表中所有的元素

2.1 带头节点的单向链表

带头结点就是先固定一个头节点,用来标识链表的初始位置,它的data域不存任何东西,它的next域用来第一个结点的地址,每次遍历链表或定位结点都需要借助一个辅助变量temp来实现。

  • 插入结点示意图:

  • 删除结点示意图:

  • 修改结点示意图:

遍历经验总结当我们想要进行的操作的结点依赖于前一个结点时,比如插入删除修改等操作操作,就必须从head结点开始遍历,否则会出现空指针异常;当我们想要进行的操作不依赖前一个结点时,就无须从head结点开始遍历,比如根据id获取结点非空判断获取链表长度展示链表等操作。

测试类

package com.hhxy.linkedlist;import java.util.Scanner;import com.hhxy.queue.ArrayQueue2;
/*** 单向链表测试类* @author ghp* 测试数据:* 1 宋江 及时雨* 2 林冲 豹子头* 3 鲁智深 花和尚* 4 吴用 智多星*/
public class SingleLinkedListTest {public static void main(String[] args) {Scanner sc = new Scanner(System.in);SingleLinkedListDemo1 sll = new SingleLinkedListDemo1();//创建链表OUT:while(true) {System.out.println("-------------------单向链表操作界面-----------------");System.out.println("请输入操作指令:");System.out.println("0 : 退出程序");System.out.println("1 : 在链尾添加结点");System.out.println("2 : 按id从小到大的顺序添加结点");System.out.println("3 : 根据id获取结点");System.out.println("4 : 根据id删除结点");System.out.println("5 : 获取链表中元素的个数");System.out.println("6 : 展示链表中所有的元素");System.out.println("7 : 根据id修改结点");System.out.println("8 : 清空链表");//用于接收用户输入int id;String name="";String alias="";Student student = null;switch(sc.next()) {case "0": //退出程序System.out.println("正在退出程序~~~");break OUT;case "1": //在链尾添加结点System.out.println("请按照 id name alias 的格式输入要添加的元素:");id = sc.nextInt();name = sc.next();alias = sc.next();student = new Student(id,name,alias);if(sll.add(student)) System.out.println("结点:"+student+"添加成功!");break;case "2"://按id从小到大的顺序添加结点System.out.println("请按照 id name alias 的格式输入要添加的元素:");id = sc.nextInt();name = sc.next();alias = sc.next();student = new Student(id,name,alias);if(sll.addById(student)) System.out.println("结点:"+student+"添加成功!");break;case "3"://根据id获取结点System.out.println("请输入要获取结点的id号:");id = sc.nextInt();try {student  = sll.get(id);System.out.println(id+"号结点为:"+student);}catch(Exception e){System.out.println(e.getMessage());}break;case "4"://根据id删除结点System.out.println("请输入要删除结点的id号:");id = sc.nextInt();try {if(sll.remove(id)) System.out.println("结点删除成功!");}catch(Exception e) {System.out.println(e.getMessage());}break;case "5"://获取链表中结点的个数(不包括头节点)System.out.println("链表中的结点个数为:"+sll.size());break;case "6"://展示链表中所有的结点(不包括头节点)sll.show();break;case "7"://根据id修改结点System.out.println("请按照 id name alias 的格式输入要修改的元素:");student = new Student(sc.nextInt(),sc.next(),sc.next());try {if(sll.update(student)) System.out.println("修改成功");}catch(Exception e) {System.out.println(e.getMessage());}break;case "8"://清空链表if(sll.clear()) System.out.println("链表已成功清空!");break;default:System.out.println("请输入有效指令!");break;}}System.out.println("程序已退出");}
}

image-20220816094057645

链表实现类:

package com.hhxy.linkedlist;//结点类
class Student{//数据域(将成员变量设置为public方便外部访问)public int id;public String name;public String alias;//引用域public Student next;public Student(int id, String name, String alias) {this.id = id;this.name = name;this.alias = alias;}@Overridepublic String toString() {return "[id=" + id + ", name=" + name + ", alias=" + alias + "]";}
}//链表类
public class SingleLinkedListDemo1 {//初始化头结点Student head = new Student(-99,"","");/*** 判断链表是否为空* @return true表示链表为空*/public boolean isEmpty() {//因为头节点是链表位置的标识,不能动,所以使用一个辅助引用来遍历链表Student temp = head.next;if(temp!=null) {//head后面存在至少一个元素,所以链表不为空return false;}return true;}/*** 在链尾添加结点* @param student 待添加的结点* @return true表示添加成功*/public boolean add(Student student) {//同理,因为链表头节点不能动。//注意:需要是从头节点开始遍历,因为链表可能为空,如果从头节点后一个遍历,当链表为空时会报空指针异常Student temp = head;//遍历寻找尾结点。因为temp=head,所以是从头节点开始遍历while(temp.next != null) {temp = temp.next;}//已找到链表尾结点,进行指向temp.next = student;return true;}/*** 按照id从小到大的顺序添加结点* @param student 待添加的结点* @return true表示添加成功*/public boolean addById(Student student) {Student temp = head;boolean flag = true;//用于判断链表是加在尾结点,还是加在结点之间while(temp.next != null) {if(student.id < temp.next.id) {//说明是添加在结点之间flag = false;break;}temp = temp.next;}if(flag) {//如果添加的结点是在尾结点temp.next = student;}else {//添加的结点是在结点之间student.next = temp.next;//切记:先改变后一个指向,再改变前一个指向temp.next = student;}return true;}/*** 根据id获取结点* @param id * @return 返回对应id的结点*/public Student get(int id) {if(isEmpty()) {throw new RuntimeException("该链表为空!");}Student temp = head.next;//从head结点后面开始遍历boolean flag = false;//判断链表中是否存在待获取的结点while(temp != null) {if(temp.id == id) {//找到id对应的结点flag = true;break;}temp = temp.next;}if(flag) {//如果找到id对应结点return temp;}else {//如果没有找到id对应的结点throw new RuntimeException("待获取的结点不存在!");}}/*** 根据id删除结点* @param id 待删除结点的id* @return true表示删除成功*/public boolean remove(int id) {if(isEmpty()) {throw new RuntimeException("链表为空!");}Student temp = head;//删除结点需要依赖前一个结点,所以从头节点开始遍历boolean flag = false;//判断链表中是否存在待删除的结点while(temp.next != null) {if(temp.next.id == id) {//找到该结点flag = true;break;}temp = temp.next;}if(flag) {//如果找到了要删除的结点temp.next = temp.next.next;}else {//如果没有找到id对应的结点throw new RuntimeException("待删除的结点不存在!");}return true;}/*** 获取链表中结点的个数(不包括头节点)*/public int size() {Student temp = head;int count = 0;//这里虽然遍历了头节点,但是没有遍历尾结点while(temp.next != null) {count++;temp = temp.next;}return count;}/*** 展示链表中所有的结点(不包括头节点)*/public void show() {if(isEmpty()) {System.out.println("链表为空!");return;}//注意:不需要展示头节点!Student temp = head;while(temp.next != null) {System.out.println(temp.next);temp = temp.next;}}/*** 根据id修改结点* @param student 待修改的结点* @return true表示修改成功*/public boolean update(Student student) {if(isEmpty()) {throw new RuntimeException("链表为空!");}Student temp = head;boolean flag = false;//判断链表是否修改成功while(temp.next != null) {if(temp.next.id == student.id) {//找到要修改的链表flag = true;student.next = temp.next.next;temp.next = student;break;}temp = temp.next;}if(flag) {//如果修改成功return true;}else {//如果链表中没有找到待删除的结点throw new RuntimeException("链表中不存在该结点");}}/*** 清空链表* @return true表示清空成功*/public boolean clear() {/*方式一:直接将头节点指向空(节约时间,但占内存)* head.next = null;* 除头节点以外其它结点存在引用,JVM不会回收结点内存,仍然会占据内存* 这种清楚方法很费内存! *///方式二:将所有结点占据的内存都进行释放(耗时,但不占内存)Student2 temp = head;//这里需要从后往前遍历,逐步去掉所有结点的引用,否则无法遍历下取while(head.next != null) {for (int i = size(); i > 1; i--) {temp = temp.next;}temp.next = null;}return true;}
}

2.2 不带头节点的单向链表

略……逻辑思路都差不多,只是将头节点换成一个头指针

不带头节点和带头结点的主要区别:带头结点遍历的时候、不能将头节点进行计数;而不带结点能够直接进行遍历

本质上两者并没有什么太大区别,带头节点的链表没有指针,头结点就相当于头指针,而不带头节点的链表是由头指针的

注意:这里所谓的指针和结点其实都是结点对象,只是指针初始值为null,结点要进行初始化

2.3 练习

  • 反转链表示意图

image-20220815155534532

  • 合并链表示意图

测试类:

/*** 练习题1:获取链表倒数第k个结点* 练习题2:将链表反转* 练习题3:从尾到头打印链表的结点* 练习题4:合并两个有序链表,合并后仍然有序*/
//测试类:
public class SingleLinkedListDemo3{public static void main(String[] args) {Node node1 = new Node(1);Node node2 = new Node(2);Node node3 = new Node(3);SingleLinkedList sll = new SingleLinkedList();sll.add(node1);sll.add(node2);sll.add(node3);System.out.println("链表原始状态:");sll.show();System.out.println("------------------------");//测试1:测试获取链表倒数第k个结点Node t = sll.findLastIndexNode(sll, 1);System.out.println(sll.findLastIndexNode(sll,1));System.out.println(sll.findLastIndexNode(sll,2));System.out.println(sll.findLastIndexNode(sll,3));System.out.println("-------------------------");//测试2:测试将链表反转sll.reverset(sll);System.out.println("反转后的链表:");sll.show();System.out.println("-------------------------");//测试3:从头到位打印链表System.out.println("反向打印链表:");sll.reversetPrint(sll);System.out.println("-------------------------");//测试4:将两个有序链表合并,合并后仍然有序SingleLinkedList list1 = new SingleLinkedList();SingleLinkedList list2 = new SingleLinkedList();Node node4 = new Node(4);Node node5 = new Node(5);Node node6 = new Node(6);Node node7 = new Node(7);Node node8 = new Node(8);Node node9 = new Node(9);Node node10 = new Node(10);Node node11 = new Node(11);list1.add(node4);list1.add(node7);list1.add(node8);list1.add(node10);list1.add(node11);System.out.println("链表1:");list1.show();System.out.println("链表2:");list2.add(node5);list2.add(node6);list2.add(node9);list2.show();SingleLinkedList list = new SingleLinkedList();list = list.combine(list1, list2);System.out.println("合并后的链表:");list.show();}
}

image-20220816092954578

image-20220816093034382

链表实现类:

package com.hhxy.linkedlist;import java.util.Stack;//结点类
class Node {int n;Node next;public Node(int n) {this.n = n;}@Overridepublic String toString() {return "[n=" + n + "]";}
}
//链表类:
public class SingleLinkedList {//初始化头节点public Node head = new Node(-99);/*** 添加结点*/public void add(Node node) {Node current = head;while(current.next != null) {current = current.next;}current.next = node;}/*** 获取链表的长度* @return*/public int size() {Node current = head.next;int count = 0;while(current != null) {count++;current = current.next;}return count;}/*** 展示*/public void show() {Node current = head.next;while(current != null) {System.out.println(current);current = current.next;}	}/*--------------------核心方法-------------------------*//*** 寻找链表倒数第k个结点* @param index* @return*/public Node findLastIndexNode(SingleLinkedList sll,int index) {Node head = sll.head;if(index <0 || index>sll.size() || head.next == null) {return null;}Node current = head.next;//将指针从第二个结点开始往后移动index位for (int i = 0; i < size()-index; i++) {current = current.next;}return current;}/*** 将链表反转* @param sll 待反转的链表*/public void reverset(SingleLinkedList sll) {Node head = sll.head;if(head.next == null || head.next.next == null) {//当前链表为空,或者只有一个结点,直接返回return;}SingleLinkedList sllTemp = new SingleLinkedList();//创建一个新链表Node headTemp = sllTemp.head;Node temp = null;//用来存旧链表的引用,方便遍历旧链表Node current = head.next;//辅助遍历旧链表while(current != null) {temp = current.next;//不保存,新链表就会断开,就无法进行遍历了current.next = headTemp.next;//指向新创建的头结点的后面的结点headTemp.next = current;//新创建的头结点,指向插入的结点current = temp;//指针往后移}head.next = headTemp.next;}/*** 反向打印链表* @param sll*/public void reversetPrint(SingleLinkedList sll) {Node head = sll.head;if(head.next == null) {return;}/*//方式一:使用findLastIndexNode方法(要先实现findLastIndexNode方法,不值得推荐)for(int i=1;i<=sll.size();i++) {System.out.println(sll.findLastIndexNode(sll, i));}//方式二:使用reverset方法(要先实现reverset方法,并且改变了链表的结构,不值得推荐)reverset(sll);sll.show();*///方式三:使用栈(强烈推荐)Stack<Node> stack = new Stack<>();Node current = head.next;while(current != null) {stack.push(current);current = current.next;}while(stack.size()>0) {System.out.println(stack.pop());}}/*** 合并两个有序链表,合并后仍然有序(这里我是默认按从小到大排序的)*/public SingleLinkedList combine(SingleLinkedList sll1,SingleLinkedList sll2) {Node head1 = sll1.head.next;//用于遍历sll1链表Node head2 = sll2.head.next;if(head1 == null || head2  == null) {//只要有一个链表为空就直接返回return head1 != null ? sll1 : sll2;}SingleLinkedList sll = new SingleLinkedList();//合并后的链表Node temp=sll.head;//用来给sll链表添加结点的while (head1 != null && head2 != null){if (head1.n < head2.n){//链表1的结点是当前最小结点temp.next = head1;//新链表连接最小结点temp = temp.next;//每新增一个结点,temp就往后移一位,保证他在尾结点方便连接新结点head1 = head1.next;//链表1的指针也往后移一位}else{//链表2的结点是当前最小结点temp.next = head2;temp = temp.next;head2 = head2.next;}}if (head1 != null && head2 == null){//经过一一段时间的合并后,sll2的链表为空了,直接就将sll1链表后面的结点拼接上去temp.next = head1;}if (head1 == null && head2 != null){temp.next = head2;}return sll;}/*------------------------------------------------*/
}

3、双向链表

双向链表相对单向链表就较为简单了,因为每个结点既能往后遍历,又能往前遍历 ,对于插入、删除、修改都无需像单链表一样依靠前一个结点。

与单链表的主要区别

  1. 遍历不仅可以往前遍历,还可以往后遍历

  2. 插入、删除、修改不需要依赖前一个结点(在链尾插入需要依赖尾结点)

  3. 添加的时候,需要进行双向绑定!

  • 双向链表插入示意图

    链表插入演示

    image-20220816113620558

  • 双向链表删除示意图

    链表删除演示

    image-20220816114235701

测试类:

和单向链表的测试方法相同

示意图:

image-20220816163415447

双向链表实现类:

其实只要理解了单向链表,再来看双向链表就会觉得so easy😄单向链表的方法双向链表都能使用,只是添加和修改的时候,需要多修改下prev的的指向。

package com.hhxy.linkedlist.doubles;
//结点类
class Student2{public int id;public String name;public String alias;public Student2 prev;//指向前一个结点public Student2 next;//指向后一个结点public Student2(int id, String name, String alias) {super();this.id = id;this.name = name;this.alias = alias;}@Overridepublic String toString() {return "Student [n=" + id + ", name=" + name + ", alias=" + alias + "]";}
}
//链表类
public class DoubleLinkedListDemo1 {//初始化头节点public Student2 head = new Student2(-99,"","");/*** 判断链表是否为空* @return true表示链表为空*/public boolean isEmpty() {//因为头节点是链表位置的标识,不能动,所以使用一个辅助引用来遍历链表Student2 temp = head.next;if(temp!=null) {//head后面存在至少一个元素,所以链表不为空return false;}return true;}/*** 在链尾添加结点* @param student2 待添加的结点* @return true表示添加成功*/public boolean add(Student2 student2) {//同理,因为链表头节点不能动。//注意:需要是从头节点开始遍历,因为链表可能为空,如果从头节点后一个遍历,当链表为空时会报空指针异常Student2 temp = head;//遍历寻找尾结点。因为temp=head,所以是从头节点开始遍历while(temp.next != null) {temp = temp.next;}//形成双向链表temp.next = student2;student2.prev = temp;return true;}/*** 按照id从小到大的顺序添加结点* @param student2 待添加的结点* @return true表示添加成功*/public boolean addById(Student2 student2) {Student2 temp = head;boolean flag = true;//用于判断链表是加在尾结点,还是加在结点之间while(temp.next != null) {if(student2.id < temp.next.id) {//说明是添加在结点之间flag = false;break;}temp = temp.next;}if(flag) {//如果添加的结点是在尾结点//形成双向链表temp.next = student2;student2.prev = temp;}else {//添加的结点是在结点之间,注意要形成双向指向student2.next = temp.next;//切记:先改变后一个指向,再改变前一个指向temp.next.prev = student2;//前面一根线temp.next = student2;student2.prev = temp;}return true;}/*** 根据id获取结点* @param id * @return 返回对应id的结点*/public Student2 get(int id) {if(isEmpty()) {throw new RuntimeException("该链表为空!");}Student2 temp = head.next;//从head结点后面开始遍历boolean flag = false;//判断链表中是否存在待获取的结点while(temp != null) {if(temp.id == id) {//找到id对应的结点flag = true;break;}temp = temp.next;}if(flag) {//如果找到id对应结点return temp;}else {//如果没有找到id对应的结点throw new RuntimeException("待获取的结点不存在!");}}/*** 根据id删除结点* @param id 待删除结点的id* @return true表示删除成功*/public boolean remove(int id) {if(isEmpty()) {throw new RuntimeException("链表为空!");}//使用双向链表可以进行自我删除Student2 temp = head.next;//删除结点需要依赖前一个结点,所以从头节点开始遍历boolean flag = false;//判断链表中是否存在待删除的结点while(temp != null) {if(temp.id == id) {//找到该结点flag = true;break;}temp = temp.next;}if(flag) {//如果找到了要删除的结点temp.prev.next = temp.next;//前一根线if(temp.next != null) {//要排除最后一个结点的可能,否则会出现空指针异常temp.next.prev = temp.prev;//后一根线}}else {//如果没有找到id对应的结点throw new RuntimeException("待删除的结点不存在!");}return true;}/*** 获取链表中结点的个数* @return*/public int size() {Student2 temp = head;int count = 0;while(temp.next != null) {count++;temp = temp.next;}return count;}/*** 展示链表中所有的结点*/public void show() {if(isEmpty()) {System.out.println("链表为空!");return;}Student2 temp = head;while(temp.next != null) {System.out.println(temp.next);temp = temp.next;}}/*** 根据id修改结点* @param student2 待修改的结点* @return true表示修改成功*/public boolean update(Student2 student2) {if(isEmpty()) {throw new RuntimeException("链表为空!");}Student2 temp = head.next;boolean flag = false;//判断链表是否修改成功while(temp != null) {if(temp.id == student2.id) {//找到要修改的链表flag = true;//后一根线student2.next = temp.next;if(temp.next!=null) {//排除最后一个节点temp.next.prev = student2;}//前一根线temp.prev.next = student2;student2.prev = temp.prev;break;}temp = temp.next;}if(flag) {//如果修改成功return true;}else {//如果链表中没有找到待删除的结点throw new RuntimeException("链表中不存在该结点");}}/*** 清空链表* @return*/public boolean clear() {/*方式一:直接将头节点指向空(节约时间,但占内存)* head.next = null;* 除头节点以外其它结点存在引用,JVM不会回收结点内存,仍然会占据内存* 这种清楚方法很费内存! * return true;*///方式二:将所有结点占据的内存都进行释放(耗时,但不占内存)Student2 temp = head;//这里需要从后往前遍历,逐步去掉所有结点的引用,否则无法遍历下取Student2 current = head;while(current.next != null) {current = temp;current.next = null;current.prev = null;temp = temp.next;}return true;}
}

4、单向环形链表

基本和单向链表类似,也可以分为带头节点,和不带头结点点,这里演示的是不带头结点的单向环形链表,单向环形链表和单向链表唯一的区别:尾结点的next不指向空,而是指向开始节点

主要思想还是在单链表那一节😄只要掌握单向链表,这些双向链表还有单向循环链表就是弟弟(′д` )…彡…彡直接套用第一节的接口,实现所有的方法

image-20220816150410003

测试类

和2.1一样,换个对象就行了(这个测试类真渣😆)

image-20220816190136942

image-20220816190151341

image-20220816190201109

image-20220816190207580

实现类

这里主要记录以下按顺序插入结点的思路,怕以后忘记了。其实主要思想还是和单向链表的addById方法的逻辑是一致的,主要是要考虑循环!思路主要如下:

  1. 先将链表添加分为两大类,首结点的添加非首结点的添加,因为首结点的添加需要自动成环
  2. 再将非首结点的添加又分为在 添加在首结点之前之后,之前需要移动first指针,之后不需要移动

示意图:

删除结点:

  1. 先将删除分为两大类,删除头结点删除普通结点
  2. 删除头结点又可以分为两类,链表只有一个头结点除了头结点还有其它结点
  3. 删除普通结点时需要注意链表是单向的,删除操作需要依赖待删除结点的前一个结点

修改结点的逻辑思路和删除类似,不在赘述,示意图:

  • 清空链表:链表的清空有两种方法,一种是直接让first=null,这种清空简单省事,但是是假清空,链表仍然存在内存中!

    第二种方法是让每个结点的next指向空,然后将first=null,这种费脑子但是省空间

package com.hhxy.linkedlist.circular;//结点类
class Student{public int id;public String name;public String alias;public Student next;public Student(int id, String name, String alias) {super();this.id = id;this.name = name;this.alias = alias;}@Overridepublic String toString() {return "[id=" + id + ", name=" + name + ", alias=" + alias + "]";}
}//链表类
public class CircularLinkedListDemo1 {private Student first = null;//注意这是头指针,不是头结点!/*** 非空判断* @return true表示为空*/public boolean isEmpty() {//因为是没有头结点,所以直接判断firstif(first != null) {return false;}else {return true;}}/*** 在尾结点添加结点* @param student* @return true表示结点添加成功*/public boolean add(Student student) {if(first == null) {//对第一个结点进行单独考虑first =  student;first.next = first;//构成环状return true;}else {Student current = first;//找到最后一个结点while(current.next != first) {current = current.next;}//找到后将节点加入环形链表中current.next = student;student.next = first;return true;}}/*** 根据id从小到大的顺序添加结点* @return true表示添加成功*/public boolean addById(Student student) {if(first == null) {//第一个结点单独成环first = student;first.next = first;}else {Student current = first;if(student.id < current.id && current == first) {//说明这个结点比头结点还要小,要将头指针移动位置current.next = student;student.next = current;first = student;//将first移动到student上return true;}while(current.next != first) {//寻早结点添加的位置if(student.id < current.next.id) {//已找到添加的位置break;}current = current.next;}student.next = current.next;//切记:一定要先改变后一根线,不然链表会断裂!current.next = student;}return true;}/*** 根据id获取结点*/public Student get(int id) {if(isEmpty()) {throw new RuntimeException("链表为空!");}Student current = first;boolean flag = false;while(true) {if(current.id == id) {//找到就直接结束遍历flag = true;break;}if(current.next == first) {//如果是最后一个结点,就表明还没有找到,直接结束遍历break;}current = current.next;//辅助指针后移,遍历链表}if(flag) {//找到就返回这个结点return current;}else {//没有找到,打印提示信息throw new RuntimeException("链表中不存在该结点!");}}/*** 根据id删除结点*/public boolean remove(int id) {if(isEmpty()) {throw new RuntimeException("链表为空!");}if(first.id == id) {//当头结点就是要删除的结点时,分类讨论if(first.next == first) {//如果链表只有头结点时first = null;return true;}else {//如果链表除了头结点还有其它结点时,需要移动first指针Student current = first;//找到尾结点while(current.next != first) {current = current.next;}current.next = current.next.next;first = current.next;//移动头指针return true;}}//删除普通结点Student current = first;boolean flag = false;//判断链表中是否存在该结点while(true) {if(current.next.id == id) {//这里使用current.next判断是为了使用前一个结点//找到结点直接退出flag = true;break;}if(current.next == first) {//遍历完成,直接退出break;}current = current.next;}if(flag) {//找到待删除的结点,利用前一个结点将其删除(思路和单项链表是一样的)current.next = current.next.next;return true;}else {throw new RuntimeException("该结点不存在!");}}/*** 获取链表长度*/public int size() {Student current = first;if(isEmpty()) {return 0;//这里要不要都无所谓,只是习惯了~~~}int count = 0;while(true) {count++;if(current.next == first) {break;}current = current.next;}return count;}/*** 展示链表*/public void show() {Student current = first;if(first == null) {//排除空链表throw new RuntimeException("链表为空!");}while(true) {System.out.println(current);if(current.next == first) {//当发现结点是最后一个结点直接退出打印break;}current = current.next;}}/*** 根据id修改结点*/public boolean update(Student student) {if(isEmpty()) {throw new RuntimeException("链表为空!");}if(student.id == first.id) {//修改的结点是头节点if(first.next == first) {//只有一个头节点first = student;first.next = student;return true;}else {//除了头节点还有其它结点Student current = first;//找到尾结点while(current.next != first) {current = current.next;}student.next = current.next.next;//修改后一根线current.next = student;//修改前一根线first = student;//修改头指针return true;}}//修改的结点是普通结点Student current = first;boolean flag = false;//判断链表中是否存在该结点while(current.next != first) {if(current.next.id == student.id) {//找到结点,直接退出flag = true;break;}current = current.next;}if(flag) {student.next = current.next.next;//修改后一根线,防止链表断裂current.next = student;return true;}else {throw new RuntimeException("不存在该结点!");}}/*** 	清空链表*/public boolean clear() {/*方式一:直接将头节点指向空(节约时间,但占内存)* first.next = null;* 除头节点以外其它结点存在引用,JVM不会回收结点内存,仍然会占据内存* 这种清楚方法很费内存! * return true;*///方式二:将所有结点占据的内存都进行释放(耗时,但不占内存)if(isEmpty()) {return true;}if(first == first.next) {//只有一个结点first = null;return true;}Student temp = first;//临时存储current的引用,辅助遍历Student current = first;//将链表每个结点的引用都断开,这样每个结点都没有被引用,就能被JVM给回收while(current.next != first) {temp = temp.next;current.next = null;current = temp;}//将最后一个结点的引用 和头指针 设为空current.next = null;//不断开最后一个接待你的引用,头节点就不会被回收first = null;return true;}
}

  1. 引用域:是链表结点中一片很小的空间,用来存放后继结点的地址 ↩︎

相关文章:

【数据结构篇】手写双向链表、单向链表(超详细)

文章目录 链表1、基本介绍2、单向链表2.1 带头节点的单向链表测试类&#xff1a;链表实现类&#xff1a; 2.2 不带头节点的单向链表2.3 练习测试类&#xff1a;链表实现类&#xff1a; 3、双向链表测试类&#xff1a;双向链表实现类&#xff1a; 4、单向环形链表**测试类**&…...

linux 中的串口驱动

1.流程描述 打开串口设备&#xff1a;首先需要打开串口设备文件&#xff0c;通常是/dev/ttyX&#xff08;如/dev/ttyUSB0&#xff0c;/dev/ttyS0等&#xff09;。可以使用open()系统调用打开串口设备文件&#xff0c;获取一个文件描述符。 配置串口属性&#xff1a;打开…...

棱镜七彩正式加入龙蜥社区安全联盟(OASA)

近日&#xff0c;龙蜥社区安全联盟&#xff08;OASA&#xff09;正式成立&#xff0c;棱镜七彩成为该联盟成员单位。 龙蜥社区安全联盟是促进产业合作的非营利组织&#xff0c;致力于打造中立开放、聚焦操作系统信息安全的交流平台&#xff0c;推进龙蜥社区乃至整个产业安全生态…...

STM32——STM32F401x系列标准库的下载+环境搭建+建工程步骤(更完整)

文章目录 标准库的下载环境搭建建工程最后的话 标准库的下载 1.STM32标准库的官网下载网站https://www.st.com/content/st_com/en.html 2. 3. 4. 5. 6. 7.点击之后下滑 8.选择自己需要的版本下载 环境搭建建工程 大致步骤同之前我写的一篇STM32——建工程差不多&#xff0…...

基于ArcGIS土地利用量化人类活动的分析及模型构建

ArcGIS产品线为用户提供一个可伸缩的&#xff0c;全面的GIS平台。ArcObjects包含了许多的可编程组件&#xff0c;从细粒度的对象&#xff08;例如单个的几何对象&#xff09;到粗粒度的对象&#xff08;例如与现有ArcMap文档交互的地图对象&#xff09;涉及面极广&#xff0c;这…...

特性Attribute

本文只提及常用的特性&#xff0c;更多特性请查看官方文档。 AddComponentMenu - Unity 脚本 API 常用特性 AddComponentMenu 添加组件菜单 使用 AddComponentMenu 属性可在“Component”菜单中的任意位置放置脚本&#xff0c;而不仅是“Component > Scripts”菜单。 使用…...

pyqt5, 如何在窗口上显示10个点地循环进度条。

要在PyQt5窗口上显示从1个点逐渐增加到10个点&#xff0c;然后周而复始地循环&#xff0c;可以使用PyQt5的图形绘制功能和定时器来实现。以下是一个简单的例子&#xff1a; import sys from PyQt5.QtWidgets import QApplication, QWidget from PyQt5.QtGui import QPainter, …...

VM里ubuntu虚拟无法启动

开始认为是VM的设置问题&#xff0c;按照这个链接关闭的3d加速图像显示&#xff0c;以及那个cmd命令&#xff0c;但是没什么用。 后来看到一篇博文和我的错误一模一样&#xff0c;都是只有一个光标在闪。于是按照这个操作进行了一遍&#xff0c;发现是home文件满了&#xff0c…...

信息学奥赛一本通——1156:求π的值

文章目录 题目【题目描述】【输入】【输出】【输入样例】【输出样例】 AC代码 题目 【题目描述】 根据公式&#xff1a; a r c t a n x ( x ) x − x 3 3 x 5 5 − x 7 7 ⋯ arctanx\left ( x \right ) x- \frac{x^3}{3} \frac{x^5}{5}-\frac{x^7}{7} \cdots arctanx(x…...

BI报表工具有哪些作用?奥威BI全面剖析数据

BI报表工具有哪些作用&#xff1f;主要的作用是通过整合多业务来源数据&#xff0c;全面分析挖掘数据&#xff0c;来帮助企业实现数据化运营、支持智能决策、实现数据资产沉淀和增值、进行数据挖掘和预测分析、提高数据可读性和数据可视化程度等&#xff0c;从而提高企业的竞争…...

【云原生K8s】初识Kubernetes的理论基础

K8S由google的Borg系统(博格系统&#xff0c;google内部使用的大规模容器编排工具)作为原型&#xff0c;后经GO语言延用Borg的思路重写并捐献给CNCF基金会开源。 云原生基金会&#xff08;CNCF&#xff09;于2015年12月成立&#xff0c;隶属于Linux基金会。CNCF孵化的第一个项目…...

javaAPI(三):jdk8之前的日期API

jdk 8之前的日期时间API 1、System类中currentTimeMillis()。 2、 java.util.Date和子类java.sql.Date。 3、SimpleDateFormat 4、Calendar System返回时间戳 long time System.currentTimeMillis();System.out.println(time);Date类 java.util.Date类 实例化 构造器一&a…...

驱动开发(中断)

头文件&#xff1a; #ifndef __LED_H__ #define __LED_H__#define PHY_LED1_MODER 0X50006000 #define PHY_LED1_ODR 0X50006014 #define PHY_LED1_RCC 0X50000A28#define PHY_LED2_MODER 0X50007000 #define PHY_LED2_ODR 0X50007014 #define PHY_LED2_RCC 0X50000A28#def…...

TypeScript最新语法总结

注意注意&#xff01;&#xff01;&#xff01;本文介绍的是最新的TypeScript4的重要语法 第一部分&#xff1a;TypeScript的简介 TypeScript 是由微软开发的一款开源的编程语言&#xff0c;TypeScript 是 Javascript 的超集&#xff0c;遵循最新的 ES6、ES5 规范&#xff0c…...

sentinel组件

目录 定义 4.加SentinelResource,blockHander是超过阈值之后执行的函数 5.设置阈值 6.springboot集成sentinel 定义 1.sentinel知道当前流量大小&#xff0c;在浏览器和后端之间加sentinel控制流量&#xff0c;避免大批量的瞬时请求都达到服务上&#xff0c;将服务压垮 2.…...

26 MFC序列化函数

文章目录 Serialize对于存储文件的序列化 Serialize Serialize 是一个在 MFC (Microsoft Foundation Classes) 中常用的函数或概念。它用于将对象的数据进行序列化和反序列化&#xff0c;便于在不同的场景中保存、传输和恢复对象的状态。 在 MFC 中&#xff0c;Serialize 函数…...

GC 深入(小白,对gc有一个进一步的了解)

垃圾回收器的搭配 一般固定 一般这年轻代垃圾回收器&#xff0c;老年代垃圾回收器&#xff0c;如上图搭配着使用 1.8呢默认就是最后边那哥俩 jvm调优 一个就是增加吞吐量 一个就是减少STW的时间。 三色标记算法&#xff08;理解根可达算法&#xff09; 并发的可达性分析 有…...

CSS前端面试

文章目录 rem、em、vh、px各自代表的含义&#xff1f;盒模型poison 定位属性flex属性让元素水平垂直居中页面适配的方法有哪些 rem、em、vh、px各自代表的含义&#xff1f; px&#xff1a;绝对单位&#xff0c;页面按精确像素展示 em&#xff1a;相对单位&#xff0c;基准点为…...

VB+SQL餐饮管理系统设计与实现

第一章 前言 1.1 绪论 当今世界已进入了在计算机信息管理领域中激烈竞争的时代,应用计算机已经变得十分普遍了,如同我们离不开的自行车、汽车一样。我们应该承认,谁掌握的知识多,信息量大,信息处理速度快,批量大,谁的效率就高,谁就能够在各种竞争中立于不败之地。随着…...

React入门学习笔记2

jsx语法规则 定义虚拟DOM时&#xff0c;不要写引号。标签中混入JS表达式时要用{ }。样式的类名指定不要用class&#xff0c;要用className。内联样式&#xff0c;要用style{{key&#xff1a;value}}的形式去写。只有一个根标签标签必须闭合标签首字母 )若小写字母开头&#xf…...

[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?

&#x1f9e0; 智能合约中的数据是如何在区块链中保持一致的&#xff1f; 为什么所有区块链节点都能得出相同结果&#xff1f;合约调用这么复杂&#xff0c;状态真能保持一致吗&#xff1f;本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里&#xf…...

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

边缘计算医疗风险自查APP开发方案

核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

Python爬虫实战:研究feedparser库相关技术

1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...

五年级数学知识边界总结思考-下册

目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解&#xff1a;由来、作用与意义**一、知识点核心内容****二、知识点的由来&#xff1a;从生活实践到数学抽象****三、知识的作用&#xff1a;解决实际问题的工具****四、学习的意义&#xff1a;培养核心素养…...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异&#xff0c;它们的数据同步要求既要保持数据的准确性和一致性&#xff0c;又要处理好性能问题。以下是一些主要的技术要点&#xff1a; 数据结构差异 数据类型差异&#xff…...

Cinnamon修改面板小工具图标

Cinnamon开始菜单-CSDN博客 设置模块都是做好的&#xff0c;比GNOME简单得多&#xff01; 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南

&#x1f680; C extern 关键字深度解析&#xff1a;跨文件编程的终极指南 &#x1f4c5; 更新时间&#xff1a;2025年6月5日 &#x1f3f7;️ 标签&#xff1a;C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言&#x1f525;一、extern 是什么&#xff1f;&…...