JAVA面试白板编程常见算法

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. 快速排序

快速排序,编写时书写好以下两个函数

  1. partition 这是快排的核心,保证每一次能够有一个数字回归到正确的位置上,并且该数字两边满足 左边 < 该数 < 右边
  2. 一个递归调用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. 堆排序

对于堆的性质有以下两个(以大根堆为例子)

  1. 堆中某个节点的值总是不大于其父节点的值
  2. 堆总是一棵完全二叉树

如果面试官让你写堆排序,那么其实是让你实现一个堆(大根堆或小根堆),包括插入、删除等操作!下面以大根堆为例,我们来建立自己的堆

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,算法也即最近未使用算法的实现使用两个数据结构进行实现

  1. 一个双向链表(方便对数的增加和删除)
  2. 一个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);
    }
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值