链表
1.前言
上一篇博客说了普通线性表,我们发现了该表的缺陷是在插入数据或添加数据的时候会大量移动数组元素,从而造成时间复杂度的提升,
为了解决这个问题,需要使用链表,链表是相对于数组更加先进的一种数据结构,更加灵活,而且便于管理。
为了便于理解链表,画了一个示意图,这里以双向链表为例:
2.原理分析
1.双向链表的每一个元素可以看成一个Node节点,每一个Node节点应该拥有要存储的数据类型,上一个节点的引用,下一个节点的引用,
链表有开始和结束,所以应该有开始节点(beginNode)和末尾节点(endNode)。
因为对于外部呈现的是LinkedList这个类,所以Node节点应该在LinkedList内部,而且是私有的并且是静态的。私有的是为了是的外部无法直接访问,
静态是为了减少new对象的开销。头结点(beginNode)和末尾节点(endNode)属于特殊节点,所以应该作为LinkedList的成员。
因为是链表,存储位置是不确定的,但是为了在会获取指定索引未出的值应该有一个索引代表元素的位置(这里的位置不是物理空间位置,是顺序的索引位置)。
LinkedList和Node节点应该是通用的类,可以接受不同数据类型的值,所以应该有泛型。
3.模拟实现
分析完毕,我们开始模拟实现双向链表,这里选择双向链表是因为,它比单向链表更加具有通用性。双向链表理解之后,单向链表也就没什么了。
第一步确定目标,搭建框架。
3.1 搭建框架
/**
* 模拟双向链表
* @author Administrator
*
*/
public class MyLinkedList<T>{
/**
* 内部静态节点类,保存当前节点和下一个节点
*/
private static class Node<T>{
//Node节点的内容
}
//链表的属性字段
private int size;//当前链表大小
private int count=0;//计数器
private Node<T> beginMark;//开始节点
private Node<T> endMark;//末尾节点标记
//构造函数初始化一个空链表
public MyLinkedList(){clear();}
/**
* 清空链表
*/
public void clear(){
}
/**
* 返回当前链表的长度
* @return
*/
public int getSize(){
return this.size;
}
/**
* 向链表中插入一个数据
* 插入前begin->end
* 插入后begin->node1->node0->end,从右向左插入节点
* @param t
*/
public void add(T t){
}
/**
* 通过索引标志位得到对应的数据
* @param idx
* @return
*/
public T get(int idx){
return null;
}
/**
* 删除指定索引位置的节点,并返回被删除的节点的元素值
* @param idx
* @return
*/
public T remove(int idx){
return null;
}
/**
* 向指定索引位置添加元素
* @param idx
* @param t
* @return
*/
public boolean insert(int idx,T t){
return true;
}
}
3.2 Node节点类
分析:
Node节点类是关键,我们首先分析一下,一个Node节点应该包含三个内容,第一:要存放的数据,第二:该节点的直接前置节点,
第三:该节点的直接后置节点。有了这三个关键,我们才能实现双向链表。
分析完开始创建:
/**
* 内部静态节点类,保存当前节点和下一个节点
*/
private static class Node<T>{
/**
* 构造方法
* @param data:节点中存放的数据类型
* @param prev:上一个节点
* @param next:下一个节点
*/
public Node(T data,Node<T> prev,Node<T> next){
this.data = data;
this.prev = prev;
this.next = next;
}
public T data;//当前节点内容
public Node<T> next ;//下一个节点
public Node<T> prev ;//上一个节点
}
需要注意的是Node类应该可以存放各种对象,所以应该上加上泛型
3.3 clear():清空链表
该方法作用是请空链表,我们来分析:
1. 使得链表的(头结点)beginMark.next指向(尾节点)endMark,endMark.prev指向头结点beginMark.
2. size索引清空
3. 只需要两部操作,即可完成清空链表,剩下的元素交给虚拟机的垃圾回收期来管理就好了。
/**
* 清空链表
*/
public void clear(){
beginMark = new Node<T>(null, null, null);//创建一个空节点作为开始节点
endMark = new Node<T>(null, beginMark, null);//创建末尾的节点
beginMark.next = endMark;//开始节点的下一个节点指向结尾节点
this.size = 0;//空节点的长度是0
this.count++;//计数器加一
}
3.4 add(T t):添加元素
分析:
1. 添加元素首先创建一个新的Node节点接收元素的值。
2. 把开始节点beginMark指向新创建的节点,新节点指向endMark。
/**
* 向链表中插入一个数据
* 插入前begin->end
* 插入后begin->node1->node0->end,从右向左插入节点
* @param t
*/
public void add(T t){
Node<T> newNode = new Node<T>(t, beginMark, beginMark.next);
beginMark.next.prev = newNode;//首先把末尾节点的prev节点更改为当前节点
beginMark.next = newNode;//再把开始节点的next节点指向当前节点
this.size++;//当前容量加1
}
注意:
我们假设beginMark在左边,endMark在右边,元素插入的时候是按照从右到左的顺序来进行的,如下所示:
beginMark<—>an<—>an-1<—>…<—>a2<—>a1<—>endMark
3.5 T get(int idx):返回指定索引位置的元素
分析:
首先,链表创建的时候并不像数组一样有下表,但是链表是有序的,我们使用了size来记录链表的长度。
这里的size就相当于数组下表的效果,我们根据当前索引位置idx和size从右到左遍历,知道idx位置就是
我们想要找的那个元素节点。
/**
* 通过索引标志位得到对应的数据
* @param idx
* @return
*/
public T get(int idx){
//先判断idx是否非法
if( idx<0 || idx>this.size-1){
throw new RuntimeException("索引位置非法!");
}
Node<T>data = endMark;//接收返回的值
for(int i=0;i<idx+1;i++){
data = data.prev;
}
return data.data;
}
3.6 T remove(int idx):删除指定索引位置的元素并返回旧值
分析:
该方法和get(int idx)
方法类似,首先需要找到指定索引位置的node,然后删除该元素,
再把该元素的prev和next分别指向node的next和prev。
/**
* 删除指定索引位置的节点,并返回被删除的节点的元素值
* @param idx
* @return
*/
public T remove(int idx){
//先判断idx是否非法
if( idx<0 || idx>this.size){
throw new RuntimeException("索引位置非法!");
}
Node<T>data = endMark;//接收返回的值
for(int i=0;i<idx+1;i++){//遍历得到要删除的节点
data = data.prev;
}
Node<T>prev = data.prev;//要删除的节点的前一个节点
prev.next = data.next;//当前节点的prev的next指向当前节点的下一个节点
data.next.prev = prev;//当前节点的next指向当前节点的prev节点
this.size--;//索引标记为减1
return data.data;//返回删除的节点的数据
}
3.7 boolean insert(int idx,T t):指定索引位置添加元素
分析:
加入要在AB中间插入C,首先遍历找到b元素的索引位置,然后让A.next指向C,C.prev指向A,C.next指向B,B.prev
指向C,这样就完成了插入操作。最后别忘了让size++;
/**
* 向指定索引位置添加元素
* @param idx
* @param t
* @return
*/
public boolean insert(int idx,T t){
//先判断idx是否非法
if( idx<0 || idx>this.size){
throw new RuntimeException("索引位置非法!");
}
//创建一个包含插入元素的空节点
Node<T> newNode = new Node<T>(t,null,null);
Node<T> data = endMark;//把末尾元素赋值给中间节点变量
//根据索引找到该位置的元素
for(int i=0;i<idx+1;i++){
data = data.prev;//找到索引位置的元素节点
}
//尾部的prev指向newNode节点
endMark.prev = newNode;
//被插入的索引位置的节点的next指向newNode节点
data.next = newNode;
//新插入的节点prev指向就得索引位置的data节点
newNode.prev = data;
//新插入节点next指向末尾节点endMark
newNode.next = endMark;
this.size++;//索引位置加1
return true;
}
测试输出
/**
* 测试主函数
* @param args
*/
public static void main(String[] args) {
MyLinkedList<Integer>list = new MyLinkedList<>();//创建一个新的空的双向链表
list.add(10);
list.add(20);
list.add(30);
System.out.println("total size:"+list.getSize());//3
//遍历list
for(int i=0;i<list.getSize();i++){
System.out.println(list.get(i));
}
System.out.println("remove node:"+list.remove(0));
System.out.println("total size:"+list.getSize());//2
System.out.println("inser:"+list.insert(0, 1000));
//遍历list
for(int i=0;i<list.getSize();i++){
System.out.println(list.get(i));
}
new LinkedList<String>().iterator();
}
输出结果:
total size:3
10
20
30
remove node:10
total size:2
inser:true
1000
20
30