shaoxyz

你看上去像是体面人

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
industry_list = [
{
"parent_ind" : "女装",
"name" : "连衣裙"
},
{
"name": "女装"
},
{
"parent_ind" : "女装",
"name" : "半身裙"
},
{
"parent_ind" : "女装",
"name" : "A字裙"
},
{
"name": "数码"
},
{
"parent_ind" : "数码",
"name": "电脑配件"
},
{
"parent_ind" : "电脑配件",
"name": "内存"
},
]

为了取用方便,我们希望可以将其转换为树状格式,例如:

1
2
3
4
5
6
7
8
9
10
11
12
{
"数码": {
"电脑配件": {
"内存" : {}
}
},
"女装" : {
"连衣裙": {},
"半身裙": {},
"A字裙": {}
}
}

实现一个方法完成这个转换,时间复杂度请控制在O(n)

1
2
def convert_format(data):
pass

考察的知识点

1.数据类型,Python里set的查找效率比list高
2.字典推导,更Pythonic的写法
3.对象引用,Python的赋值是给值贴上标签,而不是把值放入盒子

思路

  • 先根据industry_list构建一个包含所有项的字典data_dict
  • 遍历industry_list并利用对象引用的特点自动更新data_dict的嵌套关系;
  • 最后过滤掉那些只存在于嵌套之内的值;

代码

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# -*- coding: utf-8 -*-
"""
@author: shaoxyz
@file: convert_format.py
@time: 2020-07-20 11:09
@desc:
"""
from typing import List
industry_list = [
{"parent_ind": "女装", "name": "连衣裙"},
{"name": "女装"},
{"parent_ind": "女装", "name": "半身裙"},
{"parent_ind": "女装", "name": "A字裙"},
{"parent_ind": "数码", "name": "电脑配件"},
{"parent_ind": "电脑配件", "name": "内存"},
{"name": "数码"},
]

expect_output = {
"数码": {
"电脑配件": {
"内存": {}
}
},
"女装": {
"连衣裙": {},
"半身裙": {},
"A字裙": {}
}
}


def convert_format(data: List[dict]) -> dict:
"""
将包含父子关系值的列表 转换为 嵌套字典
"""

data_dict = {i.get("name"): {} for i in data}
has_parent = set()

for i in data:
parent_ind = i.get("parent_ind")
name = i.get("name")
if parent_ind:
has_parent.add(name)
if parent_ind in data_dict:
data_dict[parent_ind][name] = data_dict[name] # KEY

# Filter
res = {key: val for key, val in data_dict.items() if key not in has_parent}

return res


def test_convert_format():
res = convert_format(industry_list)
assert res == expect_output
print("well done")

老东家运营妹子要做数据分析提的一个需求

统计所有用户在注册当天是否完成过消费

涉及用户表和订单表的联表查询, 主要字段为用户id,注册时间,第一笔消费时间

我们的MySQL版本是5.7.18

先说遇到的问题

首先,用 GROUP BY 会触发 ONLY_FULL_GROUP_BY

1
2
3
4
SELECT u.id, u.created_at, o.created_at 
FROM user u
LEFT JOIN orders o ON o.buyer_id = user.id AND o.state = 1
GROUP BY o.buyer_id

简单来说,MySQL5.7.5之后,SQL需要检测函数依赖关系,SELECT & ORDER BY & HAVING 不能引用非聚合列

之所以要这么做是为了避免当出现不正确和不可预测的查询结果时MySQL没有任何报错和警告,这里有具体例子与说明

再者,用MIN || MAX || ANY_VALUE将非聚合列包起来也是可行的,但实测在数据量较大的情况下很慢。

1
2
3
SELECT MIN(u.id), MIN(u.created_at), MIN(o.created_at) 
FROM user u LEFT JOIN orders o ON o.buyer_id = u.id AND o.state = 1
GROUP BY o.buyer_id

推测是这么做依然没有真正意义上的去重,而是类似与把原来的查询作为子查询又过滤了一遍

想要的效果是:直接限制辅助表只取一行

stackoverflow上找到了最佳答案

最后修改SQL如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT 
user.id,
CASE
WHEN DATE(o.created_at) = DATE(u.created_at) THEN 1
ELSE 0
END AS '是否在注册当天购买',
DATE(u.created_at) AS '注册时间'
FROM user u
LEFT JOIN orders o ON o.id = ( # 注意这里必须是主键
SELECT o2.id
FROM orders o2
WHERE o2.buyer_id = u.id
AND o2.state = 1
ORDER BY o2.id
LIMIT 1
)

用户表30w
订单表70w
时间大概在10~20ms, 可以接受

这是最近求职碰到的一道真实笔试题,HR把题目发来后,完整的编码花了大半个下午,格式化输出实在是有点麻烦
话不多说,直接上题

架构小操练

需求描述

某快餐品牌推出了它独家的外卖应用,用户可以在手机上直接下单。该应用会根据用户选择的菜品(Item)、数量(Count)和优惠方式(Promotion)进行计算,告诉用户需要支付的金额(Charge)。

优惠活动有多种形式。假设用户一次只能使用一种优惠,那么使用哪种优惠省钱最多就会是一个让用户头疼的问题。所以该外卖应用为了方便用户,在用户下单时,会自动选择最优惠的方式并计算出最终金额让用户确认。

我们需要实现一个名为bestCharge的函数,它能够接收用户选择的菜品和数量(以特定格式呈现)作为输入,然后返回计算后的汇总信息。

已知:

  • 该店的菜品每一个都有一个唯一的id
  • 当前的优惠方式有:
    • 满30减6元
    • 指定菜品半价
  • 除菜品外没有其它收费(如送餐费、餐盒费等)
  • 如果两种优惠方式省钱一样多,则使用前一种优惠方式

输入样例

1
["ITEM0001 x 1", "ITEM0013 x 2", "ITEM0022 x 1"]

输出样例

1
2
3
4
5
6
7
8
9
10
============= 订餐明细 =============
黄焖鸡 x 1 = 18元
肉夹馍 x 2 = 12元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
指定菜品半价(黄焖鸡,凉皮),省13元
-----------------------------------
总计:25元
===================================

使用另一种优惠的样例

输入:

1
["ITEM0013 x 4", "ITEM0022 x 1"]

输出:

1
2
3
4
5
6
7
8
9
============= 订餐明细 =============
肉夹馍 x 4 = 24元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
满30减6元,省6元
-----------------------------------
总计:26元
===================================

如果没有优惠可享受

输入:

1
["ITEM0013 x 4"]

输出:

1
2
3
4
5
============= 订餐明细 =============
肉夹馍 x 4 = 24元
-----------------------------------
总计:24元
===================================

答案

看到题就感觉似曾相识,参考流畅的Python第六章

好了,把朕的🐎牵上来!

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# -*- coding: utf-8 -*-
"""
@version: python3.7
@author: shaoxyz
@file: strategy.py
@time: 2020-07-01 18:54
@desc: 操练策略模式
"""
import re
import textwrap
import threading
from decimal import Decimal
from abc import ABC, abstractmethod
from functools import wraps
from typing import Dict

"""
菜品属性说明
item = {
_id: <INCR>, # 自增id
name: str, # 菜品名
price: int, # 单价,单位:分
is_half_price: bool # 是否半价
}
"""
data = {
"ITEM0001": {"_id": 1, "name": "黄焖鸡", "price": 1800, "is_half_price": True},
"ITEM0002": {"_id": 2, "name": "蛋炒饭", "price": 1500, "is_half_price": False},
"ITEM0003": {"_id": 3, "name": "牛肉面", "price": 2200, "is_half_price": True},
"ITEM0004": {"_id": 4, "name": "东北饺子", "price": 1800, "is_half_price": False},
"ITEM0013": {"_id": 13, "name": "肉夹馍", "price": 600, "is_half_price": True},
"ITEM0022": {"_id": 22, "name": "凉皮", "price": 800, "is_half_price": True},
"ITEM0023": {
"_id": 23,
"name": "超级无敌海鲜霸王担担面之超级无敌海鲜霸王担担面",
"price": 3000,
"is_half_price": True,
},
}

## Exception


class ObjectDoesNotExist(Exception):
"""The requested object does not exist"""

pass


class InvalidParams(Exception):
"""Invalid parameters"""

pass


class EmptyInput(Exception):
"""Invalid parameters"""

pass


## Obj


class Item:
def __init__(self, item_id, count):
self.item_info = data.get(item_id)
if self.item_info is None:
raise ObjectDoesNotExist(f"不存在菜品: {item_id}")
self.count = count

def __repr__(self):
return self.name

@property
def price(self) -> int:
return self.item_info.get("price")

@property
def name(self) -> str:
return self.item_info.get("name")

@property
def is_half_price(self) -> bool:
return self.item_info.get("is_half_price", False)

@property
def total(self) -> int:
return self.price * self.count


class Order:
_lock = threading.RLock() # 无论类有多少个实例,都用一个锁

def __init__(self, promotion=None):
self.basket = []
self.promotion = promotion
self.half_price_items = set()

self._final_promotion = None
self._discount = 0
self._finished = False

def add_to_basket(self, *items):
with Order._lock:
self._finished = False
for item in items:
self.basket.append(item)

@property
def final_promotion(self) -> str:
if not self._finished:
self.charge()
return self._final_promotion

@property
def discount(self) -> int:
if not self._finished:
self.charge()
return self._discount

@property
def total(self) -> int:
total = 0
for item in self.basket:
total += item.total
return total

def charge(self):
if not self.promotion:
discount = 0
else:
with Order._lock:
discount = self.promotion.discount(self)
self._finished = True

return self.total - discount


## Promotion
PROMOS = set()


def add_to_promos(func):
"""注册到优惠机制集合"""

PROMOS.add(func)

@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

return wrapper


class Promotion(ABC):
"""优惠策略抽象基类"""

@abstractmethod
def discount(self, order):
pass


@add_to_promos
class FullReductionPromo(Promotion):
"""满30减6元"""

def discount(self, order):
return 600 if order.total > 3000 else 0


@add_to_promos
class HalfPricePromo(Promotion):
"""指定菜品半价"""

def discount(self, order):
discount = 0
for item in order.basket:
if item.is_half_price:
order.half_price_items.add(item.name)
discount += item.total / 2
return discount


class BestPromo(Promotion):
"""最佳优惠"""

def discount(self, order):

# 使用装饰器注册代替
# all_promotion = [
# globals()[name]
# for name in globals()
# if name.endswith("Promo") and name != "BestPromo"
# ]

for promo in PROMOS:
discount = promo().discount(order)
if discount != 0 and discount > order._discount:
order._discount = discount
order._final_promotion = promo().__doc__

return order._discount


## utils


def format_output(order):
"""
:return:
============= 订餐明细 =============
黄焖鸡 x 1 = 18元
肉夹馍 x 2 = 12元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
指定菜品半价(黄焖鸡,凉皮),省13元
-----------------------------------
总计:25元
===================================
"""
head = "============= 订餐明细 =============\n"
divide = "\n-----------------------------------\n"
bottom = "==================================="

items_output = "\n".join(
[
"\n".join(
textwrap.wrap(f"{i.name} x {i.count} = {Decimal(i.total / 100)}元", 20)
)
for i in order.basket
]
)
output = head + items_output + divide
charge_output = f"总计:{Decimal(order.charge() / 100)}元\n"

best_promo = order.final_promotion
if best_promo == "指定菜品半价":
lines = "\n".join(
textwrap.wrap(
f"{best_promo}({','.join(order.half_price_items)}), 省{Decimal(order.discount/100)}元",
20,
)
)
promotion_output = f"使用优惠: \n{lines}"
output += promotion_output + divide
elif best_promo == "满30减6元":
promotion_output = f"使用优惠: \n{best_promo}, 省{Decimal(order.discount/100)}元"
output += promotion_output + divide

output += charge_output + bottom

return output


def verify_input_format(input_item: str) -> None:
pattern = re.compile(r"^ITEM\d+( x ){1}[1-9]+")
if not pattern.match(input_item):
raise InvalidParams("输入格式有误")


def parse_input(input_items: list) -> Dict[str, int]:
if not input_items:
raise EmptyInput("未挑选菜品")

items_map = {}
for i in input_items:
verify_input_format(i)
item_id, count = i.split(" x ")
if item_id in items_map:
items_map[item_id] += int(count)
else:
items_map[item_id] = int(count)

return items_map


## main


def bestCharge(input_items: list) -> str:
items_map = parse_input(input_items)

order = Order(promotion=BestPromo())

order.add_to_basket(
*[Item(item_id, items_map[item_id]) for item_id in items_map.keys()]
)

return format_output(order)


if __name__ == "__main__":
input_demo3 = ["ITEM0001 x 1", "ITEM0001 x 1", "ITEM0013 x 4", "ITEM0013 x 4"]

print(bestCharge(input_demo3))

当时还写了单元测试一起给HR发过去,就不在这写了 : )

场景

面试官老王:给你出一道题吧,很简单,统计字符串中各个字符出现的次数

求职者小李:嗯,好的!

( 于是小李开始输出..

1
from collections import Counte...

老王打断小李:emm.. 不,不要用Counter

小李被打断有点慌:啊好的

老王温馨提醒:就用字典吧

小李诧异心想:就这?

( 于是小李又开始输出..

1
2
3
4
5
6
7
8
def count(s):
res = {}
for i in s:
if i not in res:
res[i] = 1
else:
res[i] += 1
print(res)

老王:嗯,好,那你分析一下复杂度吧

小李心想,这有什么好分析的,不就O(n)么?小李十分肯定答:O(N)!

老王:em..还有优化的空间吗?

小李又看了一眼代码,自己嘀咕:必须扫一遍所有字符,那O(N)还能怎么优化,面试官莫不是在搞我?

气氛有点尴尬,小李怎么也想不出来还能怎么优化..



解答

还可以再减少一次对res的查询

先来分析分析最初的写法

1
2
3
4
5
6
7
8
def count(s):
res = {}
for i in s:
if i not in res: # 第一次查res
res[i] = 1
else:
res[i] += 1 # 等价于`res[i] = res[i] + 1` 第二次查res
print(res)

第一种写法:

1
2
3
4
5
def count(s):
res = {}
for i in s:
res[i] = res.get(i, 0) + 1 # 只查了一次res
print(res)

第二种写法:

1
2
3
4
5
6
from collections import defaultdict
def count2(s):
res = defaultdict(int)
for i in s:
res[i] += 1 # 只查了一次res
print(res)