一,链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表是顺序表在链式存储结构上的另一种实现。链表一般可分为单链表,双链表和循环列表。
单链表:由结点组成,每个结点包含两部分,数据域和指针域,数据域存储真正需要存储的数据部分,指针域是一个指向其后继结点(或前驱结点)的指针。
双链表:双链表也是由结点组成,不同的是除了数据域以外,它有两个指针域,一个指向其前驱结点,另一个指向其后继结点。
循环列表:循环列表一般指单循环列表,即只有一个指针域的链表,在与单链表不同的是,在单链表中其尾结点的指针通常为null,而循环列表其尾结点的指针指向其头节点,构成一个循环结构。
二, 链表实现
这里我们采用的是双链表,即链表结点包含数据域和两个指针域,一个指向前驱结点,一个指向后继结点。
1,定义一个类MyLinkedList实现接口MyList
MyList的定义见文章https://blog.csdn.net/YIXIANG0234/article/details/79901468
实现MyList接口并支持泛型
public class MyLinkedList<T> implements MyList<T> {}
2,成员变量
private Node<T> head; //头节点,指向链表中的第一个结点
private Node<T> rear; //尾结点,指向链表中的最后一个结点
private int length; //链表的长度
3,静态内部类——结点类
//私有的静态内部类,用于构建链表结点
private static class Node<T>
{
public Node<T> prev; //指向前驱结点
public Node<T> next; //指向后继结点
public T data; //存储数据
public Node(Node<T> prev,Node<T>next,T data)
{
this.prev = prev;
this.next = next;
this.data = data;
}
}
4,构造方法
//构造方法,初始化一个带有头尾结点的空链表
public MyLinkedList() {
head = new Node<T>(null,null,null);
rear = new Node<T>(head,null,null);
head.next = rear;
length = 0;
}
5,getNode方法,私有方法,方便链表操作的实现,查找指定位置的结点
private Node<T> getNode(int index)//获取指定位置的结点
{
Node<T> node;
if(index<0 || index>length-1)
throw new RuntimeException("查找操作失败,下标越界:"+index);
//分为两部分查找,下标小于length/2在前半部分查找
if(index<length/2)
{
node = head.next;
for(int i=0;i<index;i++)
node = node.next;
}
else { //下标大于length/2在后半部分查找
node = rear.prev;
for(int i=length-1;i>index;i--)
node = node.prev;
}
return node;
}
6,get方法,根据下标查找结点数据
@Override
public T get(int index) { //查找指定位置的数据
Node<T> node = getNode(index);
if(node==null)
return null;
return node.data;
}
7,indexOf方法,查找某个数据所在结点的下标
@Override
public int indexOf(T data) {
int index = 0;
Node<T> node = head.next; //找到链表第一个节点
while(index<length && node!=null)
{
if(node.data.equals(data))//找到即返回
return index;
node = node.next; //循环查找下一个结点
index++;
}
return -1; //未找到以-1表示
}
8,size方法,返回链表的大小
@Override
public int size() { //返回此链表的长度
return length;
}
9,isEmpty方法,判断链表是否为空
@Override
public boolean isEmpty() { //判断链表是否为空
return length == 0;
}
10,toString方法,给出链表的字符串表示
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("{");
for(int i=0;i<length-1;i++)
{
String s = get(i).toString();
sb.append(s+",");
}
sb.append(get(length-1).toString()+"}");
return sb.toString();
}
11,set方法,修改结点的数据
@Override
public boolean set(int index, T data) {
Node<T> node = getNode(index); //找到该结点,然后更新数据域
node.data = data;
return true;
}
12,remove方法,分别根据下标和数据移除结点
@Override
public T remove(int index) {
Node<T> node = getNode(index); //查找到该结点
node.prev.next = node.next; //调整结点间的前驱和后继关系
node.next.prev = node.prev;
T data = node.data; //保存数据域
node = null; //将该节点的引用置为空,存储空间的处理由垃圾回收机制管理
length--;
return data; //返回数据域
}
@Override
public T remove(T data) {
int index = indexOf(data); //查找到该数据在链表中的位置
Node<T> node = getNode(index); //找到表示该数据的结点
if(node == null)
return null;
node.prev.next = node.next; //调整前驱和后继关系
node.next.prev = node.prev;
T nodeData = node.data;
node = null;
length--;
return nodeData; //返回数据
}
13,add方法,分别在末尾和指定位置插入结点
@Override
public boolean add(T data) { //在链表结尾插入
Node<T> newNode = new Node<T>(rear.prev,rear,data);
newNode.prev.next = newNode;
rear.prev = newNode;
length++;
return true;
}
@Override
public boolean add(int index, T data) { //在指定的位置插入结点
Node<T> node = getNode(index); //获取指定位置的结点
if(node == null)
return false;
//在指定位置的结点前加入新节点,即调整各结点间的前驱和后继关系
Node<T> newNode = new Node<T>(node.prev,node,data);
newNode.prev.next = newNode;
node.prev = newNode;
length++;
return true;
}
14,说明:
方法getNode,add,remove,indexOf,get等是链表实现的核心算法
三,测试
针对MyLinkedList类中的方法编写测试类TestMyLinkedList
import static java.lang.System.*;
public class TestMyLinkedList {
public static void main(String[] args) {
MyLinkedList<String> list = new MyLinkedList<String>();//测试构造方法
list.add("常州"); //测试add方法,在链表末尾添加
list.add("北京");
list.add("China");
list.add("SuZhou");
out.println("添加插入操作:");
out.println(list.toString());//测试头String方法
list.add(1,"南京"); //测试ad方法,在指定位置添加
out.println(list.toString());
out.println("--------------------------------------------------");
list.remove(1); //测试remove方法,删除指定位置元素
out.println("删除操作:");
out.println(list.toString());
list.remove("China");
out.println(list.toString());
out.println("--------------------------------------------------");
out.println("其他操作:");
list.set(2, "苏州");
String data = list.get(2);
out.println(data);
out.println(list.toString());
int index = list.indexOf("北京");
out.println("北京的下标是:"+index);
out.println("链表list的长度为:"+list.size());
out.println("list是否为空:"+list.isEmpty());
}
}
程序运行结果:
添加插入操作:
{常州,北京,China,SuZhou}
{常州,南京,北京,China,SuZhou}
--------------------------------------------------
删除操作:
{常州,北京,China,SuZhou}
{常州,北京,SuZhou}
--------------------------------------------------
其他操作:
苏州
{常州,北京,苏州}
北京的下标是:1
链表list的长度为:3
list是否为空:false
从运行结果可以看出,我们的链表得到了较好的实现,如有bug,望各位斧正。
四,顺序表和链表的优缺点
顺序表:
1,优点:
- 方法简单,存储结构是顺序结构,可以采用数组实现
- 无须为表示结点间的逻辑关系而增加额外的存储空间
- 顺序表具有按元素随机访问的特点
2,缺点:
- 在顺序表中进行插入删除操作时,由于数据在内存中顺序存储,所以需要移动元素,平均移动表中大约一般的元素,n较大时效率低。
- 需要预先分配足够大的存储空间,若估计过大造成内存浪费,若分配过小会溢出,造成顺序表内部实现对数组元素的移动和复制。
链表:
与顺序表相反,对元素的插入删除效率高,而不具有对元素随机访问的特点,所以访问元素的效率很低。
顺序表和链表的选择
如何选择顺序表和链表呢?结合以上的优缺点可以看出,如果对数据基本不存在改动的操作,即很少对数据进行增删操作,反而常做查询的操作,那么选用顺序表。如果需要对数据进行频繁的增删改,查询操作较少,那么选择链表。