什么是链表?
链表是一种在物理上非连续,非顺序的数据结构,由若干节点(node)所组成。
单向链表的每一个节点包含两个部分,一部分存放数据的变量,另一部分是指向下一个节点的指针。链表的第一个节点称为头节点,最后一个节点称为尾结点,尾结点的指针指向空。与数组按照索引来随机查找数据不同,对于链表的其中一个节点A,我们只能根据节点A的next指针来找到该节点的下一个节点B,在根据节点B的next指针找到一下个节点C,以此类推。
那么如何找到该节点的前一个节点呢?可以使用双向链表。
单向链表存储结构如图:
节点代码如下:
/**
* 定义存储数据的节点
*/
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
什么是双向链表?
双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有data和next指针还包含一个指向前置节点的prev指针。
双向链表存储结构如图:
链表的实现
1、查找节点
查找元素时,链表不像数组那样可以通过索引来进行快速定位,只能从头节点向后一个一个的查找。
2、更新节点
如果不考虑查找节点的过程,链表的更新过程非常节点,直接把旧数据替换成新数据即可。
3、插入节点
与数组类似,链表插入节点同样可以分为三种情况:
-
头部插入
头部插入,分为两个步骤:
1、把新节点的next指针指向当前头节点。
2、把新节点变为链表的头节点。
-
尾部插入
尾部插入是最简单的情况,直接把尾结点的next执行指向新节点即可。
-
中间插入
中间插入,分为两个步骤:
1、新节点的next指针指向插入位置的节点。
2、插入位置的前置节点的next指针指向新节点。
4、删除节点
同样分为三种情况:
-
头部删除
把当前链表的头结点更新为原头节点的next指针即可。
-
尾部删除
把尾结点的前置节点的next指针指向为空即可。
-
中间删除
把要删除的前置节点的next指针指向要删除节点的next指针即可。
整体代码如下:
/**
* 描述:链表。
* <p>
* Create By ZhangBiao
* 2020/5/11
*/
public class LinkedList<E> {
/**
* 虚拟头结点
*/
private Node dummyHead;
private int size;
public LinkedList() {
this.dummyHead = new Node(null, null);
this.size = 0;
}
/**
* 获取链表中的元素个数。
*
* @return
*/
public int getSize() {
return size;
}
/**
* 返回链表是否为空。
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 在链表的index(0-based)位置添加新的元素e,在链表中不是一个常用的操作,当做练习。
*
* @param index
* @param e
*/
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index.");
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
prev.next = new Node(e, prev.next);
size++;
}
/**
* 在链表头添加新的元素e。
*
* @param e
*/
public void addFirst(E e) {
add(0, e);
}
/**
* 在链表末尾添加新的元素e。
*
* @param e
*/
public void addLast(E e) {
add(size, e);
}
/**
* 获得链表的第index(0-based)个位置的元素。
* 在链表中不是一个常用的操作,当做练习用。
*
* @param index
* @return
*/
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed. Illegal index.");
}
Node cur = dummyHead.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.e;
}
/**
* 获得链表的第一个元素。
*
* @return
*/
public E getFirst() {
return get(0);
}
/**
* 获得链表的最后一个元素。
*
* @return
*/
public E getLast() {
return get(size - 1);
}
/**
* 修改链表的第index(0-based)个位置的元素为e。
* 在链表中不是一个常用的操作,当做练习用。
*
* @param index
* @param e
*/
public void set(int index, E e) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed. Illegal index.");
}
Node cur = dummyHead.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
cur.e = e;
}
/**
* 查找链表中是否有元素e。
*
* @param e
* @return
*/
public boolean contains(E e) {
Node cur = dummyHead.next;
while (cur != null) {
if (cur.e.equals(e)) {
return true;
}
cur = cur.next;
}
return false;
}
/**
* 从链表中删除index(0-based)位置的元素并返回删除的元素。
*
* @param index
* @return
*/
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed. Index is illegal.");
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
size--;
return retNode.e;
}
/**
* 从链表中删除第一个元素并返回删除的元素。
*
* @return
*/
public E removeFirst() {
return remove(0);
}
/**
* 从链表中删除最后一个元素并返回删除的元素。
*
* @return
*/
public E removeLast() {
return remove(size - 1);
}
/**
* 从链表中删除元素e
*
* @param e
*/
public void removeElement(E e) {
Node prev = dummyHead;
while (prev.next != null) {
if (prev.next.e.equals(e)) {
break;
}
prev = prev.next;
}
if (prev.next != null) {
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size--;
}
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
/*Node cur = dummyHead.next;
while (cur != null) {
result.append(cur + " -> ");
cur = cur.next;
}*/
for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
result.append(cur + " -> ");
}
result.append("NULL");
return result.toString();
}
/**
* 定义存储数据的节点
*/
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
}
在这里我使用了虚拟头结点,为什么使用虚拟头结点?
在添加元素的过程中遇到一个问题:现在要在任意位置上添加一个元素,在链表头添加元素与在其他位置逻辑会有差别,那么为什么在链表头添加元素比较特殊呢?这是因为我们为链表添加元素的过程要找到待添加元素位置的前置节点,但是由于对于链表头来说,它没有前置节点,所以在逻辑上就比较特殊一些,解决方式也比较简单,我们的核心问题不就是链表头它没有前置节点嘛,那么我们就可以造一个链表头的前置节点,对于这个前置节点,他不存储任何的元素,这样一来,对于我们的链表来说,它第一个元素就是虚拟头结点的next所对应的那个元素,而不是虚拟头结点。注意:==虚拟头节点的那个元素是根本不存在的,对于用户来讲也是没有意义的,这只是为了我们编写逻辑方便而出现的虚拟头节点==。
添加虚拟头节点后的存储结构如图:
链表的优劣势
1、优点
真正动态,不需要处理固定容量的问题。
2、缺点
丧失随机访问能力。