【数据结构】链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

链表也是属于「线性表」的一种,它和数组最大的区别就是数据元素无需存储在一块连续的内存空间里。内存空间不连续了,那该如何判断元素之间的前后顺序呢?

链表要求每个节点除了保存数据元素本身,还需要包含一个或多个额外的指针,来指向它关联的元素,元素之间通过指针来形成链结构。

链表的分类:

  1. 单向链表
  2. 双向链表
  3. 单向循环链表
  4. 双向循环链表

如下图,是一个单向链表结构。

在Java语言中,LinkedList就是一个底层采用双向链表结构的容器。

java 复制代码
public void linkedList() {
    LinkedList<Integer> list = new LinkedList();
    list.add(1);
    list.add(2);
    Integer first = list.getFirst();
	Integer last = list.getLast();
}

1. 链表的特点

1.1 地址可以不连续

链表的数据元素可以存储在地址连续的存储单元里,也可以存储在地址不连续的存储单元中。这是它和数组最大的区别。

1.2 无界

数组需要一块连续的内存空间,创建时需要申明容量,定长且不支持扩容,属于有界表。

链表没有这些限制,添加元素时,直接链尾的next指针指向新元素即可,理论上,只要内存足够,链表是没有界限的,可以一直添加,即使是碎片化的内存。

1.3 插入、删除效率高

数组元素的插入和删除,需要移动元素,时间复杂度是O(n)。

链表相反,它的插入和删除效率极高,只需要修改指针的指向即可,时间复杂度是O(1)。

1.4 访问效率低

链表由于内存空间不是连续的,因此它是没有索引的概念的。尽管Java中的LinkedList依然提供了get(int index)方法获取第index个元素,但是你可以看下它的源码实现,它依然是循环访问next来查找的,时间复杂度是O(n)。

java 复制代码
// 获取第index个节点
Node<E> node(int index) {
    // 如果index过半,从链尾开始查找
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        // 否则从链头开始查找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

2. 链表的实现

使用Java语言编写一个简单的单向链表,支持addget操作。

java 复制代码
public class Linked<T> {
	private int size;
	private Node<T> first;
	private Node<T> last;

	public void add(T data) {
		Node<T> node = new Node(data, null);
		if (first == null) {
			first = last = node;
		} else {
			last.next = node;
			last = node;
		}
		size++;
	}

	public T get(int index) {
		if (index >= size) {
			throw new IndexOutOfBoundsException("size:" + size + ",index:" + index);
		}
		Node<T> node = first;
		while (index-- > 0) {
			node = node.next;
		}
		return node.data;
	}

	private class Node<T> {
		private T data;
		private Node<T> next;

		public Node(T data, Node<T> next) {
			this.data = data;
			this.next = next;
		}
	}
}

3. 链表实现LRU淘汰算法

【要求】

设计一个给定容量的容器,支持add操作,元素不可重复。支持print操作,保证输出的元素顺序就是其访问顺序。如果元素超出最大容量,则自动淘汰掉最久未被访问的元素。

【分析】

使用一个单向链表,如果添加的元素存在,则将它移动到链头,如果不存在则插入到链头,如果链表到达存储阈值,则移除掉链尾元素。这样从链头到链尾就依次是元素的访问顺序。

【实现】

java 复制代码
public class LRULinked<T> {
	private int capacity;//容量
	private int size;//元素数量
	private Node<T> head;//链头

	public LRULinked(int capacity) {
		head = new Node<>(null, null);
		this.capacity = capacity;
	}

	/**
	 * 向链表中添加data
	 * 1.如果存在则移动到链头
	 * 2.不存在则插入到链头
	 * 3.到达阈值,要淘汰掉最久的数据
	 * @param data
	 */
	public void add(T data) {
		Node<T> node;
		Node<T> prevNode = findPrevNode(data);
		if (prevNode != null) {
			// 存在,移除原节点
			node = prevNode.next;
			prevNode.next = prevNode.next.next;
		} else {
			// 不存在
			node = new Node<>(data, null);
			if (size == capacity) {
				// 淘汰链尾
				Node<T> tail = head;
				while (tail.next != null) {
					if (tail.next.next == null) {
						tail.next = null;
						break;
					}
					tail = tail.next;
				}
			}else {
				size++;
			}
		}
		node.next = head.next;
		head.next = node;
	}

	// 找到data节点的前一个节点
	private Node<T> findPrevNode(T data) {
		Node<T> node = head;
		while (node.next != null) {
			node = node.next;
			if (data == node.data || (data.hashCode() == node.hashCode() && data.equals(node.data))) {
				return node;
			}
		}
		return null;
	}

	// 打印链表
	public void print() {
		Node<T> node = head.next;
		while (node != null) {
			System.out.println(node.data);
			node = node.next;
		}
	}

	// 链表内部的Node节点
	private class Node<T> {
		private final T data;
		private Node<T> next;

		public Node(T data, Node<T> next) {
			this.data = data;
			this.next = next;
		}
	}
}

4. 总结

链表和数组最大的区别是存储空间无需连续,因此理论上只要内存足够,它是没有界限的。元素的插入和删除只需修改节点的指针,时间复杂度为O(1),效率极高。链表没有索引的概念,数据的访问效率低,要访问第n个元素,只能通过next不断查找,时间复杂度是O(n)。

相关推荐
喵手几秒前
反射机制:你真的了解它的“能力”吗?
java·后端·java ee
用户466537015052 分钟前
git代码压缩合并
后端·github
武大打工仔5 分钟前
从零开始手搓一个MVC框架
后端
开心猴爷11 分钟前
移动端网页调试实战 Cookie 丢失问题的排查与优化
后端
kaika111 分钟前
告别复杂配置!使用 1Panel 运行环境功能轻松搭建 Java 应用
java·1panel·建站·halo
用户57240561411 分钟前
解析Json
后端
舒一笑12 分钟前
Mac 上安装并使用 frpc(FRP 内网穿透客户端)指南
后端·网络协议·程序员
每天学习一丢丢18 分钟前
Spring Boot + Vue 项目用宝塔面板部署指南
vue.js·spring boot·后端
邹小邹18 分钟前
Go 1.25 强势来袭:GC 速度飙升、并发测试神器上线,内存检测更精准!
后端·go
有梦想的攻城狮18 分钟前
Java 11中的Collections类详解
java·windows·python·java11·collections