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

news2024/11/17 5:59:49

文章目录

  • 链表
    • 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;
	}

	@Override
	public 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;
	}
	@Override
	public 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;
	}
	@Override
	public 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;
	}

	@Override
	public String toString() {
		return "[id=" + id + ", name=" + name + ", alias=" + alias + "]";
	}
}

//链表类
public class CircularLinkedListDemo1 {
	private Student first = null;//注意这是头指针,不是头结点!
	
	/**
	 * 非空判断
	 * @return true表示为空
	 */
	public boolean isEmpty() {
		//因为是没有头结点,所以直接判断first
		if(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.coloradmin.cn/o/838202.html

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

相关文章

初级web前端开发工程师的岗位职责描述(合集)

初级web前端开发工程师的岗位职责描述1 职责&#xff1a; 1. 根据功能需求设计编写页面原型; 2. 前后端联调保证功能流畅; 3. 提高页面易用性、美观提出合理建议。 4、与后台工程师配合开发联调并交付产品; 5、持续优化前端页面体验和访问速度&#xff0c;保证页面精美高效…

命令行快捷键Mac Iterm2

原文:Jump forwards, backwards and delete a word in iTerm2 on Mac OS iTerm2并不允许你使用 ⌥← 或 ⌥→ 来跳过单词。 你也不能使用 ⌥backspace 来删除整个单词。 下面是在Mac OS上如何配置iTerm2以便能做到这一点的方法。 退格键 首先&#xff0c;你需要将你的左侧 ⌥…

kubernetes基于helm部署gitlab

kubernetes基于helm部署gitlab 这篇博文介绍如何在 Kubernetes 中使用helm部署 GitLab。 先决条件 已运行的 Kubernetes 集群负载均衡器&#xff0c;为ingress-nginx控制器提供EXTERNAL-IP&#xff0c;本示例使用metallb默认存储类&#xff0c;为gitlab pods提供持久化存储&…

python#django数据库一对一/一对多/多对多

一对一OneToOneField 用户和用户信息 搭建 # 一对一 class TestUser(models.Model): usernamemodels.CharField(max_length32) password models.CharField(max_length32) class TestInfo(models.Model): mick_namemodels.CharField(max_length32) usermode…

蓝桥杯上岸必刷!!! (进制、数位专题)

蓝桥杯上岸必刷&#xff01;&#xff01;&#xff01;(进制、数位专题) 距离蓝桥杯省赛倒数最后1天 ❗️ ❗️ ❗️ 还没背熟模板的伙伴们背起来 &#x1f4aa; &#x1f4aa; &#x1f4aa; 大家好 我是寸铁&#x1f4aa; 真题千千万万遍&#xff0c;蓝桥省一自然现&#…

“科创中国”青百会轮值主席吴甜:以大语言模型为代表的AI将引发产业变革

8月1日&#xff0c;“科创中国”青年百人会&#xff08;后文简称青百会&#xff09;联合百度举办“青创汇”高端对话&#xff0c;围绕人工智能技术创新与产业发展交流研讨&#xff0c;同时正式成立“科创中国”青年百人会女性工作委员会。该委员会将鼓励更多女性投身科技创新事…

easyExcel如何实现自定义标题,前两个(多个)标题占满一行,最后标题有规律

实现效果 实现方式 1&#xff0c;标题&#xff1a; Data public class ExportPurchaseSortingCustDto {/** 序号 */ExcelProperty(value {"${bigHead}","${dateHead}","序号"},index 0)ApiModelProperty(value "序号")private int…

RAFT:Recurrent All-Pairs Field Transforms for Optical Flow

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 Abstract1 Introduction2 Related WorkOptical Flow as Energy Minimization 3 Approach3.1 Feature Extraction3.2 Computing Visual Similarity Abstract 光流学…

RISC-V基础之浮点指令(包含实例)

RISC-V体系结构定义了可选的浮点扩展&#xff0c;分别称为RVF、RVD和RVQ&#xff0c;用于操作单精度、双精度和四倍精度的浮点数。RVF/D/Q定义了32个浮点寄存器&#xff0c;f0到f31&#xff0c;它们的宽度分别为32位、64位或128位。当一个处理器实现了多个浮点扩展时&#xff0…

企业人力资源管理系统servlet+jsp人事招聘培训薪资java源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 企业人力资源管理系统servletjsp 系统1权限&#xff…

基于面向对象基础设计——里氏替换原则

在Java中&#xff0c;支持抽象和多态的关键机制之一是继承。正是使用了继承&#xff0c;我们才可以创建实现父类中抽象方法的子类。那么&#xff0c;是什么规则在支配着这种特殊的继承用法呢&#xff1f;最佳的继承层次的特征又是什么呢&#xff1f;在什么情况下会使我们创建的…

Python中的copy和deepcopy

一、定义 浅拷贝&#xff1a;创建一个新的对象&#xff0c;不拷贝内部子对象&#xff0c;只拷贝子对象的引用。这意味着&#xff0c;如果原对象的子对象发生变化&#xff0c;拷贝后的对象也会受到影响。 深拷贝&#xff1a;创建一个新的对象&#xff0c;并递归地拷贝原对象的所…

删除这4个文件夹,流畅使用手机无忧

在现代社会中&#xff0c;手机已经成为我们生活中不可或缺的一部分。然而&#xff0c;随着使用时间的增长&#xff0c;我们可能会遇到手机卡顿和内存不足的问题&#xff0c;让我们感到十分困扰。手机卡顿不仅影响使用体验&#xff0c;还可能导致应用程序运行缓慢&#xff0c;甚…

简单高效,教你如何制作食品小程序商城

食品行业的发展越来越繁荣&#xff0c;为了满足人们对食品的需求&#xff0c;许多商家都开始转向线上销售。而制作一个食品小程序商城&#xff0c;不仅可以提高销售效率&#xff0c;还可以增加用户粘性。下面&#xff0c;我们就来详细介绍如何制作食品小程序商城。 首先&#x…

python日志logging的用法

python日志看起比较简单&#xff0c;要用起来稍微有点复杂&#xff0c;基础用法网上也介绍得比较多&#xff0c;下面就最近遇见的问题&#xff0c;作一个简单的介绍。就是在两个以上的python文件中要记录日志&#xff0c;怎么才能实现在一个地方配置&#xff0c;多个地方使用的…

11.函数递归与迭代

递归与迭代 1.什么是递归&#xff1f;2.递归的限制条件3.递归举例3.1 求n的阶乘3.2 顺序打印一个整数的每一位 4.递归与迭代4.1 求第n个斐波那契数&#xff08;递归 不推荐&#xff09;4.2 求第n个斐波那契数&#xff08;迭代 推荐&#xff09;4.3 总结 1.什么是递归&#xff1…

奥威BI—数字化转型首选,以数据驱动企业发展

奥威BI系统BI方案可以迅速构建企业级大数据分析平台&#xff0c;可以将大量数据转化为直观、易于理解的图表和图形&#xff0c;推动和促进数字化转型的进程&#xff0c;帮助企业更好地了解自身的运营状况&#xff0c;及时发现问题并采取相应的措施&#xff0c;提高运营效率和质…

使用socket实现UDP版的回显服务器

文章目录 1. Socket简介2. DatagramSocket3. DatagramPacket4. InetSocketAddress5. 实现UDP版的回显服务器 1. Socket简介 Socket&#xff08;Java套接字&#xff09;是Java编程语言提供的一组类和接口&#xff0c;用于实现网络通信。它基于Socket编程接口&#xff0c;提供了…

【测试学习五】测试类型的划分(重点:白盒与黑盒测试)

目录 一、测试类型的分类 1、按测试对象划分 2、是否查看代码划分&#xff08;重点&#xff09; &#x1f337;&#xff08;1&#xff09;黑盒测试 &#x1f337;&#xff08;2&#xff09;白盒测试 &#x1f337;&#xff08;3&#xff09;灰盒测试 3、按照开发阶段划…

JVM分析工具JProfiler介绍及安装

目录 一、什么是JProfiler&#xff1f; 二、JProfiler 功能结构 1、分析代理 2、记录数据 3、快照 三、安装 一、什么是JProfiler&#xff1f; JProfiler是一个专业的工具&#xff0c;用于分析运行中的JVM内部发生的事情。当您的生产系统出现问题时&#xff0c;您可以…