1. 简介
链表也是最基础的数据结构,属于线性表。链表就像火车一样,每一个车厢互相连接,这些车厢就是一个个结点(Node)。链表就是通过这些结点的连接形成的。
对比于数组,链表不支持随机访问,所以数组的访问速度非常快,而链表就慢了。但是链表的长度是动态的,这一点比数组好,不会浪费空间。
2. 创建链表
把结点Node封装在类中,因为用户是不需要知道有Node结点这些概念。
public class LinkedList<E> {
// 结点
private class Node{
public E data;
public Node next;
public Node(E data, Node next) {
this.data = data;
this.next = next;
}
public Node(E data) {
this(data, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return data.toString();
}
}
}
3. 链表的添加
在添加之前,我们需要去访问,因为链表中没有索引这种概念,那访问就需要一个头结点head,从头结点开始访问。
public class LinkedList<E> {
// 结点
private class Node{
...
}
// 新增
private Node head;
// 链表长度
private int size;
public LinkedList() {
head = null;
size = 0;
}
/**
* 获取链表中的元素个数
* @return
*/
public int getSize() {
return size;
}
/**
* 判断链表是否为空
* @return
*/
public boolean isEmpty() {
return size == 0;
}
}
如图:主要有三步:
- 第一步先创建一个node结点并把值传进去;
- 第二步把新创建的结点的next指针指向head;
- 第三步把head指向node结点。
/**
* 添加操作
* @param data
*/
public void addFirst(E data){
// Node node = new Node(data);
// node.next = head;
// head = node;
// 前面三条语句可以结合成一条
head = new Node(data, head);
size++;
}
有些书上的头结点不存储值。其实头结点可以存储值也可不存储,无论如何就是一个标记,根据该标记方便我们可以操作链表。当然头结点不存值的情况代码需要修改,下面会说。
4. 链表的插入操作
现在假设链表从头结点到尾,可用索引0,1,2…表示,那么假如要把一个结点node插入到索引2,则需要怎么操作。
注意:链表中没有索引的,这里只是为了演示插入操作,因为该操作是一个非常重要的思维。
首先能想到的是,先去查询该位置,查询是利用头结点head,但必须创建一个头结点head的副本来查询,因为头结点head只能一直标记头结点。我们需要查询出该索引的前一个位置的结点,记为prev。
然后将node的next指向prev:
最后将prev的next指向node,就成功插入了:
这两条顺序的顺序不可换,可以试试换了两条语句顺序后的结果,就是错误的:
需要注意,当如果要从索引0插入时,要怎么办,头结点可没有前一个结点。这可以调用addFirst方法。
/**
* 插入操作
* index 范围为0到size
* 链表中是没有索引的概念,该操作只能理解思维
* @param index
* @param data
*/
public void insert(int index, E data){
if(index < 0 || index > size){
throw new IllegalArgumentException("Insert failed. Illegal index.");
}
if(index == 0){
addFirst(data);
} else {
Node prev = head;
for(int i = 0; i < index - 1; i++){
prev = prev.next;
}
// Node node = new Node(data);
// node.next = prev.next;
// prev.next = node;
// 另一种写法,就是上面三句的结合
prev.next = new Node(data, prev.next);
size++;
}
}
上面的代码还可以修改,比如如果超出size,那么可以把插入的结点添加到链表尾。
现在写个末尾添加结点的方法:
/**
* 添加到尾部
* @param data
*/
public void addLast(E data){
insert(size, data);
}
这些东西都是涉及到引用的知识,比如查询:
// 创建一个head副本
Node prev = head;
for(int i = 0; i < index - 1; i++){
// 此时改变引用指向,并不会影响到head
prev = prev.next;
}
如果这样写:
// 不创建head副本
for(int i = 0; i < index - 1; i++){
// 此时改变引用指向,那就影响到了head的指向,即前面的结点会丢失,永远找不回来。
head = head.next;
}
5. 链表改为使用虚拟头结点
有些书用的头结点不放任何东西时,每次添加结点是添加到头节点后面的,该头结点称为虚拟头结点,记为dummyHead,比如:
思路都是一样的,其实这是一个插入操作。因为每次都知道要插入的位置的前一个结点,所以完全不需要索引的概念,现在可以来写下,另一种添加操作:(把刚刚的head改成dummyHead)
// 把 head名称改为 dummyHead
private Node dummyHead;
// 修改构造函数
public LinkedList() {
// 创建虚拟头结点
dummyHead = new Node(null);
size = 0;
}
/**
* 插入操作
* index 范围为0到size
* 链表中是没有索引的概念,该操作只能理解思维
* 插入操作的关键点在于:找到目标位置的前一个位置
* @param index
* @param data
*/
public void insert(int index, E data){
if(index < 0 || index > size){
throw new IllegalArgumentException("Insert failed. Illegal index.");
}
// 此时head是虚拟头结点
Node prev = dummyHead;
// 注意边界
for(int i = 0; i < index; i++){
prev = prev.next;
}
// Node node = new Node(data);
// node.next = prev.next;
// prev.next = node;
// 另一种写法
prev.next = new Node(data, prev.next);
size++;
}
/**
* 添加头结点操作
* @param data
*/
public void addFirst(E data){
insert(0, data);
}
跟前面的添加操作对比看看:
- 前面的添加操作,每次添加的结点都当成头结点head。
- 这里的添加操作,每次添加的结点都放在头结点head后面。
两种方法都可以使用任意一种。现在我们把前面的代码换成使用虚拟头结点来做。
5. 链表的查询
为了练习还是引入索引。
注意查询是要查询哪个结点,像前面的插入操作是为了查询某位置的前一个结点。下面的查询是为了查询某位置的结点。
查询有两种方式,一种使用while,一种使用for。
/**
* 获取链表第index个元素(0~size)
* @param index
* @return
*/
public E get(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed. Illegal index.");
}
// 查找第index位置的结点
Node cur = dummyHead.next;
for(int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.data;
}
/**
* 获取首结点的值
* @return
*/
public E getFirst() {
return get(0);
}
/**
* 获取尾结点的值
* @return
*/
public E getLast() {
return get(size - 1);
}
/**
* 查询链表中是否包含元素data
* @param data
* @return
*/
public boolean contains(E data) {
Node cur = dummyHead.next;
while(cur != null) {
if(cur.data.equals(data)) {
return true;
}
cur = cur.next;
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
// 第一种,使用while
// Node cur = dummyHead.next;
// while(cur != null) {
// sb.append(cur + "->");
// cur = cur.next;
// }
// 第二种,使用for
for(Node cur = dummyHead.next; cur != null; cur = cur.next) {
sb.append(cur + "->");
}
sb.append("NULL");
return sb.toString();
}
最好测试一下:
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for(int i = 0; i < 5; i++) {
linkedList.addFirst(i+1);
System.out.println(linkedList);
}
System.out.println("新添加:");
linkedList.insert(2, 5);
linkedList.addLast(6);
System.out.println(linkedList);
System.out.println("首结点元素:" + linkedList.getFirst());
System.out.println("尾结点元素:" + linkedList.getLast());
System.out.println("是否包含元素1:" + linkedList.contains(1));
}
6. 链表的修改
无非还是先查询,在修改。
/**
* 为了练习还是引入索引。
* 表示修改index位置的结点的元素
* @param index 索引
* @param data 要修改的值
*/
public void set(int index, E data) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed. Index is illegal");
}
// 查询
Node cur = dummyHead.next;
for(int i = 0; i < index; i++) {
cur = cur.next;
}
cur.data = data;
}
测试:
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for(int i = 0; i < 5; i++) {
linkedList.addFirst(i+1);
System.out.println(linkedList);
}
System.out.print("修改索引为2的元素,改为10:");
linkedList.set(2, 10);
System.out.println(linkedList);
}
8. 链表的删除
还是引入索引方便演示,假设要删除索引为2的结点。
每个结点都有一个next,在查询时是根据这个next来找到下一个结点。所以通过索引1结点的next就可以找到索引2的结点,以此为前提,那么只要让索引1结点的next不指向索引2的结点,不就找不到索引2的结点了吗,这不就是删除了吗。所以我们得先找到要删除的结点的前一个结点,记为prev。
但是我们还得保证索引2的结点后面的结点不丢失,所以可以把索引1结点的next指向索引2结点的next。这就删除了。
看看图:
第一步:初始状态
第二步:查询,找到delNode的前一个结点
第三步:删除
小优化:可以看到delNode虽然是删了,但是还没有被垃圾挥手器回收,因为delNode还是有引用。所以我们主动把delNode指向null。
代码:
/**
* 删除
* @param index
* @return 返回删除元素
*/
public E remove(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("delete failed. Index is illegal");
}
// 待删除的结点之前的结点
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.data;
}
/**
* 删除首结点
* @return 返回删除元素
*/
public E removeFirst() {
return remove(0);
}
/**
* 删除尾结点
* @return 返回删除元素
*/
public E removeLast() {
return remove(size - 1);
}
/**
* 从链表中删除元素e
* @param data
*/
public void removeElement(E data){
Node prev = dummyHead;
while(prev.next != null){
if(prev.next.data.equals(data))
break;
prev = prev.next;
}
if(prev.next != null){
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
}
}
测试:
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for(int i = 0; i < 5; i++) {
linkedList.addFirst(i+1);
System.out.println(linkedList);
}
System.out.println("新添加:");
linkedList.insert(2, 5);
linkedList.addLast(6);
System.out.println(linkedList);
System.out.println("首结点元素:" + linkedList.getFirst());
System.out.println("尾结点元素:" + linkedList.getLast());
System.out.println("是否包含元素1:" + linkedList.contains(1));
System.out.print("修改索引为2的元素,改为10:");
linkedList.set(2, 10);
System.out.println(linkedList);
// 删除
System.out.println("删除索引为2的元素" + linkedList.remove(2));
System.out.println("删除首结点" + linkedList.removeFirst());
System.out.println("删除尾结点" + linkedList.removeLast());
System.out.println(linkedList);
}
9. 复杂度分析
-
添加操作:总体为O(n)
- addFirst():O(1)
- addLast():O(n)
- insert(index):平均情况下index可能在前半部分也可能在后半部分,所以均摊起来为:O(n/2)=O(n)
-
删除操作:总体为O(n)
- removeFirst():O(1)
- removeLast():O(n)
- remove(index):平均情况下index可能在前半部分也可能在后半部分,所以均摊起来为:O(n/2)=O(n)
-
修改操作:总体为O(n)
- set(idnex, data):O(n)
-
查找操作:总体为O(n)
- getFirst(index):O(1)
- getLast():O(n)
- get(index):O(n)
- contains():O(n)
相对于数组来说,链表的总体时间复杂度确实是比数组差,因为在知道索引的情况下,数组支持随机访问。
但是,我们可以发现,如果链表只在头结点操作,那么对于增,删,查的操作都是O(1),而且可以知道链表其实不去修改好。
所以现在如果只对头结点操作,那么链表的总体复杂度跟数组差不多,而且比数组好的就是,链表是动态的,不会浪费空间。
10. 链表的全部代码
public class LinkedList<E> {
// 结点
private class Node{
public E data;
public Node next;
public Node(E data, Node next) {
this.data = data;
this.next = next;
}
public Node(E data) {
this(data, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return data.toString();
}
}
private Node dummyHead;
private int size;
public LinkedList() {
// 创建虚拟头结点
dummyHead = new Node(null);
size = 0;
}
/**
* 获取链表中的元素个数
* @return
*/
public int getSize() {
return size;
}
/**
* 判断链表是否为空
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 插入操作
* index 范围为0到size
* 链表中是没有索引的概念,该操作只能理解思维
* 插入操作的关键点在于:找到目标位置的前一个位置
* @param index
* @param data
*/
public void insert(int index, E data) {
if(index < 0 || index > size){
throw new IllegalArgumentException("Insert failed. Illegal index.");
}
// 此时head是虚拟头结点,查找index位置的前一个结点
Node prev = dummyHead;
// 注意边界
for(int i = 0; i < index; i++){
prev = prev.next;
}
// Node node = new Node(data);
// node.next = prev.next;
// prev.next = node;
// 另一种写法
prev.next = new Node(data, prev.next);
size++;
}
/**
* 添加到虚拟头结点的下一个位置
* @param data
*/
public void addFirst(E data) {
// Node node = new Node(data);
// node.next = head;
// head = node;
// 前面三条语句可以结合成一条
// head = new Node(data, head);
//
// size++;
insert(0, data);
}
/**
* 添加到尾部
* @param data
*/
public void addLast(E data){
insert(size, data);
}
/**
*
* @param index 索引
* @return 获取链表第index个元素(0~size)
*/
public E get(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed. Illegal index.");
}
// 查找第index位置的结点
Node cur = dummyHead.next;
for(int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.data;
}
/**
* @return 获取首结点的值
*/
public E getFirst() {
return get(0);
}
/**
* @return 获取尾结点的值
*/
public E getLast() {
return get(size - 1);
}
/**
* 查询链表中是否包含元素data
* @param data
* @return
*/
public boolean contains(E data) {
Node cur = dummyHead.next;
while(cur != null) {
if(cur.data.equals(data)) {
return true;
}
cur = cur.next;
}
return false;
}
/**
* 为了练习还是引入索引。
* 表示修改index位置的结点的元素
* @param index 索引
* @param data 要修改的值
*/
public void set(int index, E data) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed. Index is illegal");
}
// 查询
Node cur = dummyHead.next;
for(int i = 0; i < index; i++) {
cur = cur.next;
}
cur.data = data;
}
/**
* 删除
* @param index
* @return 返回删除元素
*/
public E remove(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("delete failed. Index is illegal");
}
// 待删除的结点之前的结点
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.data;
}
/**
* 删除首结点
* @return 返回删除元素
*/
public E removeFirst() {
return remove(0);
}
/**
* 删除尾结点
* @return 返回删除元素
*/
public E removeLast() {
return remove(size - 1);
}
/**
* 从链表中删除元素e
* @param data
*/
public void removeElement(E data){
Node prev = dummyHead;
while(prev.next != null){
if(prev.next.data.equals(data))
break;
prev = prev.next;
}
if(prev.next != null){
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
// 第一种,使用while
// Node cur = dummyHead.next;
// while(cur != null) {
// sb.append(cur + "->");
// cur = cur.next;
// }
// 第二种,使用for
for(Node cur = dummyHead.next; cur != null; cur = cur.next) {
sb.append(cur + "->");
}
sb.append("NULL");
return sb.toString();
}
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for(int i = 0; i < 5; i++) {
linkedList.addFirst(i+1);
System.out.println(linkedList);
}
System.out.println("新添加:");
linkedList.insert(2, 5);
linkedList.addLast(6);
System.out.println(linkedList);
System.out.println("首结点元素:" + linkedList.getFirst());
System.out.println("尾结点元素:" + linkedList.getLast());
System.out.println("是否包含元素1:" + linkedList.contains(1));
System.out.print("修改索引为2的元素,改为10:");
linkedList.set(2, 10);
System.out.println(linkedList);
// 删除
System.out.println("删除索引为2的元素" + linkedList.remove(2));
System.out.println("删除首结点" + linkedList.removeFirst());
System.out.println("删除尾结点" + linkedList.removeLast());
System.out.println(linkedList);
}
}
11. 使用链表来实现栈
Stack接口:
public interface Stack<E> {
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E peek();
}
使用链表实现:
public class LinkedListStack<E> implements Stack<E> {
private LinkedList<E> linkList;
public LinkedListStack() {
linkList = new LinkedList<>();
}
@Override
public int getSize() {
return linkList.getSize();
}
@Override
public boolean isEmpty() {
return linkList.isEmpty();
}
@Override
public void push(E e) {
// 从链表头添加
linkList.addFirst(e);
}
@Override
public E pop() {
// 从链表头删除
return linkList.removeFirst();
}
@Override
public E peek() {
// 从链表头查看
return linkList.getFirst();
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Stack top :").append(linkList.toString());
return stringBuilder.toString();
}
public static void main(String[] args) {
LinkedListStack<Integer> stack = new LinkedListStack<>();
for (int i = 0; i < 5; i++) {
stack.push(i);
System.out.println(stack);
}
stack.pop();
System.out.println(stack);
}
}
虽然基于链表实现的栈在头结点入栈和出栈的时间复杂度都是O(1)。但是链表是需要创建节点的,如果这两个操作的次数很大很大,比如入栈和出栈各1000000次,那么是需要很久的。
12. 使用链表实现队列
我们知道链表如果操作尾结点,那么时间复杂度为O(n)。如果在尾结点加个标记,那么每次操作就不用去找尾节点。该标记跟head一样,我们记为tail。
但是如果要删除尾结点,必须遍历一次链表,因为要找到删除尾结点的前一个结点,即使有tail也无法改变。
所以我们可以在链表首做队列头,而在链表尾做队列尾。这样,我们在入队和出队的两个操作的时间复杂度都是O(1)。
比如添加操作:
- 先让head和tail初始化为null。
- 只有一个结点是特殊情况:此时tail和head共同指向同一个结点。
- 当添加第二个结点时,规定是在tail后面添加的,并且此时就要改变tail的指向。
代码:
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E data);
E dequeue();
E getFront();
}
public class LinkedListQueue<E> implements Queue<E> {
// 结点
private class Node{
public E data;
public Node next;
public Node(E data, Node next) {
this.data = data;
this.next = next;
}
public Node(E data) {
this(data, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return data.toString();
}
}
private Node head, tail;
private int size;
public LinkedListQueue() {
this.head = null;
this.tail = null;
this.size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void enqueue(E data) {
if(tail == null) {
tail = new Node(data);
head = tail;
} else {
tail.next = new Node(data);
tail = tail.next;
}
size++;
}
@Override
public E dequeue() {
if(isEmpty()) {
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
}
Node ret = head;
head = head.next;
// 如果队列只有一个元素,删除后就没了,要修改tail
if(head == null) {
tail = null;
}
size--;
return ret.data;
}
@Override
public E getFront() {
if(isEmpty()) {
throw new IllegalArgumentException("Queue is Empty");
}
return head.data;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Node cur = head;
sb.append("Queue top: ");
while(cur != null) {
sb.append(cur + "->");
cur = cur.next;
}
sb.append("NULL tail");
return sb.toString();
}
public static void main(String[] args) {
LinkedListQueue<Integer> queue = new LinkedListQueue<>();
for(int i = 0; i < 5; i++){
queue.enqueue(i);
System.out.println(queue);
if(i % 3 == 2){
queue.dequeue();
System.out.println(queue);
}
}
}
}
使用链表实现的队列比使用数组实现的队列性能更好。因为数组一定有一头要O(n)。当然数组可以修改成循环数组来解决此问题。