什么是链表
链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址
特点
- 不需要连续的内存空间
- 有指针引用
- 三种最常见的链表结构:单链表、双向链表和循环链表
单链表
第一个结点叫作头结点,把最后一个结点叫作尾结点
头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表
尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点
while(p.next != null){}
数组VS链表
重要区别
数组 | 链表 | |
---|---|---|
访问效率 | 数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高 | 链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读 |
写效率 | 插入、删除 | |
内存连续 | 连续 | 不连续,通过指针链接 |
存储空间 | 数组的缺点是大小固定,一经声明就要占用整块连续内存空间 1. 如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out ofmemory)” 2. 如果声明的数组过小,则可能出现不够用的情况 | 节省空间 |
- 数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高
- 链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读
- 数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out ofmemory)”。如果声明的数组过小,则可能出现不够用的情况。
循环链表
循环链表的尾结点指针是指向链表的头结点
双向链表
每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址
虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。那相比单链表,双向链表适合解决哪种问题呢?
- B+Tree:Mysql索引 叶子节点 双向链表
- Spring AOP 注解,最新的技术,红黑树和链表查找
编程题
如何设计一个LRU缓存淘汰算法
最近使用,只需要维护一个有序的单链表就可以了。有序的指的就是加入的时间排序
实现一个单链表
List:ArrayList、LinkedList
稀疏数组:一般是针对多维
1 2 -1 -1
4 -1 6 -1
9 -1 1 -1
// -1表示没有数据
// a[3][4]; 3*4=12空间,稀疏数组就是真正存的数据远远小于我们开的空间。这种情况 往往会用链表来代替
链表特性:
- 知道头结点,通过头结点能遍历整个链表【所有需要声明头结点】
- 通过指针指向下一个节点 ,所有有ListNode结构,内存不连续
- 访问效率低,更新操作高效
class ListNode<E> {
E value; //值
ListNode next; //下一个的指针
ListNode(E value) {
this.value = value;
this.next = null;
}
}
package list;
import java.util.Objects;
/**
* 单向链表
*
* @author zw
* @create 2023-03-22 22:24
*/
public class MyLinkedList<E> implements MyList<E> {
/**
* 单向链表需要指定头节点
*/
private ListNode<E> head;
/**
* 链表长度
*/
private int size;
@Override
public void add(E e) { // O(n)
ListNode<E> node = new ListNode<>(e);
ListNode cur = head;
while (cur != null) {
cur = cur.next;
}
cur.next = node;
size++;
}
@Override
public void addHead(E e) { // O(1)
ListNode<E> newNode = new ListNode<>(e);
// 当前链表为空的情况
if(head == null){
head = newNode;
}else {
newNode.next = head;
head = newNode;
}
size++;
}
@Override
public void add(E e, int position) { // O(n)
if (position == 0) {
addHead(e);
} else {
ListNode<E> node = new ListNode<>(e);
ListNode cur = head;
for (int i = 1; i <= position; i++) {
if(position == i){
break;
}
cur = cur.next;
}
cur.next = node;
size++;
}
}
@Override
public void removeHead() { // O(1)
if (size == 0) return;
head = head.next;
size--;
}
@Override
public void remove(int index) { // O(n)
if (index > size - 1) throw new ArrayIndexOutOfBoundsException("越界");
if (index == 0) {
removeHead();
return;
};
ListNode curr = head;
for (int i = 1; i <= index; i++) {
if (i == index) break;
curr = curr.next;
}
ListNode removeNode = curr.next;
curr.next = removeNode.next;
size--;
}
@Override
public void remove(E e) { // O(n)
if (head.value.equals(e)) {
removeHead();
return;
}
ListNode beforNode = head;
ListNode cur = beforNode.next;
while (cur != null) {
// 当前节点是首节点
if (cur.value.equals(e)) {
beforNode.next = cur.next;
size--;
return;
}
beforNode = beforNode.next;
}
}
@Override
public void removeAll(E e) {// O(n)
ListNode beforNode = head;
ListNode cur = beforNode.next;
while (cur != null) {
// 当前节点是首节点
if (cur.value.equals(e)) {
beforNode.next = cur.next;
size--;
}
beforNode = beforNode.next;
}
if (head.value.equals(e)) {
removeHead();
}
}
@Override
public E get(int index) { // O(n)
if(index > size -1 ) throw new ArrayIndexOutOfBoundsException("下标越界");
ListNode<E> curr = head;
for (int j = 0; j < size; j++) {
if (index == j) break;
curr = curr.next;
}
return curr.value;
}
@Override
public Boolean isExit(E data) { // O(n)
ListNode cur = head;
while(cur != null){
if(cur.value == data) return true;
cur = cur.next;
}
return false;
};
/**
* 时间复杂度 O(1)
* @return
* @throws Exception
*/
@Override
public E getFirst() throws Exception {
return head.value;
}
/**
* 时间复杂度 O(n)
* @return
* @throws Exception
*/
@Override
public E getLast() throws Exception {
ListNode<E> curr = head;
for (int j = 0; j < size; j++) {
if (size == j) break;
curr = curr.next;
}
return curr.value;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return Objects.isNull(head.next);
}
public void print() {
StringBuffer sb = new StringBuffer();
if (size == 0) {
System.out.println("[]");
return;
}
ListNode curr = head;
sb.append("size=").append(size).append(" ").append("[");
while (curr != null) {
sb.append(curr.value.toString()).append(",");
curr = curr.next;
}
sb.append("]");
System.out.println(sb.toString());
}
}
实现一个双向链表
删除头结点
完整链表实现
package list;
/**
* @author zw
* @create 2023-03-23 0:35
*/
class DNode<E> {
E value; //值
DNode<E> next; //下一个的指针
DNode<E> pre; //指向的是前一个指针
DNode(E value) {
this.value = value;
}
}
public class DoubleLinkList<E> implements MyList<E> {
private DNode<E> head; //头
private DNode<E> tail; // 尾
transient int size = 0;
@Override
public void add(E e) {
if (head == null) addHead(e);
else {
DNode newNode = new DNode(e);
newNode.pre = tail;
tail.next = newNode;
tail = newNode;
size++;
}
}
@Override
public void addHead(E data) { // O(1)
DNode newNode = new DNode(data);
if (head == null) {
tail = newNode;
} else {
head.pre = newNode;
newNode.next = head;
}
head = newNode;
size++;
}
@Override
public void add(E e, int position) {
if (position == 0) {
addHead(e);
} else if (position == size - 1) {
add(e);
} else {
DNode<E> newNode = new DNode(e);
DNode<E> node = this.head;
for (int i = 1; i <= position; i++) {
node = node.next;
}
node.pre.next = newNode;
newNode.next = node.next;
node.next.pre = newNode;
newNode.pre = node.pre;
size++;
}
}
@Override
public void removeHead() {
if (head == null) return;
if (head.next == null) {
tail = null;
} else {
head.next.pre = null;
}
head = head.next;
size--;
}
@Override
public void removeTail() {
// 没有节点的情况
if (head == null) return;
// 一个节点的情况
if (head.next == null) {
tail = null;
head = null;
} else {
tail.pre.next = null;
tail = tail.pre;
}
size--;
}
@Override
public void remove(int position) {
// 删除的是头结点
if (position == 0) {
removeHead();
return;
}
// 删除的是尾节点
if (position == size - 1) {
removeTail();
}
if(position>0 && position < size - 1){
DNode<E> removeNode = head;
for (int j = 1; j <= position; j++) {
removeNode = removeNode.next;
}
removeNode.pre.next = removeNode.next;
removeNode.next.pre = removeNode.pre;
size--;
}
}
@Override
public void remove(E data) throws Exception {
DNode<E> removeNode = head.next;
while (removeNode != null) {
if (removeNode.value.equals(data)) break;
removeNode = removeNode.next;
}
// 找到了删除节点
if (removeNode.value.equals(data)) {
// 头结点
if (removeNode.equals(head)) removeHead();
// 尾节点
else if (removeNode.equals(tail)) removeTail();
else {
removeNode.next.pre = removeNode.pre;
removeNode.pre.next = removeNode.next;
size--;
}
}
}
@Override
public void removeAll(E data) {
DNode<E> removeNode = head.next;
while (removeNode != null) {
if (removeNode.value.equals(data)) {
// 头结点
if (removeNode.equals(head)) removeHead();
// 尾节点
else if (removeNode.equals(tail)) removeTail();
else {
removeNode.next.pre = removeNode.pre;
removeNode.pre.next = removeNode.next;
size--;
}
}
removeNode = removeNode.next;
}
}
@Override
public E get(int position) throws Exception {
if (position > size - 1) throw new ArrayIndexOutOfBoundsException("下标越界");
DNode<E> node = head;
for (int j = 0; j <= position; j++) {
if (position == j) return node.value;
node = node.next;
}
return null;
}
@Override
public Boolean isExit(E e) {
return MyList.super.isExit(e);
}
@Override
public E getFirst() {
if (head == null) return null;
return head.value;
}
@Override
public E getLast() {
if (tail == null) return null;
return tail.value;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return head == null;
}
public void print() {
StringBuffer sb = new StringBuffer();
if (size == 0) {
System.out.println("[]");
return;
}
DNode curr = head;
sb.append("size=").append(size).append(" ").append("[");
while (curr != null) {
sb.append(curr.value.toString()).append(",");
curr = curr.next;
}
sb.append("]");
System.out.println(sb.toString().replaceAll(",]","]"));
}
}
测试用例
public static void main(String[] args) throws Exception{
DoubleLinkList<Integer> list = new DoubleLinkList<>();
System.out.println("-----添加测试--------");
list.add(-1);
list.print();
for (int i = 0; i < 5; i++) {
list.add(i);
}
list.print();
list.addHead(-2);
list.print();
list.add(100, 1);
list.print();
System.out.println("-----删除测试--------");
list.removeHead();
list.removeTail();
list.print();
list.remove(1);
list.print();
System.out.println("-----查找测试--------");
System.out.println(list.getFirst());
System.out.println(list.getLast());
System.out.println(list.get(1));
}
-----添加测试--------
size=1 [-1]
size=6 [-1,0,1,2,3,4]
size=7 [-2,-1,0,1,2,3,4]
size=8 [-2,100,0,1,2,3,4]
-----删除测试--------
size=6 [100,0,1,2,3]
size=5 [100,1,2,3]
-----查找测试--------
100
3
1
实现一个单向循环链表
在链表的基础上,将尾节点指向头节点就好
package list;
/**
* 环形链表
*
* @author zw
* @create 2023-03-23 17:50
*/
public class CircularLinkedList<E> implements MyList<E> {
private ListNode<E> head;
private int size;
@Override
public void add(E e) {
ListNode<E> newNode = new ListNode<>(e);
if (head == null) {
head = newNode;
} else {
ListNode<E> lastNode = getNode(size - 1);
lastNode.next = newNode;
newNode.next = head;
}
size++;
}
@Override
public void removeHead() {
remove(0);
}
@Override
public void remove(int position) {
if (size == 0) return;
if (position == 0) {
if (size == 1) {
head = null;
} else {
ListNode<E> lastNode = getNode(size - 1);
head = head.next;
lastNode.next = head;
}
} else {
ListNode<E> removeBeforeNode = getNode(position - 1);
removeBeforeNode.next = removeBeforeNode.next.next;
}
size--;
}
@Override
public void remove(E e) {
if (size == 0) return;
ListNode<E> curr = this.head;
for (int i = 0; i < size; i++) {
if (curr.value.equals(e)) {
break;
}
curr = curr.next;
}
size--;
}
@Override
public E get(int position) {
ListNode<E> node = getNode(position);
if (node == null) return null;
return node.value;
}
@Override
public ListNode<E> getNode(int position) {
if (size == 0) {
return null;
}
if (position == 0) {
return head;
}
ListNode<E> curr = this.head;
for (int i = 1; i <= position; i++) {
curr = curr.next;
if (i == position) {
break;
}
}
return curr;
}
@Override
public E getFirst() throws Exception {
ListNode<E> node = getNode(0);
if (node == null) {
return null;
}
return node.value;
}
@Override
public E getLast() throws Exception {
ListNode<E> node = getNode(size - 1);
if (node == null) {
return null;
}
return node.value;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return !(size > 0);
}
public void print() {
StringBuffer sb = new StringBuffer();
if (size == 0) {
System.out.println("[]");
return;
}
ListNode curr = head;
sb.append("size=").append(size).append(" ").append("[");
for (int i = 0; i < size; i++) {
sb.append(curr.value.toString()).append(",");
curr = curr.next;
}
sb.append("]");
System.out.println(sb.toString().replaceAll(",]","]"));
}
}
测试用例
public static void main(String[] args) throws Exception {
CircularLinkedList<Integer> list = new CircularLinkedList<>();
System.out.println("-----添加测试--------");
list.add(-1);
list.print();
for (int i = 0; i < 5; i++) {
list.add(i);
}
list.print();
System.out.println("-----删除测试--------");
list.removeHead();
list.removeTail();
list.print();
list.remove(1);
list.print();
System.out.println("-----查找测试--------");
System.out.println(list.getFirst());
System.out.println(list.getLast());
System.out.println(list.get(1));
System.out.println("-----监测尾节点是否链接到头节点--------");
ListNode<Integer> lastNode = list.getNode(list.size - 1);
ListNode<Integer> headNode = list.getNode(0);
System.out.println(lastNode.next.equals(headNode));
}
执行结果
-----添加测试--------
size=1 [-1]
size=6 [-1,0,1,2,3,4]
-----删除测试--------
size=5 [0,1,2,3,4]
size=4 [0,2,3,4]
-----查找测试--------
0
4
2
-----监测尾节点是否链接到头节点--------
true
约瑟夫问题
详细描述我会写在这里
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
1 2 3 4 5 6 => 6 1 2 3 4 => 6 1 2 3 =>1 2 3 => 1 3 => 1
核心代码
/**
*
* @param n 数到n的人被杀
* @return
*/
private ListNode<E> josephProblem(int n) {
// 下一个报数人
ListNode<E> curr = this.head;
// 上一个报数人
ListNode<E> beforNnode = null;
while (!(this.size == 1)) {
// 循环链表开始杀人
for (int i = 1; i <= n; i++) {
if (i == n) {
// 杀人,剔除链表
beforNnode.next = curr.next;
curr = curr.next;
size--;
String log = String.format("当前报数=%s,被杀人=%s", n, curr.value);
System.out.println(log);
print();
} else {
curr = curr.next;
beforNnode = curr;
}
}
}
return head;
}
测试用例
public static void main(String[] args) throws Exception {
System.out.println("------------约瑟夫问题--------------");
// 构建链表
int m = 6;
CircularLinkedList<Integer> josephProblemList = new CircularLinkedList<>();
for (int i = 1; i <= m; i++) {
josephProblemList.add(i);
}
josephProblemList.print();
ListNode<Integer> node = josephProblemList.josephProblem(5);
System.out.println("剩下的人=" + node.value);
}
测试结果
size=6 [1,2,3,4,5,6]
当前报数=5,被杀人=6
size=5 [1,2,3,4,5]
当前报数=5,被杀人=5
size=4 [1,2,3,4]
当前报数=5,被杀人=4
size=3 [1,2,3]
当前报数=5,被杀人=3
size=2 [1,2]
当前报数=5,被杀人=2
size=1 [1]
剩下的人=1
实现一个双向循环链表
package list;
/**
* 双向循环链表
*
* @author zw
* @create 2023-03-23 0:35
*/
public class DoubleCircularLinkedList<E> implements MyList<E> {
private DNode<E> head; //头
private DNode<E> tail; // 尾
transient int size = 0;
/**
* 构建前后两个节点的环
*
* @param beforNode
* @param afterNode
*/
void buildRing(DNode<E> beforNode, DNode<E> afterNode) {
beforNode.next = afterNode;
afterNode.pre = beforNode;
print();
}
@Override
public void add(E e) {
if (head == null) addHead(e);
else {
DNode newNode = new DNode(e);
buildRing(tail, newNode);
buildRing(newNode, head);
tail = newNode;
size++;
}
}
@Override
public void addHead(E data) { // O(1)
DNode newNode = new DNode(data);
if (head == null) {
head = newNode;
tail = newNode;
buildRing(head, tail);
} else {
buildRing(tail, newNode);
buildRing(newNode, head);
head = newNode;
}
size++;
}
@Override
public void add(E e, int position) {
if (position == 0) {
addHead(e);
} else if (position == size - 1) {
add(e);
} else {
DNode<E> newNode = new DNode(e);
DNode<E> node = getNode(position);
buildRing(node.pre, newNode);
buildRing(newNode, node.next);
size++;
}
}
@Override
public void removeHead() {
if (size == 0) return;
else if (size == 1) {
head = null;
tail = null;
} else {
buildRing(tail, head.next);
head = head.next;
}
size--;
}
@Override
public void removeTail() {
// 没有节点的情况
if (head == null) return;
// 一个节点的情况
if (head.next == null) {
tail = null;
head = null;
} else {
buildRing(head, tail.pre);
tail = tail.pre;
}
size--;
}
@Override
public void remove(int position) {
// 删除的是头结点
if (position == 0) {
removeHead();
return;
}
// 删除的是尾节点
if (position == size - 1) {
removeTail();
}
if (position > 0 && position < size - 1) {
DNode<E> removeNode = getNode(position);
buildRing(removeNode.pre, removeNode.next);
size--;
}
}
@Override
public void remove(E data) throws Exception {
DNode<E> removeNode = head.next;
while (removeNode != null) {
if (removeNode.value.equals(data)) break;
removeNode = removeNode.next;
}
// 找到了删除节点
if (removeNode.value.equals(data)) {
// 头结点
if (removeNode.equals(head)) removeHead();
// 尾节点
else if (removeNode.equals(tail)) removeTail();
else {
buildRing(removeNode.pre, removeNode.next);
size--;
}
}
}
@Override
public void removeAll(E data) {
DNode<E> removeNode = head.next;
for (int i = 0; i < size; i++) {
if (removeNode.value.equals(data)) {
// 头结点
if (removeNode.equals(head)) removeHead();
// 尾节点
else if (removeNode.equals(tail)) removeTail();
else {
buildRing(removeNode.pre, removeNode.next);
size--;
}
}
removeNode = removeNode.next;
}
}
@Override
public E get(int position) throws Exception {
if (position > size - 1) throw new ArrayIndexOutOfBoundsException("下标越界");
DNode<E> node = head;
for (int j = 0; j <= position; j++) {
if (position == j) return node.value;
node = node.next;
}
return null;
}
@Override
public DNode<E> getNode(int position) {
if (size == 0) {
return null;
}
if (position == 0) {
return head;
}
DNode<E> curr = this.head;
for (int i = 1; i <= position; i++) {
curr = curr.next;
if (i == position) {
break;
}
}
return curr;
}
@Override
public Boolean isExit(E e) {
return MyList.super.isExit(e);
}
@Override
public E getFirst() {
if (head == null) return null;
return head.value;
}
@Override
public E getLast() {
if (tail == null) return null;
return tail.value;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return head == null;
}
public void print() {
StringBuffer sb = new StringBuffer();
if (size == 0) {
System.out.println("[]");
return;
}
DNode curr = head;
sb.append("size=").append(size).append(" ").append("[");
for (int i = 0; i < size; i++) {
sb.append(curr.value.toString()).append(",");
curr = curr.next;
}
sb.append("]");
System.out.println(sb.toString().replaceAll(",]", "]"));
}
public static void main(String[] args) throws Exception {
// 构建链表
int m = 6;
DoubleCircularLinkedList<Integer> josephProblemList = new DoubleCircularLinkedList<>();
for (int i = 1; i <= m; i++) {
josephProblemList.add(i);
}
josephProblemList.print();
DNode<Integer> node = josephProblemList.josephProblem(5);
System.out.println("剩下的人=" + node.value);
}
}
约瑟夫问题
详细描述我会写在这里
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
1 2 3 4 5 6 => 6 1 2 3 4 => 6 1 2 3 =>1 2 3 => 1 3 => 1
/**
* @param n 数到n的人被杀
* @return
*/
private DNode<E> josephProblem(int n) {
// 下一个报数人
DNode<E> curr = this.head;
while (!(this.size == 1)) {
// 循环链表开始杀人
for (int i = 1; i <= n; i++) {
if (i == n) {
// 杀人,剔除链表
curr.pre.next = curr.next;
curr.next.pre = curr.pre;
curr = curr.next;
size--;
String log = String.format("当前报数=%s,被杀人=%s", n, curr.value);
System.out.println(log);
print();
} else {
curr = curr.next;
}
}
}
return head;
}
与单向链表实现的区别:
双向循环链表能通过pre拿到上一个报数节点,而单向循环链表需要临时变量记录
测试用例
public static void main(String[] args) throws Exception {
// 构建链表
int m = 6;
DoubleCircularLinkedList<Integer> josephProblemList = new DoubleCircularLinkedList<>();
for (int i = 1; i <= m; i++) {
josephProblemList.add(i);
}
josephProblemList.print();
DNode<Integer> node = josephProblemList.josephProblem(5);
System.out.println("剩下的人=" + node.value);
}
运行结果
当前报数=5,被杀人=6
size=5 [1,2,3,4,6]
当前报数=5,被杀人=6
size=4 [1,2,3,6]
当前报数=5,被杀人=1
size=3 [1,2,3]
当前报数=5,被杀人=3
size=2 [1,3]
当前报数=5,被杀人=1
size=1 [1]
剩下的人=1