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

文章目录

  • 链表
    • 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.mfbz.cn/a/61397.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SPINN:基于设备和云的神经网络协同递进推理

SPINN&#xff1a;基于设备和云的神经网络协同递进推理 论文标题&#xff1a;SPINN: synergistic progressive inference of neural networks over device and cloud 原文链接&#xff1a;https://dl.acm.org/doi/10.1145/3372224.3419194 论文动机 现代CNN过多的计算需求&am…

springboot+vue私人健身和教练预约管理系统 nt5mp

随着世界经济信息化、全球网络化的到来&#xff0c;信息线上管理的飞速发展&#xff0c;为私人健身和教练预约管理的改革起到关键作用。若想达到安全、快捷的目的&#xff0c;就需要拥有信息化的组织和管理模式&#xff0c;建立一套合理、畅通、高效的私人健身和教练预约管理系…

nginx 配置多域名多站点 Ubuntu

nginx 配置多域名多站点 Ubuntu 一、安装 nginx apt install nginx二、配置文件说明 nginx 的配置文件在 /etc/nginx 目录下&#xff0c;它的默认内容是这样的 root2bd0:/etc/nginx# ll total 72 drwxr-xr-x 8 root root 4096 Jul 31 15:21 ./ drwxr-xr-x 104 root root …

十四.redis哨兵模式

redis哨兵模式 1.概述2.测试3.哨兵模式优缺点 redis哨兵模式基础是主从复制 1.概述 主从切换的技术方法&#xff1a;当主节点服务器宕机后&#xff0c;需要手动把一台从服务器切换为主服务器&#xff0c;这就需要人工干预&#xff0c;费时费力&#xff0c;还会造成一段时间内服…

并查集练习 —岛屿数量(解法一)

题目&#xff1a; 给定一个二维数组matrix&#xff08;char[][]&#xff09;&#xff0c;里面的值不是1就是0&#xff0c;上、下、左、右相邻的1认为是一片岛。返回matrix中岛的数量。 本题共有2种解法&#xff0c;本篇先介绍最快的一种解法—递归。 分析&#xff1a; 递归的方…

RadioButton基本使用

作用&#xff1a;单选框&#xff0c;一般用于设置或者选择某项任务。 常用属性&#xff1a; 常用事件&#xff1a; 选中事件 后台代码&#xff1a; private void radioButton1_CheckedChanged(object sender, EventArgs e){if (radioButton1.Checked){MessageBox.Show(radioB…

JVM之类加载与字节码

1.类文件结构 一个简单的HelloWorld.Java package cn.itcast.jvm.t5; // HelloWorld 示例 public class HelloWorld { public static void main(String[] args) { System.out.println("hello world"); } }编译为 HelloWorld.class 后的样子如下所示&#xff1a; […

SpringBoot3---核心特性---2、Web开发II

星光下的赶路人star的个人主页 大鹏一日同风起&#xff0c;扶摇直上九万里 文章目录 1、内容协商1.1 多端内容适配1.1.1 默认规则1.1.2 效果演示1.1.3 配置协商规则与支持类型 1.2 自定义内容返回1.2.1 增加yaml返回支持 1.2.2 思考&#xff1a;如何增加其它1.2.3 HttpMessageC…

Flutter 实现按位置大小比例布局的控件

文章目录 前言一、如何实现&#xff1f;1、数值转成分数2、RowFlexible布局横向3、ColumnFlexible布局纵向 二、完整代码三、使用示例1、基本用法2、四分屏3、六分屏4、八分屏5、九分屏6、414分屏 总结 前言 做视频监控项目时需要需要展示多分屏&#xff0c;比如2x2、3x3、414…

docker安装MinIO

简介 Minio 是一个面向对象的简单高性能存储服务。使用 Go 语言编写&#xff0c;性能高、具有跨平台性。 Minio 官网为&#xff1a;https://min.io &#xff0c;有一个中文站点&#xff0c;单内容更新不是很及时&#xff0c;建议从原始官网学习。 本文采用 Docker 安装&…

grid map学习笔记3之详解grid_map_pcl库实现point cloud点云转换成grid map栅格地图

文章目录 0 引言1 grid_map_pcl示例1.1 主要文件1.2 示例数据1.3 启动文件1.4 配置文件1.5 主要实现流程1.6 启动示例1.7 示例结果 2 D435i 点云生成栅格地图2.1 D435i 点云文件2.2 修改启动文件2.3 测试和结果2.4 修改配置文件2.5 重新测试和结果 0 引言 grid map学习笔记1已…

Go学习第六天

Golang变量内置pair结构详细说明 变量包括&#xff08;type, value&#xff09;两部分type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string)&#xff0c;concrete type是runtime系统看见的类型类型断言能否成功&#xff0c;取决…

electron+vue3全家桶+vite项目搭建【25】使用electron-updater自动更新应用

文章目录 引入实现效果实现步骤引入依赖配置electron-buidler文件封装版本升级工具类主进程调用版本更新校验渲染进程封装方法调用 测试版本更新 引入 demo项目地址 electron-updater官网 我们不可能每次发布新的版本都让用户去手动下载安装最新的包&#xff0c;而是应用可以…

vue2-组件和插件的区别

1、组件是什么&#xff1f; 组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念&#xff08;组件&#xff09;来实现开发的模式&#xff0c;在vue中每一个.vue文件都可以被视为一个组件。 组件的优势&#xff1a; 降低整个系统的耦合度&#xff0c;在保持接口不变的情况下…

Qt QThread的moveToThread方法使用

Qt线程简介 从 Qt4.4 版本之后&#xff0c;因为 QThread 的 run 方法创建新线程这样实现与 Qt 设计的理念不符&#xff0c;Qt 主推使用 moveToThread 方法来创建新线程。QThread 应该被看做是操作系统线程的接口或控制点&#xff0c;而不应该包含需要在新线程中运行的代码。需…

kodi 电视遥控器 不起作用 ,无法唤醒视频OSD

最近电视盒子上安装了kodi播放器&#xff0c;但是安装好之后&#xff0c;盒子本身的遥控器就不起作用了&#xff0c;之前看网络上其他的文章&#xff0c;需要重新安装tv 控制器&#xff0c;重新映射&#xff0c;比较麻烦&#xff0c;而且观看视频的时候没办法唤起OSD切换字幕和…

在美国,信用分数超过800,这是一个很好的分数吗?

超过800的信用评分被认为是极好的&#xff0c;而且这确实是可以实现的。事实上&#xff0c;美国有很多人的信用评分都在这个范围内。 信用评分是一个数字化的表示&#xff0c;反映了一个人的信用值得信赖度&#xff0c;贷款机构用它来评估这个人偿还债务的可能性。最广泛使用的…

PyCharm安装使用2023年教程,PyCharm与现流行所有编辑器对比。

与PyCharm类似的功能和特性的集成开发环境&#xff08;IDE&#xff09;和代码编辑器有以下几种&#xff1a; Visual Studio Code&#xff08;VS Code&#xff09;&#xff1a;由Microsoft开发&#xff0c;VS Code是一个高度可定制和可扩展的代码编辑器。它支持多种编程语言&am…

为什么list.sort()比Stream().sorted()更快?

真的更好吗&#xff1f; 先简单写个demo List<Integer> userList new ArrayList<>();Random rand new Random();for (int i 0; i < 10000 ; i) {userList.add(rand.nextInt(1000));}List<Integer> userList2 new ArrayList<>();userList2.add…

ORA-48913: Writing into trace file failed, file size limit [50000000] reached

检查某环境的alert_orcl1.log时&#xff0c;发现有很多的ORA-48913报错&#xff0c;细节如下 Sat Jul 22 19:34:04 2023 Non critical error ORA-48913 caught while writing to trace file "/u01/app/oracle/diag/rdbms/orcl/orcl1/trace/orcl1_dw00_138010.trc" E…
最新文章