前面学习一种数据结构-队列,在队列的实现中有一种实现叫链表实现,今天就学习一下链表这种数据结构。
链表
百度百科权威描述:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表可以在多种编程语言中实现。像Lisp和Scheme这样的语言的内建数据类型中就包含了链表的存取和操作。程序语言或面向对象语言,如C,C++和Java依靠易变工具(这个不知道是什么意思)来生成链表。
根据上面关于链表的描述,链表有一下特点:
- 存储结构是非连续的、非顺序的;
- 链表相邻的节点有关系;
- 链表的插入节点和删除节点很方便,只要修改节点关系即可;
- 链表遍历取值较为复杂,需要从头取值,知道找到节点为止;
单向链表
单向链表,每个节点都包含下个节点的信息,最后一个节点除外(最后一个节点的后一个节点为null)。
ps:前面用链表实现队列的时候,就可以认为是一个单向链表
首先先定义一个接口,让所有的实现都实现这个接口:
public interface Linked<E> {
/**
* 返回当前链表的数量
* @return size
*/
public int size();
/**
* 在尾部增加一个节点, 并返回该节点
* @param e
* @return E
*/
public E add(E e);
/**
* 删除一个怨怒射
* @param i 元素位置
* @return E
*/
public E remove(int i);
/**
* 在指定位置插入一条元素
* @param e 被插入的元素
* @param index 被插入的位置的元素
* @return E
*/
public E insert(E e, int index);
/**
* 取出某个位置的元素
* @param i 要取出的位置
* @return E
*/
public E get(int i);
}
下面用java实现一个单向链表:
public class SingleLinked<E> implements Linked<E> {
// 头部节点
private Node head;
//尾部节点
private Node tail;
//链表元素的个数
private int size;
@Override
public int size () {
return this.size;
}
/**
* 如果head为空,则说明当前尚无节点,就把添加的值赋给头节点,
* 把头结点的值赋给tail节点
* 如果head节点不为空,则把新增的节点赋值给tail节点的下一个节点
* 并把新节点赋值给tail
* @param e
* @return
*/
@Override
public E add (E e) {
if (null == head) {
head = new Node(e, null);
tail = head;
} else {
Node temp = new Node(e, null);
tail.next = temp;
tail = temp;
}
size++;
return e;
}
/**
* 删除指定位置的节点
* 删除逻辑:
* 如果删除的是head,判断tail是否和head相等,如果相等,就把head和tail设成null
* 如果不相等,就把head的next设成head
* 如果删除的不是head,就把pre的next设成删除节点的next
* @param index
* @return
*/
@Override
public E remove (int index) {
if (index > size) {
return null;
}
Node pre = null;
Node next = head;
Node temp = null;
int sum = 0;
while (sum <= index) {
if (sum == index) {
if (null == pre) {
temp = head;
if (tail == head) {
head = null;
tail = null;
} else {
head = head.next;
}
} else {
temp = next;
pre.next = next.next;
}
break;
} else {
pre = next;
next = pre.next;
}
sum++;
}
size--;
return temp.e;
}
/**
* 在指定位置插入节点
* 操作逻辑:
* 找到指定位置的节点,把上一个节点的下一个节点属性设为要插入的节点,插入的节点的下一个节点设为原来位置的节点;
* 如果插入的位置为head节点的位置,则把插入的节点设为head
*
* @param e 被插入的元素
* @param index 被插入的位置的元素
* @return
*/
@Override
public E insert (E e, int index) {
if (index > size) {
return null;
}
Node pre = null;
Node next = head;
Node eNode = new Node(e, null);
int sum = 0;
while (sum <= index) {
if (sum == index) {
eNode.next = next;
if (null == pre) {
eNode.next = head;
head = eNode;
} else {
pre.next = eNode;
}
break;
} else {
pre = next;
next = pre.next;
}
sum++;
}
size++;
return e;
}
/**
* 默认取出第一个元素
* @return
*/
@Override
public E get() {
return this.get(0);
}
/**
* 取出指定位置的节点
* 取值逻辑:
* 如果取第一个节点,判断头head和tail是否相等,如果相等,就把head和tail都赋值为null,
* 如果不相等,就把取值节点的上一个节点(pre)的下一个节点设为取值节点的下一个节点;
* 如果取的不是第一个节点,判断是否是tail节点,如果是就把上一个节点设为tail
* 如果不是,就把取值节点的上一个节点(pre)的下一个节点设为取值节点的下一个节点;
* @param i 要取出的位置
* @return
*/
@Override
public E get (int i) {
Node next = head;
Node pre = head;
Node temp ;
int sum = 0;
if (i >= size) { // 查找的位置大于总数量时
return null;
}
for (;;) {
if (sum == i) {
temp = next;
if (i == 0) {// 如果取第一个就把下一个节点设为head
if (head.equals(tail)) {
head = null;
tail = null;
} else {
head = pre.next;
}
} else {
if (null == next.next) {
pre.next = next.next;
tail = pre;
} else {
pre.next = next.next;
}
}
break;
} else {
pre = next;
next = next.next;
}
sum++;
}
E e = null;
if (null != temp) {
e = temp.getE();
size--;
}
return e;
}
/**
* 用于存储链表的结构
*/
private class Node {
private E e;
private Node next;
public Node () {
}
public Node (E e) {
this.e = e;
}
public Node (E e, Node next) {
this.e = e;
this.next = next;
}
public E getE () {
return e;
}
public void setE (E e) {
this.e = e;
}
public Node getNext () {
return next;
}
public void setNext (Node next) {
this.next = next;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SingleLinked.Node)) {
return false;
} else {
Node node = (Node) o;
if (!e.equals(node.e)) return false;
return next != null ? next.equals(node.next) : node.next == null;
}
}
@Override
public int hashCode() {
int result = e.hashCode();
result = 31 * result + (next != null ? next.hashCode() : 0);
return result;
}
}
}
测试代码:
public class LinkedTest {
public static void main(String[] args) {
SingleLinked<String> singleLinked = new SingleLinked<>();
singleLinked.add("123");
singleLinked.add("456");
singleLinked.add("789");
singleLinked.add("147");
singleLinked.add("258");
System.out.println("链表的大小===" + singleLinked.size());
System.out.println("第一个节点==" + singleLinked.get(0));
System.out.println("链表的大小===" + singleLinked.size());
//singleLinked.insert(""369", 0);
System.out.println("删除的节点===" + singleLinked.remove(2));
System.out.println("链表的大小===" + singleLinked.size());
//System.out.println("第二个节点==" + singleLinked.get());
//System.out.println("第三个节点==" + singleLinked.get());
//System.out.println("第四个节点==" + singleLinked.get());
//System.out.println("第五个节点==" + singleLinked.get());
//System.out.println("第六个节点==" + singleLinked.get());
//System.out.println("第七个节点==" + singleLinked.get());
}
}
测试结果:
链表的大小===5
第一个节点==123
链表的大小===4
删除的节点===147
链表的大小===3
上面的一个简单实现是我用业余时间修修改改几天才写出来的,深刻体会到做比说难。在写的过程中发现,虽然百科里面解释说链表插入删除比较简单,但是实现过程中发现并不简单。
经常听说的一个冷笑话,如何把大象装进冰箱里?
第一步,打开冰箱们;第二部把大象塞进去;第三部,关上冰箱们;
如何插入一个节点:
第一步,找到上一个节点;第二部,把节点的下一个位置设为插入的节点;第三部把插入节点的下一个节点设为原来节点的下一个节点;
解释起来非常的简单,实际实现起来并不简单,查找节点要从头开始遍历,还要考虑其它问题;同理删除节点也存在同样的问题;
***ps:
代码中的实现都是以位置来决定插入位置,而不是以已有节点来决定插入位置,如果由节点决定,后插操作相对容易一点,前插操作依然需要遍历*
以节点决定插入位置后插实现:
/**
* 后插实现
* @param e
* @param node
* @return
*/
public E insert (E e, Node node) {
if (null != node ) {
Node temp = new Node(e);
node.next = temp;
tail = temp;
size++;
}
return e;
}
前插实现和SingleLinked中的插入实现类似就不再写了。
因此,单向链表优化方向是减少删除和插入时查找节点的时间复杂度。目前的实现是通过从头开始遍历直到找到节点为止,时间复杂读为O(n),如果能根据节点或者位置直接找到要操作的节点,就能减少时间复杂度到O(1);
基于以上分析,要建立位置或者节点与节点的对应关系。
我目前的想法是用节点的hashcode与节点的上一个节点简历对应关系,至于index位置因为随着插入和删除会随时变化不具备唯一型暂不考虑;
修改后的插入和删除:
private Map<String, Node> map = new HashMap<>();
public E add (E e) {
String hashcode;
if (null == head) {
head = new SingleLinkedTwo.Node(e, null);
hashcode = String.valueOf(head.hashCode());
tail = head;
map.put(hashcode, null);
} else {
SingleLinkedTwo.Node temp = new SingleLinkedTwo.Node(e, null);
hashcode = String.valueOf(temp.hashCode());
tail.next = temp;
map.put(hashcode, tail);
tail = temp;
}
size++;
return e;
}
public E remove (Node node) {
String hashcoed = String.valueOf(node.hashCode());
Node temp = map.get(hashcoed);
E e = node.e;
if (null == temp) {
head = null;
head = head.next;
if (null == head) {
tail = null;
}
} else {
temp.next = node.next;
if (null == node.next) {
tail = temp;
}
}
size--;
return e;
}
上面实现了两个方法,只是为了表达自己的想法,没有测试是否正确。
总结
这次学习了解了链表的特征,目前先实现一个单向链表,后面继续实现双向链表和循环链表。