线性表实现之二:链表
思路:链表采用链式存储结构,在内部只需要将一个一个结点链接起来(每个结点中有关于此结点下一个结点的引用)即可。
优点:因为每个结点记录下个结点的引用,则在进行插入和删除操作时,只需要改变对应下标下结点的引用即可。
缺点:要得到某个下标的数据,不能通过下标直接得到,需要遍历整个链表。
/**
* 自己用链式存储实现的线性表
*/
public class LinkedList<E> {
private Node<E> header = null;// 头结点
int size = 0;// 表示数组大小的指标
public LinkedList() {
this.header = new Node<E>();
}
public boolean add(E e) {
if (size == 0) {
header.e = e;
} else {
// 根据需要添加的内容,封装为结点
Node<E> newNode = new Node<E>(e);
// 得到当前最后一个结点
Node<E> last = getNode(size-1);
// 在最后一个结点后加上新结点
last.addNext(newNode);
}
size++;// 当前大小自增加1
return true;
}
public boolean insert(int index, E e) {
Node<E> newNode = new Node<E>(e);
// 得到第N个结点
Node<E> cNode = getNode(index);
newNode.next = cNode.next;
cNode.next = newNode;
size++;
return true;
}
/**
* 遍历当前链表,取得当前索引对应的元素
*
* @return
*/
private Node<E> getNode(int index) {
// 先判断索引正确性
if (index > size || index < 0) {
throw new RuntimeException("索引值有错:" + index);
}
Node<E> tem = new Node<E>();
tem = header;
int count = 0;
while (count != index) {
tem = tem.next;
count++;
}
return tem;
}
/**
* 根据索引,取得该索引下的数据
*
* @param index
* @return
*/
public E get(int index) {
// 先判断索引正确性
if (index >= size || index < 0) {
throw new RuntimeException("索引值有错:" + index);
}
Node<E> tem = new Node<E>();
tem = header;
int count = 0;
while (count != index) {
tem = tem.next;
count++;
}
E e = tem.e;
return e;
}
public int size() {
return size;
}
/**
* 设置第N个结点的值
*
* @param x
* @param e
* @return
*/
public boolean set(int index, E e) {
// 先判断索引正确性
if (index > size || index < 0) {
throw new RuntimeException("索引值有错:" + index);
}
Node<E> newNode = new Node<E>(e);
// 得到第x个结点
Node<E> cNode = getNode(index);
cNode.e = e;
return true;
}
/**
* 用来存放数据的结点型内部类
*/
class Node<e> {
private E e;// 结点中存放的数据
Node() {
}
Node(E e) {
this.e = e;
}
Node<E> next;// 用来指向该结点的下一个结点
// 在此结点后加一个结点
void addNext(Node<E> node) {
next = node;
}
}
}
问题:关于add()方法—我们每次添加一个数据时,都需要遍历整个表,得到表尾,再在表尾添加,这是很不科学的。
改进:设立一个Node<E>类的成员变量end来指示表尾,这样每次添加时,就不需要再重新遍历得到表尾。
改进后add()方法如下:
public boolean add(E e) {
if (size == 0) {
header.e = e;
} else {
// 根据需要添加的内容,封装为结点
Node<E> newNode = new Node<E>(e);
//在表尾添加元素
last.addNext(newNode);
//将表尾指向当前最后一个元素
last = newNode;
}
size++;// 当前大小自增加1
return true;
}
PS:在Java语言中,通过链表实现的线性表,对应着Collection接口中的子接口List接口,其数组实现可参考类LinkedList源码(实现的是双向链表)。
/**
* Java 实现的双向链表。
* 注:java自带的集合包中有实现双向链表,路径是:java.util.LinkedList
*/
public class DoubleLink<T> {
// 表头
private DNode<T> mHead;
// 节点个数
private int mCount;
// 双向链表“节点”对应的结构体
private class DNode<T> {
public DNode prev;
public DNode next;
public T value;
public DNode(T value, DNode prev, DNode next) {
this.value = value;
this.prev = prev;
this.next = next;
}
}
// 构造函数
public DoubleLink() {
// 创建“表头”。注意:表头没有存储数据!
mHead = new DNode<T>(null, null, null);
mHead.prev = mHead.next = mHead;
// 初始化“节点个数”为0
mCount = 0;
}
// 返回节点数目
public int size() {
return mCount;
}
// 返回链表是否为空
public boolean isEmpty() {
return mCount==0;
}
// 获取第index位置的节点
private DNode<T> getNode(int index) {
if (index<0 || index>=mCount)
throw new IndexOutOfBoundsException();
// 正向查找
if (index <= mCount/2) {
DNode<T> node = mHead.next;
for (int i=0; i<index; i++)
node = node.next;
return node;
}
// 反向查找
DNode<T> rnode = mHead.prev;
int rindex = mCount - index -1;
for (int j=0; j<rindex; j++)
rnode = rnode.prev;
return rnode;
}
// 获取第index位置的节点的值
public T get(int index) {
return getNode(index).value;
}
// 获取第1个节点的值
public T getFirst() {
return getNode(0).value;
}
// 获取最后一个节点的值
public T getLast() {
return getNode(mCount-1).value;
}
// 将节点插入到第index位置之前
public void insert(int index, T t) {
if (index==0) {
DNode<T> node = new DNode<T>(t, mHead, mHead.next);
mHead.next.prev = node;
mHead.next = node;
mCount++;
return ;
}
DNode<T> inode = getNode(index);
DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
inode.prev.next = tnode;
inode.next = tnode;
mCount++;
return ;
}
// 将节点插入第一个节点处。
public void insertFirst(T t) {
insert(0, t);
}
// 将节点追加到链表的末尾
public void appendLast(T t) {
DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
mHead.prev.next = node;
mHead.prev = node;
mCount++;
}
// 删除index位置的节点
public void del(int index) {
DNode<T> inode = getNode(index);
inode.prev.next = inode.next;
inode.next.prev = inode.prev;
inode = null;
mCount--;
}
// 删除第一个节点
public void deleteFirst() {
del(0);
}
// 删除最后一个节点
public void deleteLast() {
del(mCount-1);
}
}
对比顺序表和链式表
前面已经说过顺序表和链式表各自的特点,这里在重申一遍
数组操作优点:1.通过索引快速定位到下标,查询快速。
缺点:1.数组声明时即需要确定数组大小。当操作中超过容量时,则需要重新声明数组,并且复制当前所有数据;
2.当需要在中间进行插入或者删除时,则需要移动大量元素(size-index个)。
链表操作优点:1.因为每个结点记录下个结点的引用,则在进行插入和删除操作时,只需要改变对应下标下结点的引用即可。
缺点:1.要得到某个下标的数据,不能通过下标直接得到,需要遍历整个链表。