概念
链表(Linked list)是一种线性表。由一系列节点对象组成,每个节点包含数据元素和一个指向下一个节点的引用,也就是说,各个节点通过“引用”相连接。
1 2 3 4 5 6 7 8 9
| class ListNode { int elem; ListNode next; ListNode(int element) { elem = element; } }
|
“引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。因此,链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。”
链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理,同时,也失去了快速随机存取的优点(数组),同时增大了内存开销(存储下一个节点)。
链表类型
常见链表类型有三种,分别是单链表,环形链表和双向链表。对比如下:
特性 |
单向链表 |
环形链表 |
双向链表 |
简介 |
简称单链表,是一种常规链表,其节点包含值和指向下一节点的引用两项数据,头节点指向第一个实际存储数据的节点,尾节点指向null 。 |
首尾相接,将单链表的尾节点指向头节点就得到了一个环形链表。在环形链表中,任意节点都可以视为头节点。 |
与单链表相比,双向链表记录了两个方向的引用,也就是前驱结点prev 和后继节点next ,因此更加灵活,可以朝两个方向遍历链表。 |
节点结构 |
数据 + 下一节点的指针 |
数据 + 下一节点的指针 |
数据 + 下一节点的指针 + 前一节点的指针 |
遍历特点 |
单向遍历 |
循环遍历 |
双向遍历(前向和后向) |
头节点
在使用单链表的时候,我们通常会使用一个头节点(head
)。
该头节点不存储数据,而是指向第一个实际存储数据的节点。
尾节点就是最后一个节点,该节点指向的下一个节点为null
。
为什么需要头节点?
使用头节点,可以简化对链表的操作,提供一个固定的起点。如果没有头节点,我们在对链表进行插入、删除等操作时需要考虑处理第一个节点的特殊情况。头节点的存在使得链表的操作更加一致,使代码更加简洁和统一。
单链表的使用效果
下面,我们来编写一个单链表 LinkedList
,实现之后可以这样使用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public class Main {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.addLast(10); linkedList.addLast(20); linkedList.addLast(30); linkedList.addLast(40); linkedList.addLast(50); linkedList.addLast(60); System.out.println("(1) 初始化后,当前链表为:" + linkedList.printList());
System.out.println("(2) 链表的大小:" + linkedList.size());
linkedList.add(1, 15); System.out.println("(3) 在索引为1的位置插入元素15后,链表:" + linkedList.printList());
linkedList.addFirst(100); System.out.println("(4) 在链表的头部添加一个元素100,链表:" + linkedList.printList()); System.out.println("(5) 判断链表是否包含元素15:" + linkedList.contains(15)); linkedList.set(5, 15); System.out.println("(6) 将链表中索引为5的元素修改值为15,链表:" + linkedList.printList()); linkedList.remove(1); System.out.println("(7) 将链表中索引为1的元素移除,链表:" + linkedList.printList()); linkedList.removeElement(50); System.out.println("(8) 删除值为50的节点后,链表:" + linkedList.printList()); System.out.println("(9) 链表中索引为3的节点的值为:" + linkedList.get(3)); System.out.println("(10) 链表中第一次出现值为15的节点,其索引为:" + linkedList.indexOf(15)); System.out.println("(11) 链表中最后一次出现值为15的节点,其索引为:" + linkedList.lastIndexOf(15)); } }
|
运行结果:
1 2 3 4 5 6 7 8 9 10 11
| (1) 初始化后,当前链表为:[10 → 20 → 30 → 40 → 50 → 60] (2) 链表的大小:6 (3) 在索引为1的位置插入元素15后,链表:[10 → 15 → 20 → 30 → 40 → 50 → 60] (4) 在链表的头部添加一个元素100,链表:[100 → 10 → 15 → 20 → 30 → 40 → 50 → 60] (5) 判断链表是否包含元素15:true (6) 将链表中索引为5的元素修改值为15,链表:[100 → 10 → 15 → 20 → 30 → 15 → 50 → 60] (7) 将链表中索引为1的元素移除,链表:[100 → 15 → 20 → 30 → 15 → 50 → 60] (8) 删除值为50的节点后,链表:[100 → 15 → 20 → 30 → 15 → 60] (9) 链表中索引为3的节点的值为:30 (10) 链表中第一次出现值为15的节点,其索引为:1 (11) 链表中最后一次出现值为15的节点,其索引为:4
|
实现一个单链表
Node
在链表中,其组成元素都是节点(Node
)对象。并且每个节点都包含两项数据:
- 节点的“值”(
elem
)
- 指向下一个节点的“引用”(
next
)
我们来写一下这个Node
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| public class Node {
private int elem;
private Node next;
public Node(int element, Node next) { this.elem = element; this.next = next; }
public Node() { this(0, null); }
public void setElem(int element) { this.elem = element; }
public int getElem() { return this.elem; }
public void setNext(Node next) { this.next = next; }
public Node getNext() { return this.next; } }
|
LinkedList
这里我们定义一个类,用于表示一个链表类。
1 2 3 4 5 6 7 8
| public class LinkedList { private Node head; public LinkedList() { this.head = new Node(); } }
|
这样,我们就成功地初始化了一个单链表,可以在这个基础上进行后续的节点插入、删除等操作。
size
size()
方法用于获取链表的大小,即链表中包含的元素数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public int size() { int count = 0;
Node current = head.getNext(); while (current != null) { count++; current = current.getNext(); }
return count; }
|
isEmpty
判断链表是否为空,可以有两种策略:
- 策略一:使用
size()
方法,判断链表存储的元素数量是否为0。
- 策略二:判断头节点的指针域是否为
null
。
1 2 3 4 5 6 7 8
|
public boolean isEmpty() { return head.getNext() == null; }
|
clear
clear()
方法用于清空链表,移除所有节点。实际就是将头节点的下一个节点设为null
。
1 2 3 4 5 6 7
|
public void clear() { head.setNext(null); }
|
contains
contains(int element)
方法用于判断链表是否含有指定元素element
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
public boolean contains(int element) { Node current = head.getNext(); while (current != null) { if (current.getElem() == element) { return true; } current = current.getNext(); } return false; }
|
set
set(int index, int element)
方法用于设置链表中指定位置的元素值,并且返回原元素的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public int set(int index, int element) throws IndexOutOfBoundsException { if (index < 0 || index >= size()) { throw new IndexOutOfBoundsException("指定位置超出链表范围"); }
Node current = head.getNext(); for (int i = 0; i < index; i++) { current = current.getNext(); }
int oldValue = current.getElem();
current.setElem(element);
return oldValue; }
|
printList
printList()
方法用于打印链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
public String printList() { StringBuilder result = new StringBuilder(); result.append("[");
Node current = head.getNext(); while (current != null) { result.append(current.getElem());
if (current.getNext() != null) { result.append(" → "); }
current = current.getNext(); } result.append("]");
return result.toString(); }
|
插入元素
add
add(index, element)
方法用于向在指定位置插入新元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
public void add(int index, int element) throws IndexOutOfBoundsException { Node newNode = new Node(element, null);
if (index < 0 || index > size()) { throw new IndexOutOfBoundsException("插入位置超出链表范围"); }
Node current = head; for (int i = 0; i < index; i++) { current = current.getNext(); }
newNode.setNext(current.getNext()); current.setNext(newNode); }
|
Node current = head;
用于初始化一个临时节点(current
),并将其设置为链表头节点(head
),从头节点开始遍历链表。循环的次数为插入位置(index
),每次迭代将current
移动到下一个节点,最终将current
移动到插入位置的前一个节点。
最后两行代码,用于将新节点的下一个节点设置为当前节点的下一个节点,然后将当前节点的下一个节点设置为新节点。这样就成功地在指定位置插入了新节点。
addLast
addLast(int element)
方法,用于向链表的尾部添加一个新的元素,插入的新元素总是位于链表的末尾(也就是指针域为null
的节点),我们称为尾插法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public void addLast(int element) {
Node node = new Node(element, null);
Node tail = head; while (tail.getNext() != null) { tail = tail.getNext(); }
tail.setNext(node); }
|
addFirst
addFirst()
方法用于向链表的头部添加一个新的元素,插入的新元素总是位于链表的头部(也就是头节点指向的节点),我们称之为头插法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public void addFirst(int element) {
Node node = new Node(element, null);
node.setNext(head.getNext());
head.setNext(node); }
|
可以看到,在链表头部插入元素的步骤如下:
- 根据新元素的值,构建一枚新节点
- 将新节点指针域置为原先链表中头节点指向的节点
- 最后,将头节点指向新节点
移除元素
remove
remove(int index)
方法用于删除链表中指定位置index
的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public int remove(int index) throws IndexOutOfBoundsException { if (index < 0 || index >= size()) { throw new IndexOutOfBoundsException("删除位置超出链表范围"); }
Node current = head; for (int i = 0; i < index; i++) { current = current.getNext(); }
Node removedNode = current.getNext();
current.setNext(removedNode.getNext());
return removedNode.getElem(); }
|
removeElement
removeElement(int element)
方法用于删除链表中第一个出现的指定元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public boolean removeElement(int element) { Node current = head; while (current.getNext() != null && current.getNext().getElem() != element) { current = current.getNext(); }
if (current.getNext() != null) { current.setNext(current.getNext().getNext()); return true; } else { return false; } }
|
removeFirst
removeFirst()
方法用于删除并返回链表中的第一个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public int removeFirst() throws NoSuchElementException { if (isEmpty()) { throw new NoSuchElementException("链表为空"); }
Node firstNode = head.getNext();
head.setNext(firstNode.getNext());
return firstNode.getElem(); }
|
removeLast
removeLast()
方法用于删除并返回链表中的最后一个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public int removeLast() throws NoSuchElementException { if (isEmpty()) { throw new NoSuchElementException("链表为空"); }
Node secondLastNode = head; while (secondLastNode.getNext().getNext() != null) { secondLastNode = secondLastNode.getNext(); }
Node lastNode = secondLastNode.getNext();
secondLastNode.setNext(null);
return lastNode.getElem(); }
|
获取元素
get
get(int index)
方法用于返回链表中指定位置的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public int get(int index) throws IndexOutOfBoundsException { if (index < 0 || index >= size()) { throw new IndexOutOfBoundsException("指定位置超出链表范围"); }
Node current = head.getNext(); for (int i = 0; i < index; i++) { current = current.getNext(); }
return current.getElem(); }
|
getFirst
getFirst()
方法用于获取链表中的第一个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public int getFirst() throws NoSuchElementException { if (isEmpty()) { throw new NoSuchElementException("链表为空"); }
Node firstNode = head.getNext();
return firstNode.getElem(); }
|
getLast
getLast()
方法,用于获取尾部元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public int getLast() throws NoSuchElementException { if (isEmpty()) { throw new NoSuchElementException("链表为空"); }
Node lastNode = head.getNext(); while (lastNode.getNext() != null) { lastNode = lastNode.getNext(); }
return lastNode.getElem(); }
|
indexOf
indexOf(int element)
方法用于查找链表中指定元素从前往后第一次出现的索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
public int indexOf(int element) {
int index = -1;
Node current = head.getNext(); int currentIndex = 0;
while (current != null) { if (current.getElem() == element) { index = currentIndex; break; } current = current.getNext(); currentIndex++; }
return index; }
|
lastIndexOf
lastIndexOf(int element)
方法用于查找链表中指定元素最后一次出现的索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public int lastIndexOf(int element) {
int lastIndex = -1;
Node current = head.getNext(); int currentIndex = 0;
while (current != null) { if (current.getElem() == element) { lastIndex = currentIndex; } current = current.getNext(); currentIndex++; }
return lastIndex; }
|
数组 VS 链表
特性 |
数组 |
链表 |
存储方式 |
连续的内存块 |
分散的节点 |
访问元素 |
O(1) |
O(n) |
插入元素 |
O(n) |
O(1) |
删除元素 |
O(n) |
O(1) |
大小变化 |
固定,长度不可变 |
动态,可灵活扩展 |
内存使用 |
较少,预先分配空间 |
较多,动态分配空间 |