什么是链表
链表是基础数据结构之一,与数组一样,链表也支持数据的查找、插入和删除操作。
从底层存储来看,链表主要分为单向链表和双向链表
- 单向链表:它的每个节点存储着数据和后继节点指针,这意味着它可以在零散的内存空间中将自己的数据联系起来。
- 双向链表:唯一不同于单向链表的地方就是它的每个节点还额外存储了前驱节点指针
实现链表需要注意的几个点
- 实现插入操作时,需要先将target的next指向data2,然后再将data1的next指向target,不然会出现指针丢失的问题
- 链表为空时,代码是否能正常工作?
- 链表只包含一个结点时,代码是否能正常工作?
- 链表只包含两个结点时,代码是否能正常工作?
- 代码逻辑在处理头结点和尾结点的时候,代码是否能正常工作?
Java实现
单向链表
/**
* 实现一个单项链表
*
* @author ohYoung
* @date 2020/11/25 17:04
*/
public class Singly {
/**
* 头节点
*/
Node head = null;
public class Node {
Node next = null;
int data;
public Node(int data) {
this.data = data;
}
}
/**
* 根据下标获取节点
*/
public Node get(int targetIndex) {
assert (targetIndex >= 0 && targetIndex < length()) : "[query(int index)] illegal index: " + targetIndex;
int index = 0;
Node tmp = head;
while (index != targetIndex) {
tmp = tmp.next;
++index;
}
return tmp;
}
/**
* 根据节点查找其下标
*/
public int get(Node node) {
assert node != null : "[get(Node node)] illegal node";
if (head == null) {
return -1;
}
int index = 0;
Node tmp = head;
while (tmp.data != node.data) {
if (tmp.next == null) {
return -1;
}
tmp = tmp.next;
++index;
}
return index;
}
public void addNode(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
return;
}
Node tmp = head;
while (tmp.next != null) {
tmp = tmp.next;
}
tmp.next = newNode;
}
public void addNode(int index, int data) {
Node newNode = new Node(data);
int length = length();
if (index == length) {
addNode(data);
return;
}
if (index == 0) {
newNode.next = head;
head = newNode;
return;
}
assert (index > 0 && index < length) : "addNode with illegal index: " + index;
Node tmp = head;
int count = 0;
while (count != index) {
tmp = tmp.next;
count++;
}
// 找到tmp的前驱节点
Node pre = head;
while (tmp != pre) {
pre = pre.next;
}
newNode.next = tmp;
pre.next = newNode;
}
public boolean deleteNode(Node target) {
assert head != null : "empty linked list";
if (head == target) {
head = head.next;
return true;
}
Node tmp = head;
while (tmp.data != target.data) {
if (tmp.next == null) {
return false;
}
tmp = tmp.next;
}
// 找到tmp的前驱节点
Node pre = head;
while (pre.next != tmp) {
pre = pre.next;
}
pre.next = pre.next.next;
return true;
}
public boolean deleteNode(int index) {
assert (index >= 0 && index < length()) : "deleteNode with illegal index: " + index;
int sum = 0;
Node tmp = head;
while (sum != index) {
tmp = tmp.next;
sum++;
}
// 如果是头节点则没有前驱节点
if (tmp == head) {
head = head.next;
return true;
}
// 找到tmp的前驱节点
Node pre = head;
while (pre.next != tmp) {
pre = pre.next;
}
pre.next = pre.next.next;
return true;
}
public int length() {
int length = 0;
Node tmp = head;
while (tmp != null) {
length++;
tmp = tmp.next;
}
return length;
}
public void print() {
Node tmp = head;
while (tmp != null) {
System.out.println(tmp.data);
tmp = tmp.next;
}
}
public Node createNode(int data) {
return new Node(data);
}
}
双向链表
/**
* 实现一个双向链表
* @author ohYoung
* @date 2020/11/25 18:15
*/
public class Doubly {
Node head = null;
class Node {
Node pre = null;
int data;
Node next = null;
public Node(int data) {
this.data = data;
}
}
/**
* 根据下标获取节点
*/
public Node get(int targetIndex) {
assert (targetIndex >= 0 && targetIndex < length()) : "[query(int index)] illegal index: " + targetIndex;
int index = 0;
Node tmp = head;
while (index != targetIndex) {
tmp = tmp.next;
++index;
}
return tmp;
}
/**
* 根据节点查找其下标
*/
public int get(Node node) {
assert head != null : "empty linked list";
assert node != null : "[get(Node node)] illegal node";
int index = 0;
Node tmp = head;
while (tmp.data != node.data) {
if (tmp.next == null) {
return -1;
}
tmp = tmp.next;
++index;
}
return index;
}
public void addNode(int data) {
Node nextNode = new Node(data);
if (head == null) {
head = nextNode;
return;
}
Node tmp = head;
while (tmp.next != null) {
tmp = tmp.next;
}
tmp.next = nextNode;
nextNode.pre = tmp;
}
public void addNode(int index, int data) {
if (index == length()) {
addNode(data);
return;
}
Node newNode = new Node(data);
if (index == 0) {
newNode.next = head;
head = newNode;
return;
}
assert (index > 0 && index < length()) : "addNode illegal index: " + index;
Node tmp = head;
int count = 0;
while (count != index) {
tmp = tmp.next;
count++;
}
newNode.next = tmp;
newNode.pre = tmp.pre;
tmp.pre = newNode;
tmp.pre.next = newNode;
}
public boolean deleteNode(Node target) {
assert head != null : "empty linked list";
if (head == target) {
head = head.next;
head.pre = null;
return true;
}
Node tmp = head;
while (tmp.data != target.data) {
if (tmp.next == null) {
return false;
}
tmp = tmp.next;
}
tmp.pre.next = tmp.next;
tmp.next.pre = tmp.pre;
return true;
}
public boolean deleteNode(int index) {
assert (index >= 0 && index < length()) : "deleteNode illegal index: " + index;
// 只存在一个节点的情况
if (index == 0 && length() == 1) {
head = null;
return true;
}
int sum = 0;
Node tmp = head;
while (sum != index) {
tmp = tmp.next;
sum++;
}
// 如果是头节点则没有前驱节点
if (tmp == head) {
head = head.next;
head.pre = null;
return true;
}
tmp.pre.next = tmp.next;
tmp.next.pre = tmp.pre;
return true;
}
public int length() {
int length = 0;
Node tmp = head;
while (tmp != null) {
length++;
tmp = tmp.next;
}
return length;
}
public void print() {
Node tmp = head;
while (tmp != null) {
System.out.println(tmp.data);
tmp = tmp.next;
}
}
public Node createNode(int data) {
return new Node(data);
}
}
时间复杂度
删除数据的情况一般有以下两种
- 删除 值等于某个给定值 的节点
- 删除 给定指针指向 的节点
针对第一种情况,单纯删除节点的时间复杂度为O(1),但是找到这个节点需要从头节点开始遍历查找,时间复杂度为O(n),所以总的时间复杂度为O(n);
针对第二种情况,已知了要被删除节点x的位置;由于单链表没有存储x的前驱节点位置,所以需要遍历查找x的前驱节点,时间复杂度为O(n);而双向链表存储了前驱节点的位置,所以查找时间复杂度为O(1),总时间复杂度为O(1),由此可以看出来双向链表的优势了。
使用单链表实现LRU缓存
什么是LRU缓存
LRU全称为Least Recently Used,翻译过来就是最近最少使用;它是一种策略,当缓存容量满了之后删除最近最少使用的数据然后将新数据插入缓存
代码实现
/**
* 使用单链表实现 最近最少使用LRU(Least Recently Used)策略
*
* @author ohYoung
* @date 2020/11/29 16:14
*/
public class LRU {
private Singly singly;
private int length;
public LRU(int length) {
this.singly = new Singly();
this.length = length;
}
public void addData(int data) {
// 1. 如果数据在链表中存在, 将数据删除然后在链表头部插入
int i = singly.get(singly.createNode(data));
if (i != -1) {
singly.deleteNode(i);
singly.addNode(0, data);
return;
}
// 2. 如果数据在链表中不存在
if (singly.length() >= length) {
// 2.2. 如果链表满了, 则删除尾节点然后从头部插入最新数据
singly.deleteNode(singly.length() - 1);
}
// 2.1 如果链表未满, 则从头部插入数据
singly.addNode(0, data);
}
public void print() {
singly.print();
}
public static void main(String[] args) {
LRU lru = new LRU(3);
lru.addData(1);
lru.print();
System.out.println("==========================");
lru.addData(2);
lru.print();
System.out.println("==========================");
lru.addData(3);
lru.print();
System.out.println("==========================");
lru.addData(4);
lru.print();
System.out.println("==========================");
lru.addData(3);
lru.print();
System.out.println("==========================");
lru.addData(6);
lru.print();
System.out.println("==========================");
}
}
知识巩固
其实除了基础的添加、删除、查询功能,链表还有很多常见的操作,例如:
- 单链表反转
- 链表中环的检测
- 两个有序的链表合并
- 删除链表倒数第 n 个结点
- 求链表的中间结点
如果能自己实现上述几个功能的话,对链表的理解应该是够够的了~
总结
本文是在学习了王争老师在极客时间专栏推出的《数据结构与算法之美》并自己实现一边后总结出来的一些知识点,强烈推荐大家去学习一下这个专栏,感谢王争老师写出的这么高质量的内容!