Java数据结构与算法之单链表

一、前言

数组对于内存的要求比较高,需要一块连续的内存空间来存储,如果你申请的一个100M大小的数组,当内存中没有连续的,足够大的存储空间时,即便剩余内存可用空间大于100MB,申请仍然会是失败的。第二方面在当顺序表中内存容量不足时,则必须重新申请一个数组,将原来的数据复制到新的数组中,数据搬移消耗了大量的性能和时间(数据达到一定量级)。在顺序表中元素的插入和删除也可能存在数据的搬移,也会消耗一定的性能和时间,而这时链表这种数据结构就出现了。

   

 二、链表的特点

先来解下链表,如上图,链表是和数组恰恰是相反的一种基础数据结构。链表是一组零散的内存结构,而连接这些内存的介质我们称之为指针(也就是图中的next,而在Java语言中,指针就是引用,也就是下一节点的地址值)。在单链表中,我们把图中内存块称之为“”结点”,结点包括存储数据和指向下一节点的后继指针(next)。在单链表中有两个特殊的结点,第一个结点我们称之为头结点用来存储链表的基地址,而最后一个结点称为尾结点,尾结点指向一个空地址(null),表示链表的终点。链表同数组一样也具有插入、查找和删除的操纵,下面我就从这个几个方面来实现下单链表。

三、单链表的实现细节

3.1、首先先实现链表的存储单元,也就是结点类,结点类包括存储数据和后继指针两个元素

        /**
	 * 结点类
	 * @author Administrator
	 */
	public class Node<E> {
		private E data;//存储数据
		private Node<E> next;
		public Node(E data){
			this.data=data;
		}
		public Node(E data,Node<E> next){
			this.data=data;
			this.next=next;
		}
	}

3.2、实现单链表的顶级接口,包含链表的具体操作方法

package cn.cast.LinkedList;
/**
 * 链表的顶级接口,包含所有
 * 链表的基本方法
 * @author Administrator
 *
 */
public interface SingList<T> {
	//判断链表是否为空
	boolean isEmpty();
	//链表长度
	int length();
	//获取元素
	T get(int index);
	//根据index添加元素data
	T set(int index,T data);
	//根据index添加元素data
	boolean add(T data);
	//根据index插入元素
	boolean add (int index,T data);
	//删除指定位置的元素
	T remove(int index);
	//删除指定元素
	boolean remove(T data);
	//根据data移除所有相同元素
	T removeAll(T data);
	//清空链表
	void clear();
	//是否包含特定元素
	boolean contains(T data);
	//获取元素的位置
	int indexOf(T data);
	//根据data值查询最后一个出现在顺序表的下标
	int lastIndexOf(T data);
	// 输出格式
	String toString();
}

3.3、实现顶级接口SingList<T>,声明链表的头结点代表链表的开始位置以及两个基本的构造方法。

public class MySingList<T> implements SingList<T>{
	private Node<T> head;//头结点
	private int length;//链表的长度
	
	public MySingList(Node<T> head){
		this.head=head;
	}

判断链表是否为空:由于链表的结构特点,头结点是链表的开始位置,所有判断链表是否为空,只要判断链表的头结点是为空

	/**
	 * 判断链表是否为空
	 */
	@Override
	public boolean isEmpty() {
		return this.head==null;
	}

链表的长度大小:链表是存储的单元是结点,所有结点的数量也就是链表的长度。结点中存储着后继结点的引用地址,链表的终点是尾结点的指针为null,因此只需要判断在首结点不为空的情况下,依次沿着后继指针循环,直到指针为null,就可以得到链表的长度。

       /**
	 * 获取链表的长度
	 */
	@Override
	public int length() {
		Node<T> p=head;//将头结点赋值到临时变量p
		if(p!=null){
			while(p.next!=null){
				p=p.next;//获取指向下一结点的指针
				length++;//链表长度+1
			}
		}
		return length;
	}

获取元素:由于链表的结构特点,获取链表首先声明变量count(从0开始)来表示结点指向的位置,需要依次按照后继指针循环直到获取结点从而取得结点存储的数据。从程序来看,链表获取元素在最坏情况下需要依次遍历所结点,最好情况下时间复杂度为O(1),最坏情况下时间复杂度O(n)。从而看出链表获取的元素效率要比顺序表低。而在LinkedList中,获取元素已经采用二分查找来进步提高获取元素的效率。(方法还存在一些问题,可利用二分查找优化提高效率)

	/**
	 * 获取元素
	 */
	@Override
	public T get(int index) {
		if (head != null && index >= 0) {
			int count = 0;// 用来元素的索引位置
			Node<T> p = this.head;// 存储结点的临时变量
			// 获取对应的索引位置
			while (p != null && count < index) {
				p = p.next;
				count++;
			}
			if (p != null) {
				return p.data;
			}
		}
		return null;
	}

修改指定位置的元素:在获取元素位置时,与获取元素位置相同,修改元素只是在获取元素后用新的存储数据替换旧的存储数据data。在时间复杂度上,与获取元素相同。

        /**
	 *修改元素 
	 */
	@Override
	public T set(int index, T data) {
		if (head != null && index >= 0) {
			int count = 0;// 用来元素的索引位置
			Node<T> p = this.head;// 存储结点的临时变量
			// 获取对应的索引位置
			while (p != null && count < index) {
				p = p.next;
				count++;
			}
			//获取需要替换的存储数据,用新的存储数据替换旧的存储数据,返回旧值
			if(p!=null){
			    T oldData=p.data;
			    p.data=data;
			    return oldData;
			}
		}
		return null;
	}

添加元素:单链表添加元素主要分为四种场景,a.空链表插入结点;b.头结点插入元素,新增结点为头结点;c.中间情况插入节点;d.尾结点插入元素,也就是在插入在单链表末尾,下面从流程图分析下四种场景 。

 

 下面具体来分析下四种具体情况:

a.插入空链表:此时链表为空,插入结点即为空结点

if(head==null){
	head=new Node<T>(data,null);
}

b.头结点前插入元素:新插入结点即为新头结点,插入结点的指针指向原头结点

if(index==0 && head!=null){
	Node<T> node =new Node<T>(data);//新结点实例
	node.next=head;//将新结点的后继指针指向原头结点
	head=node;//新结点赋值为头结点
}

c.中间结点插入元素:在链表中间位置插入新元素,首先获取插入位置的前一个结点,将插入位置的索引向前移动,将新插入元素的后继指针指向原插入位置的结点。

if(index>0 && head!=null && index<length()){
	int scanIndex = 0;// 扫描索引
	Node<T> p = this.head;// 存储结点的临时变量
	// 获取对应的索引位置
	while (p != null && scanIndex < index-1) {
		p = p.next;//将索引向前移
		scanIndex++;
	}
	Node<T> node =new Node<T>(data);
	node.next=p.next;//新结点后继指针指向原结点的后继结点
	p.next=node;			
}

d.链表末尾插入元素:在链表末尾插入结点,即新插入结点为新的尾结点,同时,尾结点后继指针为null。

if(index>0 && head!=null && index=length()){
	int scanIndex = 0;// 扫描索引
	Node<T> p = this.head;// 存储结点的临时变量
	// 获取对应的索引位置
	while (p != null && scanIndex < index-1) {
		p = p.next;//将索引向前移
		scanIndex++;
	}
		Node<T> node =new Node<T>(data);
		node.next=p.next;//新结点后继指针指向原结点的后继结点
		p.next=node;
		return true;
	}

在末尾和中间插入代码可以合并,最终代码如下:

	@Override
	public boolean add(int index, T data) {
		if(head==null){
			head=new Node<T>(data,null);
			return true;
		}
		if(index==0 && head!=null){
			Node<T> node =new Node<T>(data);
			node.next=head;
			head=node;
			return true;
		}
		if(index>0 && head!=null && index<=length()){
			int scanIndex = 0;// 扫描索引
			Node<T> p = this.head;// 存储结点的临时变量
			// 获取对应的索引位置
			while (p != null && scanIndex < index-1) {
				p = p.next;//将索引向前移
				scanIndex++;
			}
			Node<T> node =new Node<T>(data);
			node.next=p.next;//新结点后继指针指向原结点的后继结点
			p.next=node;
			return true;
		}
		
		return false;
	}

在尾部插入结点:

@Override
public boolean add(T data) {
	return this.add(length(),data);
}

删除指定位置的元素:删除元素与插入结点类型,首先要先找到要删除指定位置的索引位置分为三种情况,删除头结点,删除中间位置结点,删除尾结点;


	@Override
	public T remove(int index) {
		// 链表尾空
		if (isEmpty()) {
			return null;
		}
		// 删除头结点
		if (index == 0) {
			Node<T> oldHead = head;
			head = head.next;
			return oldHead.data;
		}
		// 删除中间结点
		if (index != 0 && index <= length()) {
			int scanIndex = 0;// 扫描索引
			Node<T> p = this.head;// 存储结点的临时变量
			// 找到目标结点的前一个结点
			while (p != null && scanIndex < index - 1) {
				p = p.next;// 将索引向前移
				scanIndex++;
			}
			Node<T> targetNode = p.next;// 目标结点
			if (targetNode != null) {
				T oldData = targetNode.data;
				p.next = targetNode.next;
				targetNode = null;// 将目标结点置为null,切断关联关系
				return oldData;
			}
		}
		return null;
	}

其他方法:

	//清空链表
    @Override
	public void clear() {
		this.head=null;
	}
    //判断链表是否含有某元素结点
    @Override
	public boolean contains(T data) {
		//数据合法性校验
		if(data==null){
			return false;
		}
		//链表为空时
		if(isEmpty()){
			return false;
		}
		//依据后继指针循环链表
		Node<T> p = head;
		while(p!=null){
			T nodeData= p.data;
			if(data.equals(nodeData)){
				return true;
			}
			p=p.next;
		}
		return false;
	}

总结:从上面分析的结构可以看出,由于链表不需要保存内存数据的连续性,不需要搬移结点,因此链表的插入和删除动作时非常快;同时,由于不支持随机访问,无法根据首地址和下标获取元素,需要依次遍历甚至是整个链表,直达找到相应的节点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
/* * 基于链表实现树结构 */ package dsa; public class TreeLinkedList implements Tree { private Object element;//树根节点 private TreeLinkedList parent, firstChild, nextSibling;//父亲、长子及最大的弟弟 //(单节点树)构造方法 public TreeLinkedList() { this(null, null, null, null); } //构造方法 public TreeLinkedList(Object e, TreeLinkedList p, TreeLinkedList c, TreeLinkedList s) { element = e; parent = p; firstChild = c; nextSibling = s; } /*---------- Tree接口中各方法的实现 ----------*/ //返回当前节点中存放的对象 public Object getElem() { return element; } //将对象obj存入当前节点,并返回此前的内容 public Object setElem(Object obj) { Object bak = element; element = obj; return bak; } //返回当前节点的父节点;对于根节点,返回null public TreeLinkedList getParent() { return parent; } //返回当前节点的长子;若没有孩子,则返回null public TreeLinkedList getFirstChild() { return firstChild; } //返回当前节点的最大弟弟;若没有弟弟,则返回null public TreeLinkedList getNextSibling() { return nextSibling; } //返回当前节点后代元素的数目,即以当前节点为根的子树的规模 public int getSize() { int size = 1;//当前节点也是自己的后代 TreeLinkedList subtree = firstChild;//从长子开始 while (null != subtree) {//依次 size += subtree.getSize();//累加 subtree = subtree.getNextSibling();//所有孩子的后代数目 } return size;//即可得到当前节点的后代总数 } //返回当前节点的高度 public int getHeight() { int height = -1; TreeLinkedList subtree = firstChild;//从长子开始 while (null != subtree) {//依次 height = Math.max(height, subtree.getHeight());//在所有孩子中取最大高度 subtree = subtree.getNextSibling(); } return height+1;//即可得到当前节点的高度 } //返回当前节点的深度 public int getDepth() { int depth = 0; TreeLinkedList p = parent;//从父亲开始 while (null != p) {//依次 depth++; p = p.getParent();//访问各个真祖先 } return depth;//真祖先的数目,即为当前节点的深度 } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值