当前位置: 首页 > 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…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

深入理解JavaScript设计模式之单例模式

目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式&#xff08;Singleton Pattern&#…...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

C# SqlSugar:依赖注入与仓储模式实践

C# SqlSugar&#xff1a;依赖注入与仓储模式实践 在 C# 的应用开发中&#xff0c;数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护&#xff0c;许多开发者会选择成熟的 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;SqlSugar 就是其中备受…...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3

一&#xff0c;概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本&#xff1a;2014.07&#xff1b; Kernel版本&#xff1a;Linux-3.10&#xff1b; 二&#xff0c;Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01)&#xff0c;并让boo…...

FFmpeg avformat_open_input函数分析

函数内部的总体流程如下&#xff1a; avformat_open_input 精简后的代码如下&#xff1a; int avformat_open_input(AVFormatContext **ps, const char *filename,ff_const59 AVInputFormat *fmt, AVDictionary **options) {AVFormatContext *s *ps;int i, ret 0;AVDictio…...

人工智能 - 在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型

在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型。这些平台各有侧重&#xff0c;适用场景差异显著。下面我将从核心功能定位、典型应用场景、真实体验痛点、选型决策关键点进行拆解&#xff0c;并提供具体场景下的推荐方案。 一、核心功能定位速览 平台核心定位技术栈亮…...

高防服务器价格高原因分析

高防服务器的价格较高&#xff0c;主要是由于其特殊的防御机制、硬件配置、运营维护等多方面的综合成本。以下从技术、资源和服务三个维度详细解析高防服务器昂贵的原因&#xff1a; 一、硬件与技术投入 大带宽需求 DDoS攻击通过占用大量带宽资源瘫痪目标服务器&#xff0c;因此…...

用递归算法解锁「子集」问题 —— LeetCode 78题解析

文章目录 一、题目介绍二、递归思路详解&#xff1a;从决策树开始理解三、解法一&#xff1a;二叉决策树 DFS四、解法二&#xff1a;组合式回溯写法&#xff08;推荐&#xff09;五、解法对比 递归算法是编程中一种非常强大且常见的思想&#xff0c;它能够优雅地解决很多复杂的…...