数据结构学习笔记 - 05 链表
介绍
- 之前学习的动态数组、栈、队列底层都是依托于静态数组,通过 resize 解决容量问题。
- 链表是最简单的动态数据结构。
- 优点:真正的动态,不需要处理固定容量的问题。
- 缺点:丧失了随机访问的能力(这也是数组的优点)。
- Java 中
java.util.LinkedList这个链表类底层使用双向循环链表实现。 - 链表如果看作一个抽象的数据结构,也可以使用数组来实现。只需要将指向下一个节点的指针改存为数组的某的位置的索引。
复杂度
链表各个操作时间复杂度:
addLast(e) O(n)
addFirst(e) O(1)
add(e) O(n)
removeLast(e) O(n)
removeFirst(e) O(1)
remove(e) O(n)
set(index, e) O(n)
get(index) O(n)
contains(e) O(n)
也就是说,对于链表来说,增删改查全是 O(n) 级别的。
但是需要注意,如果只是对链表头进行操作,那么不管是增删还是查,都是 O(1) 级别的。
链表的复杂度主要是花费在查找节点上,而具体删除或者插入的操作是很方便的,这点和数组不同,数组体现在查找特别快,但是删除或者插入比较麻烦。
链表的增删改查操作可以尝试使用递归实现。
链表的实现
public class LinkedList<E> {
/**
* Node
* 链表的节点,设置为一个内部的私有类,只在链表类内部可以访问到。
*/
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);
}
@Override
public String toString() {
return e.toString();
}
}
// 第一个节点,即链表的头。对于链表来说,我们只跟踪链表的头,并没有跟踪链表的尾巴。
// 这一点和数组不同,数组我们往往设计一个变量去跟踪数组的尾部。
// 为了添加元素的统一,我们在真正的 head 前放置一个虚拟头节点。
// private Node head;
private Node dummyHead;
// 元素个数。
int size;
public LinkedList() {
// head = null;
dummyHead = new Node();
size = 0;
}
/**
* 获取链表中元素的个数。
*/
public int getSize() {
return size;
}
/**
* 链表是否为空。
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 在链表的指定 index 位置插入新的元素 e
* 这个操作在链表中是一个不常用操作,主要是练习使用。
*/
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index.");
}
// 如果为填表头添加元素需要特殊处理,那么我们可以使用为链表设立虚拟头节点的方式解决。
// if (index == 0 ) {
// addFirst(e);
// } else {
// Node prev = head;
// for (int i = 0; i < index - 1; i++) {
// prev = prev.next;
// }
// // Node node = new Node(e);
// // node.next = prev.next;
// // prev.next = node;
// prev.next = new Node(e, prev.next);
// size++;
// }
// 因为虚拟头节点的添加,我们不再需要 if-else
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
prev.next = new Node(e, prev.next);
size++;
}
/**
* 为链表头添加一个节点。
* 对于链表来讲,在头部添加一个节点是最容易的。
*/
public void addFirst(E e) {
// Node node = new Node(e, head);
// head = node;
// size++;
add(0, e);
}
/**
* 在链表末尾添加元素。
*/
public void addLast(E e) {
add(size, e);
}
/**
* 获得链表 index 位置的元素。
* 这也不是一个常用操作,仅作练习使用。
*/
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;
}
public E getFirst() {
return get(0);
}
public E getLast() {
return get(size - 1);
}
/**
* 更新 index 位置的元素。
* 不常用操作,练习使用。
*/
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
*/
public boolean contains(E e) {
Node cur = dummyHead.next;
// for (int i = 0; i < size; i++) {
// if (cur.e.equals(e)) {
// return true;
// }
// cur = cur.next;
// }
// return false;
while (cur != null) {
if (cur.e.equals(e)) {
return true;
}
cur = cur.next;
}
return false;
}
/**
* 删除指定 index 位置的元素,并返回所删除的元素。
* 不常用操作,练习使用。
*/
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed. Illegal index.");
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size--;
return delNode.e;
}
public E removeFirst() {
return remove(0);
}
public E removeLast() {
return remove(size - 1);
}
// 从链表中删除元素 e
public void removeElements(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;
}
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while (cur != null) {
res.append(cur + "->");
cur = cur.next;
}
res.append("NULL");
return res.toString();
}
}