什么是图
想象一下,有一颗树,你想把它放到木箱子里面,但是箱子不够大,即便放得下,存取也很麻烦,那怎么才能让这棵树既能放到木箱子里,又能恢复成一棵树呢。
这里的"树"是你能看到的任何由节点(Vertices)和边(Edges)组成的数据结构,比如文件目录,脑图,流程图,富文本内容,甚至推荐算法后面的类似人群算法(当然这个纬度比较多,你可以理解为像神经节点一样的一个3D的图)。
图又分为有向图(Directed Graph)和无向图(Undirected Graph),在现实生活中,它们以不同的方式呈现,下面是有向图和无向图在现实生活中的一些场景:
有向图(Directed Graph)的场景:
-
社交网络关系: 在社交网络中,有向图可以表示用户之间的关注关系。如果用户A关注用户B,但B不一定关注A,那么存在一个有向边从A指向B。
-
网页链接: 互联网中的网页之间的链接关系可以用有向图表示。如果网页A链接到网页B,但B不一定链接到A,那么可以用有向边表示这种链接关系。
-
交通流向: 道路交叉口和交通网络中的车辆流向可以用有向图表示。有向边表示车辆的行驶方向。
-
任务依赖关系: 项目管理中的任务和活动之间的依赖关系可以用有向图表示。一个任务可能依赖于另一个任务的完成。
-
电路: 电路中的元件和信号传递关系可以用有向图表示,其中有向边表示信号的传递方向。
无向图(Undirected Graph)的场景:
-
朋友关系: 在社交网络中,如果用户A和用户B互为朋友,那么可以用无向边连接节点A和B。
-
交通道路: 道路系统中,道路的连接关系通常是双向的,因此可以使用无向图来表示道路网络。
-
共享资源: 如果多个节点共享某个资源,而且这种共享是双向的,那么可以使用无向图表示这种关系。
-
通信网络: 通信网络中,设备之间的通信连接通常是双向的,可以用无向图表示。
-
共同兴趣: 如果两个人或实体有共同的兴趣或关系,而这种关系不依赖于方向,那么可以用无向图表示。
这些是一些基本的例子,实际上,有向图和无向图都可以用来建模和分析各种复杂的关系和系统。
图的本质
在工业界,我们常提到一种数据结构------"链表",事实上,图的在计算机上的呈现就是以"链表"的形式,有向图是单向链表,无向图是三向链表,而包含无数条边的"神经网络"就是N向链表(我们管它叫做"向量"/"矩阵")。
图的计算和存储
前面说到,一棵"树",如果不经过处理直接放进"木箱子",是会造成很多"问题"的,事实上,世界上的大部分人造物,都是树形结构,也就是一张"图",自然界大部分事物也是如此,为了解决上述"问题",主要有两个方案:
方案一:
有人选择把"树"拆分(拆成链表的节点),把数据"打"平,要用的时候再根据节点上的指针,构建"边",恢复成原来的树。
拆分之后的链表节点可以存入关系型数据库中,在恢复成"树"之后,对"树"进行添加、删除、移动、修改时,经过一个抽象数据层,直接去操作"树"(当然,如果你不介意性能问题或者觉得麻烦,可以去操作存储起来的数据节点数据,然后根据节点数据转换成"树",这种方案确实省事,但是其性能在节点多的场景是无法接受的)。
每一个对树的操作都会返回一系列对数据库的节点操作,这样在保证"最少计算成本"的情况下也能保证数据的一致性。
这种方案,关键在于抽象数据层的构建(涉及到许多算法知识,不是leetcode或者书本上的一些算法,更多的是一些算法延伸出来的一种思维方式,我将之称为"具象思维"(福尔摩斯归纳演绎法的一种思维实现)------能够在计算机世界与现实世界之间建立联系的能力)。
方案二:
发明一种能够直接存储整颗"树"的数据库,需要用到树的时候,甚至能够需要哪一部分就取哪一部分,对树的操作也可以基于数据库提供的方法直接去操作数据库中的树。这种数据库我们管它叫做"图"数据库。
业界有名的数据库有如下几款:
以下是一些著名的图数据库,它们在处理图形结构和复杂关系方面具有显著的性能和功能:
-
Neo4j: Neo4j 是一款广受欢迎的开源图数据库,它使用图形模型来表示和存储数据,提供强大的图形查询和分析功能。
-
Amazon Neptune: Amazon Neptune 是由亚马逊提供的云图数据库服务,支持图和属性图模型,适用于在亚马逊云上构建图数据库应用。
-
OrientDB: OrientDB 是一款支持多模型的开源数据库,兼容图形、文档、键值和对象模型。它具有分布式和多主复制的特性。
-
Titan: Titan 是一个分布式图数据库,它支持图模型和图计算。Titan 可以与Apache Cassandra、HBase和BerkeleyDB等存储后端集成。
-
Dgraph: Dgraph 是一款开源的分布式图数据库,支持GraphQL查询语言,适用于构建实时应用和复杂的图形查询。
-
Alibaba Graph Database (GDB): GDB 是阿里巴巴云提供的图数据库服务,支持高度并发、大规模图计算和分布式图数据存储。
-
Microsoft Azure Cosmos DB: Azure Cosmos DB 是微软提供的多模型数据库服务,支持图形模型、文档模型等,并提供全球分布式、多-API支持的特性。
我遇到的问题
我遇到的问题是,需要将文件目录、富文本、流程图、脑图、数据流等图数据的数据节点以一种扁平化的数据结构存储到本地的关系型(文档型)数据库当中,所以我需要构建一个方案一种的抽象数据层,我把它定义为了一个class,叫做NodeTree,它负责将扁平链表节点转换成"树",并支持对这棵"树"的各种操作(insert、remove、update、move),并在执行完操作之后知道有哪些节点受到了"影响",用effect_items去更新本地数据库中的节点数据,从而保证数据的一致性。
这个抽象数据层周二开始构思,到今天(周五)晚上才最终通过了所有测试,交付给了业务数据层,下面是这个 抽象层的代码实现:
ts
import { get, flatMap, last, initial, find, reduceRight, flatten } from 'lodash-es'
import { makeAutoObservable, toJS } from 'mobx'
type RawNode = {
id: string
pid?: string
prev_id?: string
next_id?: string
[key: string]: any
}
type RawNodes = Array<RawNode>
type TreeItem = RawNode & { children?: Tree }
type Tree = Array<TreeItem>
type TreeMap = Record<string, TreeItem>
type ArgsMove = {
active_parent_index: Array<number>
over_parent_index: Array<number>
droppable: boolean
}
type ArgsPlace = {
type: 'push' | 'insert'
active_item: RawNode
over_item?: RawNode
target_level: RawNodes
over_index?: number
}
export default class Index {
tree = [] as Tree
constructor() {
makeAutoObservable(this, null, { autoBind: true })
}
public init(raw_nodes: RawNodes) {
const raw_tree_map = this.setRawMap(raw_nodes)
const { tree, tree_map } = this.getTree(raw_nodes, raw_tree_map)
this.tree = this.sortTree(tree, tree_map)
}
public insert(item: RawNode, focusing_index?: Array<number>) {
const { target_level, cloned_item: over_item } = this.getDroppableItem(focusing_index)
const { active_item, effect_items } = this.place({
type: 'push',
active_item: item,
over_item,
target_level
})
return { item: active_item, effect_items }
}
public remove(focusing_index: Array<number>) {
const { cloned_item, effect_items } = this.take(focusing_index)
let remove_items = [] as Tree
if (cloned_item?.children?.length) {
remove_items = this.getherItems(cloned_item.children)
}
return { id: cloned_item.id, remove_items, effect_items }
}
public update(focusing_index: Array<number>, data: Omit<RawNode, 'id'>) {
const { item, target_level, target_index } = this.getItem(focusing_index)
const target = { ...item, ...data }
target_level[target_index] = target
return target
}
public move(args: ArgsMove) {
const { active_parent_index, over_parent_index, droppable } = args
const effect_items = [] as RawNodes
const { cloned_item: active_item } = this.getItem(active_parent_index)
const { cloned_item: over_item, target_level } = droppable
? this.getDroppableItem(over_parent_index)
: this.getItem(over_parent_index)
if (over_item.next_id === active_item.id && !droppable) {
return { effect_items }
}
const swap = active_item.next_id === over_item.id && !droppable
let execs = []
const place = () => {
const { effect_items } = this.place({
type: droppable ? 'push' : 'insert',
active_item,
over_item,
target_level,
over_index: last(over_parent_index)
})
return effect_items
}
const take = () => {
const { effect_items } = this.take(active_parent_index, swap)
return effect_items
}
if (active_item.pid === over_item.pid) {
if (last(active_parent_index) < last(over_parent_index)) {
execs = [place, take]
} else {
execs = [take, place]
}
} else {
if (active_parent_index.length > over_parent_index.length) {
execs = [take, place]
} else {
execs = [place, take]
}
}
const all_effect_items = flatten(execs.map((func) => func()))
return { effect_items: this.getUniqEffectItems(all_effect_items) }
}
public getItem(indexes: Array<number>) {
let target_level = [] as Array<TreeItem>
let target_index = 0
let target_item = null as TreeItem
const target_indexes = this.getIndexes(indexes)
const level_indexes = initial(target_indexes)
target_index = last(indexes)
target_item = get(this.tree, target_indexes)
if (!level_indexes.length) {
target_level = this.tree
} else {
target_level = get(this.tree, level_indexes)
}
return {
item: target_item,
cloned_item: toJS(target_item),
target_level,
target_index
}
}
private getDroppableItem(indexes: Array<number>) {
if (!indexes.length) return { target_level: this.tree, cloned_item: null }
let target_item = null as TreeItem
let target_indexes = [] as Array<number | string>
if (indexes.length === 1) {
target_indexes = indexes
} else {
target_indexes = this.getIndexes(indexes)
}
target_item = get(this.tree, target_indexes)
if (!target_item.children) {
target_item.children = []
}
return { target_level: target_item.children, cloned_item: toJS(target_item) }
}
private setRawMap(raw_nodes: RawNodes) {
const tree_map = {} as TreeMap
raw_nodes.map((item) => {
tree_map[item.id] = item
})
return tree_map
}
private getTree(raw_nodes: RawNodes, tree_map: TreeMap) {
const tree = [] as Tree
raw_nodes.forEach((item) => {
if (item.pid) {
if (!tree_map[item.pid].children) {
tree_map[item.pid].children = []
}
if (!tree_map[item.pid]?.children?.length) {
tree_map[item.pid].children = [item]
} else {
tree_map[item.pid].children.push(item)
}
} else {
tree.push(item)
}
})
return { tree, tree_map }
}
private sortTree(tree: Tree, tree_map: TreeMap) {
const target_tree = [] as Tree
const start_node = find(tree, (item) => !item.prev_id)
if (!start_node) return []
let current = start_node.id
while (current) {
const item = tree_map[current]
if (item?.children?.length) {
item.children = this.sortTree(item.children, tree_map)
}
target_tree.push(item)
current = item.next_id
}
return target_tree
}
private take(indexes: Array<number>, swap?: boolean) {
const { cloned_item, target_level, target_index } = this.getItem(indexes)
const effect_items = [] as Array<TreeItem>
if (cloned_item.prev_id) {
const prev_item = target_level[target_index - 1]
prev_item.next_id = cloned_item.next_id ?? ''
effect_items.push(toJS(prev_item))
}
if (cloned_item.next_id) {
const next_item = target_level[target_index + 1]
next_item.prev_id = cloned_item.prev_id ?? ''
if (swap) next_item.next_id = cloned_item.id
effect_items.push(toJS(next_item))
}
target_level.splice(target_index, 1)
return { cloned_item, effect_items }
}
private place(args: ArgsPlace) {
const { type, active_item, over_item, target_level, over_index } = args
const effect_items = [] as RawNodes
if (type === 'push') {
active_item.pid = over_item ? over_item.id : ''
if (target_level.length) {
const last_item = last(target_level)
last_item.next_id = active_item.id
active_item.prev_id = last_item.id
effect_items.push(toJS(last_item))
} else {
active_item.prev_id = undefined
}
active_item.next_id = undefined
effect_items.push(toJS(active_item))
target_level.push(active_item)
} else {
active_item.pid = over_item.pid
active_item.prev_id = over_item.id
active_item.next_id = over_item.next_id
if (over_item.next_id) {
const next_item = target_level[over_index + 1]
next_item.prev_id = active_item.id
effect_items.push(toJS(next_item))
} else {
active_item.next_id = undefined
}
if (active_item.next_id === over_item.id) {
over_item.next_id = active_item.next_id
} else {
over_item.next_id = active_item.id
}
effect_items.push(toJS(active_item))
effect_items.push(toJS(over_item))
target_level.splice(over_index + 1, 0, active_item)
}
return { active_item, effect_items }
}
private getUniqEffectItems(effect_items: Tree) {
return reduceRight(
effect_items,
(acc, curr) => {
if (!acc.some((item) => item['id'] === curr['id'])) {
acc.unshift(curr)
}
return acc
},
[] as Tree
)
}
private getIndexes(indexes: Array<number>) {
return flatMap(indexes, (value, index) => {
return index < indexes.length - 1 ? [value, 'children'] : [value]
})
}
private getherItems(tree: Tree) {
return tree.reduce((total, item) => {
total.push(item)
if (item?.children?.length) {
total.push(...this.getherItems(item.children))
}
return total
}, [] as Tree)
}
}
一些废话
这种图数据的抽象数据层在很多地方都能用到,有需要的同学,请拿去,不用谢我🤪。
生成树的函数时间复杂度比较高,getTree 和 sortTree 应该可以合并,无奈我太菜了,算法大佬可以指点小弟一二(当然,前提是能通过数据测试)。
前段时间因为公司没钱发工资从Yao App Engine这个项目里面出来了,然后离职了。说实话是有不甘心在的,毕竟那些基础设施你都搞好了,不用那么忙了,这个时候公司没钱了,似乎是一个很"巧"也是一个很"坏"的时间点(拖到了11月,这个月份以及后面的几个月都没法找工作),老板的意思是,你看你现在没多少活,我也没钱发工资,要不你别拿工资了,等后面做项目了,项目回款了,再分钱给你(你看我选到11月跟你说,你知道我的"诚意"了吧,你看我考虑的多"周到",为了能让这事运转下去)。
我看了看我的房租以及银行卡余额。
在杭州和很多人聊了一下,很多也还聊得还不错。但是现实是,你只能是一个p6小兵。
面试蚂蚁因为学历和无大厂经历被一叶姓p7在面试开始时开门见山"唾弃",30分钟走个过场。和字节小组leader聊了两个小时,一面时被p6小兵拿着前端八股问的不想开口(可能面试官只想听自己想听的答案),在大量沉默中宣告GG,最后简单的平衡二叉树算法也因为前面的情绪积累而没法心平气和地去写出来。
还有市值只有几个亿的有赞AI和支付负责人找到我,透漏他们想要借助AI重新打造商业闭环,重回"巅峰"(请恕我直白,有可能,但不会是有赞这种被抽干了血的企业)。
乌冬科技(阿里内保)的某位同学自己在做一个叫revezone的应用,BOSS上一个内推的人把我内推给了他,听我讲了两个小时YAO这个项目发展过程中遇到的问题,开源的一些经历,商业化过程中的阵痛,聊得还挺深入。
然后就溜了,都没搞招聘流程,这个有点小无语(你直说你想听听开源方面的经验即可,完全不必浪费大家时间用招人这个幌子)。
太多太多,面了两周,我不想被八股(尽管我背了,但哪背的完),我也不想刷leetcode(一题没刷,倒是研究了很多动态规划方面的东西)。
这注定我不是"面试"专家,我可能是低代码方面和设计系统的专家,但对面试,我是纯"菜",不然也不会一直都辗转于小公司。
即然面试能力不行,那我还是决定走我自己喜欢和擅长的路,做产品,做开源。再不济也能通过开源和产品证明自己的实力,找到一份不是大头兵的能够发挥自身实力的工作。
其实很早之前我写了一款桌面应用叫做IF,它是个Todo应用,给我自己用(主要是Microsoft Todo功能太简单,太难用了)。
而现在,我在写一个开源的低代码富文本编辑器&所见即所得markdown编辑器(开源版Typora),上述NodeTree就是富文本和应用内模块用到的底层技术。
我给我自己一年的时间,IF拥有不低于1w个用户(todo、富文本和markdown免费,会做付费模块,但一年内不考虑任何付费指标),用户增速不要太慢,那就算是第一阶段及格了,至于商业化,对于这种复杂度还蛮高的应用,我的计划是打磨两年,再花更多的精力去做商业化。
对了,之前还联系了飞猪的tw汤威,在推上认识的他,他说他有HC,能给我推,但因为各种机缘巧合,没能推上,我还是决定回家,我决定回老家开发产品的时候,我们聊了一下,我说,每当我没有力量,没有头绪时,我会去看乔布斯的各种访谈和演讲(我是一个孤独的人,而这些文字和话语有时候能触达到我的内心深处,让我充满力量),我说在我考虑是否回老家做产品时,乔布斯的话一直在我脑中回响:
"I'm pretty sure none of this would have happened if I hadn't been fired from Apple. It was awful tasting medicine, but I guess the patient needed it. Sometimes life hits you in the head with a brick. Don't lose faith. I'm convinced that the only thing that kept me going was that I loved what I did. You've got to find what you love."
我可以非常肯定,如果我不被Apple开除, 这其中任何一件事情都不会发生。这件事本身是一味非常苦的药,但是我猜病人需要它。有些时候, 生活会拿起一块砖头猛拍向你的脑袋。不要失去信心。我很清楚唯一使我一直走下去的,就是我无比钟爱我做的事情。你得去找到你所爱的东西。
"And that is as true for your work as it is for your lovers. Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what you believe is great work. And the only way to do great work is to love what you do. If you haven't found it yet, keep looking. Don't settle. As with all matters of the heart, you'll know when you find it. And, like any great relationship, it just gets better and better as the years roll on. "
对于工作是如此, 对于你的爱人亦然。你的工作将会占据生活中很大的一部分。让自己真正满意的唯一方式就是,只做那些你认为是杰出工作的事情。如果你还没有找到, 那么就继续找、不要停下来、全心全意的去找, 当你找到的时候你就会知道的。就像任何伟大的关系, 随着岁月的流逝只会越来越好。
"So keep looking until you find it. Don't settle."
所以继续找,直到你找到它,不要停下来。