Java线性表之无头非循环单向和双向链表的实现

1.1链表的概念及结构

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
单向、双向
带头、不带头
循环、非循环

  1. 不带头结点的非循环单向链表
    在这里插入图片描述

  2. 不带头结点的非循环双向链表
    在这里插入图片描述

  3. 不带头结点的循环单向链表
    在这里插入图片描述

  4. 不带头结点的循环双向链表
    在这里插入图片描述

  5. 带头结点的非循环单向链表(头结点不存放数据)
    在这里插入图片描述

  6. 带头结点的非循环双向链表(头结点不存放数据)
    在这里插入图片描述

  7. 带头结点的循环单向链表(头结点不存放数据)
    在这里插入图片描述

  8. 带头结点的循环双向链表(头结点不存放数据)
    在这里插入图片描述
    虽然有这么多的链表的结构,但是现在先重点实现无头单向非循环链表和无头双向非循环链表,其余的后面在实现。

1.2链表的简单实现

1.2.1无头单向非循环链表的实现

首先先创建一个节点类,里面包含了data数据域和next域

class Node{
    
    
	public int data;
	public Node next;
	public Node(int data) {
    
    
		this.data = data;
	}
}

然后就是链表的接口和方法实现

public class MyLinkNode {
    
    
	//定义一个链表的头待连接链表
	public Node head;
}

注意:以下方法均在MyLinkNode 类中

(1)显示单链表
public void disPlay() {
    
    
		if(this.head==null) {
    
    
			System.out.println("Over!");
			return;
		}
		Node p = this.head;
		while(p!=null) {
    
    
			System.out.print(p.data+"->");
			p = p.next;
		}
		System.out.println("Over!");
	}
(2)头插法

头插法不需要判空
图示:
在这里插入图片描述
代码:

public void addFirst(int data) {
    
    
		Node node = new Node(data); //创建一个新的节点	
		Node node = new Node(data);
		node.next = this.head;
		this.head = node;
	}
(2)尾插法

尾插法需要判空
图示:
在这里插入图片描述

代码:

 public void addEnd(int data) {
    
    
		 Node node = new Node(data); 
		 //如果头为空,则直接让头指向新节点
		 if(this.head == null) {
    
    
			 this.head = node;
		 }else {
    
    
			 Node p = this.head;
			 //先找到链表的最后一个结点
			 while(p.next!=null) {
    
    
			 	//如果当前结点的next不为空,则说明后面还有结点
			 	//如果为空,则说明已找到最后一个结点的位置
				 p = p.next;
			 }
			 //让最后一个结点的next等于新结点
			 p.next = node;
		 }
	 }
(3)获取单链表的长度
	 public int size() {
    
    
		 int count = 0;
		 if(this.head == null) {
    
    
		 	//如果头为空,返回0
			 return 0;
		 }else {
    
    
			 Node p = this.head;
			 //采用遍历单链表的思路
			 //每走过一个结点,count++,直到当前结点为空
			 while(p!=null) {
    
    
				 count++;
				 p = p.next;
			 }
		 }
		 return count;
	 }
(4)任意位置插入,第一个数据节点为0号下标

要在任意位置插入,首先要判断下标合法性

public boolean checkIndex(int index) {
    
    
		 if(index < 0||index > this.size()) {
    
    
			 return false;
		 }
		 return true;
	 }

然后,查找 (index-1)节点的位置,也就是[前驱节点]的位置,找到返回该节点

 public Node searchPrev(int index) {
    
    
		 Node prev = this.head;
		 int i = 0;
		 while(i<index-1) {
    
    
			 prev = prev.next;
			 i++;
		 }
		 return prev;
	 }

最后,根据下标插入节点(当然,你也可以将这些都写到一个方法中)
图示:
在这里插入图片描述
代码:

	 public void addIndex(int index,int data) {
    
    
		 Node node = new Node(data);
		 Node prev = null;
		 //如果下标位置非法,直接return
		 if(!this.checkIndex(index))
		 {
    
    
			 return;
		 }
		 if(index==0) {
    
    
		 	//如果下标位置为0,则直接调用头插法
			 addFirst(data);
			 return;
		 }
		 if(index==this.size()) {
    
    
		 	//如果下标位置等于链表的长度,调用尾插法
			 addEnd(data);
			 return;
		 }else {
    
    
		 	//接收前驱节点
			 prev = this.searchPrev(index);
			 node.next = prev.next;
			 prev.next = node;
		 }
	 }
(5)查找是否包含关键字key是否在单链表当中
	 public boolean findVal(int data) {
    
    
	 	//思路简单,采用遍历的方法
		 Node p = this.head;
		 while(p!=null) {
    
    
			 if(p.data==data) {
    
    
				 return true;
			 }
			 p = p.next;
		 }
		 return false;
	 }
(6)删除第一次出现关键字为key的节点

首先找到要删除的结点的前驱节点

	 public Node searchPrevNode(int key) {
    
    
		 Node prev = this.head;
		 //同样采用遍历的思路
		 if(findVal(key)) {
    
    
			 while(prev.next!=null) {
    
    
				 if(prev.next.data==key) {
    
    
					 return prev;
				 }
				 prev = prev.next;
			 }
		 }
		 return null;
	 }

然后分情况删除节点
图示:
在这里插入图片描述

代码:

public void remove(int key) {
    
    
		//接收待删除结点的前驱节点
		Node prev = this.searchPrevNode(key);
		Node p = null;
		//如果链表为空,则直接返回
		if(this.head==null) {
    
    
			return;
		}
		//特殊情况 头删
		if(this.head.data==key) {
    
     
			this.head = this.head.next;
			 System.out.println("删除成功!");
			 return;
		}
		//如果不存在前驱节点,说明要删除的节点不存在
		 if(prev==null) {
    
    
			 System.out.println("不存在该值!");
			 return;
		 }else {
    
    
					 p = prev.next;
					 prev.next = p.next;
					 System.out.println("删除成功!");
			 }
	 }
(7)删除所有值为key的节点

图示:(假设删除所有值为1的节点)
在这里插入图片描述

代码:

 public void removeAllKey(int key) {
    
    
		 if(head==null){
    
    
	            return;
	        }
		 Node p = this.head.next;
		 Node prev = this.head;
		 //先调用查找方法确定待删除的结点是否存在
		 if(!findVal(key)) {
    
    
			 System.out.println("不存在该值!");
			 return;
		 }
		 //也可以对头节点的判断放到下面的while循环里
//		 while(this.head.data==key&&this.head.next!=null) {
    
    
//					this.head = this.head.next;
//					p = this.head.next;
//					prev = this.head;	
//		 }
//		 if(p==null&&this.head.data==key) {
    
    
//			 this.head=null;
//		 }
		 while(p!=null) {
    
    
			 if(p.data==key) {
    
    
				prev.next = p.next;
				p = p.next;
			 }else {
    
    
				 prev = p;
				 p = p.next;
			 }
			 if(this.head.data==key) {
    
    
				 this.head = this.head.next;
			 }
		 }
		 System.out.println("删除成功!");
	 }
(8)尾删法
public void popBack() {
    
    
		 Node p = this.head;
		 Node prev = null;
		 if(this.head==null) {
    
    
		 	//如果链表头为空,则直接返回
			 return;
			 }
		 if(this.head.next==null) {
    
    
		 	//如果表头的next为空,则说明要删除的为链表头
			 this.head = null;
			 System.out.println("尾删成功!");
			 return;
		 }
		 //通过遍历找到最后一个节点的前驱节点位置
		 while(p.next!=null) {
    
    
			 prev = p;
			 p= p.next;
		 }
		 prev.next = null;
		 System.out.println("尾删成功!");
	 }
(9)清空单链表
	 public void clear() {
    
    
		 if(this.head==null) {
    
    
			 return;
		 }
		this.head = null;	
	 }
1.2.2无头双向非循环链表的实现

首先先创建一个节点类,里面包含了data数据域、next域、prev域

class Node{
    
    
	public int data;
	public Node next;
	public Node prev;
	public Node(int data) {
    
    
		this.data = data;
	}
}

然后就是链表的接口和方法实现

public class MyDoubleLinkNode {
    
    
	//定义双向链表的头和尾节点用来标记链表的头部和尾部
	public Node head;//头节点
	public Node tail;//尾节点
}

注意:以下方法均在MyDoubleLinkNode类中

(1)显示单链表
public void display() {
    
    
		if(this.head==null) {
    
    
			System.out.println("Over!");
			return;
		}
		Node p = this.head;
		while(p!=null) {
    
    
			//依旧采用循环遍历的方式
			System.out.print(p.data+"->");
			p = p.next;
		}
		System.out.println("Over!");
	}
(2)头插法

图示:
在这里插入图片描述
代码:

	public void addFirst(int data) {
    
    
		Node node = new Node(data);
		if(this.head==null) {
    
    
			//如果表头为空,则让head和tail都指向新节点node
			this.head = node;
			this.tail = node;
		}else {
    
    
			node.next = this.head;
			this.head.prev = node;
			this.head = node;
		}
	}
(3)尾插法

图示:
在这里插入图片描述

代码:

	public void addLast(int data) {
    
    
		Node node = new Node(data);
		if(this.head==null) {
    
    
			//如果表头为空,则让head和tail都指向新节点node
			this.head = node;
			this.tail = node;
		}
		//由于有表尾指针,所以不用遍历寻找最后一个节点的位置
		this.tail.next = node;
		node.prev = this.tail;
		this.tail = node;
	}
(4)单链表长度
	public int size() {
    
    
		int count = 0;
		Node p = this.head;
		while(p!=null) {
    
    
			//依然采用遍历思路
			count++;
			p = p.next;
		}
		return count;
	}
(5)任意位置插入,第一个数据节点为0号下标

图示:
在这里插入图片描述
代码:

 public boolean addIndex(int index,int data) {
    
    
		 if(index < 0) {
    
    
			 return false;
		 }
		 int size = this.size();
		 if(index == 0) {
    
    
		 	//index==0调用头插法
			 this.addFirst(data);
			 return true;
		 }
		 if(index == size) {
    
    
		 	//index==size调用尾插法
			 this.addLast(data);
			 return true;
		 }
		 Node p = this.head;
		 Node node = new Node(data);
		 int n = index - 1;
		 while(n>0) {
    
    
		 	//通过遍历index-1次找到插入位置的前驱节点
			 p = p.next;
			 n--;
		 }
		 p.next.prev = node;
		 node.next = p.next;
		 node.prev = p;
		 p.next = node;
		 return true;
	 }
(6)查找是否包含关键字key是否在单链表当中
	 public boolean contains(int key) {
    
    
		 if(this.head==null) {
    
    
			 return false;
		 }
		 Node p = this.head;
		 while(p!=null) {
    
    
		 	//依然采用遍历思路
			 if(p.data == key) {
    
    
				 return true;
			 }
			 p = p.next;
		 }
		 return false;
	 }
(7)删除第一次出现关键字为key的节点

图示:
在这里插入图片描述
代码:

	 public void remove(int key) {
    
    
	 	//如果表中没有该关键字直接返回
		 if(!this.contains(key)) {
    
    
			 return;
		 }
		 Node p = this.head;
		 while(p!=null) {
    
    
		 	//当关键字相等时,需要分情况
			 if(p.data==key) {
    
    
				 if(p == this.head) {
    
    
				 	//当删除表头节点时
					 this.head = this.head.next;
					 this.head.prev = null;
				 }else {
    
    
					 p.prev.next = p.next;
					 if(p.next!=null) {
    
    
						 //删除的不是尾节点
						 p.next.prev = p.prev;
					 }else {
    
    
					 	//说明删除的是尾节点
						 this.tail = p.prev;
					 }
				 }
				 //到这删除完毕
				 return;
			 }else {
    
    
			 	//只要关键值相等p就不往下走了,换言之不相等,p才会往下走
				 p = p.next;
			 }
		 }
	 }
(8)删除所有值为key的节点
 public void removeAllKey(int key) {
    
    
		 if(this.head==null) {
    
    
			 return;
		 }
		 Node p = this.head;
		 while(p!=null) {
    
    
			 if(p.data==key) {
    
    
				 if(p == this.head) {
    
    
					 this.head = this.head.next;
					 this.head.prev = null;
				 }else {
    
    
					 p.prev.next = p.next;
					 if(p.next!=null) {
    
    
						 //删除的不是尾节点
						 p.next.prev = p.prev;
					 }else {
    
    
						 this.tail = p.prev;
					 }
				 }
			 }
			 //跟删除第一次出现关键字为key的节点不同的是让p一直往下走,直到p==null,就可以删除所有的值为关键字的节点了
				 p = p.next;
		 }	
	 }
(9)清空单链表
	 public void clear() {
    
    
		 if(this.head == null) {
    
    
			 return;
		 }
	 	 Node p = this.head;
		 while(p!=null) {
    
    
		 	//通过遍历让每个节点的next和prev都指向null
			 Node s = p.next;
			 p.next = null;
			 p.prev = null;
			 p = s;
		 }
		 this.head = null;
		 this.tail = null;
	 }

1.3总结

  1. 链表以节点为单位存储,不支持随机访问
  2. 任意位置插入时间复杂度为O(1)。
  3. 无扩容问题,插入一个开辟一个空间。
  4. 对于链表一些方法,建议多画图。

猜你喜欢

转载自blog.csdn.net/weixin_46078890/article/details/113147587