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

链表

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识汲取者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值