N日一篇——Java实现链式表

Posted 从零开始的智障生活

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了N日一篇——Java实现链式表相关的知识,希望对你有一定的参考价值。

线性表详解:数据结构第四篇——(一般)线性表(基于C语言)_从零开始的智障生活的博客-CSDN博客 

这里出现了一个Java 的-128到127的问题,解法放在:Java的int -128到127的问题与解决办法_从零开始的智障生活的博客-CSDN博客

一、实现链表的注意点

实现链表的方式有很多种,但是真的要应用,就要考虑到很多情形。

链表有很多类型:

单链表:有一个头指针指向单链表的第一个结点

循环单链表:有一个头指针指向单链表的第一个结点单链表还有一个尾指针其指针域指向单链表的第一个有效数据域的结点,即带头结点,即头结点的数据域无效,就指向头结点的下一节点,如果不带头结点,即头结点的数据域有效,就指向头结点。

双链表:有一个头指针指向单链表的第一个结点每一个结点的指针域还有一个前驱结点指针。也就是可以回头。这样就是往前是一个单链表,往后也是一个单链表。

循环双链表:有一个头指针指向单链表的第一个结点,单链表还有一个尾指针其指针域指向单链表的第一个有效数据域的结点,每一个结点的指针域还有一个前驱结点指针

问:那不如直接选择循环双链表来实现不是最好?

答:考虑到空间复杂度,假设一个链表有10000个大小,有100个单链表,而数据域和指针域耗费空间大小一样大。

  1. 单链表就只有一个数据域,指针域只有一个指向下一节点的指针,就需要20000*100个大小;
  2. 双链表因为结点指针域增加了一个前驱指针,就要30000*100个大小;
  3. 循环单链表因为单链表增加了一个尾指针,就要20000*100+100个大小;
  4. 循环双链表因为不仅结点指针增加了一个指针,单链表还增加了一个前驱指针,就要30000*100+100个大小。

以单链表2000000为基准,双链表增加了1000000个大小,循环单链表增加了100个大小,循环双链表增加了1000100个大小。对于数据基数比较小的情形还无所谓,但是如果基数增加,那么就会显得差别很大。所以选择合适的链表类型很重要。

二、实现链表结点的类ListNode

针对单链表和循环链表的结点类型

public class ListNode 
	private Object element;
	private ListNode next;
	public ListNode(Object it,ListNode nextval) element = it;next = nextval;
	public ListNode(ListNode nextval) next=nextval;
	public Object getElement() return element;
	public void setElement(Object element) this.element = element;
	public ListNode getNext() return next;
	public void setNext(ListNode next) this.next = next;

 针对双链表和循环双链表的结点类型:

public class DoubleListNode extends SingleListNode
	private	DoubleListNode prev;
	public DoubleListNode(Object it, DoubleListNode nextval,DoubleListNode prev) 
		super(it, nextval);
		this.prev = prev;
	
	public DoubleListNode getPrev() return prev;
	public void setPrev(DoubleListNode prev) this.prev = prev;

 二、用接口表示链表的抽象数据类型

public interface SingleLinkedList 

	void inserttoHead(Object item);
	void inserttoRear(Object item);// 在尾部插入结点
	boolean isEmpty();//判断是否为空
	void insertbyLocate(int locate, Object item);
	void removebyLocate(int locate);
	void removebyValue(Object item);
	Object accessbyValue(Object item);
	Object accessbyLocate(int locate);
	int Length();

 三、实现链表数据类型

基本的单链表的实现方式:

/**
 *  实现一个有头结点单向链表
 *  一个单向链表的字段只有数据项和后继结点,当然如果想的话可以直接设置一个参数length来统计链表长度。
 */
public class SLList implements LinkedList 
	private SingleListNode head;
	
	public SingleListNode getHead() 
		return head;
	
	public void setHead(SingleListNode hd) 
		this.head = hd;
	
	// 初始化一个带头结点的单链表
	public SLList() 
		head=new SingleListNode(null);
	
	/**
	 * 插入到链表头
	 * @param item
	 */
	@Override
	public void inserttoHead(Object item) 
		// 将头结点的子节点设置为新节点的子节点
		// 将新节点作为头结点的子节点
		head.setNext(new SingleListNode(item,head.getNext()));
	
	/**
	 * 由于每追加一个结点都要遍历到底一次,故时间复杂度为O(n)
	 * @param L
	 * @param item
	 */
	// 插入到尾部
	@Override
	public void inserttoRear(Object item) 
		// 设置一个当前节点用于遍历
		SingleListNode curNode = head;
		// 遍历到最后一个结点
		while(curNode.getNext() != null)
			curNode = curNode.getNext();
		// 用传进来的数据创建一个新节点,将新的结点放到最后
		curNode.setNext(new SingleListNode(item, null));
	// 在尾部插入结点
	@Override
	public boolean isEmpty() return head.getNext()==null;//判断是否为空

	/**
	 * 由插入位序的随机性知,按位序插入的时间复杂度也是O(n)
	 * @param L
	 * @param locate
	 * @param item
	 */
	@Override
	public void insertbyLocate(int locate, Object item) 
		// 首先判断传入的单链表为空时locate必须是1,位序必须>1
		if((isEmpty() == true&&locate!=1)||locate<1)return;
		// 设置一个当前节点用于遍历
		SingleListNode curNode = head;
		// 遍历到第locate-1个结点数据有效的结点
		for(int i=0;(i<locate)&&(curNode!=null);i++) 
			curNode = curNode.getNext();
		
		// 当要插入的位序前一个结点为空,则不能插入
		if(curNode!=null) 
			curNode.setNext(new SingleListNode(item, curNode.getNext()));
		
	
	/**
	 * 按位序删除结点:时间复杂度为O(n)
	 * locate,从1计,到n
	 * @param locate
	 */
	
	public void removebyLocate(int locate) 
		if(isEmpty()==true||locate<1) return;
		SingleListNode curNode = head; // 带头结点,所以从头结点的子节点计
		// 遍历到第locate-1个结点数据有效的结点 或 尾结点的下一节点null
		for(int i = 0;(i<locate-1)&&(curNode!=null);i++) 
			curNode=curNode.getNext();
		
		// 当要删除的结点父节点为空或要删除的是null则其不用被删除
		if((curNode!=null)&&(curNode.getNext()!=null)) 
			// 将要删除结点的后一个结点接到父节点的指针域
			curNode.setNext(curNode.getNext().getNext());
		
	
	/**
	 * 按值删除:时间复杂度为O(n)
	 * @param item
	 */
	public void removebyValue(Object item) 
		/**
		 * 用一个游标来记录当前节点的父节点
		 */
		if(isEmpty()==true)return;
		SingleListNode curNode = head.getNext();
		SingleListNode curNode_father = head;
		while((curNode!=null)&&(curNode.equals(new SingleListNode(item,curNode.getNext()))==false)) 
			curNode_father = curNode;
			curNode = curNode.getNext();
		
		if((curNode!=null)&&(curNode.equals(new SingleListNode(item,curNode.getNext())))) 
			curNode_father.setNext(curNode.getNext());
//			System.out.println("删除"+item);
		
	
	public void removebyValue_veryslow(Object item) 
		// 当链表为空,退出
		if(isEmpty()==true)return;
		SingleListNode curNode = head.getNext();
		/**
		 * 找到与这个值匹配的结点的结点
		 * 遍历到的位置有3种:链表中间、链表尾、找不到
		 * 金蝉脱壳式脱离
		 * 找到后,如果此结点在链表中间,将此结点子结点的数据域赋给它,并将子节点的指针域赋给它,实现逻辑剔除
		 * 如果此结点在链表尾,则将此结点设为null,
		 * 如果找不到此结点,则不用管它
		 * 这种方式处理的基础是此链表必须至少要有两个结点
		 */
		// 当单链表只有一个有效数据结点时
		if(curNode.getNext()==null) 
			if(curNode.getElement()==item) 
				head.setNext(null);
				System.out.println(item);
			
			return;
		
		// 当前结点子节点非空且未找到这个节点时,继续遍历,当curNode是尾结点的前一节点时
		// 在单链表中间寻找,在尾结点的前面结点找到
		while((curNode.getElement()!=item)&&(curNode.getNext().getNext()!=null)) 
			curNode = curNode.getNext();
		
		// 如果已经在单链表中间找到此结点
		if(curNode.getElement()==item) 
			
			// 当此结点不是尾结点
			curNode.setElement(curNode.getNext().getElement());
			curNode.setNext(curNode.getNext().getNext());	
			System.out.println(item);
		
		// 找不到此结点只能是遇到尾结点的前一节点,
		else// 只能是遇到了尾结点的前一节点
			// 如果尾结点是是要找的结点
			if(curNode.getNext().getElement()==item) 
				curNode.setNext(null);
				System.out.println(item);
			
				
		
	
	/**
	 * 时间复杂度为O(n)
	 * @param item
	 * @return
	 */
	@Override
	public Object accessbyValue(Object item) 
		if(isEmpty()==true) return null;
		SingleListNode curNode=head;
		// 找到这个与item匹配的结点
		while((curNode.equals(new SingleListNode(item,curNode.getNext()))==false)&&(curNode!=null))
			curNode = curNode.getNext();
		if(curNode!=null) 
			return curNode.getElement();
		
		return null;
	
	/**
	 * 时间复杂度为O(n)
	 * @param locate
	 * @return
	 */
	@Override
	public Object accessbyLocate(int locate) 
		if((isEmpty()==true&&locate!=1)||locate<1) return null;
		SingleListNode curNode=head;
		// 找到这个第locate个有效数据 或 到尾结点的下一节点null
		for(int i=0;i<=locate||(curNode!=null);i++)
			curNode = curNode.getNext();
		return curNode;
	
	/**
	 * 时间复杂度为O(n)
	 * @return
	 */
	@Override
	public int Length() 
		/**
		 * 头结点不计,计带有有效数据的结点
		 */
		int i=0;
		SingleListNode curNode = head;
		while(curNode.getNext() != null) 
			curNode=curNode.getNext();
			i++;
		
		return i;
	
单链表实现操作时间复杂度
插入到头插入到尾按位插入按值删除按位删除按位索引按值索引长度是否为空
O(1)O(n)O(n)O(n)O(n)O(n)O(n)O(n)O(1)

可以看出单链表的【插入到头】和【是否为空】因为单链表一个字段head的原因,时间复杂度只有O(1)。明显可以发现,如果增加一个指向尾结点的指针tail,插入到尾的时间复杂度就是O(1),这个tail可以单纯指向尾结点,而tail指针域可以指向null,就单纯定位尾结点,也可以指向头结点head的子节点,这就是循环单链表的数据类型。但这样如果结点基数比较大,肯定会增加大量的空间消耗。

也可以增加一个字段length来保存单链表长度信息,但是这样要增加大量空间。可以看情况是否增加。

测试内容:

public class TestSLL 
	public static void main(String[] args) 
		SLList sll = new SLList();
		SingleListNode head = sll.getHead();
		SingleListNode curNode = sll.getHead();
		long  startTime = System.currentTimeMillis();// 精确到毫秒
		long startnsTime = System.nanoTime();//精确到微秒
		for(int i=1;i<1001;i++) 
			sll.inserttoHead(i);
		
		long endTime = System.currentTimeMillis();
		long endnsTime = System.nanoTime();//精确到纳秒
		System.out.println("插入到头1~1000时间:"+(endTime-startTime)+"ms\\t"+(endnsTime-startnsTime)+"ns");
		long startRemoveTime = System.nanoTime();
		while(head.getNext()!=null) 
			sll.removebyLocate(1);
		
		long endRemoveTime = System.nanoTime();
		System.out.println("按位从头删除1~1000时间:"+(endRemoveTime-startRemoveTime)+"ns");
		long startTime_inserttoRear = System.nanoTime();
		for(int i=1;i<1001;i++) 
			sll.inserttoRear(i);// 单链表顺序 1、...、1000
		
		long endTime_inserttoRear = System.nanoTime();
		System.out.println("插入到尾:"+(endTime_inserttoRear-startTime_inserttoRear)+"ns");
		int i=0;
		long startTime_removebyvalue_allfromhead = System.nanoTime();
		while((i++)<1001) 
			sll.removebyValue(i);// 删除顺序 1、...、1000
		
		long endTime_removebyvalue_allfromhead = System.nanoTime();
		System.out.println("按值从头删除:"+(endTime_removebyvalue_allfromhead-startTime_removebyvalue_allfromhead)+"ns");
	

运行结果:

插入到头1~1000时间:1ms	354800ns
按位从头删除1~1000时间:354000ns
插入到尾:4238300ns
按值从头删除:3050300ns

以上是关于N日一篇——Java实现链式表的主要内容,如果未能解决你的问题,请参考以下文章

N日一篇——Java实现栈

N日一篇——Java实现队列

N日一篇——Java实现队列

N日一篇——Java实现队列

N日一篇——Java实现栈

N日一篇——二叉树