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

文章目录

链表

1、基本介绍

  • 什么是链表

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

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

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

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

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

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

  • 链表的分类

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

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

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

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

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

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

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

2、单向链表

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

  • 单链表的基本操作

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

2.1 带头节点的单向链表

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

  • 插入结点示意图:

  • 删除结点示意图:

  • 修改结点示意图:


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

测试类:

java 复制代码
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("程序已退出");
	}
}

链表实现类:

java 复制代码
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 练习

  • 反转链表示意图
  • 合并链表示意图

测试类:

java 复制代码
/**
 * 练习题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();
	}
}

链表实现类:

java 复制代码
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. 添加的时候,需要进行双向绑定!

  • 双向链表插入示意图

  • 双向链表删除示意图

测试类:

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

示意图:

双向链表实现类:

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

java 复制代码
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不指向空,而是指向开始节点。

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

测试类

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

实现类

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

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

示意图:

删除结点:

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

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

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

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

java 复制代码
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. 引用域:是链表结点中一片很小的空间,用来存放后继结点的地址 ↩︎
相关推荐
闪电悠米3 小时前
力扣hot100-41.缺失的第一个正数-原地哈希详解
数据结构·算法·哈希算法
WL学习笔记4 小时前
堆(数据结构)
数据结构·
wabs6668 小时前
关于图论【卡码网100.最大岛屿的面积的思考】
数据结构·算法·图论
yuannl109 小时前
图的最短路径Dijkstra
数据结构
zander25810 小时前
138. 随机链表的复制
数据结构·算法·链表
过期动态11 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
来一碗刘肉面11 小时前
栈在递归中的应用
数据结构·算法
皓月斯语11 小时前
程序设计语言的特点
开发语言·数据结构·c++
邪修king11 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
Adios79419 小时前
设置交集大小至少为2
数据结构·算法·leetcode