两题解析

解析

一、第一题

题目: 有n个人围成一圈,顺序排号。从第一个人开始报数报到第K个数,该人退出圈子,问最后留下的是原来第几号的那位。

例:1、2、3、4、5、6、7、8、9 K=3,结果1号留下

1、解法1 - 使用双向环形链表模拟

思路: 就是先构造一个环形链表,然后每次遍历到k个数的时候,就删除那个数,然后模拟从下一个数开始报。时间复杂度 O(n * k)

例子: 1 -> 2 -> 3 -> 4k = 3

第一次删除3

两题解析_第1张图片

第二次删除2:

两题解析_第2张图片

第三次删除4,最后剩下1

代码:

/**
 * 思路: 构造一个环形链表,每次删掉第k个。
 *      然后指针从k+1继续围着环形链表走。
 */
public class Solution {

    static class Node {
        int val;
        Node next;

        Node(int v) {
            val = v;
        }
    }

    static int solve(int n, int k) {
        if (k == 0 || n == 0)
            return -1;
        // 构造环形链表
        Node head = new Node(1);
        Node pre = head;
        for (int i = 2; i <= n; i++) {
            Node cur = new Node(i);
            pre.next = cur;
            pre = cur;
        }
        pre.next = head; // 构成环形
        Node tail = pre;
        int count = 0;
        while (head != tail) {
            if (++count == k) {
                tail.next = head.next; // del head
                head = tail.next;
                count = 0;
            } else {
                tail = tail.next;  // del tail
                head = tail.next;
            }
        }
        return head.val;
    }

    public static void main(String[] args){
        System.out.println(solve(9, 3)); // out --> 1
    }
}

2、解法二 - 使用一个数组记录删除的位置

时间复杂度还是O(n * m)

我们使用一个数组记录这个位置是否被删除了(-1表示删除),直接模拟,每次统计count == m,就删除当前移动到的位置,然后删除,然后找到下一个开始的位置。

两题解析_第3张图片

代码:

public class Solution {

    static int solve(int n, int k) {
        if (n == 0 || k == 0) return -1;
        int deleteNum = 0; // 已经删除的人的个数
        int curPos = 1; // 当前位置
        int[] a = new int[n + 1]; // 记录这个位置是否被删,如果是-1就是被删了
        for (int i = 1; i <= n; i++) a[i] = i; // 一开始赋值为i
        while (deleteNum != n - 1) { // 删完n-1个
            int count = 1;
            while (count < k) {
                curPos = curPos % n + 1;
                if (a[curPos] != -1) count++;
            }
            deleteNum++;
            a[curPos] = -1; // 删除curPos位置的人
            while (a[curPos] == -1) curPos = curPos % n + 1; //找到一下起点,如果从0开始就是curPos = (curPos + 1) % n
        }
        return a[curPos];
    }

    public static void main(String[] args) {
        System.out.println(solve(4, 3)); // out --> 1
    }
}

3、解法三 - 约瑟夫环问题经典解法

动态规划解法

假设问题是从n个人编号分别为0...n-1,每次取第k个,则第k个人编号为k-1的淘汰。

看当由n个人淘汰第k个变成n-1个人之后重新编号的顺序。如下:

两题解析_第4张图片

例如由最后一个人推到两个人: 由f[1] = 0得到f[2]=(f[1]+k)%2,这样就可以求出最终胜利者在2个人的时候的情况下的编号。

总结递推公式f[n]=(f[n-1]+k)%n,可递推到最初编号序列中该胜利者的编号。

不过由于要求编号从1开始,只要把f[n]+1即可

代码:

public class Solution {

    static int solve(int n, int k){
        if(n == 0 || k == 0) return -1;
        int s = 0;
        for(int i = 2; i <= n; i++) s = (s + k) % i;
        return s + 1;//因为是从1开始,所以返回s+1即可
    }

    public static void main(String[] args){
        System.out.println(solve(9, 3)); // out --> 1
    }
}

二、第二题

提供一个懒汉模式的单实例类实现。
要求:
1)、考虑线程安全。
2)、基于junit提供测试代码,模拟并发,测试线程安全性,给出对应的断言。

直接在newInstance()上加上一把锁就是懒汉式的线程安全版本。在newInstance()中休眠一段时间。如果不加锁,多个线程同时访问就存在问题。

这里我如果将Singleton看做是封装的,在外面测,也能偶尔测试成功,但是需要尝试次数比较多。原因是因为newInstance()方法执行时间过短,线程间切换概率小

单例类:

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton(){

    }
    //  直接在 懒汉式 的方法上加上一把锁,这样才会线程安全, 但是性能一般
    public static /*synchronized*/ Singleton newInstance(){
        if(uniqueInstance == null) {

            // 休眠, 来测试线程安全性
            try {
               TimeUnit.MILLISECONDS.sleep((int) (50 * Math.random()));

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

测试类:

import static org.junit.Assert.assertTrue;

import org.junit.Test;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.*;

public class AppTest {

    @Test
    public void test() throws InterruptedException {
        int threadCount = 20;
        ExecutorService service = Executors.newFixedThreadPool(threadCount);
        List<Future<Singleton>> futures = new ArrayList<>();
        for (int c = 0; c < threadCount; c++) {
            Future<Singleton> submit = service.submit(() -> {
                // 如果不在Singleton中测,而是在这里测,比较难测出来,因为单例那个方法执行时间太短了
//                TimeUnit.MILLISECONDS.sleep((int)(1000 * Math.random()));
                return Singleton.newInstance();
            });
            futures.add(submit);
        }
        HashSet<Singleton> set = new HashSet<>();
        for (Future<Singleton> future : futures) {
            try {
                set.add(future.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        System.out.println(set.size());
        assertTrue(set.size() == 1);//断言
    }
}

不加synchronized的时候运行会出现:

两题解析_第5张图片

还有一种实现方式,使用SemaphoreCountDownLatch实现:

import org.junit.Test;
import static org.junit.Assert.assertTrue;

import java.util.HashSet;
import java.util.concurrent.*;

public class AppTest2 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    @Test
    public void test()throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        HashSet<Singleton> set = new HashSet<>();

        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();

                    set.add(Singleton.newInstance());

                    semaphore.release();
                } catch (Exception e) {
                    
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();

        System.out.println(set.size());
        assertTrue(set.size() == 1);
    }
}

你可能感兴趣的:(杂七杂八)