【大厂面试合集】每日一刷——4. 百度2021Java工程师秋招真题

本文介绍了链表环检测的Floyd算法,堆数据结构的特点及Java实现,以及常见的排序算法如快速排序。此外,深入探讨了Redis的String数据结构及其优势,包括二进制安全和内存管理策略,并讨论了使用Redis可能遇到的缓存问题及解决方案。
摘要由CSDN通过智能技术生成

每日一句

在这里插入图片描述

每日一刷

考试时间 100min

一面

1. 如何判断链表是否有环?

像模像样的要我现场写了一遍,说了一遍思路。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

在这里插入图片描述
示例 2:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

在这里插入图片描述
进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

思路:首先我们要明白,链表不可能出现这种情况:

在这里插入图片描述
因为一个结点只有一个指针,所以链表只可能向实例一那样,在末尾出现一个环。

慢指针一次一步,快指针一次两步。能相遇就是有环,反之没有环。就像操场跑步,跑的快的总有一天可以追慢的一圈,相遇。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while (slow != fast) {
            if (fast == null || fast.next == null) {
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

那么,这对于懂算法的人来说可能是烂大街的问题,有些人可能不屑于看了,那么,第二个问题来了:如果让那个快指针一次走三步,还能不能做正确的答案呢?一次四步呢?五步呢?

大家可以在评论区给出答案哦~

2. 介绍一下堆这种数据结构

大根堆要求

①根节点的关键字既大于或等于左子树的关键字值,又大于或等于右子树的关键字值。

②为完全二叉树。

注意这是递归定义的。

对于大根小根堆,递归定义,实现,空间复杂度,各种操作的时间复杂度,真实写二叉树的情况和数组模拟的情况都要会。

有人要问了,会这些算法有啥用?其实Java的优先队列就是堆结构。八大排序之一的堆排序也是数组上堆结构,面试官让我手动实现一个,以下是实现。

/*
================================================
功能:堆排序
输入:数组名称(也就是数组首地址)、数组中元素个数
注:画画
================================================
*/
/*
功能:建堆
输入:数组名称(也就是数组首地址)、参与建堆元素的个数、从第几个元素开始
*/
void sift(int *x, int n, int s)
{
    int t, k, j;
    t = *(x+s); /*暂存开始元素*/
    k = s;   /*开始元素下标*/
    j = 2*k + 1; /*左子树元素下标*/
    while (j<n)
    {
        if (j<n-1 && *(x+j) < *(x+j+1))/*判断是否存在右孩子,并且右孩子比左孩子大,成立,就把j换为右孩子*/
        {
            j++;
        }
        if (t<*(x+j)) /*调整*/
        {
            *(x+k) = *(x+j);
            k = j; /*调整后,开始元素也随之调整*/
            j = 2*k + 1;
        }
        else /*没有需要调整了,已经是个堆了,退出循环。*/
        {
            break;
        }
    }
    *(x+k) = t; /*开始元素放到它正确位置*/
}
/*
功能:堆排序
输入:数组名称(也就是数组首地址)、数组中元素个数
注:
            *
         *     *
       *   -  *   *
      * * * 
建堆时,从从后往前第一个非叶子节点开始调整,也就是“-”符号的位置
*/
void heap_sort(int *x, int n)
{
    int i, k, t;
//int *p;
    for (i=n/2-1; i>=0; i--)
    {
        sift(x,n,i); /*初始建堆*/
    }
    for (k=n-1; k>=1; k--)
    {
        t = *(x+0); /*堆顶放到最后*/
        *(x+0) = *(x+k);
        *(x+k) = t;
        sift(x,k,0); /*剩下的数再建堆*/
    }
}

3. 排序知道哪些?来介绍介绍?

答:全知道,全会写,然后只说了冒泡的所有思路和优化、和快排BFPRT就被叫停了。我就把所有排序介绍和实现分享给大家。

🔗 全排序

二面

二面小哥说,一面说你算法贼强,咱们这次就不聊算法了,说说项目。

1. 看我项目里用了redis,就问redis都有哪些数据结构。

我说有string、list、hash、set、zset。

问:你说的这些Java以及其他语言基本也都有,你说了解redis,那这些数据结构到底是不是快?咋实现的呢?

我举例子说了一下:

  • 字符串

redis并未使用传统的c语言字符串表示,它自己构建了一种简单的动态字符串抽象类型。

当需要一个可以被修改的字符串时,redis就会使用自己实现的SDS(simple dynamic string)。比如在redis数据库里,包含字符串的键值对底层都是SDS实现的,不止如此,SDS还被用作缓冲区(buffer):比如AOF模块中的AOF缓冲区以及客户端状态中的输入缓冲区。

下面来具体看一下sds的实现:

struct sdshdr

{

int len;//buf已使用字节数量(保存的字符串长度)

int free;//未使用的字节数量

char buf[];//用来保存字符串的字节数组

};

sds遵循c中字符串以’\0’结尾的惯例,这一字节的空间不算在len之内。

这样的好处是,我们可以直接重用c中的一部分函数。比如printf;

sds相对c的改进

获取长度:c字符串并不记录自身长度,所以获取长度只能遍历一遍字符串,redis直接读取len即可。

缓冲区安全:c字符串容易造成缓冲区溢出,比如:程序员没有分配足够的空间就执行拼接操作。而redis会先检查sds的空间是否满足所需要求,如果不满足会自动扩充。

内存分配:由于c不记录字符串长度,对于包含了n个字符的字符串,底层总是一个长度n+1的数组,每一次长度变化,总是要对这个数组进行一次内存重新分配的操作。因为内存分配涉及复杂算法并且可能需要执行系统调用,所以它通常是比较耗时的操作。   

redis内存分配:

1、空间预分配:如果修改后大小小于1MB,程序分配和len大小一样的未使用空间,如果修改后大于1MB,程序分配 1MB的未使用空间。修改长度时检查,够的话就直接使用未使用空间,不用再分配。

2、惰性空间释放:字符串缩短时不需要释放空间,用free记录即可,留作以后使用。

二进制安全

c字符串除了末尾外,不能包含空字符,否则程序读到空字符会误以为是结尾,这就限制了c字符串只能保存文本,二进制文件就不能保存了。

而redis字符串都是二进制安全的,因为有len来记录长度。

这就是redis中string的实现和要点,我大概都给他讲了一遍。然后他说你不用说了,又说到算法数据结构了,咱们聊点别的。

2. 聊点实际的吧,你一直在吹redis,知道用redis会给你带来什么问题吗?

我有点懵,就给他说nosql和传动数据库的区别之类的,然后他说你不要给我说这些比较,举个例子,缓存雪崩听说过吗?我就明白他想聊什么了,又给他说了说以下内容。

  • 缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就去后端系统查找(比如DB)。

一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

在这里插入图片描述
如何避免?

1:对查询结果为空的情况也进行缓存,这样,再次访问时,缓存层会直接返回空值。缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

2:对一定不存在的key进行过滤。具体请看布隆过滤器

  • 缓存击穿

是针对缓存中没有但数据库有的数据。

场景是,当Key失效后,假如瞬间突然涌入大量的请求,来请求同一个Key,这些请求不会命中Redis,都会请求到DB,导致数据库压力过大,甚至扛不住,挂掉。

解决办法

1、设置热点Key,自动检测热点Key,将热点Key的过期时间加大或者设置为永不过期,或者设置为逻辑上永不过期

2、加互斥锁。当发现没有命中Redis,去查数据库的时候,在执行更新缓存的操作上加锁,当一个线程访问时,其它线程等待,这个线程访问过后,缓存中的数据会被重建,这样其他线程就可以从缓存中取值。

  • 缓存雪崩

是指大量Key同时失效,对这些Key的请求又会打到DB上,同样会导致数据库压力过大甚至挂掉。

解决办法

1)让Key的失效时间分散开,可以在统一的失效时间上再加一个随机值,或者使用更高级的算法分散失效时间。

2)构建多个redis实例,个别节点挂了还有别的可以用。

3)多级缓存:比如增加本地缓存,减小redis压力。

4)对存储层增加限流措施,当请求超出限制,提供降级服务(一般就是返回错误即可)

三面

1. 输入网址到看到网页的过程

答:(能多细就多细,不仅要背答案,要理解)

域名解析 --> TCP3次握手 --> 发http请求 --> 响应http请求,浏览器得到html代码 --> 浏览器解析代码,请求html代码中的资源(js、css、图片等) --> 浏览器对页面进行渲染呈现给用户

2. 让手写快排

(我寻思真就没别的可问了呗)


public class QuickSort {
public static void main(String[] args) {
    int[] arr = new int[] {9,4,6,8,3,10,4,6};
    quickSort(arr,0,arr.length - 1);
    System.out.println(Arrays.toString(arr));
    
}
public static void quickSort(int[] arr,int low,int high) {
    int p,i,j,temp;
    
    if(low >= high) {
        return;
    }
    //p就是基准数,这里就是每个数组的第一个
    p = arr[low];
    i = low;
    j = high;
    while(i < j) {
        //右边当发现小于p的值时停止循环
        while(arr[j] >= p && i < j) {
            j--;
        }
                        
        //这里一定是右边开始,上下这两个循环不能调换(下面有解析,可以先想想)    

        //左边当发现大于p的值时停止循环
        while(arr[i] <= p && i < j) {
            i++;
        }
        
            temp = arr[j];
            arr[j] = arr[i];
            arr[i] = temp;
    }
    arr[low] = arr[i];//这里的arr[i]一定是停小于p的,经过i、j交换后i处的值一定是小于p的(j先走)
    arr[i] = p; 
    quickSort(arr,low,j-1);  //对左边快排
    quickSort(arr,j+1,high); //对右边快排
    
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李一恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值