模拟实现单链表(三级)

讲了顺序表的实现呢,下面我们来讲单链表

每个节点包括两部分,哪两部分,一部分是存放数据的,另一部分是存放地址的,存放下一个节点的地址,这叫单链表,单链表也就是

单向链表,有单向的就应该有双向的,双向表该怎么办啊,每个节点包括三部分,一个数据,一个是指向后面节点的,一个是指向前面

节点的,这就是双向链表,现在我们来看一下最简单的单向链表


链表是一系列的存储数据元素的单元通过指针串联起来的,因此每个单元至少有两个域,单向表有两个域就可以了,一个是存放数据的,

一个是指向下一个元素的,我们称之为节点Node,一个是存放数据的,一个是作为指针域指向下一个元素的,我们要想实现单相链表的话

首先要给出Node类

package com.learn.datastructure;

/**
 * 新建一个类Node
 * 单链表的节点,这个节点有两部分组成
 * 
 * 这个就是一个单链表的结构,单链表的最后一个元素他的指针写成空,
 * 这个单链表有一个特点,他只能通过一个节点找到他的后继,
 * 我知道a1了,我就知道他的后继是谁,但不能知道他的前驱是谁
 * 有人说我就想知道他的前驱,可以吗,可以,就是比较繁琐,
 * 你就写两个指针呗,一个只想前一个,一个只想后一个,
 * 或者你就设一个指针,我想找一下a1的后继,我想找一下a1当前前驱
 * 我想找a2的前驱,我怎么知道a2的后继呢,但是我怎么知道a2的前驱是a1
 * 从a0开始,我们是不是找a2的前驱,那你就从a0开始找,a0的下一个节点数据是a2吗
 * 不是,怎么办,指针往下移,指向下一个,他的下一个节点是a2吗,是,那就是你了
 * 得这么来找
 * 
 * 单链表的特征是只能通过前驱节点找到后继节点,不能从后继直接找到前驱,你想从头来找比较繁琐
 * 可以实现,我们来看一下查询操作,添加操作,删除操作,这个我们讲单链表的时候讲过
 * 我要找第三个元素,就是索引是3的吧,怎么来找,索引是3的,只能一个一个的找,效率比较低
 * 为什么呢,因为他的地址是不连续的,是没有规律的,同样我要找值是a3的元素,那你更得一个一个找
 * 所以这一块他的查询是比较低的,后面我们回来讲怎么来动这个指针
 * 
 * 添加操作:这是一个节点,我们要把一个新的节点加到两个节点之间,改一下指针,改两个指针就可以了
 * 当然你首先要找到前驱节点,从头找找到他
 * 
 * 删除操作:删除的话就和添加类似,我准备把这个节点删了,怎么就把它删了,待删除的节点,Java
 * 里面就自动就回收了
 * 
 * 下边我们说一个特别重要的内容,下面所有的操作都是基于这一点的,第一个节点它是存数据的,第二个
 * 节点也是存数据的,为了方便,我们都是这样来处理,在整个链表的前面再加上一个节点,整个节点不存任何
 * 数据,它是不存任何数据的,这个头结点head,它是一直存在,他不存任何数据,为什么这么做,就是
 * 为了我们编程的方便,相当于你删第一个元素,你如果没有这个头节点的话,你删第一个元素和删其他节点的操作
 * 是不一样的,他的处理代码会有所不同,但是加上头结点之后会怎么办,相当于第一个节点也不是首节点了,
 * 他可以向后面节点一样处理,为了是程序更加的简洁,我们通常在单链表的最前面添加一个哑元节点,叫头结点
 * 他里面不存储任何实质的对象,他的next指向真正的第一个,存储数据的节点,这么一来有什么好处,
 * 对空表和非空表的处理,对首节点的处理,都是一样的,代码就简化了,下面我们就来实现了
 * 
 * 
 * 
 * @author Leon.Sun
 *
 */
public class Node {
	
	/**
	 * 他什么也不做,就相当于两个值都为空
	 */
	public Node() {
		
	}

	/**
	 * 只给数据赋值
	 * 这个类没有任何的难度
	 * @param data
	 */
	public Node(Object data) {
		super();
		this.data = data;
	}

	/**
	 * 提供 一个构造方法
	 * 同时给数据和指针赋值的
	 * 单链表每个节点都写好了
	 * @param data
	 * @param next
	 */
	public Node(Object data, Node next) {
		super();
		this.data = data;
		this.next = next;
	}

	/**
	 * 代表要存储的数据
	 * 我们先把private去了,为什么要去了他,因为在同一个包里面
	 * 就可以直接点data,点next,这样就更简单,要不让私有的,
	 * 出了这个类,就需要get和set,我们为了便于理解我们把private去了
	 * 这样一来就只限于当前包的
	 */
	Object data;
	
	/**
	 * 引用指向下一个节点
	 * 我们取个名字叫next
	 * 关键它是什么类型,选项的下一个元素是不是还是Node,
	 * 所以这里写一个Node
	 */
	Node next;

	Object getData() {
		return data;
	}

	void setData(Object data) {
		this.data = data;
	}

	Node getNext() {
		return next;
	}

	void setNext(Node next) {
		this.next = next;
	}
	
}
package com.learn.datastructure;

/**
 * 线性接口表
 * 我怎么觉得这些方法我们都学过,
 * 是不是都学过,大同小异,
 * 注意这是一个接口,和存储结构无关
 * 无论是顺序表还是链表,都要把这些功能给我实现,
 * 首先我们来讲顺序表啦
 * 
 * 我们现在写了一个添加,然后带着查找
 * 那下面就是写删除了,删除和这个应该是类似的,
 * 这个需要大家好好的想一想
 * 举一反三,看能不能自己把删除写出来
 * 
 * 节点的类,我们已经实现了添加操作,
 * 同时把查询操作也写了,写了一个get
 * @author Leon.Sun
 *
 */
public interface List {
	
	// 返回线性表的大小,即数据元素的个数。
	/**
	 * 线性表里又几个元素
	 * @return
	 */
	public int size();
	
	// 返回线性表中序号为 i 的数据元素
	/**
	 * 获取第i个元素
	 * @param i
	 * @return
	 */
	public Object get(int i);

	// 如果线性表为空返回 true,否则返回 false。
	/**
	 * 线性表是不是空的,
	 * @return
	 */
	public boolean isEmpty();

	// 判断线性表是否包含数据元素 e
	/**
	 * 线性表是不是包括某个元素
	 * 是不是查找
	 * @param e
	 * @return
	 */
	public boolean contains(Object e);

	// 返回数据元素 e 在线性表中的序号
	/**
	 * 某个元素在线性表的索引
	 * @param e
	 * @return
	 */
	public int indexOf(Object e);

	// 将数据元素 e 插入到线性表中 i 号位置
	/**
	 * 添加
	 * 这是加到指定位置,线性表的插入操作
	 * @param i
	 * @param e
	 */
	public void add(int i, Object e);

	// 将数据元素 e 插入到线性表末尾
	/**
	 * 这两个添加有什么区别,
	 * 这是加到最后,又插入就有添加
	 * @param e
	 */
	public void add(Object e);

	// 将数据元素 e 插入到元素 obj 之前
	/**
	 * 在谁谁之前加
	 * @param obj
	 * @param e
	 * @return
	 */
	public boolean addBefore(Object obj, Object e);

	// 将数据元素 e 插入到元素 obj 之后
	/**
	 * 在谁谁之后加
	 * 这个大家自己都可以来写
	 * @param obj
	 * @param e
	 * @return
	 */
	public boolean addAfter(Object obj, Object e);

	// 删除线性表中序号为 i 的元素,并返回之
	/**
	 * 删除第几个,这是删除第几个元素
	 * 比如我删除第5个元素
	 * @param i
	 * @return
	 */
	public Object remove(int i);

	// 删除线性表中第一个与 e 相同的元素
	/**
	 * 删除指定值的元素
	 * 比如我删除值是30的元素
	 * @param e
	 * @return
	 */
	public boolean remove(Object e);

	// 替换线性表中序号为 i 的数据元素为 e,返回原数据元素
	/**
	 * 修改,把第几个元素改成新的值
	 * @param i
	 * @param e
	 * @return
	 */
	public Object replace(int i, Object e);

}
package com.learn.datastructure;

/**
 * 单链表就是他了,同样也要实现List
 * @author Leon.Sun
 *
 */
public class SingleLinkedList implements List {
	
	/**
	 * 在这里面首先要提供一个头结点
	 * 他本来就存在的,头结点首先是一个Node类型
	 * 名字叫head,头结点,不存储数据,为了编程方便,
	 * head节点我们给他指向new Node(),
	 */
	private Node head = new Node();
	
	/**
	 * 我们再存一个整形的变量,size是一个有几个节点
	 * 一共有几个元素,有人问没有他不行吗,没有他也可以
	 * 但是有他的话要数量我们就直接拿就行了,没有他的话每次需要数一下
	 * 那不是效率更低了,一共有多少个节点,
	 */
	private int size;

	@Override
	public int size() {
		/**
		 * size太简单了,直接size
		 */
		return size;
	}

	/**
	 * 这个可就不一样了,可就和顺序表不一样了
	 * 不能通过索引直接计算定位,而需要从头结点开始进行查找
	 * 这个并不难,只要把添加写完,这里只是一个循环,移动指针就可以
	 */
	@Override
	public Object get(int i) {
		Node p = head;
		/**
		 * 找索引等于5的,
		 */
		for(int j=0;j<=i;j++) {
			p = p.next;
		}
		/**
		 * p指向这个节点,我怎么把他的789找出来
		 * 这不是p.data吗,是不是叫他啊,
		 */
		return p.data;
	}

	/**
	 * 是不是空的
	 */
	@Override
	public boolean isEmpty() {
		return size==0;
	}

	@Override
	public boolean contains(Object e) {
		return false;
	}

	@Override
	public int indexOf(Object e) {
		return 0;
	}

	/**
	 * 这两个有什么区别,这个是加到指定位置,我们说谁是谁的特殊情况
	 * 只要把这个实现了,下面的就非常的简单,
	 */
	@Override
	public void add(int i, Object e) {
		
		/**
		 * 我们在这里写一个完整的,如果i的位置错误报异常
		 * i可以等于size,等于size就是加到最后
		 * 我的这个界是i
		 */
		if(i<0 || i>size) {
			throw new MyArrayIndexOutOfBoundsException("数组指针越界异常:" + i);
		}
		
		/**
		 * 做这个之前要先做一个操作,找到前一个节点
		 * 怎么找到前一个节点,从头开始找,
		 * 定义一个变量,header值是0X2012,
		 * 你把head存的地址值赋值给p,那就相当于p和head都指向于第一个节点
		 */
		Node p = head;
		
		/**
		 * 然后我们来个循环,不是第i个吗,
		 * 这个一直做一个操作,做什么操作啊,让这个p指向下一个节点
		 * j等于0的时候动一下,这个操作是什么,现在我的p要指向后一个节点,
		 * 一条语句就够了,0X4012是哪个变量的值,是p点next的,
		 * 
		 */
		for(int j=0;j<i;j++) {
			/**
			 * 把我们的p.next的值赋值给p就可以了
			 * 有人说暂时还不理解,我们还有呢,怎么样我们的p指向0X5012了
			 * 因为我们的p指向这一块的话我们知道,怎么表示123,123怎么表示
			 * p.data是p的数据,我们想在这个节点和这个节点中间加数据,
			 * 效率比较低,需要逐个的来找,i要是5的话,j小于5那就是4,
			 * 这里不能差不多,要对就是对,要错就是错,一个都不能错,
			 * 目前分析的是没有发现任何问题,找到这个节点了这里该怎么办,
			 * 
			 */
			p = p.next;
		}
		
		
		/**
		 * 我们就从中间某个节点来加吧
		 * 先写思路,没有思路怎么写代码呢
		 * 新创建一个节点,指向新节点的前驱
		 * 第一步新创建一个节点,只是666,我们现在调用的是add方法
		 * 我们在栈里创建一个add方法变量,add里面有一个变量Node
		 * newNode,这个地址指向了0X5555,newNode就指向了0X5555
		 * 这是我们的第一步,第二步怎么办,第二步存一个后继的地址,第三步是把前驱的所存的指向
		 * 地址改成newNode地址,我们选择存数据的构造方法,我们只要存值就可以
		 */
		// Node newNode = new Node(e);
		
		/**
		 * 我们这么来写,他就没有值了
		 * 
		 */
		Node newNode = new Node();
		
		/**
		 * 他直接给data赋值
		 */
		newNode.data = e;
		
		/**
		 * 这个可以不写,因为本来默认就是空
		 * 你明白为什么Node里面的属性不加private
		 * 因为加private他就不让你直接访问data了
		 * 基于这一点考虑,真可谓用心良苦,也就是newNode.data
		 * 存指针的就是newNode.next,我要给next赋值了,
		 * 
		 */
		// newNode.next = null;
		
		/**
		 * 指明新节点的直接后继
		 */
		newNode.next = p.next;
		
		/**
		 * 指明新节点的直接后继节点
		 * newNode是指向新节点
		 * 把newNode的值赋值给p.next
		 */
		p.next = newNode;
		
		/**
		 * 指明新节点的直接前驱节点
		 */
		
		/**
		 * 加了这个节点之后别忘了再做一件事size++
		 * 数量加加,你加了这么多值size没有变过,
		 * 每增加一个节点这个size就要加1
		 */
		size++;		
		
	}

	/**
	 * 我们来写添加吧,这个是加到最后,这个是上面的特殊情况,
	 */
	@Override
	public void add(Object e) {
		this.add(size, e);
	}

	@Override
	public boolean addBefore(Object obj, Object e) {
		return false;
	}

	@Override
	public boolean addAfter(Object obj, Object e) {
		return false;
	}

	@Override
	public Object remove(int i) {
		return null;
	}

	@Override
	public boolean remove(Object e) {
		return false;
	}

	@Override
	public Object replace(int i, Object e) {
		return null;
	}
	
	/**
	 * 链表里面哪有elementData
	 */
	@Override
	public String toString() {
		if(size==0) {
			return "[]";
		}
		
		StringBuilder builder = new StringBuilder("[");
		
		/**
		 * 我们定义一个Node指向head
		 */
		Node p = head.next;
				
		for(int i=0;i<size;i++) {
			/**
			 * 然后循环加p.data
			 */
			builder.append(p.data);
			if(i!=size-1) {
				builder.append(",");
			}
			
			/**
			 * 同时要移动指针到下一个节点
			 * 因为不移动永远指向第一个节点
			 * 死循环 了
			 */
			p = p.next;			
			
		}
		
		builder.append("]");
		
		return builder.toString();
	}

}
package com.learn.datastructure;

/**
 * 这个永远都不会出现越界的问题,底层不是数组
 * 
 * @author Leon.Sun
 *
 */
public class TestSingleLinkedList {

	public static void main(String[] args) {
		
		// java.util.ArrayList list;
		/**
		 * 代码不用变,变的是底层不一样了,顺序表里的删除需要大量的移动
		 * 链式表里的删除不需要移动
		 * 
		 * 这条语句发生了什么,我们在栈里面建立一个变量list,我们画SingleLinkedList的时候
		 * 它里面有属性吗,有两个属性,他就在堆里面创建了一个节点,这里面有两个元素,第一个元素叫head,
		 * 第二个叫size,那不用说了,size是0,head是new了一个Node,head的data是null,
		 * 栈里面的list变量指向了堆里面的一块空间,head指向了一个头结点,0X2012是头结点,不存储数据的,
		 * 头结点在这里,下面我们要一个一个的添加了,代码我们先不写,当我们加123会怎样,就会创建一个节点,
		 * 这是往最后加的,这个地址是多少,是0X4012,可不能说0X2013,不可能只占一个字节,2013那这两个
		 * 只占一个字节,不可能的,创建一个新的节点,值是123,怎么头就只想他了,就是在head里存一个地址指向它
		 * 刚刚又新建的节点索引是0,就是第0个节点,再加个321,321给一个地址是0X5012,往下456,0X6012
		 * 还有678,0X8012,地址是没有规律的,画了图一行代码也没有写,这是为什呢,第一个当我们一个一个添加节点
		 * 的时候,一共有7个节点在这里,
		 */
		List list = new SingleLinkedList();
		
		 list.add(123);
		 list.add(321);
		 list.add(456);
		 list.add(678);
		 list.add(789);
		 list.add(111);
		 list.add(222);
		 list.add(111);
		 list.add(222);
		 
		 /**
		  * 我们只要这个代码写了,上面的代码就写了,为什么呢,刚才已经说了
		  * 它是他的一种特殊情况,加在最后就是加在中间的一种特殊情况,
		  * 我们就不在20加了,那我们加在哪里比较合适,加在4和5中间,
		  * 那我应该写几,我应该写4还是写5,应该写5,为什么,因为写了4,
		  * 就是3和4中间了
		  * 
		  * 10就报java.lang.NullPointerException这个异常了
		  * 
		  */
		 list.add(10, 666);
		
		System.out.println(list.size());
		
		System.out.println(list.isEmpty());
		
		/**
		 * 同样是get(3),数组里面是怎么get的,直接计算就可以了
		 * 链表里面就是一个一个数了
		 * 
		 * get(3)怎么是null了,因为我们的get没有写
		 */
		System.out.println(list.get(5));
		
		System.out.println(list);
		
	}
	
}
package com.learn.datastructure;

/**
 * 这个叫自定义异常,他要继承RuntimeException
 * 这里面也非常的简单,只要实现两个构造方法,
 * 我们要一个无参的,和一个带有异常信息的
 * @author Leon.Sun
 *
 */
public class MyArrayIndexOutOfBoundsException extends RuntimeException {

	public MyArrayIndexOutOfBoundsException() {
		super();
	}

	public MyArrayIndexOutOfBoundsException(String message) {
		super(message);
	}

}

猜你喜欢

转载自blog.csdn.net/Leon_Jinhai_Sun/article/details/89602843