杭州炎魂网络

面试时间

2025.4.15 9:00

面试职位

后端开发实习生(Go)

面试题目

笔试

算法

力扣Top100 [1]

[两数之和] 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值target 的那两个整数,并返回它们的数组下标

Go

1
2
3
4
5
6
7
8
9
10
func twoSum(nums []int, target int) []int {
map1 := make(map[int]int)
for i, num := range nums {
if idx, ok := map1[target - num]; ok {
return []int{idx, i}
}
map1[num] = i
}
return []int{}
}

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map1 = new HashMap<>();
for(int i = 0; i < nums.length; i++) {
int temp = target - nums[i];
if(map1.containsKey(tmp)) {
return new int[]{map1.get(temp), i};
}
map1.put(nums[i], i);
}
return new int[]{0};
}
}

其他解法

排序+双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type Pair struct {
Value int
Index int
}

func twoSum(nums []int, target int) []int {
// 创建包含值和原始索引的结构体切片
pairs := make([]Pair, len(nums))
for i, v := range nums {
pairs[i] = Pair{Value: v, Index: i}
}

sort.Slice(pairs, func(i, j int) bool {
return pairs[i].Value < pairs[j].Value
})

left, right := 0, len(pairs)-1

for left < right {
sum := pairs[left].Value + pairs[right].Value
if sum == target {
return []int{pairs[left].Index, pairs[right].Index}
} else if sum < target {
left++
} else {
right--
}
}

return []int{}
}

拓展:三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func threeSum(nums []int) (ans [][]int) {
slices.Sort(nums)
n := len(sum)
for i, x := range nums[:n-2] {
if i > 0 && x == nums[i-1] {
continue
}
if x + nums[i+1] + nums[i+2] > 0 {
break
}
if x + nums[n-2] + nums[n-1] < 0 {
continue
}
j, k := i+1, n-1
for i < k {
s := x + nums[j] + nums[k]
if s > 0 {
k--
} else if s < 0 {
j++
} else {
ans = append(ans, []int{x, nums[j], nums[k]})
for j++; j < k && nums[j] == nums[j-1]; j++{}
for k--; k > j && nums[k] == nums[k+1]; k--{}
}
}
}
return
}

情景题

云文档协作

  1. 云文档的实时编辑操作怎么同步到不同用户查看

    通信机制:使用 WebSocket 建立客户端与服务器之间的持久连接,实现双向通信,使编辑操作能够即时传输到其他用户端

    操作变换(OT):将用户的编辑操作转换为操作序列,并在服务器上合并,最后同步给用户,确保文档状态一致

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    **OT算法**
    在协同编辑中,多个用户可能同时对文档进行修改,导致操作冲突。OT 算法通过以下机制解决这一问题:
    `操作抽象`:将用户的编辑行为抽象为操作(Operation),如插入(Insert)、删除(Delete)等。
    `操作传播`:用户的操作首先在本地应用,并通过网络发送到服务器。
    `操作转换`:当服务器接收到多个并发操作时,会根据一定的规则对操作进行转换(Transform),以解决冲突。
    `操作分发`:服务器将转换后的操作分发给所有客户端,客户端再将其应用到本地文档中。
    通过上述步骤,OT 算法确保了即使操作的执行顺序不同,最终的文档状态仍然一致。

    假设初始文档内容为 "abc",用户 A 和用户 B 同时进行以下操作:
    用户 A:在位置 0 插入字符 "x",操作记为 O1 = Insert(0, "x")。
    用户 B:删除位置 2 的字符 "c",操作记为 O2 = Delete(2, "c")。
    在用户 A 的客户端,操作顺序为 O1 → O2。由于 O1 在位置 0 插入了一个字符,导致原位置 2 的字符 "c" 移动到了位置 3。因此,O2 需要转换为 O2' = Delete(3, "c"),以正确删除字符 "c"。
    在用户 B 的客户端,操作顺序为 O2 → O1。由于 O2 删除了位置 2 的字符 "c",O1 的插入位置不受影响,仍为位置 0。
    通过操作转换,两个客户端最终的文档内容都为 "xab",实现了一致性

    **CRPT算法(无冲突复制数据类型)**
    CRDT 允许多个副本在不进行协调的情况下独立并发地更新,并通过设计确保这些副本最终能够自动合并,达到一致的状态。这意味着即使在网络分区或离线操作的情况下,各个副本也能保证数据的一致性。
    `状态型 CRDT`
    每个副本维护自己的完整状态,并周期性地将状态发送给其他副本。接收方通过合并操作将接收到的状态与本地状态合并。
    `操作型 CRDT`
    副本之间只传递操作(如插入、删除等),而不是整个状态。每个操作在本地应用,并传播给其他副本。
  2. 如果用户在离线状态下编辑,恢复网络后怎么同步到云文档中

    本地缓存:用户在离线状态下的编辑操作被保存到本地缓存中。

    网络恢复检测:系统检测到网络恢复后,自动触发同步机制。

    增量同步:仅将离线期间的变更部分上传到服务器,减少数据传输量。

    冲突检测与解决:如果在离线期间其他用户也对同一部分进行了编辑,系统会进行冲突检测,并提供合并工具或提示用户手动解决冲突。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    非冲突操作的自动合并:对于不同用户在文档的不同部分所做的修改,系统可以自动合并,确保文档内容的完整性。
    冲突操作的检测与提示:当检测到多个用户对同一部分内容进行了不同的修改,系统会标记为冲突,并提示用户进行处理。

    3. 如果有很多人同时编辑同一个地方,你该怎么处理?

    **冲突解决算法**:OT 和 CRDT,能够在多个用户同时编辑时,通过操作转换或特定的数据结构,自动解决冲突,保持文档一致性。

    **锁定机制**:在用户编辑某部分内容时,系统自动锁定该区域,防止其他人同时进行编辑,减少冲突的发生。

    **版本控制**:记录文档的不同版本,在出现冲突时可以进行回溯和比较,帮助用户解决冲突。

    ### 面试

    1. Go的泛型

    泛型是一种编程范式,允许在函数或类型定义中使用占位符类型(类型参数),这些类型在实际使用时由具体的类型实例化。这使得相同的函数或类型可以适用于多种不同的数据类型,提高了代码的灵活性和复用性。

    **函数示例**

    ```go
    package main

    import "fmt"

    func Add[T int | float64](a, b T) T {
    return a + b
    }

    func main() {
    fmt.Println(Add(3, 4)) // 输出: 7
    fmt.Println(Add(2.5, 4.3)) // 输出: 6.8
    }

    定义泛型类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Slice[T int | float64] []T

    func main() {
    var intSlice Slice[int] = []int{1, 2, 3}
    var floatSlice Slice[float64] = []float64{1.1, 2.2, 3.3}

    fmt.Println(intSlice) // 输出: [1 2 3]
    fmt.Println(floatSlice) // 输出: [1.1 2.2 3.3]
    }

    使用自定义的接口作为类型约束

    1
    2
    3
    4
    5
    6
    7
    type Number interface {
    int | float64
    }

    func Multiply[T Number](a, b T) T {
    return a * b
    }
  3. context有哪些功能,你具体用它做了什么,可以举个具体的例子吗?

    context 包提供了一种机制,用于在多个 goroutine 之间传递取消信号、截止时间和请求范围内的数据。

    主要功能

    取消信号传递:通过 context.WithCancel 创建的上下文可以在需要时取消操作,通知所有相关的 goroutine 停止工作。

    超时控制:使用 context.WithTimeoutcontext.WithDeadline 可以设置操作的超时时间,防止因某个操作阻塞而导致资源泄露。

    传递请求范围的数据:通过 context.WithValue 可以在上下文中存储键值对,传递如用户认证信息、请求 ID 等数据。

    示例:使用 context 控制 goroutine 的取消

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package main

    import (
    "context"
    "fmt"
    "time"
    )

    func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel(

    go func(ctx context.Context) {
    for {
    select {
    case <-ctx.Done():
    fmt.Println("Goroutine received cancel signal")
    return
    default:
    fmt.Println("Goroutine is working")
    time.Sleep(500 * time.Millisecond)
    }
    }
    }(ctx)

    time.Sleep(2 * time.Second)
    fmt.Println("Main function cancels context")
    cancel()
    time.Sleep(1 * time.Second)
    }
  4. 你的项目中都用到了鉴权,可以介绍一下你用到的鉴权吗?(JWT和session)

    JWT(无状态):Token 存储在客户端,服务器不保存会话信息,适用于前后端分离、微服务架构。

    Session(有状态):会话信息存储在服务器。

    多端唯一登录

    登录时生成 Token:用户登录成功后,服务器生成一个包含用户信息的 JWT,并存储在客户端。

    在 Redis 中存储 Token:以用户 ID 为键,将生成的 Token 存储在 Redis 中。

    验证 Token:每次请求时,服务器从请求头中获取 Token,并与 Redis 中存储的 Token 进行比对。

    实现单设备登录:如果用户在另一设备上登录,服务器会生成新的 Token,并更新 Redis 中的记录。旧设备上的 Token 与 Redis 中的不一致,验证失败,需重新登录。

    限制两台设备登录,第三台登录时踢出最早登录设备

    1. 在 Redis 中维护登录设备列表:以用户 ID 为键,存储一个列表,记录每个设备的 Token 和登录时间。Sorted Set(有序集合)

    2. 登录时检查设备数量:当用户在新设备上登录时,检查列表中的设备数量。

      • 如果少于两台,允许登录,并将新设备信息添加到列表中。

      • 如果已达到两台,找出最早登录的设备,将其 Token 标记为无效或从列表中移除,然后添加新设备信息。

    3. 验证 Token 时检查有效性:每次请求时,服务器检查请求中的 Token 是否在 Redis 的设备列表中,若不在,说明已被踢出,需重新登录。

  5. 微精弘支持全校两万师生使用,你可以讲讲它的具体架构吗?或者说,有没有什么特别的实现

    详见面试知识准备 | 浅浅子の小屋

面试总结

第一次面试,经验不足,还需要多多准备