1. 单例模式
单例模式在面试中掌握饿汉和懒汉中的一种就可以了,下面都是懒汉模式
1.1 线程不安全的单例模式
class SingletonDemo {
private static SingletonDemo instance = null; //私有化参数,无法从通过外部函数获取
private SingletonDemo(){} //构造方法私有化,无法通过外部创建
public static Singleton getInstance() { //提供一个获取对象的方法
if(instance == null)
instance = new SingletonDemo();
return instance;
}
}
1.2 考虑多线程使用DCL单例模式
class SingletonDemo {
private static SingletonDemo instance = null; //私有化参数,无法从通过外部函数获取
private SingletonDemo(){} //构造方法私有化,无法通过外部创建
public static Singleton getInstance() { //提供一个获取对象的方法
if(instance == null) //第一次判断保证instance没有创建
synchronized(SingletonDemo.class) { //加锁
if(instance == null) { //第二次判断保证在加锁过程中没有创建新的对象
instance = new SingletonDemo();
}
}
return instance;
}
}
1.3 再次优化(建议背诵版本)
对于上面DCL的模式,大部分情况下已经可以避免多线程带来的问题,但是还会因为指令重排出现一些意想不到的问题,为了解决指令重排,使用volatile
去优化上面的代码
class SingletonDemo {
private static volatile SingletonDemo instance = null; //通过加上volatile来解决指令重排引发的问题
private SingletonDemo(){}
public static Singleton getInstance() {
if(instance == null)
synchronized(SingletonDemo.class) {
if(instance == null) {
instance = new SingletonDemo();
}
}
return instance;
}
}
关于为何指令重排会引起问题,在这里进行一个解释,对于instance = new SingletonDemo()
在字节码层面它的运行分为三个指令
memory = allocate();//1. 分配对象内存空间
instance(memory); //2. 初始化对象
instance = memory; //3. 设置instance指向刚分配的内存地址,此时Instance != null
对于指令2和指令3之间并没有所谓的相互依赖,因此操作系统会可能会对它进行指令重排,首先运行指令3,此时instance不为null了,但是instance的初始化还没有完成,当别的线程取instance时就会出现错误!
2. 手写线程池
基于生产者消费者模式,我们自己手写一个简单的线程池!
2.1 生产者消费者模式
方式一:使用synchronized实现(最简单版本实现建议背诵)
class ProducerAndConsumerDemo{
private int number = 0;
//生产者
public synchronized void Producer throws InterruptedException{
while(number != 0) {
this.wait();
}
number++;
this.notifyAll();
}
//消费者
public synchronized void Consumer throws InterruptedException{
while(number == 0) {
this.wait();
}
number--;
this.notifyAll();
}
}
方式二:使用ReentrantLock实现(建议掌握)
class ProducerAndConsumerDemo{
private int number = 0;
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//生产者
public void Producer{
lock.lock();
try{
//判断
while(number != 0) {
condition.await();
}
//干活
number++;
//通知
condition.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
//消费者
public void Consumer {
lock.lock();
try{
//判断
while(number == 0) {
condition.await();
}
//干活
number--;
//通知
condition.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
2.2 手写线程池(背诵)
//基于生产者消费者模式的一个任务队列
class RunnableTaskQueue{
private LinkedList<Runnable> runnableTaskList = new LinkedList<>();
public Runnable getTask() throws InterruptedException {
synchronized(runnableTaskList.class) {
while(runnableTaskList.isEmpty()) {
runnableTaskList.wait();
}
return runnableTaskList.removeLast();
}
}
public void addTask(Runnable runnable) {
synchronized(runnableTaskList.class) {
runnableTaskList.add(runnable);
runnableTaskList.notifyAll();
}
}
}
class MyExecutor {
private final int poolSize;
//任务队列
private RunnableTaskQueue runnableTaskQueue;
//用于存放线程池中的线程的List
private final List<Thread> threads = new ArrayList<>();
MyExecutor(int poolSize) {
this.poolSize = poolSize;
runnableTaskQueue = new RunnableTaskQueue();
//JAVA的流式计算 等同于一个for循环
Stream.iterate(1, item->item + 1).limit(poolSize).forEach(item->{
initExecutor();
});
}
private void initExecutor(){
//循环创建线程直到达到线程池的poolSize数
if(threads.size() <= poolSize) {
Thread thread = new Thread(()->{
//这里线程的死循环保证线程的持续运行,去执行循环体中的任务
while(true) {
try{
Runnable task = runnableTaskQueue.getTask();
task.run();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//加入到队列中统计已经开启线程的数量
threads.add(thread);
thread.start();
}
}
//对外暴露的方法用于执行任务
public void executor(Runnable runnable){ runnableTaskQueue.addTask(runnable); }
}
3. 快速排序
快速排序,编写时书写好以下两个函数
- partition 这是快排的核心,保证每一次能够有一个数字回归到正确的位置上,并且该数字两边满足 左边 < 该数 < 右边
- 一个递归调用partition最终让整个数组有序的函数
对于快排的优化均是对于partition的优化
1. 最原始的快排
class qucikSortBasic {
private void swap(int[] arr, int index1, int index2) {
int t = arr[index1];
arr[index1] = arr[index2];
arr[index2] = t;
}
private int partition(int[] arr, int l, int r){
int v = arr[l];
int j = l;
for(int i = l + 1; i <= r; i++) {
if(arr[i] < v) {
j++;
swap(arr, i, j);
}
}
swap(arr, l, j);
return j;
}
private void recursion(int[] arr, int l, int r){
if(l >= r)
return;
int p = partition(arr, l, r);
recursion(arr, l, p - 1);
recursion(arr, p + 1, r);
}
public int[] quickSort(int[] arr) {
recursion(arr, 0, arr.length - 1);
return arr;
}
}
2. 升级一次
Q:为何升级?
对于一个近乎有序的数组,我们每次选取最左边的一个元素,很可能会导致每次取到的都是最小的,从而导致数组的时间复杂度退化为O(n²),因此我们用一个随机算法,随机选取一个数作为我们进行比较的数
private int partition(int[] arr, int l, int r){
//通过设置随机数,可以避免在近乎有序时每次取到较小的值,导致算法性能退化
Random random = new Random(System.currentTimeMillis());
swap(arr, l , random.nextInt(r - l + 1) + l);
int v = arr[l];
int j = l;
for(int i = l + 1; i <= r; i++) {
if(arr[i] < v) {
j++;
swap(arr, i, j);
}
}
swap(arr, l, j);
return j;
}
3. 双路快排(建议背诵)
对于上面的升级,还有一定问题就是当数组高度重复时,如数组中有100万个数都是0-10,此时我们的快速排序再次退化,具体原因如下图所示
此时每次进行partition都导致极度的不平衡,从而性能下降!
private int partition(int[] arr, int l, int r){
//通过设置随机数,可以避免在近乎有序时每次取到较小的值,导致算法性能退化
Random random = new Random(System.currentTimeMillis());
swap(arr, l , random.nextInt(r - l + 1) + l);
int v = arr[l];
int left = l + 1, right = r;
while(left <= right) {
while(left <= right && arr[left] < v)
left++;
while(left <= right && arr[right] >= v)
right--;
if(left <= right)
swap(arr, left++, right--);
}
swap(arr, l, right);
return right;
}
其实快排最终极的还有一个三路快排,也就是一次排序中可以将数组分为三路快排,也就是一次partition将数组分为三个部分 【小于V】 【等于V】 【大于V】 建议有兴趣的可以自行去了解一下
4. 归并排序
class MergeSort {
//归并操作
private void merge(int[] nums, int l, int mid, int r) {
int[] aux = new int[r - l + 1];
for(int i = l; i <= r; i++) {
aux[i - l] = nums[i];
}
int i = l, j = mid + 1;
for(int k = l; k <= r; k++) {
if(i > mid) {
nums[k] = aux[j - l];
j++;
}else if(j > r) {
nums[k] = aux[i - l];
i++;
}else if(aux[i - l] < aux[j - l]) {
nums[k] = aux[i - l];
i++;
}else{
nums[k] = aux[j - l];
j++;
}
}
}
private void mergeSort(int[] arr, int l, int r) {
if(l >= r)
return;
int mid = l + (r - l)/2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
//如果arr[mid] <= arr[mid + 1]那么久不需要进行merge了
if(arr[mid] > arr[mid + 1])
merge(arr, l, mid, r);
}
public int[] sort(int[] arr) {
mergeSort(arr, 0, arr.length - 1);
return arr;
}
}
5. 堆排序
对于堆的性质有以下两个(以大根堆为例子)
- 堆中某个节点的值总是不大于其父节点的值
- 堆总是一棵完全二叉树
如果面试官让你写堆排序,那么其实是让你实现一个堆(大根堆或小根堆),包括插入、删除等操作!下面以大根堆为例,我们来建立自己的堆
class MaxHead {
//这里我们第一位不要,即List从1开始存储
List<Integer> list = new ArrayList<>();
int count;
MaxHead() {
count = 0;
}
private void shiftUp(int k) {
while(list.get(k) > list.get(k / 2)) {
Collections.swap(list, k, k / 2);
k = k / 2 ;
}
}
private void shiftDown(int k) {
while(2 * k <= count) {
int j = 2 * k;
if(j + 1 <= count && data[j + 1] > data[j]) {
j++;
}
if(list.get(k) >= list.get(j))
break;
Collections.swap(list, j, k);
k = j;
}
}
public void insert(int k) {
list.add(k);
count++;
shiftUp();
}
public int getMax() throws Exception{
if(count == 0)
throw new Exception("这是空堆!")
return list.get(1);
}
public int extractMax() throws Exception{
if(count == 0)
throw new Exception("这是空堆!")
int res = list.get(1);
Collections.swap(list, 1, list.size() - 1);
list.remove(list.size() - 1);
shiftDown(1);
return res;
}
public boolean isEmpty() {
return count == 0;
}
public int getCount() {
return count;
}
}
6. LRU算法
LRU算法算是各大厂商面试的常客了,目前已经收到至少3-4位同学的反馈在面试中让讲出LRU的实现方式或者手写LRU算法
关于LRU,算法也即最近未使用算法的实现使用两个数据结构进行实现
- 一个双向链表(方便对数的增加和删除)
- 一个HashMap(方便快速遍历链表),每一个key都指向一个链表的node
//建立一个节点类(双向链表节点)
class Node {
int key, value;
Node pre, next;
Node(int key, int value) {
this.key = key;
this.value = value;
pre = null;
next = null;
}
}
//双向链表
class DoubleList {
Node head; //头结点
Node tail; //尾结点
int count; //计数器
DoubleList() {
count = 0;
head = new Node(-1, -1);
tail = new Node(-1, -1);
head.next = tail;
tail.pre = head;
}
public Node getLast() {
if(tail.pre != head)
return tail.pre;
return null;
}
public void removeLast() {
if(tail.pre != head)
remove(tail.pre);
}
public void remove(Node node) {
Node preNode = node.pre;
Node nextNode = node.next;
preNode.next = nextNode;
nextNode.pre = preNode;
count--;
}
public void insert(Node node) {
Node nextNode = head.next;
head.next = node;
node.pre = head;
node.next = nextNode;
nextNode.pre = node;
count++;
}
public int getSize() {
return count;
}
}
class LRUCache {
HashMap<Integer, Node> rec = new HashMap<>();
DoubleList doubleList = new DoubleList();
int capacity = 2;
LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if(!rec.containsKey(key))
return -1;
Node node = rec.get(key);
int res = node.value;
doubleList.remove(node);
rec.remove(node.key);
put(key, res);
return res;
}
public void put(int key, int value){
if(rec.containsKey(key)) {
Node node = rec.get(key);
doubleList.remove(node);
rec.remove(key);
Node newNode = new Node(key, value);
doubleList.insert(newNode);
rec.put(key, newNode);
}else{
if(doubleList.getSize() >= capacity) {
Node node = doubleList.getLast();
doubleList.removeLast();
rec.remove(node.key);
}
Node newNode = new Node(key, value);
doubleList.insert(newNode);
rec.put(key, newNode);
}
}
}
6.2 使用LinkedHashMap实现LRU
import java.util.LinkedHashMap;
class LRUCache {
LinkedHashMap<Integer,Integer> map;
int capacity;
public LRUCache(int capacity) {
map = new LinkedHashMap<>();
this.capacity = capacity;
}
public int get(int key) {
if(map.containsKey(key)){
int value = map.get(key);
map.remove(key);
map.put(key,value);
return value;
}else{
return -1;
}
}
public void put(int key, int value) {
if(map.containsKey(key)){
map.remove(key);
}
if(map.size() == capacity){
map.remove(map.entrySet().iterator().next().getKey());
}
map.put(key,value);
}
}