一 面试经典:
1. 如何设计一个LRU缓存淘汰算法?基础
思想:新加的点来了, 首先去链表里面遍历,如果找到了。删掉 然后插入到头部。头部就是最新的吧如果不在原来的链表里:如果有空间就插入头部。LRU有内存限制的,如果没有空间了怎么办? 删除最后一个,完成了这个算法!
( 最近使用,只需要维护一个有序的单链表就可以了。有序的指的就是加入的时间排序 )
2. 约瑟夫问题 (对手娟,丢到谁谁淘汰,只留最后一个人)
二 什么是链表?
1.链表的定义
链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。
2.特点
(1)不需要连续的内存空间。
(2)有指针引用
(3)三种最常见的链表结构:单链表、双向链表和循环链表
单向链表说明:
从单链表图中,可以发现,有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们一般把第一个结点叫作头结点,把最后一个结点叫作尾结点。
其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点。while(p.next != null){} head 自己记录的
循环链表说明:
循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。从我画的循环链表图中,你应该可以看出来,它像一个环一样首尾相连,所以叫作“循环”链表。
三 代码实现单链表和双向链表
package list;
/**
* @author:Kevin
* @create: 2023-08-14 10:45
* @Description: 单向列表
*/
public class MyLinkedList {
private ListNode head;
private int size = 0;
public void insertHead(int data){ //插入链表头部 O(1)
ListNode newNode = new ListNode(data);
newNode.next = head;
head = newNode;
}
public void inertNth(int data,int postion){ //插入链表中间位置 O(n)
if (postion == 0){
insertHead(data);
}else {
ListNode cur = head;
for (int i = 1;i<postion;i++){
cur = cur.next;
}
ListNode listNode = new ListNode(data);
listNode.next = cur.next;
cur.next = listNode;
}
}
public void deleteHead(){ //O(1)
head = head.next;
}
public void deleteNth(int position){ //O(n)
if (position == 0){
deleteHead();
}else {
ListNode cur = head;
for (int i =1;i<position;i++){
cur = cur.next;
}
cur.next=cur.next.next;
}
}
public void print(){
ListNode cur = head;
while (cur!=null){
System.out.println(cur.value);
cur = cur.next;
}
}
}
class ListNode{
int value; //值
ListNode next; //指针
ListNode(int value) {
this.value = value;
this.next = null;
}
}
双向链表
package list;
/**
* 双向列表
*/
public class DoubleLinkList { // 双向链表
private DNode head; //头
private DNode tail; // 尾
DoubleLinkList(){
head = null;
tail = null;
}
public void inserHead(int data){
DNode newNode = new DNode(data);
if(head == null){
tail = newNode;
}else{
head.pre = newNode;
newNode.next = head;
}
head = newNode;
}
public void deleteHead(){
if(head == null) return ; //没有数据
if(head.next == null){ //就一个点
tail = null;
}else{
head.next.pre = null;
}
head = head.next;
}
public void deleteKey(int data){
DNode current = head;
while (current.value != data) {
if (current.next == null) {
System.out.println("没找到节点");
return ;
}
current = current.next;
}
if (current == head) {// 指向下个就表示删除第一个
deleteHead();
} else {
current.pre.next = current.next;
if(current == tail){ //删除的是尾部
tail = current.pre;
current.pre = null;
}else{
current.next.pre = current.pre;
}
}
}
}
class DNode{
int value; //值
DNode next; //下一个的指针
DNode pre; //指向的是前一个指针
DNode(int value){
this.value = value;
this.next = null;
this.pre = null;
}
}
四 数组vs链表
重要区别:
1.数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。
2.链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。
3.数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,
导致“内存不足(out ofmemory)”。如果声明的数组过小,则可能出现不够用的情况。
4.动态扩容:数组需再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容。