目录
(一) 单链表
1.定义
链表是一种物理存储结构上非连续,非顺序的线性数据存储结构,数据元素的逻辑顺序是通过链表中的节点(Node)链接次序实现的。每个链表中都包含两部分: 存储的数据 和 下一个节点(Node).
class Node{
private E e; // 存储的数据
private Node next; // 下一个节点
}
在之前的博客中实现的动态数组、栈以及队列时,底层都是依托的静态数组,靠resize来解决固定容量的问题, 而链表是真正的动态数据结构, 无需处理固定容量问题.
2. 图解
- 数据存储在节点(Node)中, 当一个节点(next)为NULL时, 代表此元素为链表的最后一个元素
- 链表插入元素
- 链表删除元素
-
3.数组和链表的对比
- 数组静态分配内存, 链表动态分配内存.
- 数组在内存中是连续的,链表是不连续的。
- 数组利用索引定位,查找的时间复杂度是O(1),链表通过遍历定位元素,查找的时间复杂度是O(N)。
- 数组插入和删除需要移动其他元素,时间复杂度是O(N),链表的插入或删除不需要移动其他元素,时间复杂度是O(1)。
(二) 自定义链表LinkedList
1. 新建自定义链表实体类LinkedList
public class LinkedList<E> {
/**
* 节点内部类: 可以单独定义出来, 但用户无需关注链表底层实现, 因此定义成LinkedList的内部类
*
* @author Admin_Lian
*
*/
private class Node {
/**
* 存储的数据
*/
public E e;
/**
* 下一个节点
*/
public Node next;
public Node() {
this(null, null);
}
public Node(E e) {
this(e, null);
}
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
@Override
public String toString() {
return e.toString();
}
}
/**
* 头部节点: 链表第一元素
*/
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;
}
}
2. 自定义链表的增加操作
- 在链表头部添加新元素e
public void addFirst(E e) {
// 创建一个新头节点
Node headNode = new Node(e);
// 新头节点的下一节点next 等于 当前链表的头节点
headNode.next = this.head;
// 当前链表的头节点 等于 新头节点
this.head = headNode;
// 第二种写法
// this.head = new Node(e, this.head);
size++;
}
- 在链表index位置处添加新元素e
public void add(int index, E e) {
if(index < 0 || index > size) {
throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
}
// index == 0, 相当于在链表头部添加新元素, 但index == 0 没有上一个节点. 因此需要单独处理(待优化)
if (index == 0) {
addFirst(e);
} else {
// 查询要添加元素的前一个节点, 因此查询index-1位置处的节点
Node prev = this.head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
Node node = new Node(e);
// 新添加的节点next 指向 index-1位置处的节点
node.next = prev.next;
// index-1位置处的节点next 指向 新添加的节点
prev.next = node;
// prev.next = new Node(e, prev.next);
size++;
}
}
- 在链表末尾添加新元素e
public void addLast(E e) {
add(size, e);
}
3. 为自定义链表设置一个虚拟的节点
- 为链表设置一个虚拟的节点: 在add(idnex, e)操作, 也能找到上一个节点, 无需多余的判断. 初始链表时dummyHead, 存储的数据为Null, 下一个节点next也为Null.
优化自定义链表LinkedList
/**
* 虚拟头部节点: 存储的数据为Null, 下一个节点next也为Null
*/
private Node dummyHead;
/**
* 链表元素个数
*/
private int size;
public LinkedList() {
dummyHead = new Node(null, null);
size = 0;
}
// ....
/**
* 在链表index位置处添加新元素e: 新添加一个虚拟节点, 因此index=0时, 也能找到上一个节点
*
* @param index
* @param e
*/
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
}
// 查询要添加元素的前一个节点, 因为新添加一个虚拟节点, 所以查询index位置处的节点
Node prev = this.dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node node = new Node(e);
// 新添加的节点next 指向 index-1位置处的节点
node.next = prev.next;
// index-1位置处的节点next 指向 新添加的节点
prev.next = node;
// prev.next = new Node(e, prev.next);
size++;
}
/**
* 在链表头部添加新元素e
*
* @param e
*/
public void addFirst(E e) {
add(0, e);
}
4. 自定义链表的查询操作
- 获取链表的第index位置处的元素
publicE get(int index) {
if (isEmpty()) {
throw new IllegalArgumentException("add(index, e) failed. LinkedList is empty.");
}
if (index < 0 || index > size) {
throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
}
// 获取当前真正第一个元素的节点
Node cur = this.dummyHead.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.e;
}
- 获取链表第一个元素 和 获取链表最后一个元素
/**
* 获取链表第一个元素
* @return
*/
public E getFirst() {
return get(0);
}
/**
* 获取链表最后一个元素
*
* @return
*/
public E getLast() {
return get(size - 1);
}
5. 自定义链表的修改操作
- 修改链表第index位置的元素为e
public void set(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
}
Node cur = this.dummyHead.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
cur.e = e;
}
- 删除链表第index位置的元素
public E remove(int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("remove(index, e) failed. Illegal index.");
}
// 查找删除元素的上一个节点
Node prev = this.dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// index处的节点
Node resNode = prev.next;
// 上一个节点的next 等于 index处的节点next
prev.next = resNode.next;
resNode.next = null;
size--;
return resNode.e;
}
6. 自定义链表的删除操作
- 删除链表第一个的元素 和 删除链表最后一个的元素
/**
* 删除链表第一个的元素
*
* @return
*/
public E removeFirst() {
return remove(0);
}
/**
* 删除链表最后一个的元素
*
* @return
*/
public E removeLast() {
return remove(size - 1);
}
- 查找链表种是否存在e
public boolean contains(E e) {
Node cur = this.dummyHead.next;
// 链表最后一个元素节点的next 一定为 null
while(cur != null) {
if (cur.e.equals(e)) {
return true;
}
cur = cur.next;
}
return false;
}
- 覆盖重写toString()方法
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("LinkedList: ");
for (Node cur = this.dummyHead.next; cur != null; cur = cur.next) {
res.append(cur.e + " -> ");
}
res.append("NULL");
return res.toString();
}
测试
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 5; i++) {
linkedList.addFirst(i);
System.out.println(linkedList);
}
linkedList.add(2, -1);
System.out.println(linkedList);
linkedList.set(2, -2);
System.out.println(linkedList);
linkedList.remove(2);
System.out.println(linkedList);
}
(三) 时间复杂度分析
- 添加操作: 整体时间复杂度为 O(n)
函数 | 时间复杂度 | 分析 |
---|---|---|
addLast(e) | O(n) | 链表尾部添加元素, 需要遍历链表, 与数据规模呈线性关系 |
addFirst(e) | O(1) | 链表头部添加, 只需要改变虚拟节点的next指向, 此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
add(index, e) | O(n/2) = O(n) | 消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略 |
总结: | O(n) | 添加操作整体时间复杂度为 O(n), 分析时间复杂度一般考虑最坏的情况 |
- 删除操作: 整体时间复杂度为 O(n)
函数 | 时间复杂度 | 分析 |
---|---|---|
removeLast(e) | O(n) | 删除链表尾部元素, 需要遍历链表, 与数据规模呈线性关系 |
removeFirst(e) | O(1) | 删除链表头部元素, 只需要改变虚拟节点的next指向, 此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
remove(index, e) | O(n/2) = O(n) | 消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略 |
总结: | O(n) | 删除操作整体时间复杂度为 O(n) |
- 查询操作: 复杂度为 O(n)
函数 | 时间复杂度 | 分析 |
---|---|---|
get(index) | O(n) | 需要遍历链表, 与数据规模呈线性关系 |
contain(e) | O(n) | 需要遍历链表, 与数据规模呈线性关系 |
总结: | O(n) | 复杂度为 O(n) |
- 修改操作: 时间复杂度为 O(n)
函数 | 时间复杂度 | 分析 |
---|---|---|
set(index, e) | O(n) | 需要遍历链表, 与数据规模呈线性关系 |
总结: | O(n) | 修改操作整体时间复杂度为 O(n) |
(四) 使用链表实现栈
/**
* 自定义链表栈
*
* @author Admin_Lian
*
* @param <E>
*/
public class LinkedListStack<E> implements Stack<E> {
// 底层采用链表存储数据结构
private LinkedList<E> linkedList;
public LinkedListStack() {
linkedList = new LinkedList<>();
}
/**
* 获取栈中的有效元素个数
*
* @return
*/
@Override
public int getSize() {
return linkedList.getSize();
}
/**
* 返回栈中有效元素是否为空
*
* @return
*/
@Override
public boolean isEmpty() {
return linkedList.isEmpty();
}
/**
* 向栈(栈顶)中添加元素
*
* @param e
*/
@Override
public void push(E e) {
linkedList.addFirst(e);
}
/**
* 从栈(栈顶)中取出元素
*
* @return
*/
@Override
public E pop() {
return linkedList.removeFirst();
}
/**
* 查看栈顶元素
*
* @return
*/
@Override
public E peek() {
return linkedList.getFirst();
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Stack: top ");
res.append(linkedList);
return res.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);
}
(五) 使用链表实现队列
借用head头指针和tail尾指针实现队列
/**
* 自定义链表队列
*
* @author Admin_Lian
*
*/
public class LinkedListQueue<E> implements Queue<E>{
/**
* 节点内部类: 可以单独定义出来, 但用户无需关注链表底层实现, 因此定义成LinkedList的内部类
*
* @author Admin_Lian
*
*/
private class Node {
/**
* 存储的数据
*/
public E e;
/**
* 下一个节点
*/
public Node next;
@SuppressWarnings("unused")
public Node() {
this(null, null);
}
public Node(E e) {
this(e, null);
}
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
@Override
public String toString() {
return e.toString();
}
}
/**
* 头部节点: 链表第一个元素
*/
private Node head;
/**
* 尾部节点: 链表最后一个元素
*/
private Node 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 e) {
// 链表队列为空
if (this.tail == null) {
// 尾节点 等于 要添加元素节点
this.tail = new Node(e);
// 头节点 等于 尾节点
this.head = this.tail;
} else {
// 尾部节点的next 等于 要添加元素节点
this.tail.next = new Node(e);
// 尾部节点 等于 尾部节点的next
this.tail = this.tail.next;
}
size++;
}
/**
* 出队: 从链表队列(队首)中取出一个元素
*
* @return
*/
@Override
public E dequeue() {
if (this.tail == null) {
throw new IllegalArgumentException("dequeue() failed. LikedListQueue is empty.");
}
Node resNode = this.head;
// 新头节点 等于 要出队的节点的next
this.head = resNode.next;
resNode.next = null;
// 链表队列只存在一个元素, 且要出队
if (this.head == null) {
this.tail = null;
}
size--;
return resNode.e;
}
/**
* 查看链表队列队首元素
*
* @return
*/
@Override
public E getFront() {
if (isEmpty()) {
throw new IllegalArgumentException("getFront() failed. LikedListQueue is empty.");
}
return this.head.e;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("LinkedListQueue: front ");
// Node node = this.head;
// for (int i = 0; i < size; i++) {
// res.append(node.e + " -> ");
// node = node.next;
// }
// Node node = this.head;
// while (node != null) {
// res.append(node.e + " -> ");
// node = node.next;
// }
for (Node node = this.head; node != null; node = node.next) {
res.append(node.e + " -> ");
}
res.append("NUll tail");
return res.toString();
}
}
测试
public static void main(String[] args) {
LinkedListQueue<Integer> queue = new LinkedListQueue<>();
// 向队列中添加元素: 0-9
for (int i = 0; i < 10; i++) {
queue.enqueue(i);
System.out.println(queue);
// 每添加三个元素, 移除队列队首元素
if(i % 3 == 2) {
queue.dequeue();
System.out.println(queue);
}
}
}