场景题算法

1: 实战算法篇

1、URL黑名单(布隆过滤器)

100亿黑名单URL,每个64B,问这个黑名单要怎么存?判断一个URL是否在黑名单中

散列表:

如果把黑名单看成一个集合,将其存在 hashmap 中,貌似太大了,需要 640G,明显不科学。

布隆过滤器:

它实际上是一个很长的二进制矢量和一系列随机映射函数。

可以用来判断一个元素是否在一个集合中。它的优势是只需要占用很小的内存空间以及有着高效的查询效率。对于布隆过滤器而言,它的本质是一个位数组:位数组就是数组的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。

在数组中的每一位都是二进制位。布隆过滤器除了一个位数组,还有 K 个哈希函数。当一个元素加入布隆过滤器中的时候,会进行如下操作:

  • 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。
  • 根据得到的哈希值,在位数组中把对应下标的值置为 1。

2、词频统计(分文件)

2GB内存在20亿整数中找到出现次数最多的数

通常做法是使用哈希表对出现的每一个数做词频统计,哈希表的key是某个整数,value记录整数出现的次数。本题的数据量是20亿,有可能一个数出现20亿次,则为了避免溢出,哈希表的key是32位(4B),value也是 32位(4B),那么一条哈希表的记录就需要占用8B。

当哈希表记录数为2亿个时,需要16亿个字节数(8*2亿),需要至少1.6GB内存(16亿/2^30,1GB== 2 ^30个字节 == 10亿)。则20亿个记录,至少需要16GB的内存,不符合题目要求。

解决办法是将20亿个数的大文件利用哈希函数分成16个小文件,根据哈希函数可以把20亿条数据均匀分布到16个文件上,同一种数不可能被哈希函数分到不同的小文件上,假设哈希函数够好。然后对每一个小文件用哈希函数来统计其中每种数出现的次数,这样我们就得到16个文件中出现次数最多的数,接着从16个数中选出次数最大的那个key即可。

3、未出现的数(bit数组)

40亿个非负整数中找到没有出现的数

对于原问题,如果使用哈希表来保存出现过的数,那么最坏情况下是40亿个数都不相同,那么哈希表则需要保存40亿条数据,一个32位整数需要4B,那么40亿*4B = 160亿个字节,一般大概10亿个字节的数据需要1G的空间,那么大概需要16G的空间,这不符合要求。

我们换一种方式,申请一个bit数组,数组大小为4294967295,大概为40亿bit,40亿/8 = 5亿字节,那么需要0.5G空间, bit数组的每个位置有两种状态0和1,那么怎么使用这个bit数组呢?呵呵,数组的长度刚好满足我们整数的个数范围,那么数组的每个下标值对应4294967295中的一个数,逐个遍历40亿个无符号数,例如,遇到20,则bitArray[20] = 1;遇到666,则bitArray[666] = 1,遍历完所有的数,将数组相应位置变为1。

40亿个非负整数中找到一个没有出现的数,内存限制10MB

10亿个字节的数据大概需要1GB空间处理,那么10MB内存换算过来就是可以处理1千万字节的数据,也就是8千万bit,对于40亿非负整数如果申请bit数组的话,40亿bit / 0.8亿bit = 50,那么这样最少也得分50块来处理,下面就以64块来进行分析解答吧。

总结一下进阶的解法:

1.根据10MB的内存限制,确定统计区间的大小,就是第二次遍历时的bitArr大小。

2.利用区间计数的方式,找到那个计数不足的区间,这个区间上肯定有没出现的数。

3.对这个区间上的数做bit map映射,再遍历bit map,找到一个没出现的数即可。

自己的想法

如果只是找一个数,可以高位模运算,写到64个不同的文件,然后在最小的文件中通过bitArray一次处理掉。

40亿个无符号整数,1GB内存,找到所有出现两次的数

对于原问题,可以用bit map的方式来表示数出现的情况。具体地说,是申请一个长度为4294967295×2的bit类型的数组bitArr,用2个位置表示一个数出现的词频,1B占用8个bit,所以长度为4294967295×2的bit类型的数组占用1GB空间。怎么使用这个bitArr数组呢?遍历这40亿个无符号数,如果初次遇到num,就把bitArr[num2 + 1]和bitArr[num2]设置为01,如果第二次遇到num,就把bitArr[num2+1]和bitArr[num2]设置为10,如果第三次遇到num,就把bitArr[num2+1]和bitArr[num2]设置为11。以后再遇到num,发现此时bitArr[num2+1]和bitArr[num2]已经被设置为11,就不再做任何设置。遍历完成后,再依次遍历bitArr,如果发现bitArr[i2+1]和bitArr[i2]设置为10,那么i 就是出现了两次的数。

4、重复URL(分机器)

找到100亿个URL中重复的URL

原问题的解法使用解决大数据问题的一种常规方法:把大文件通过哈希函数分配到机器,或者通过哈希函数把大文件拆成小文件。一直进行这种划分,直到划分的结果满足资源限制的要求。首先,你要向面试官询问在资源上的限制有哪些,包括内存、计算时间等要求。在明确了限制要求之后,可以将每条URL通过哈希函数分配到若干机器或者拆分成若干小文件,这里的“若干”由具体的资源限制来计算出精确的数量。

例如,将100亿字节的大文件通过哈希函数分配到100台机器上,然后每一台机器分别统计分给自己的URL中是否有重复的URL,同时哈希函数的性质决定了同一条URL不可能分给不同的机器;或者在单机上将大文件通过哈希函数拆成1000个小文件,对每一个小文件再利用哈希表遍历,找出重复的URL;或者在分给机器或拆完文件之后,进行排序,排序过后再看是否有重复的URL出现。总之,牢记一点,很多大数据问题都离不开分流,要么是哈希函数把大文件的内容分配给不同的机器,要么是哈希函数把大文件拆成小文件,然后处理每一个小数量的集合。

5、TOPK搜索(小根堆)

海量搜索词汇,找到最热TOP100词汇的方法

最开始还是用哈希分流的思路来处理,把包含百亿数据量的词汇文件分流到不同的机器上,具体多少台机器由面试官规定或者由更多的限制来决定。对每一台机器来说,如果分到的数据量依然很大,比如,内存不够或其他问题,可以再用哈希函数把每台机器的分流文件拆成更小的文件处理。

处理每一个小文件的时候,哈希表统计每种词及其词频,哈希表记录建立完成后,再遍历哈希表,遍历哈希表的过程中使用大小为100的小根堆来选出每一个小文件的top 100(整体未排序的top 100)。每一个小文件都有自己词频的小根堆(整体未排序的top 100),将小根堆里的词按照词频排序,就得到了每个小文件的排序后top 100。然后把各个小文件排序后的top 100进行外排序或者继续利用小根堆,就可以选出每台机器上的top 100。不同机器之间的top100再进行外排序或者继续利用小根堆,最终求出整个百亿数据量中的top 100。对于top K 的问题,除哈希函数分流和用哈希表做词频统计之外,还经常用堆结构和外排序的手段进行处理。

6、中位数(单向二分查找)

10MB内存,找到100亿整数的中位数

①内存够:内存够还慌什么啊,直接把100亿个全部排序了,你用冒泡都可以...然后找到中间那个就可以了。但是你以为面试官会给你内存??

②内存不够:题目说是整数,我们认为是带符号的int,所以4字节,占32位。

假设100亿个数字保存在一个大文件中,依次读一部分文件到内存(不超过内存的限制),将每个数字用二进制表示,比较二进制的最高位(第32位,符号位,0是正,1是负),如果数字的最高位为0,则将这个数字写入 file_0文件中;如果最高位为 1,则将该数字写入file_1文件中。

从而将100亿个数字分成了两个文件,假设 file_0文件中有 60亿 个数字,file_1文件中有 40亿 个数字。那么中位数就在 file_0 文件中,并且是 file_0 文件中所有数字排序之后的第 10亿 个数字。(file_1中的数都是负数,file_0中的数都是正数,也即这里一共只有40亿个负数,那么排序之后的第50亿个数一定位于file_0中)

现在,我们只需要处理 file_0 文件了(不需要再考虑file_1文件)。对于 file_0 文件,同样采取上面的措施处理:将file_0文件依次读一部分到内存(不超内存限制),将每个数字用二进制表示,比较二进制的 次高位(第31位),如果数字的次高位为0,写入file_0_0文件中;如果次高位为1,写入file_0_1文件 中。

现假设 file_0_0文件中有30亿个数字,file_0_1中也有30亿个数字,则中位数就是:file_0_0文件中的数字从小到大排序之后的第10亿个数字。

抛弃file_0_1文件,继续对 file_0_0文件 根据 次次高位(第30位) 划分,假设此次划分的两个文件为:file_0_0_0中有5亿个数字,file_0_0_1中有25亿个数字,那么中位数就是 file_0_0_1文件中的所有数字排序之后的 第 5亿 个数。

按照上述思路,直到划分的文件可直接加载进内存时,就可以直接对数字进行快速排序,找出中位数了。

7、短域名系统(缓存)

设计短域名系统,将长URL转化成短的URL.

(1)利用放号器,初始值为0,对于每一个短链接生成请求,都递增放号器的值,再将此值转换为62进制(a-zA-Z0-9),比如第一次请求时放号器的值为0,对应62进制为a,第二次请求时放号器的值为1,对应62进制为b,第10001次请求时放号器的值为10000,对应62进制为sBc。

(2)将短链接服务器域名与放号器的62进制值进行字符串连接,即为短链接的URL,比如:t.cn/sBc。

(3)重定向过程:生成短链接之后,需要存储短链接到长链接的映射关系,即sBc -> URL,浏览器访问短链接服务器时,根据URL Path取到原始的链接,然后进行302重定向。映射关系可使用K-V存储,比如Redis或Memcache。

8、海量评论入库(消息队列)

假设有这么一个场景,有一条新闻,新闻的评论量可能很大,如何设计评论的读和写

前端页面直接给用户展示、通过消息队列异步方式入库

读可以进行读写分离、同时热点评论定时加载到缓存

9、在线/并发用户数(Redis)

显示网站的用户在线数的解决思路

维护在线用户表

使用Redis统计

显示网站并发用户数

  1. 每当用户访问服务时,把该用户的 ID 写入ZSORT队列,权重为当前时间
  2. 根据权重(即时间)计算一分钟内该机构的用户数Zrange
  3. 删掉一分钟以上过期的用户Zrem

10、热门字符串(前缀树)

假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。(一个查询串的重复度越高,说明查询它的用户越多,也就越热门。)

HashMap 法

虽然字符串总数比较多,但去重后不超过 300w,因此,可以考虑把所有字符串及出现次数保存在一个 HashMap 中,所占用的空间为 300w*(255+4)≈777M(其中,4 表示整数占用的 4 个字节)。由此可见,1G 的内存空间完全够用。

思路如下

首先,遍历字符串,若不在 map 中,直接存入 map,value 记为 1;若在 map 中,则把对应的 value 加 1,这一步时间复杂度 O(N)

接着遍历 map,构建一个 10 个元素的小顶堆,若遍历到的字符串的出现次数大于堆顶字符串的出现次数,则进行替换,并将堆调整为小顶堆。

遍历结束后,堆中 10 个字符串就是出现次数最多的字符串。这一步时间复杂度 O(Nlog10)

前缀树法

当这些字符串有大量相同前缀时,可以考虑使用前缀树来统计字符串出现的次数,树的结点保存字符串出现次数,0 表示没有出现。

思路如下

在遍历字符串时,在前缀树中查找,如果找到,则把结点中保存的字符串次数加 1,否则为这个字符串构建新结点,构建完成后把叶子结点中字符串的出现次数置为 1。

最后依然使用小顶堆来对字符串的出现次数进行排序。

11、红包算法

线性切割法,一个区间切N-1刀。越早越多

二倍均值法,【0 ~ 剩余金额 / 剩余人数 * 2】中随机,相对均匀

11、手写快排

快排核心思想就是:首先在待排序数组中随便选择一个数作为节点(pivot),然后从最后面(high)往左查找比这个节点(pivot)小的数,并且从最前面(low)往右查找比这个节点(pivot)大的数(low),情况1:找到后就把这两个数进行交换,然后接着上面的查找交换直到low等于high,球后将节点(pivot)与low位置处的数进行交换,这样比pivot小的数都在其前面,比pivot大的数就在其后面,然后把数组以pivot分为两半,重复上述操作;情况2:直到low 等于 high都没有找到,就直接交换pivot和low位置的数据,然后同样的将数组以pivot分为两半重复上述操作。(说的挺抽象的,还是看看下面举例吧)

 

int[] arr = {1, 2, 3, 4, 5};

public class QuickSort {
    //快排实现方法
    public static void quickRow(int[] array, int low, int high){
        int i,j,pivot;
        //结束条件
        if (low >= high) {
            return;
        }
        i = low;
        j = high;
        //选择的节点,这里选择的数组的第一数作为节点
        pivot = array[low];
        while (i < j){
            //从右往左找比节点小的数,循环结束要么找到了,要么i=j
            while (array[j] >= pivot && i < j){
                j--;
            }
            //从左往右找比节点大的数,循环结束要么找到了,要么i=j
            while (array[i] <= pivot && i < j){
                i++;
            }
            //如果i!=j说明都找到了,就交换这两个数
            if (i < j){
                int temp = array[i];
                array[i] = array[j];
                array[j] = temp;
            }
        }
        //i==j一轮循环结束,交换节点的数和相遇点的数
        array[low] = array[i];
        array[i] = pivot;
        //数组“分两半”,再重复上面的操作
        quickRow(array,low,i - 1);
        quickRow(array,i + 1,high);
    }

    //测试
    public static void main(String[] args) {
        int[] array = {6,3,7,1,9,4,8,5,2,10};
        int low = 0,high = array.length - 1;
        quickRow(array,low,high);
        for (int i : array){
            System.out.println(i);
        }
    }
}





public class QuickSort {
    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    /* 常规快排 */
    public static void quickSort1(int[] arr, int L , int R) {
        if (L > R)  return;
        int M = partition(arr, L, R);
        quickSort1(arr, L, M - 1);
        quickSort1(arr, M + 1, R);
    }
    public static int partition(int[] arr, int L, int R) {
        if (L > R) return -1;
        if (L == R) return L;
        int lessEqual = L - 1;
        int index = L;
        while (index < R) {
            if (arr[index] <= arr[R])
                swap(arr, index, ++lessEqual);
            index++;
        }
        swap(arr, ++lessEqual, R);
        return lessEqual;
    }
    /* 荷兰国旗 */
    public static void quickSort2(int[] arr, int L, int R) {
        if (L > R)  return;
        int[] equalArea = netherlandsFlag(arr, L, R);
        quickSort2(arr, L, equalArea[0] - 1);
        quickSort2(arr, equalArea[1] + 1, R);
    }
    public static int[] netherlandsFlag(int[] arr, int L, int R) {
        if (L > R) return new int[] { -1, -1 };
        if (L == R) return new int[] { L, R };
        int less = L - 1;
        int more = R;
        int index = L;
        while (index < more) {
            if (arr[index] == arr[R]) {
                index++;
            } else if (arr[index] < arr[R]) {
                swap(arr, index++, ++less);
            } else {
                swap(arr, index, --more);
            }
        }
        swap(arr, more, R);
        return new int[] { less + 1, more };
    }

    // for test
    public static void main(String[] args) {
        int testTime = 1;
        int maxSize = 10000000;
        int maxValue = 100000;
        boolean succeed = true;
        long T1=0,T2=0;
        for (int i = 0; i < testTime; i++) {
            int[] arr1 = generateRandomArray(maxSize, maxValue);
            int[] arr2 = copyArray(arr1);
            int[] arr3 = copyArray(arr1);
//            int[] arr1 = {9,8,7,6,5,4,3,2,1};
            long t1 = System.currentTimeMillis();
            quickSort1(arr1,0,arr1.length-1);
            long t2 = System.currentTimeMillis();
            quickSort2(arr2,0,arr2.length-1);
            long t3 = System.currentTimeMillis();
            T1 += (t2-t1);
            T2 += (t3-t2);
            if (!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) {
                succeed = false;
                break;
            }
        }
        System.out.println(T1+" "+T2);
//        System.out.println(succeed ? "Nice!" : "Oops!");
    }

    private static int[] generateRandomArray(int maxSize, int maxValue) {
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) 
                        - (int) (maxValue * Math.random());
        }
        return arr;
    }
    private static int[] copyArray(int[] arr) {
        if (arr == null)  return null;
        int[] res = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }
    private static boolean isEqual(int[] arr1, int[] arr2) {
        if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) 
            return false;
        if (arr1 == null && arr2 == null) 
            return true;
        if (arr1.length != arr2.length) 
            return false;
        for (int i = 0; i < arr1.length; i++)
            if (arr1[i] != arr2[i])
                return false;
        return true;
    }
    private static void printArray(int[] arr) {
        if (arr == null) 
            return;
        for (int i = 0; i < arr.length; i++) 
            System.out.print(arr[i] + " ");
        System.out.println();
    }
}

12、手写归并

import java.util.Arrays;
import java.util.Scanner;
 
public class xishuarr {
	
	public static void main(String[] args) {
		int arr[]= {8,4,5,7,1,3,6,2};
	    int[] add=new int[arr.length];
	    System.out.println("排序前:"+Arrays.toString(arr));
	    System.out.println("排序过程:");
	    mergeSort(arr,0,add.length-1,add);
	    System.out.println("排序后:"+Arrays.toString(arr));
	    
	    }
	
	
	/**
	 * 分
	 * @param arr  排序的原始数组
	 * @param left  左边有序序列的初始索引
	
	 * @param right    右边索引
	 * @param temp    做中转的数组
	 */
		
	public static void mergeSort(int[] arr,int left,int right,int[] temp) {
		//求中间索引
		int mid=(left+right)/2;
		
 
 
		if(left<right) {
			//左边递归分解
			mergeSort(arr,left,mid,temp);
			
			//右边递归分解
			mergeSort(arr,mid+1,right,temp);

            // 经过左边递归分解和右边递归分解之后,传入的数组变为下面左右两个都是有序的小数组;
            //传入数组{4 ,5 ,7 ,8,1 ,2, 3, 6}
			
			merge(arr,left,mid,right,temp);
		
			
			System.out.println(" 最左边索引:"+left+"\t最右边边索引:"+right+"\t"+Arrays.toString(arr));
		}
	
	}
	
	
/**
 * 
 * @param arr  排序的原始数组
 * @param left  左边有序序列的初始索引
 * @param mid   中间索引
 * @param right    右边索引
 * @param temp    做中转的数组
 */
	
	//4 5 7 8   1 2 3 6
	public static void merge(int[] arr,int left,int mid,int right,int[] temp) {
		int i=left;  //初始i,左边有序序列的初始索引
		int j=mid+1;  //初始j,右边有序序列的初始索引
		int t=0;       //指向temp数组的当前索引
		
		//1.
		//先把左右两边(有序)的数据按照规则填充到temp数组
		//直到左右两边的有序序列,有一边处理完毕为止
		while(i<=mid&&j<=right) {
		
			if(arr[i]<arr[j]) {
				temp[t]=arr[i++];
				/**
				 * 这里我们这里是:temp[t]=arr[i++];
				 * 如果不好理解,你可以写成这样:
				 * temp[t]=arr[i];i++;
				 */
				}
			else {
				
				temp[t]=arr[j++];
			
			}
			//因为无论执行if里面的语句还是else里面的语句,t都要加1,所以把t移出来.
	
			t++;
		 	
		}
		
		//2.
		//把有剩余数据的一边的的数据依次全部填充到temp
		//由上述循环条件:i<=mid&&j<=right 可知 
		//此时要么i>mid 要么j>right
		while(i<=mid) {
			temp[t]=arr[i];
			t++;
			i++;
			
		}
		while(j<=right) {
			temp[t]=arr[j];
			t++;
			j++;
			
		}
 
		
		
		//3.
		//把temp的数组转移到arr上
	   int n=0;
       int tempLeft=left;
	   while(tempLeft<=right){
		   arr[tempLeft]=temp[n];
		   n++;
		   tempLeft++;
		   
	   }
	  
 
		
		
		
	}
	
		
 
}

13、手写堆排

https://blog.csdn.net/wenwenaier/article/details/121314974?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%A0%86%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-121314974.nonecase&spm=1018.2226.3001.4187

//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
//将堆顶元素与末尾元素交换,将最大元素“沉”到数组末端
//重新调整结构使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序

private static void heapSort(int[] arr) {
        int temp = 0;
        //堆排序
        for(int i = arr.length/2-1;i>=0;i--){
            adjustHeap(arr,i,arr.length);
        }

        for (int j=arr.length-1;j>0;j--){
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr,0,j);
        }

        System.out.println("数组="+Arrays.toString(arr));
    }

    /**
     * 将以i对应的非叶子节点的树调整成一个大顶堆
     * 举例 int[] arr = {4,6,8,5,9};=>i=1 =>{4,9,8,5,6} => i=0 =>{9,6,8,5,4}
     * @param arr
     * @param i 表示非叶子节点在数组中的索引
     * @param length 对多少个元素进行调整
     */
    public static void adjustHeap(int[] arr,int i,int length){
        //a[i]>a[2i+1]&&a[i]>a[2i+2]
        int temp = arr[i];
        for (int k=i*2+1;k<length;k=k*2+1){
            //先比较左子节点和右子节点的大小,最大的那个和temp进行交换
            if(k+1<length && arr[k]<arr[k+1]){
                k++;//k指向右子节点
            }
            //如果非子节点的值小于左子节点和右子节点的值
            if(arr[k]>temp){
                //temp和arr[k]进行交换
                arr[i] = arr[k];
                i=k;//继续循环比较,假设k是左子节点,k+1是右子节点,然后引出公式
            }else{
                break;
            }
        }
        //当for循环结束后,我们已经将以i为父节点的树的最大值,放在了最顶上(局部)
        arr[i]=temp;
    }

14、手写单例

 

public class Singleton {
        private volatile static Singleton singleton;
        private Singleton() {}
        public static Singleton getSingleton() {
        if (singleton == null) {
              synchronized (Singleton.class) {
            if (singleton == null) {
                  singleton = new Singleton();
            }
        }
        }
        return singleton;
    }
}

15、手写LRUcache

1、LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的;HashMap无序;2、LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。

// 基于linkedHashMap
public class LRUCache {
    private LinkedHashMap<Integer,Integer> cache;
    private int capacity;   //容量大小
    public LRUCache(int capacity) {
        cache = new LinkedHashMap<>(capacity);
        this.capacity = capacity;
    }
    public int get(int key) {
        //缓存中不存在此key,直接返回
        if(!cache.containsKey(key)) {
            return -1;
        }
        int res = cache.get(key);
        cache.remove(key);   //先从链表中删除
        cache.put(key,res);  //再把该节点放到链表末尾处
        return res;
    }
    public void put(int key,int value) {
        if(cache.containsKey(key)) {
            cache.remove(key); //已经存在,在当前链表移除
        }
        if(capacity == cache.size()) {
            //cache已满,删除链表头位置
            Set<Integer> keySet = cache.keySet();
            Iterator<Integer> iterator = keySet.iterator();
            cache.remove(iterator.next());
        }
        cache.put(key,value);  //插入到链表末尾
    }
}

 

// 双向链表节点类
class Node {
    int key;
    int value;
    Node prev;
    Node next;
    
    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}

// LRU缓存类
class LRUCache {
    private int capacity;
    private Map<Integer, Node> cache;
    private Node head;
    private Node tail;
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        this.head = new Node(0, 0);
        this.tail = new Node(0, 0);
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }
    
    public int get(int key) {
        if (cache.containsKey(key)) {
            Node node = cache.get(key);
            moveToHead(node);
            return node.value;
        }
        return -1;
    }
    
    public void put(int key, int value){
            if (cache.containsKey(key)) {
                Node node = cache.get(key);
                node.value = value;
                moveToHead(node);
            } else {
                Node newNode = new Node(key, value);
                cache.put(key, newNode);
                addToHead(newNode);
                if (cache.size() > capacity) {
                    Node tailNode = removeTail();
                    cache.remove(tailNode.key);
                }
            }
        }
    
        private void moveToHead(Node node) {
            removeNode(node);
            addToHead(node);
        }
    
        private void addToHead(Node node) {
            node.prev = head;
            node.next = head.next;
            head.next.prev = node;
            head.next = node;
        }
    
        private void removeNode(Node node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
    
        private Node removeTail() {
            Node tailNode = tail.prev;
            removeNode(tailNode);
            return tailNode;
        }
    }

16、手写线程池

 

package com.concurrent.pool;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class MySelfThreadPool {
    //默认线程池中的线程的数量
    private static final int WORK_NUM = 5;
    //默认处理任务的数量
    private static final int TASK_NUM = 100;
    private int workNum;//线程数量
    private int taskNum;//任务数量
    private final Set<WorkThread> workThreads;//保存线程的集合
    private final BlockingQueue<Runnable> taskQueue;//阻塞有序队列存放任务
    public MySelfThreadPool() {
        this(WORK_NUM, TASK_NUM);
    }
    public MySelfThreadPool(int workNum, int taskNum) {
        if (workNum <= 0) workNum = WORK_NUM;
        if (taskNum <= 0) taskNum = TASK_NUM;
        taskQueue = new ArrayBlockingQueue<>(taskNum);
        this.workNum = workNum;
        this.taskNum = taskNum;
        workThreads = new HashSet<>();
        //启动一定数量的线程数,从队列中获取任务处理
        for (int i=0;i<workNum;i++) {
            WorkThread workThread = new WorkThread("thead_"+i);
            workThread.start();
            workThreads.add(workThread);
        }
    }
    public void execute(Runnable task) {
        try {
            taskQueue.put(task);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public void destroy() {
        System.out.println("ready close thread pool...");
        if (workThreads == null || workThreads.isEmpty()) return ;
        for (WorkThread workThread : workThreads) {
            workThread.stopWork();
            workThread = null;//help gc
        }
        workThreads.clear();
    }
    private class WorkThread extends Thread{
        public WorkThread(String name) {
            super();
            setName(name);
        }
        @Override
        public void run() {
            while (!interrupted()) {
                try {
                    Runnable runnable = taskQueue.take();//获取任务
                    if (runnable !=null) {
                        System.out.println(getName()+" readyexecute:"+runnable.toString());
                        runnable.run();//执行任务
                    }
                    runnable = null;//help gc
                } catch (Exception e) {
                    interrupt();
                    e.printStackTrace();
                }
            }
        }
        public void stopWork() {
            interrupt();
        }
    }
}

package com.concurrent.pool;
 
public class TestMySelfThreadPool {
    private static final int TASK_NUM = 50;//任务的个数
    public static void main(String[] args) {
        MySelfThreadPool myPool = new MySelfThreadPool(3,50);
        for (int i=0;i<TASK_NUM;i++) {
            myPool.execute(new MyTask("task_"+i));
        }
    }
    static class MyTask implements Runnable{
        private String name;
        public MyTask(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("task :"+name+" end...");
        }
        @Override
        public String toString() {
            // TODO Auto-generated method stub
            return "name = "+name;
        }
    }
}

17、手写消费者生产者模式

https://blog.csdn.net/qq_40480780/article/details/112104726?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171642719316800225523432%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=171642719316800225523432&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-112104726-null-null.142^v100^control&utm_term=%E6%89%8B%E5%86%99%20%E7%94%9F%E4%BA%A7%E8%80%85%E5%92%8C%E6%B6%88%E8%B4%B9%E8%80%85&spm=1018.2226.3001.4187

阻塞队列

public class MyBlockingQueue {
	private int maxSize;
	private LinkedList<Integer> queue;
	
	public MyBlockingQueue(int size) {
		this.maxSize = size;
		queue = new LinkedList<Integer>();
	}
	public synchronized void put() throws InterruptedException{
		while(queue.size() == maxSize) {
			System.out.println("队列已满,生产者:" + Thread.currentThread().getName() + "进入等待");
			wait();
		}
		Random random = new Random();
		int i = random.nextInt(100);
		System.out.println("队列未满,生产者:"+Thread.currentThread().getName() + "放入数据" + i);
		if(queue.size() == 0) {
			notifyAll();
		}
		queue.add(i);
	}
	public synchronized void take() throws InterruptedException{
		while(queue.size() == 0) {
			System.out.println("队列为空,消费者:" + Thread.currentThread().getName() + "进入等待");
			wait();
		}
		if(queue.size() == maxSize) {
			notifyAll();
		}
		System.out.println("队列有数据,消费者:"+Thread.currentThread().getName() + "取出数据" + queue.remove());//删除第一个数据,最早放入的数据
	}
	
}

生产者线程

public class Producer implements Runnable{
	private MyBlockingQueue myBlockingQueue;
	public Producer(MyBlockingQueue myBlockingQueue) {
		this.myBlockingQueue = myBlockingQueue;
	}
	public void run() {
		for(int i = 0 ; i < 5 ; i++) {
			try {
				myBlockingQueue.put();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

消费者线程

public class Consumer implements Runnable{
	private MyBlockingQueue myBlockingQueue;
	public Consumer(MyBlockingQueue myBlockingQueue) {
		this.myBlockingQueue = myBlockingQueue;
	}
	public void run() {
		for(int i = 0 ; i < 5 ; i++) {
			try {
				myBlockingQueue.take();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

18、手写阻塞队列

什么是阻塞队列

首先,阻塞队列是一个队列,满足队列的基本数据结构,先进先出。其次,当队列满时,队列会阻塞插入元素的线程,直到队列不满;当队列空时,获取元素的线程会等待队列变为非空。

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。

 

如何写一个阻塞队列

手写阻塞队列是多线程面试中常见的问题,能考察面试者对多线程和锁的基础知识。

通过synchronized关键字配合wait()和notify()方法,实现线程的交替运行:

 

对代码的一些解释

1、private final Queue<String> myQueue = new LinkedList<>();这样创建的队列实际上偷懒了,并不是完全用数组实现的队列,用了封装的队列,但毕竟重点不在于此。

2、使用了JUC包的CyclicBarrier类,通过调用await()方法,实现两个线程都执行完毕后,再执行相关代码,这不属于阻塞队列的范畴。

3、synchronized块中使用的对象锁不一定要用myQueue,使用同一个对象就行。

4、new Thread(()->{...},"生产者").start();是java8函数式写法,等同于继承Runnable覆写run方法。

 

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.CyclicBarrier;

public class MyBlockingQueue {

    //队列
    private  final Queue<String> myQueue = new LinkedList<>();

    //最大长度
    private static final int MAXSIZE = 20;
    private static final int MINSIZE = 0;

    //获取队列长度
    public int getSize(){
        return myQueue.size();
    }

    //生产者
    public void push(String str) throws Exception {
        //拿到对象锁
        synchronized (myQueue){

            //如果队列满了,则阻塞
            while(getSize() == MAXSIZE){
                myQueue.wait();
            }

            myQueue.offer(str);
            System.out.println(Thread.currentThread().getName() + "放入元素" + str);
            //唤醒消费者线程,消费者和生产者自己去竞争锁
            myQueue.notify();
        }
    }

    //消费者
    public String pop() throws Exception {
        synchronized (myQueue){
            String result = null;

            //队列为空则阻塞
            while(getSize() == MINSIZE){
                myQueue.wait();
            }
            //先进先出
            result = myQueue.poll();

            System.out.println(Thread.currentThread().getName()+"取出了元素" + result);
            //唤醒生产者线程,消费者和生产者自己去竞争锁
            myQueue.notify();

            return result;
        }
    }

    public static void main(String args[]){

        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();

        //两个线程,都执行完成了打印
        CyclicBarrier barrier = new CyclicBarrier(2, ()->{
            System.out.println("生产结束,下班了,消费者明天再来吧!");
        });

		//生产者线程
        new Thread(()->{
            //50个辛勤的生产者循环向队列中添加元素
            try {
                for(int i = 0; i < 50; i++){
                    myBlockingQueue.push("——" + i );
                }
                //生产完了
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"生产者").start();

		//消费者线程
        new Thread(()->{
            //50个白拿的消费者疯狂向队列中获取元素
            try {
                for(int j = 0; j < 50; j++){
                    myBlockingQueue.pop();
                }
                //消费完了
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"消费者").start();

    }
}

19、手写多线程交替打印ABC

https://blog.csdn.net/u200814342A/article/details/133156945?ops_request_misc=&request_id=&biz_id=102&utm_term=%E6%89%8B%E5%86%99%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0ABC&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-2-133156945.142^v100^control&spm=1018.2226.3001.4187

使用同步块和wait、notify的方法控制三个线程的执行次序。具体方法如下:从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能进行打印操作。一个对象锁是prev,就是前一个线程所对应的对象锁,其主要作用是保证当前线程一定是在前一个线程操作完成后(即前一个线程释放了其对应的对象锁)才开始执行。还有一个锁就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁(也就前一个线程要释放其自身对象锁),然后当前线程再申请自己对象锁,两者兼备时打印。之后首先调用self.notify()唤醒下一个等待线程(注意notify不会立即释放对象锁,只有等到同步块代码执行完毕后才会释放),再调用prev.wait()立即释放prev对象锁,当前线程进入休眠,等待其他线程的notify操作再次唤醒。

 

public class ABC_Synch {
    public static class ThreadPrinter implements Runnable {
        private String name;
        private Object prev;
        private Object self;
        private ThreadPrinter(String name, Object prev, Object self) {
            this.name = name;
            this.prev = prev;
            this.self = self;
        }
        @Override
        public void run() {
            int count = 10;
            while (count > 0) {// 多线程并发,不能用if,必须使用whil循环
                synchronized (prev) { // 先获取 prev 锁
                    synchronized (self) {// 再获取 self 锁
                        System.out.print(name);// 打印
                        count--;
 
                        self.notifyAll();// 唤醒其他线程竞争self锁,注意此时self锁并未立即释放。
                    }
                    // 此时执行完self的同步块,这时self锁才释放。
                    try {
                        if (count == 0) {// 如果count==0,表示这是最后一次打印操作,通过notifyAll操作释放对象锁。
                            prev.notifyAll();
                        } else {
                            prev.wait(); // 立即释放 prev锁,当前线程休眠,等待唤醒
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args) throws Exception {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        ThreadPrinter pa = new ThreadPrinter("A", c, a);
        ThreadPrinter pb = new ThreadPrinter("B", a, b);
        ThreadPrinter pc = new ThreadPrinter("C", b, c);
 
        new Thread(pa).start();
        Thread.sleep(100);// 保证初始ABC的启动顺序
        new Thread(pb).start();
        Thread.sleep(100);
        new Thread(pc).start();
        Thread.sleep(100);
    }
}

可以看到程序一共定义了a,b,c三个对象锁,分别对应A、B、C三个线程。A线程最先运行,A线程按顺序申请c,a对象锁,打印操作后按顺序释放a,c对象锁,并且通过notify操作唤醒线程B。线程B首先等待获取A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C。线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程必须按照A,B,C的顺序来启动,但是这种假设依赖于JVM中线程调度、执行的顺序。

 

wait和notify操作的异同:

wait() 与 notify/notifyAll() 是Object类的方法,在执行两个方法时,要先获得锁。

当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。

当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁 的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。

从这里可以看出,notify/notifyAll()执行后,并不立即释放锁,而是要等到执行完临界区中代码后,再释放。所以在实际编程中,我们应该尽量在线程调用notify/notifyAll()后,立即退出临界区。即不要在notify/notifyAll()后面再写一些耗时的代码。

 

20、交替打印FooBar

class FooBar {
  public void foo() {
    for (int i = 0; i < n; i++) {
      print("foo");
    }
  }

  public void bar() {
    for (int i = 0; i < n; i++) {
      print("bar");
    }
  }
}
public class ClassForTestFoobar {
    private int n;
    public ClassForTestFoobar(int n){
        this.n = n; 
    }
    Semaphore semaphore1 = new Semaphore(1);
    Semaphore semaphore2 = new Semaphore(0);
    public void foo(Runnable printFoo) throws InterruptedException {
        for(int i=0;i<n;i++){
            semaphore1.acquire();
            printFoo.run();
            semaphore2.release();
        }
    }
 
    public void bar(Runnable printBar) throws InterruptedException {
        for(int i=0;i<n;i++){
            semaphore2.acquire();
            printBar.run();
            semaphore1.release();
        }
    }
 
    public static void main(String[] args) {
        ClassForTestFoobar classForTestFoobar = new ClassForTestFoobar(3);
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                try {
                    classForTestFoobar.foo(
                            new Runnable() {
                                @Override
                                public void run() {
                                    System.out.print("foo");
                                }
                            }
                    );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
 
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                try {
                    classForTestFoobar.bar(
                            new Runnable() {
                                @Override
                                public void run() {
                                    System.out.print("bar");
                                }
                            }
                    );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
            }
        };
        thread2.start();
        thread1.start();
    }
}

 

public class ClassForTestFoobar {
    private int n;
    public ClassForTestFoobar(int n){
        this.n = n;
    }
 
    private volatile boolean flag = false;
    private Object object = new Object();
    public void foo(Runnable printFoo) throws InterruptedException {
        for(int i=0;i<n;i++){
            synchronized (object){
//              bar输出时等待
                if(flag){
                    object.wait();
                }
//              输出foo
                printFoo.run();
//              唤醒bar线程
                object.notify();
//              标识唤醒bar输出
                flag = true;
            }
        }
    }
 
    public void bar(Runnable printBar) throws InterruptedException {
        for(int i=0;i<n;i++){
            synchronized (object){
//            等待foo输出
                if(!flag){
                    object.wait();
                }
//              输出bar
                printBar.run();
//              唤醒foo线程
                object.notify();
//              控制标识,控制foo输出
                flag = false;
            }
        }
    }
 
    public static void main(String[] args) {
        ClassForTestFoobar classForTestFoobar = new ClassForTestFoobar(3);
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                try {
                    classForTestFoobar.foo(
                            new Runnable() {
                                @Override
                                public void run() {
                                    System.out.print("foo");
                                }
                            }
                    );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
 
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                try {
                    classForTestFoobar.bar(
                            new Runnable() {
                                @Override
                                public void run() {
                                    System.out.print("bar");
                                }
                            }
                    );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
            }
        };
        thread2.start();
        thread1.start(); 
    }
 }



posted @   Adara  阅读(56)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示