讲了顺序表的实现呢,下面我们来讲单链表
每个节点包括两部分,哪两部分,一部分是存放数据的,另一部分是存放地址的,存放下一个节点的地址,这叫单链表,单链表也就是
单向链表,有单向的就应该有双向的,双向表该怎么办啊,每个节点包括三部分,一个数据,一个是指向后面节点的,一个是指向前面
节点的,这就是双向链表,现在我们来看一下最简单的单向链表
链表是一系列的存储数据元素的单元通过指针串联起来的,因此每个单元至少有两个域,单向表有两个域就可以了,一个是存放数据的,
一个是指向下一个元素的,我们称之为节点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);
}
}