一、什么是链表
前言:对链表进行插入和删除操作时,指针一直在变化。在本子上画一画,可以比较容易的理解指针的变化过程。
(一)链表的定义
链表也是多个相同类型 的数据按一定顺序排列的集合,是一种数据储存结构。链表由许多个结点组成,每个结点由存放的数据和指向下一个结点的引用(指针)构成。在单链表中,头指针指向第一个结点,尾指针指向最后一个结点。链表逻辑上是相邻的,但在计算机上的储存却不一定是相邻的。链表作为链式储存结构来实现线性表。
(二)链表的分类
单链表:n 个结点链结成一个链表,此链表的每个结点中只包含一个指针,所以叫做单链表。
循环链表:将单链表中最后一个结点的指针由指向 null 改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
双向链表:双向链表是在单链表的每个结点中,再设置一个指向其前一个结点的指针。所以在双向链表中的结点都有两个指针, 一个指向直接后继(后一个结点),另一个指向直接前驱(前一个结点)。
二、为什么要用链表
(一)链表的优点
- 不需要提前分配存储空间,存放的元素个数不受限制
- 增加与删除操作的时间复杂度为 O(1),比数组的 O(N)快。
(二)链表的缺点
- 对于单链表,需要声明头指针 (head) 和尾指针 (tail),因为当前结点只储存了下一个结点的位置,所以只能从头开始(遍历)查找任意结点的数据。
- 对于循环链表,可以只声明一个尾指针 (tail),就可以找到头指针。所以可以快速地找到第一个和最后一个结点。但查找其他任意结点,仍然需要遍历。
- 对于双向链表,因为当前结点同时储存了上一结点和下一结点的位置,所以可以从头开始查找任意结点,也可以从末尾开始查找任意结点,但仍然需要遍历。
综上,相对于数组直接用索引查找指定位置的数据,时间复杂度为 O(1)。链表只能遍历,时间复杂度为 O(N)。所以链表最大的缺点就是查找操作没有数组快。
三、如何操作链表
(一)单链表
Person类作为数据存放在链表的结点中。
/**
* Person类,作为数据存放在链表的结点中
*
* @author likezhen
* @version 1.0
*/
class Person {
private String name;
private int age;
public Person() {
super();
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
单向链表类中,声明了头指针 head 指向第一个结点,声明尾指针 tail 指向最后一个结点,并提供了增加、删除、遍历等操作。
/**
* 单向链表类,提供了增加、删除、遍历等操作。
*
* @author likezhen
* @version 1.0
*/
class SinglyLinkedList<E> {
/**
* 结点内部类。
*
* @author likezhen
* @version 1.0
*/
private static class Node<E> {
private E element;
private Node<E> next; //声明指向下一个结点的指针
public Node(E e, Node<E> n) { //创建结点时必须声明其储存的数据和指针
element = e; //当前结点存放的数据
next = n; //当前结点指向下一个结点的指针
}
public void setNext(Node<E> n) {
next = n;
}
public Node<E> getNext() {
return next;
}
public E getElement() {
return element;
}
}
private Node<E> head = null; //初始化头指针,头指针指向第一个结点
private Node<E> tail = null; //初始化尾指针,尾指针指向最后一个结点
private int size = 0; //初始化链表结点个数
public SinglyLinkedList() {
super();
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void addFirst(E e) { //在表头插入一个结点
head = new Node<>(e, head);
if (size == 0)
tail = head;
size++;
}
public void addLast(E e) { //在表尾插入一个结点
Node<E> newest = new Node<>(e, null);
if (isEmpty())
head = newest;
else
tail.setNext(newest);
tail = newest;
size++;
}
public E removeFirst() { //删除表头结点(第一个结点)
if (isEmpty()) return null;
E answer = head.getElement();
head = head.getNext();
size--;
if (size == 0)
tail = null;
return answer;
}
public void displayLinkedList() {
Node<E> current = head;
while (current != null) {
System.out.println(current.getElement().toString());
current = current.getNext();
}
}
}
单向链表的测试类。
/**
* 单向链表的测试类
*
* @author likezhen
* @version 1.0
*/
public class SinglyLinkedListApp {
public static void main(String[] args) {
SinglyLinkedList<Person> singlyLinkedList = new SinglyLinkedList<>();
singlyLinkedList.addFirst(new Person("Dane", 58)); //Dane所在的结点为第一个结点
singlyLinkedList.addFirst(new Person("Jack", 25)); //Jack为第一个结点,Dane为第二个结点
singlyLinkedList.addFirst(new Person("Tale", 16)); //Tale第一,Jack第二,Dane第三
singlyLinkedList.addLast(new Person("Wily", 23)); //Wily为最后一个结点
singlyLinkedList.addLast(new Person("Rien", 33)); //Rien为最后一个结点
System.out.println("----------删除之前----------");
singlyLinkedList.displayLinkedList();
singlyLinkedList.removeFirst();
System.out.println("----------删除之后----------");
singlyLinkedList.displayLinkedList();
}
}
测试结果:
D:\Java\jdk\bin\java.exe
----------删除之前----------
Person{name='Tale', age=16}
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------删除之后----------
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
Process finished with exit code
(二)循环链表
循环链表类中,声明尾指针 tail 指向最后一个结点,头指针可通过 head = tail.getNext()获得,所以无需声明和初始化头指针。提供了增加、删除、遍历等操作。
/**
* 循环链表类,提供了增加、删除、遍历等操作。
*
* @author likezhen
* @version 1.0
*/
class CircularlyLinkedList<E> {
/**
* 结点内部类。与单链表的结点类相同。
*
* @author likezhen
* @version 1.0
*/
private static class Node<E> {
private E element;
private Node<E> next;
public Node(E e, Node<E> n) {
element = e;
next = n;
}
public Node<E> getNext() {
return next;
}
public void setNext(Node<E> next) {
this.next = next;
}
public E getElement() {
return element;
}
}
//head = tail.getNext(); 所以无需特别地单独声明
private Node<E> tail = null; //初始化尾指针,尾指针指向最后一个结点
private int size = 0; //初始化链表的结点个数
public CircularlyLinkedList() {
super();
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void rotate() { //将第一个结点变成最后一个结点
if (tail != null)
tail = tail.getNext();
}
public void addFirst(E e) { //在表头插入新结点
if (size == 0) {
tail = new Node<>(e, null);
tail.setNext(tail);
} else {
Node<E> newest = new Node<>(e, tail.getNext());
tail.setNext(newest);
}
size++;
}
public void addLast(E e) { //在表尾插入新结点
addFirst(e);
tail = tail.getNext();
}
public E removeFirst() { //删除表头结点(第一个结点)
if (isEmpty()) return null;
Node<E> head = tail.getNext();
if (head == tail) tail = null;
else tail.setNext(head.getNext());
size--;
return head.getElement();
}
public void displayLinkedList() {
Node<E> head = tail.getNext();
Node<E> current = head;
do {
System.out.println(current.getElement().toString());
current = current.getNext();
} while (current != head);
}
}
循环链表的测试类。
/**
* 循环链表的测试类
*
* @author likezhen
* @version 1.0
*/
public class CircularlyLinkedListApp {
public static void main(String[] args) {
CircularlyLinkedList<Person> circularlyLinkedList = new CircularlyLinkedList<>();
circularlyLinkedList.addFirst(new Person("Dane", 58));
circularlyLinkedList.addFirst(new Person("Jack", 25));
circularlyLinkedList.addFirst(new Person("Tale", 16));
circularlyLinkedList.addLast(new Person("Wily", 23));
circularlyLinkedList.addLast(new Person("Rien", 33));
System.out.println("----------删除之前----------");
circularlyLinkedList.displayLinkedList();
circularlyLinkedList.removeFirst();
System.out.println("----------删除之后----------");
circularlyLinkedList.displayLinkedList();
circularlyLinkedList.rotate(); //第一个结点Jack变成最后一个结点
System.out.println("----------旋转之后----------");
circularlyLinkedList.displayLinkedList();
}
}
测试结果:
D:\Java\jdk\bin\java.exe com.linkedlist.www.CircularlyLinkedListApp
----------删除之前----------
Person{name='Tale', age=16}
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------删除之后----------
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------旋转之后----------
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
Person{name='Jack', age=25}
Process finished with exit code 0
(三)双向链表
相比于单链表和循环链表中的 Node 结点类,在双向链表的 Node 结点内部类中,需多声明指向前一个结点的指针 prev(用左指针可能更准确)。单链表、循环链表和双向链表中,都声明了指向后一个结点的指针 next。
特殊地,在双向链表类中,可以通过声明不储存任何数据的空头节点 header 和空尾结点 trailer 来减少对于特殊情况(当链表为空)的判断,使得链表为空和链表不为空时的操作一致,减少判断语句。向链表中添加结点,即向 header 与 trailer 两个空结点之间添加新的结点。删除链表中的结点,即删除 header 与 trailer 两个空结点之间的结点。提供了增加、删除、遍历等操作。
/**
* 双向链表类,提供了增加、删除、遍历等操作。
*
* @author likezhen
* @version 1.0
*/
class DoublyLinkedList<E> {
private static class Node<E> {
private E element;
private Node<E> prev; //声明左指针,指向前一个结点
private Node<E> next; //声明右指针,指向后一个结点
public Node(E e, Node<E> p, Node<E> n) {
element = e;
prev = p;
next = n;
}
public E getElement() {
return element;
}
public Node<E> getPrev() {
return prev;
}
public void setPrev(Node<E> prev) {
this.prev = prev;
}
public Node<E> getNext() {
return next;
}
public void setNext(Node<E> next) {
this.next = next;
}
}
private Node<E> header; //声明头节点
private Node<E> trailer; //声明尾结点
private int size = 0; //初始化链表的结点个数
public DoublyLinkedList() { //创建双向链表类时,创建并初始化头节点和尾结点。
header = new Node<>(null, null, null); //创建并初始化头节点
trailer = new Node<>(null, header, null); //创建并初始化尾节点,尾结点的左指针指向头节点
header.setNext(trailer); //头节点的右指针指向尾结点,形成循环链表
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void addFirst(E e) { //在表头插入新结点
addBetween(e, header, header.getNext());
}
public void addLast(E e) { //在表尾插入新结点
addBetween(e, trailer.getPrev(), trailer);
}
public E removeFirst() { //移除第二个结点(第一个结点为空头结点,不能移除)
if (isEmpty()) return null;
return remove(header.getNext());
}
public E removeLast() { //移除倒数第二个结点(最后一个结点为空尾结点,不能移除)
if (isEmpty()) return null;
return remove(trailer.getPrev());
}
public void displayLinkedList() {
Node<E> current = header.getNext();
while (current != trailer) {
System.out.println(current.getElement().toString());
current = current.getNext();
}
}
private void addBetween(E e, Node<E> predecessor, Node<E> successor) {
Node<E> newest = new Node<>(e, predecessor, successor);
predecessor.setNext(newest);
successor.setPrev(newest);
size++;
}
private E remove(Node<E> node) {
Node<E> predecessor = node.getPrev();
Node<E> successor = node.getNext();
predecessor.setNext(successor);
successor.setPrev(predecessor);
size--;
return node.getElement();
}
}
双向链表的测试类。
/**
* 双向链表的测试类
*
* @author likezhen
* @version 1.0
*/
public class DoublyLinkedListApp {
public static void main(String[] args) {
DoublyLinkedList<Person> doublyLinkedList = new DoublyLinkedList<>();
doublyLinkedList.addFirst(new Person("Dane", 58));
doublyLinkedList.addFirst(new Person("Jack", 25));
doublyLinkedList.addFirst(new Person("Tale", 16));
doublyLinkedList.addLast(new Person("Wily", 23));
doublyLinkedList.addLast(new Person("Rien", 33));
System.out.println("----------删除之前----------");
doublyLinkedList.displayLinkedList();
doublyLinkedList.removeFirst();
System.out.println("----------删除之后----------");
doublyLinkedList.displayLinkedList();
}
}
测试结果:
D:\Java\jdk\bin\java.exe com.linkedlist.www.DoublyLinkedListApp
----------删除之前----------
Person{name='Tale', age=16}
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
----------删除之后----------
Person{name='Jack', age=25}
Person{name='Dane', age=58}
Person{name='Wily', age=23}
Person{name='Rien', age=33}
Process finished with exit code 0
时间复杂度分析:
链表的删除、增加操作的时间复杂度都为常数级,即 O(1),与数据量无关。
链表的查找操作的最坏时间复杂度与数据量成线性,即 O(N)。