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

文章目录

  • 链表
    • 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. 引用域:是链表结点中一片很小的空间,用来存放后继结点的地址 ↩︎

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

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

相关文章

Gitlab CI/CD笔记-第二天-GitOps的流水线常用关键词(1)

一、常用关键词 在Gitlab项目的根目录需要创建一个 .gitlab-ci.yaml的文件。 这个文件就是定义的流水线。Call :"Pipeline as code" 二、这条流水线怎么写&#xff1f; 一、掌握常用的关键词即可。 1.关键词分类 1.全局关键词 Global Keywards 2.任务关键词…

Java课题笔记~ Spring 概述

Spring 框架 一、Spring 概述 1、Spring 框架是什么 Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架&#xff0c;它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转&#xff08;IoC&#xff09;和面向切面编程&#xff08;AOP&#xff09;。 Spring…

vue 标题文字字数过长超出部分用...代替 动态显示

效果: 浏览器最大化: 浏览器缩小: 代码: html: <div class"title overflow">{{item.name}}</div> <div class"content overflow">{{item.content}}</div> css: .overflow {/* 一定要加宽度 */width: 90%;/* 文字的大小 */he…

聊聊 Docker 和 Dockerfile

目录 一、前言 二、了解Dockerfile 三、Dockerfile 指令 四、多阶段构建 五、Dockerfile 高级用法 六、小结 一、前言 对于开发人员来说&#xff0c;会Docker而不知道Dockerfile等于不会Docker&#xff0c;上一篇文章带大家学习了Docker的基本使用方法&#xff1a;《一文…

【NLP概念源和流】 01-稀疏文档表示(第 1/20 部分)

一、介绍 自然语言处理(NLP)是计算方法的应用,不仅可以从文本中提取信息,还可以在其上对不同的应用程序进行建模。所有基于语言的文本都有系统的结构或规则,通常被称为形态学,例如“跳跃”的过去时总是“跳跃”。对于人类来说,这种形态学的理解是显而易见的。 在这篇介…

设计模式-迭代器模式在Java中使用示例

场景 为开发一套销售管理系统&#xff0c;在对该系统进行分析和设计时&#xff0c;发现经常需要对系统中的商品数据、客户数据等进行遍历&#xff0c; 为了复用这些遍历代码&#xff0c;开发人员设计了一个抽象的数据集合类AbstractObjectList&#xff0c;而将存储商品和客户…

MySQL正则表达式检索数据

目录 一、使用正则表达式进行基本字符匹配 1.使用regexp关键字 2.使用正则表达式 . 二、进行OR匹配 1.为搜索两个串之一&#xff0c;使用 | 2.匹配几个字符之一[] 3.匹配范围 4.匹配特殊字符 过滤数据允许使用匹配、比较、通配符操作来寻找数据&#xff0c;但是随…

MySQL 的事件调度器

MySQL 的事件调度器可以通过以下方式进行管理&#xff1a; 1】查看事件调度器的状态 SHOW VARIABLES LIKE event_scheduler;2】启用/禁用事件调度器 SET GLOBAL event_scheduler ON;SET GLOBAL event_scheduler OFF; 注意&#xff1a;启用/禁用事件调度器需要具有 SUPE…

Spring源码之XML文件中Bean标签的解析1

读取XML文件&#xff0c;创建对象 xml文件里包含Bean的信息&#xff0c;为了避免多次IO&#xff0c;需要一次性读取xml文件中所有bean信息&#xff0c;加入到Spring工厂。 读取配置文件 new ClassPathResource("applicationContext.xml")ClassPathResource是Sprin…

10倍提升效率,号称取代Elasticsearch?

[Manticore Search](https://github.com/manticoresoftware/manticoresearch/) 是一个使用 C 开发的高性能搜索引擎&#xff0c;创建于 2017 年&#xff0c;其前身是 Sphinx Search 。Manticore Search 充分利用了 Sphinx&#xff0c;显着改进了它的功能&#xff0c;修复了数百…

Charles抓包工具使用(一)(macOS)

Fiddler抓包 | 竟然有这些骚操作&#xff0c;太神奇了&#xff1f; Fiddler响应拦截数据篡改&#xff0c;实现特殊场景深度测试&#xff08;一&#xff09; 利用Fiddler抓包调试工具&#xff0c;实现mock数据特殊场景深度测试&#xff08;二&#xff09; 利用Fiddler抓包调试工…

Linux下TCP网络服务器与客户端通信程序入门

文章目录 目标服务器与客户端通信流程TCP服务器代码TCP客户端代码 目标 实现客户端连接服务器&#xff0c;通过终端窗口发送信息给服务器端&#xff0c;服务器接收到信息后对信息数据进行回传&#xff0c;客户端读取回传信息并返回。 服务器与客户端通信流程 TCP服务器代码 …

etcd

文章目录 etcd单机安装设置键值对watch操作读取键过往版本的值压缩修订版本lease租约&#xff08;过期机制&#xff09;授予租约撤销租约keepAlive续约获取租约信息 事务基于etcd实现分布式锁原生实现官方 concurrency 包实现 服务注册与发现Go 操作 Etcd 参考 etcd etcd 是一…

02|Oracle学习(数据类型、DDL)

1. 数据类型&#xff1a; 通常为&#xff1a;字符型、数值型、日期型以及大字段型大字段型&#xff1a;存放大数据及文件。 存储大数据时&#xff0c;基本上blob就能满足。 2. DDL&#xff08;数据库定义语言&#xff09; 主要包括对数据库对象的创建、删除及修改的操作。…

2.文件的逻辑结构

第四章 文件管理 2.文件的逻辑结构 顺序文件采用顺序存储则意味着各个逻辑上相邻的记录在物理上也是相邻的存储的。所以如果第0号记录的逻辑地址为0的话&#xff0c;则i号记录的逻辑为i *L。 特别的如果这个定长记录的顺序文件采用串结构&#xff0c;也就是这些记录的顺序和他…

go 结构体 - 值类型、引用类型 - 结构体转json类型 - 指针类型的种类 - 结构体方法 - 继承 - 多态(interface接口) - 练习

目录 一、结构体 1、python 与 go面向对象的实现&#xff1a; 2、初用GO中的结构体&#xff1a;&#xff08;实例化一个值类型的数据&#xff08;结构体&#xff09;&#xff09; 输出结果不同的三种方式 3、实例化一个引用类型的数据&#xff08;结构体&#xff09; 4、…

Tomcat添加第三方jar包、如何在IDEA中启动部署Web模板

前言:公司最近维护老项目,是最原始的web项目,servlet和jsp结合的web项目,启动的时候配置了好几遍, 都起不来,很折磨人,这个文档比较全配置一遍准备工作 首先 拉取代码: git clone xxx.git ,如需要别的操作,自行baidu 也可以在idea中拉取第一步File ->Project Structure->…

Redis两种持久化方案RDB持久化和AOF持久化

Redis持久化 Redis有两种持久化方案&#xff1a; RDB持久化AOF持久化 1.1.RDB持久化 RDB全称Redis Database Backup file&#xff08;Redis数据备份文件&#xff09;&#xff0c;也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启…

VSCode插件Todo Tree的使用

在VSCode中安装插件Todo Tree。按下快捷键ctrlshiftP&#xff0c;输入setting.jspn&#xff0c;选择相应的配置范围&#xff0c;我们选择的是用户配置 Open User Settings(JSON)&#xff0c;将以下代码插入其中。 //todo-tree 标签配置从这里开始 标签兼容大小写字母(很好的功…

Spring Boot 集成Seata

Seata的集成方式有&#xff1a; 1. Seata-All 2. Seata-Spring-Boot-Starter 3. Spring-Cloud-Starter-Seata 本案例使用Seata-Spring-Boot-Starter演示&#xff1a; 第一步&#xff1a;下载Seata 第二步&#xff1a;为了更好看到效果&#xff0c;我们将Seata的数据存储改…