面试:逻辑题与思考题整理

一、红帽白帽问题

【问题】
有三顶红帽子和两顶白帽子.将其中的三顶帽子分别戴在 A、B、C三人头上.这三人每人都只能看见其他两人头上的帽子,但看不见自己头上戴的帽子,并且也不知道剩余的两顶帽子的颜色。
  问A: “你戴的是什么颜色的帽子?” A回答说:“不知道。” 接着,又以同样的问题问B。 B想了想之后,也回答说:“不知道。” 最后问C。 C也想了一会回答说:“我知道我戴的帽子是什么颜色了。” 当然,C是在听了A、B的回答之后而作出回答的。试问:C戴的是什么颜色的帽子?
  
【回答】
A回答不知道,说明A看到肯定不是两顶白帽,要么是两顶红帽,要么是一白一红;
B回答不知道,在他知道自己和C是要么两顶红帽,要么一白一红的前提下,说明C肯定不是白帽子,否则他就知道自己是红色的了。说明C是红帽子! C根据这一点推断出自己是红帽子!

二、区分硬币问题

100枚硬币平放在桌面上,10枚正面朝上,90枚反面朝上,不能看,不能仔细摸,可以移动翻转硬币。请问如何江它们分为正面朝上硬币数目相等的两堆?

方案:将其中10枚硬币移出来,翻个个。

解释 :加入移出的硬币里有9枚反,1枚正,翻转后就是9正1反,剩下的硬币里是81枚反,9枚正。

三、抢红包问题

——如何避免有的人抢不到,有的人抢的很多,有的人抢的很少?

容易想到的是:随机区间 ( 0, 剩余金额 ),这样会导致先抢的人拿得多。

思路:
假设一共有 N 元,一共有 K 个人,则可以每个人拿到的钱为 random(N - (K - 1) * 0.01),即保证后面K-1个人至少还有0.01元,然后更新N为剩余的钱,直到最后一个人就直接拿N。

生成随机数的方法:

Math.random()*100+1   //输出[0,100]的数

或者:

Random r = new Random();    
double res = 0;
res = r.nextInt(100);           
System.out.println(res);

四、生成随机数(概率相同)——n进制

——用 0-4 随机数生成器 rand4() 来生成0-6随机数 rand6()

初步印象:乘以四分之六不就行了?——不行!

  • rand4(),0-4随机数:0、1、2、3、4共5个数
  • rand6(),0-6随机数:0、1、2、3、4、5共7个数

思路:生成一个比6大的数据范围,并且能够确定每个数字出现的概率都相同。

rand4() * rand4() 和 rand4() + rand4() 得到的各个数的概率是不同的!

【n进制计算】
如果对0-4进行排列,得到16个数字,每个数字都是唯一的,说明每个数字出现的概率都是一致的!
在这里插入图片描述
故而rand4() * 5得到也是随机的 ,rand4() * 5 + rand4()也是随机的。所以可以对其中的0-20进行三等分。
结果:rand6() = (rand4() * 5 + rand4()) <= 20 ? x/3 : loop
代码:

class Main{
    public static void main(String[] args) {
        int[] result = new int[7];
        for (int i = 0; i < 50000; i++) {
            int r = rand6();
            result[r]++;
        }
        for (int i = 0; i < result.length; i++) {
            System.out.println("num:" + i + " times: " + result[i]);
        }
    }
    public static int rand4() {
        //大于等于 0.0 且小于 1.0
        double rand = Math.random() * 5;
        return (int) rand;
    }
    // 0 -6 实际 7个数字
    public static int rand6() {
        int result = rand4() * 5 + rand4();
        do {
            result = rand4() * 5 + rand4();
        } while (result > 20);
        return result / 3;
    }
}

五、两炷香测时间

两根香,一根烧完1小时,如何测量15分钟
思路:

先将一根香的一端点燃,另一根香的两端全部点燃。当第二根香全部烧完时,此时已经过了半个小时。再将第一根香的另一端也点燃,那么此时第一根香剩下部分烧完的时间就是 15 min。

六、有序数组中不重复元素的个数

七、topN问题(以及最小的k个数问题)

7.1 topN问题

——如何在很大的数量级的数据中,找出前1000大的数据?
来源于知乎

(1)数据还不够大的话:分治法

答:如果全部排序一遍的话,或者部分排序的话,时间复杂度就会很高。可以用分治法,比如快排操作。
随便找一个数t,用快排,使得左边的数大于它,右边的数小于它,如果前面一部分总数大于1000个,那么继续在前一部分进行快排查找,否则在后一部分进行查找。如果左边的数量等于1000,那么久找到了。

时间复杂度:O(N)
计算过程:快排的过程,时间是O(n),第二次减半,O(n/2),第三次O(n/4),显然是小于O(2n)的,所以就算作O(n)

——但是这样如果数据量很大,空间复杂度就会很高!

(2)数据超大,2G,怎么办?

可以把快排分出来的两部分数据放在txt文件里,第二次就从文件中读出来进行快排partition,但是这样的磁盘读写效率很低。

——用堆!

在内存中维护一个1000数的小顶堆(每个结点都比它的左右子节点小),小顶堆的根节点是最小的一个数。
即:取前1000个数(设为m),构成小顶堆(O(mlogm)),然后从文件中读取数据(共n个),和堆顶大小相比,如果比堆顶还小,就直接丢弃,如果比堆顶大,就替换堆顶,并且调整小顶堆。所有数据处理完毕后,小顶堆内就是top1000大的数了。(需要所有数据遍历一遍)

复杂度:时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

这样的话,数据就只需要读取一次,不存在多次读写的问题了。

eg:从1000个数中找出前50个

public class TopN {

    // 父节点
    private int parent(int n) {
        return (n - 1) / 2;
    }
    // 左孩子
    private int left(int n) {
        return 2 * n + 1;
    }
    // 右孩子
    private int right(int n) {
        return 2 * n + 2;
    }
    // 构建堆
    private void buildHeap(int n, int[] data) {
        for(int i = 1; i < n; i++) {
            int t = i;
            // 调整堆
            while(t != 0 && data[parent(t)] > data[t]) {
                int temp = data[t];
                data[t] = data[parent(t)];
                data[parent(t)] = temp;
                t = parent(t);
            }
        }
    }
    // 调整data[i]
    private void adjust(int i, int n, int[] data) {
        if(data[i] <= data[0]) {
            return;
        }
        // 置换堆顶
        int temp = data[i];
        data[i] = data[0];
        data[0] = temp;
        // 调整堆顶
        int t = 0;
        while( (left(t) < n && data[t] > data[left(t)]) || (right(t) < n && data[t] > data[right(t)]) ) {
            if(right(t) < n && data[right(t)] < data[left(t)]) {
                // 右孩子更小,置换右孩子
                temp = data[t];
                data[t] = data[right(t)];
                data[right(t)] = temp;
                t = right(t);
            } else {
                // 否则置换左孩子
                temp = data[t];
                data[t] = data[left(t)];
                data[left(t)] = temp;
                t = left(t);
            }
        }
    }

    // 寻找topN,该方法改变data,将topN排到最前面
    public void findTopN(int n, int[] data) {
        // 先构建n个数的小顶堆
        buildHeap(n, data);
        // n往后的数进行调整
        for(int i = n; i < data.length; i++) {
            adjust(i, n, data);
        }
    }

    // 打印数组
    public void print(int[] data) {
        for(int i = 0; i < data.length; i++) {
            System.out.print(data[i] + " ");
        }
        System.out.println();
    }

}

7.2 寻找最小的前K个数

https://blog.csdn.net/u012485480/article/details/78060752

如果是找到最小的前K个数,则用大顶堆!

——先取k个数构建大顶堆,根节点最大,每次从数据文件中取一个数,如果比堆定小,则用其替换根。然后重建大顶堆。读取完毕后,该堆中的数据为最小的k个数!

八、用2G内存找出10G乱序排列整数的中位数

在一个文件中有10G个整数,乱序排列,要求找出中位数。内存限制为2G。

解法:首先假设是32位无符号整数。

  • 1.读一遍10G个整数,把整数映射到256M个区段中,用一个64位无符号整数给每个相应区段记数。
    说明:整数范围是0 - 2^32 - 1,一共有4G种取值,映射到256M个区段,则每个区段有16(4G/256M = 16)种值,每16个值算一段, 0~15是第1段,16~31是第2段,……2^32-16 ~2^32-1是第256M段。一个64位无符号整数最大值是0~8G-1,这里先不考虑溢出的情况。总共占用内存256M×8B=2GB。
  • 2.从前到后对每一段的计数累加,当累加的和超过5G时停止,找出这个区段(即累加停止时达到的区段,也是中位数所在的区段)的数值范围,设为[a,a+15],同时记录累加到前一个区段的总数,设为m。然后,释放除这个区段占用的内存。
  • 3.再读一遍10G个整数,把在[a,a+15]内的每个值计数,即有16个计数。
  • 4.对新的计数依次累加,每次的和设为n,当m+n的值超过5G时停止,此时的这个计数所对应的数就是中位数。

九、互相关注表设计

场景题:需求:谁关注了我,我关注了谁,谁与我互相关注。表该如何设计,索引怎么建。查询语句怎么写。
粉丝关注表使用四列,主键id,userId,fansId,是否互相关注。用两行数据来保存互相的关注关系,这样查询起来更方便,用空间换时间。
主键有主键索引,剩下的字段不适合建索引,因为字段重复太多。

十、海盗分硬币

——经典博弈论问题
https://www.jianshu.com/p/ec769d3a1e89

有5个海盗,获得了100枚金币,于是他们要商量一个方法来分配金币。商议方式如下:
(1) 由5个海盗轮流提出分配方案。
(2) 如果超过半数海盗(包括提出者)同意该方案,则按照该方案分配。
(3) 如果同意该方案的人数(包括提出者)小于等于半数,则提出者要被扔到海里喂鱼,剩下的海盗继续商议分配。
(4) 海盗们都是绝对理性的,以自己尽可能多获得金币为目的。但是在收益相等的情况下,会倾向把提出者扔到海里。

问:第一个海盗应该提出怎样的分配方案,才能保证自己既不被扔到海里,又能使自己利益最大化?
在这里插入图片描述
直接分析第一个海盗的最优选择,是困难的,可以利用递归思想。
老一:如果我被扔海里,剩下4个,老二的最优方案是啥?我只要给老二多一点,就能赢得支持。
老二:如果我被扔海里,剩下3个,老三的最优方案是啥?我只要给老三多一点,就能赢得支持。
老三:如果我被扔海里,剩下2个,老四的最优方案是啥?我只要给老四多一点,就能赢得支持。

整个递归过程:
在这里插入图片描述
递归到最后两人为止:
老四:没有任何选择的余地,哪怕支持把所有钱都给老五,老五仍然反对(因为同意该方案的人小于等于半数了,提出者要被扔海里),导致老四被扔海里,金币归老五所有。
因此,

  • 老三心想:老四没有最优决策,无论我提出什么要求,老四一定会同一,二老五一定不同意。所以他的最优决策是自己独占100枚硬币。
  • 老二心想:如果没有我,老三能够获得100枚硬币,所以不会同意我,但是可以设法笼络老四和老五,形成3:1的局面。所以老二给老四老五一人一枚硬币,老三0枚,所以老四老五会同意。自己独占98枚。
  • 老一心想:如果么有我,老二能获得98枚,所以干脆笼络剩下三个人中的两个。所以如果给原本只有0枚的老三一枚,老三肯定支持。置于老四和老五,本来可以得到1枚,所以必须比老二给的多。要么给老四两枚金币,放弃老五,要么给老五两枚金币,放弃老四。

如果是七名海盗:
在这里插入图片描述

十一、找排名

【题目】:高考成绩2000万数据,分数0-750,如何快速知道你的排名,如何知道任一分数排名?

【思路】:利用桶排序。 将分数分成 0 - 150, 151 - 300, 301 - 450, 451 - 600, 601 - 750 共五个区间(每个区间内还可以再分),将 2000 万分数据按照成绩分到对应的成绩区间中。这样就可以快速查到对应分数的排名了。

十二、从大规模数据中查找(内存不足问题)

——从十亿数据中找到次数是2的数字 + 判断某个数是否在十亿数据中

【问题的提出】:遍历一遍这些数据或者二分查找,时间复杂度是没办法优化到哪里去了,但是10亿数据处理起来,占用的内存可以达到4G——内存不够!

如何用更小的内存来标记一个数字?——bit位
【位图】
如图是一个int占4个字节,32位,本来4个字节只能存一个int,现在利用位图可以存(映射)32个数字。
在这里插入图片描述
https://blog.csdn.net/lucky52529/article/details/90172264

位图法思想:
对于40亿个 unsigned int 的整数,每个数字用1个二进制数(一个二进制数占用1Bit,1Byte = 8Bit)来表示该数字是否存在,0为不存在,1为存在。从低位开始数:
第1个二进制数表示整数0是否存在,
第2个二进制数表示整数1是否存在,
第3个二进制数表示整数2是否存在,
依次类推 … …
第4294967296个二进制数用于表示整数4294967295是否存在。

unsigned int 在32&64位编译器的范围为 0~4294967295,4294967296个二进制数大约占用512M内存,是一个可以接受的范围。

(1)从十亿数据中找出次数为2的数字

思路:先遍历找出最大最小值,从而减小位图中数组的长度。再用位图做。

import java.util.*;

public class Main1{
    public static void main(String[] args){
        
        int[] a = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,12,15,16};
		findDuplicated(a);
    }
	//找出最大最小
	public static void findDuplicated(int[] a) {
		int max = 0;
		int min = 0;
		for (int i = 0; i < a.length; i++) {
			max = Math.max(max, a[i]);
			min = Math.min(min, a[i]);
		}

		// 这边使用了 byte[],而不是使用了 int[] 和 boolean[],其实从原理上都可以的
		int length = max / 8 - min / 8 + 1;
		byte[] arry = new byte[length];
		for (int i = 0; i < a.length; i++) {
			int num = a[i];
			int index = num / 8 - min / 8;
			int k = (num - min) % 8;
			// 这边使用这样的判断是因为使用的是 byte[],使用 int[] 或者 boolean[] 则使用另一种判断方法
			if((arry[index] & 1 << k) > 0) {
				System.out.println(num);
			}else {
				arry[index] |= (1<<k); 
			}
		}
	}
}

(2)判断某个数是否在四十亿数据中

https://blog.csdn.net/v123411739/article/details/86652806

public class BitMap {
    /**
     * 位图提供的最大长度,
     * 比如unsigned int的最大值为4294967295, 则需要的length为4294967296
     */
    private long length;
    /**
     * 位图桶
     */
    private static int[] bitmapBucket;
    /**
     * int用来表示32位二进制数,
     * BIT_VALUE[0]表示第1个二进制数存在、
     * BIT_VALUE[1]表示第2个二进制数存在,以此类推
     * BIT_VALUE[0] = 00000000 00000000 00000000 00000001
     * BIT_VALUE[1] = 00000000 00000000 00000000 00000010
     * BIT_VALUE[2] = 00000000 00000000 00000000 00000100
     * ...
     * BIT_VALUE[31] = 10000000 00000000 00000000 00000000
     */
    private static final int[] BIT_VALUE = {
            0x00000001, 0x00000002, 0x00000004, 0x00000008,
            0x00000010, 0x00000020, 0x00000040, 0x00000080,
            0x00000100, 0x00000200, 0x00000400, 0x00000800,
            0x00001000, 0x00002000, 0x00004000, 0x00008000,
            0x00010000, 0x00020000, 0x00040000, 0x00080000,
            0x00100000, 0x00200000, 0x00400000, 0x00800000,
            0x01000000, 0x02000000, 0x04000000, 0x08000000,
            0x10000000, 0x20000000, 0x40000000, 0x80000000};
 
    /**
     * length为1 - 32: 需要1个桶
     * length为33 - 64: 需要2个桶
     * ...
     * 以此类推
     *
     * @param length
     */
    public BitMap(long length) {
        this.length = length;
        // 根据长度算出,所需位图桶个数
        bitmapBucket = new int[(int) (length >> 5) + ((length & 31) > 0 ? 1 : 0)];
    }
 
    /**
     * 查找number是否存在于位图桶中
     *
     * @param number 要查询的数字
     * @return true: number在位图桶中, false: number不在位图桶中
     */
    public boolean getBit(long number) {
        if (number < 0 || number > length) {
            throw new IllegalArgumentException("非法参数");
        }
        // 计算该number在哪个桶
        int belowIndex = (int) (number >> 5);
        // 求出该number在桶里的下标,(求出该值在32位中的哪一位, 下标0 - 31)
        int offset = (int) (number & 31);
        // 拿到该桶的值
        int currentValue = bitmapBucket[belowIndex];
        // 计算该number是否存在
        return ((currentValue & BIT_VALUE[offset])) == 0 ? false : true;
    }
 
    /**
     * 将number在位图桶中标记为存在
     *
     * @param number 要标记的数字
     */
    public void setBit(long number) {
        // 合法性校验
        if (number < 0 || number >= length) {
            throw new IllegalArgumentException("非法参数");
        }
        // 计算该number在哪个桶
        int belowIndex = (int) (number >> 5);
        // 求出该number在桶里的下标,(求出该值在32位中的哪一位, 下标0 - 31)
        int offset = (int) (number & 31);
        // 拿到该桶的当前值
        int currentValue = bitmapBucket[belowIndex];
        // 将number在桶里标记
        bitmapBucket[belowIndex] = currentValue | BIT_VALUE[offset];
    }
 
    public static void main(String[] args) {
        BitMap bitMap = new BitMap(4294967296L);
        bitMap.setBit(4294967295L);
        System.out.println(bitMap.getBit(4294967295L));
        System.out.println(bitMap.getBit(4294967294L));
    }
}

(3)判断某个URL是否存在在100亿个URL当中

首先不能用hashMap,因为存储量太大了。
布隆过滤器:
—— 可以用于判断某个URL是否位于庞大的黑名单中(屏蔽网友/请求)、过滤垃圾邮件等
介绍:是一个很长的二进制矢量(很长的bit类型数组)和一系列随机映射函数(hash函数)。优点是空间效率和查询时间都很优秀,缺点是有一定的误识别率,
判断过程:假如创建一个bit数组,长度为m,每个元素值是0,有k个哈希函数。当输入一个url的时候,这个URL会经过k个hash函数处理,得到多个哈希值(v1, v2, …, vk),分别除以数组长度m,对m取模,可以得到数组的下标位置,将这些下标的元素都置为1。把这100亿个url都这样操作一遍,之后当输入这个需要判断的URL的时候,如果相应下标的元素值都已经是1了,说明已经在黑名单里面了。
在这里插入图片描述

如果100亿个url都放在hashMap里面,假设每个64k,共需要超过640GB,使用布隆过滤器,约需要23GB。
缺点:宁可错杀一百,也不能放过一个。在黑名单中的一定会被查出来,不在黑名单里的有可能也会被过滤出来,存在失误率,

十三、圆内任取三点/四点在同一半圆内的概率

等价于点在圆周的情况。
考虑n个点,n个点在同一半圆内,我们把逆时针方向最后一个点称作关键点。
选定一个点A,这n个点再以A为关键点的同一半圆内的充要条件是什么?
——每个点都是1/2,所以所有点都在的概率是1/2^(n-1)

由于对称性,这n个点在以其它点为关键点的同一半圆内的概率也是1/2^(n-1)

所以n个点都在同一半圆内的概率是:n / 2^(n-1)
故而:
三个点就是:3/4
四个点就是:4/8=1/2

十四、赛马问题

64匹马,8个赛道,找出前4名最少需要比赛多少场?
参考:https://zhuanlan.zhihu.com/p/398143738

解答:
先分成8组
第一轮:赛8场
每组各赛一场,得到每组的前四名

第二轮:赛1场
从8组里面选出第一名进行比赛,可以得到一定被淘汰的马:
在这里插入图片描述

第三轮:1场或2场
在这里插入图片描述

此时A1已经确定是第一名了,D1是最危险的,我们选择除了D1之外的8匹马进行比赛:
加赛的情况:
如果C1是以第二名的成绩晋级,那么第二名将在A2-4,B2-3,C2中产生,所以并不知道D1与他们比谁快,需要7匹马加赛一场。
不加赛的情况:
如果C1岁以第3名到第7名的成绩完赛,那么除了D1这8匹马中的前3名就直接进入前4,无需进行加赛。

因此:
在这里插入图片描述

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
陈教授将自己的生日M月N日中的M月告诉了小强 N告诉了小明 ;小强说我不知道,小明也不会知道 ;小明说我本来不知道的,现在我知道了 ;小强说我也知道的 ;你说陈教授到底是那一天生日? 3月4日 3 5 8 6 2 6 4 9 1 9 5 12 1 12 7 12 8 问: 1、第一个答案是b的问是哪一个? (a)2;(b) 3;(c)4;(d)5;(e)6 2、唯一的连续两个具有相同答案的问是: (a)2,3;(b)3,4;(c)4,5;(d)5,6;(e)6,7; 3、本问答案和哪一个问的答案相同? (a)1;(b)2;(c)4;(d)7;(e)6 4、答案是a的问的个数是: (a)0;(b)1;(c)2;(d)3;(e)4 5、本问答案和哪一个问的答案相同?    (a)10;(b)9;(c)8;(d)7;(e)6 6、答案是a的问的个数和答案是什么的问的个数相同?    (a)b;(b)c;(c)d;(d)e;(e)以上都不是 7、按照字母顺序,本问的答案和下一个问的答案相差几个字母?    (a)4;(b)3;(c)2;(d)1;(e)0。(注:a和b相差一个字母) 8、答案是元音字母的问的个数是:    (a)2;(b)3;(c)4;(d)5;(e)6。(注:a和e是元音字母) 9、答案是辅音字母的问的个数是:    (a)一个质数;(b)一个阶乘数;(c)一个平方数;(d)一个立方数;(e)5的倍数 10、本问的答案是: (a)a;(b)b;(c)c;(d)d;(e)e 1)。每个飞机只有一个油箱,飞机之间可以相互加油(注意是相互,没有加油机),一箱油可供一架飞机绕地球飞半圈。   问:为使至少一架飞机绕地球一圈回到起飞时的飞机场,至少需要出动几架飞机?   (所有飞机从同一机场起飞,而且必须安全返回机场,不允许中途降落,中间没有飞机场)

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值