该程序搜索算法是我最近写的软件中使用到的算法,软件的项目地址如下:https://github.com/ghost-him/QuickLaunch/。建议打开源码,找到对应的代码后再阅读本文章。
该算法已经应用在软件中,并且取得了令我自己很满意的效果。
前言结束,以下是正文:
程序搜索算法介绍
该程序使用了融合算法来实现查找目标应用程序。搜索算法的实现位于model
文件夹下的database
类中。可以对着源码来查看本文档。
该搜索算法的特点是:
- 可以用极少的字符匹配超长字符
- 支持拼音(全拼与首字母拼音)搜索
- 支持模糊查找
算法的总体流程
先对算法的运行流程熟悉后再对具体实现有所了解。
- 程序在初始化时,会根据配置读取电脑上安装的程序,并向
database
类中添加对应的记录。(通过insertProgramInfo
函数添加)。 - 用户在按下
alt + space
后,会弹出来搜索栏,每当用户向其中输入一个字符(或删除一个字符),则会调用一次database
类中的updateProgramInfo
函数,而传入的参数则是当前搜索栏中的字符。updateProgramInfo
会计算当前用户输入的字符串与存储的所有的记录的匹配度(programLevel
)- 根据匹配度对所有的内容进行从小到大的排序
uiController
会选择指定的前几个记录作为显示项,将其添加到QListWidget
中,显示给用户。
从以上的流程可以知道:
- 每当用户输入一个字符,程序都会运行一次搜索算法
- 算法的核心是根据用户输入的字符串计算每一个
programNode
中programLevel
的值 - 显示出来的匹配项为所有的
programNode
中programLevel
最小的几个。
数据结构定义
对于一个程序,使用ProgramNode
来维护,该结构体的定义如下:
struct ProgramNode {std::wstring programName;std::wstring compareName;std::wstring pinyinName;std::wstring firstLatterName;std::wstring programPath;double programLevel;int stableLevel;int launchTime;const bool operator<(const ProgramNode& other) const {if (programLevel != other.programLevel) {return programLevel < other.programLevel;} else if (launchTime != other.launchTime) {return launchTime > other.launchTime;} else {return compareName < other.compareName;}}
};
以下是字段的解释:
programName
:该字段用于在界面中显示程序的名字,不参与搜索算法。compareName
:该字段用于精确匹配程序的名字。在插入数据时,会先进行预处理:- 将大字字母全部转换为小写字母(
OneNote
->onenote
) - 将括号内的内容全部去除(
qt creator 13.0.2 (community)
->qt creator 13.0.2
)
- 将大字字母全部转换为小写字母(
pinyinName
:该字段用于存储中文名字的程序的拼音(网易云音乐
->wang yi yun yin le
,因乐
字存在多音字的情况,所以无法正确匹配拼音)firstLatterName
:该字段用于存储中文名字的程序的拼音首字母(网易云音乐
->wyyyl
)programPath
:该字段用于存储该程序在磁盘中的位置programLevel
:该字段用于存储该程序的总权重stableLevel
:该字段用于存储程序的固定权重launchTime
:该字段用于存储程序的启动次数
具体流程
programLevel
的值取决于以下三个值:directValue
,pinyinValue
,firstLatterValue
,其意思分别是:精确匹配的匹配值,使用拼音匹配的匹配值,使用拼音首字母匹配的匹配值。而programLevel
的值取这三个中最小的那个。
至于三个值的计算均由computeCombinedValue
函数完成,该函数也是程序搜索算法的核心。
该函数有两个参数:storeName
与inputName
,storeName
表示存储的程序的名字,而inputName
表示用户输入的程序的名字。融合算法由以下三种子算法构成:
- 最短编辑距离变体
- KMP算法变体
- 最长公共子序列变体
除了以上的子算法,还有其他的条件来确保匹配:
- 判断输入的长度是否大于程序的名字的长度(精确名字与拼音名字的最大值),如果已经大于程序的长度了,则一定不是该程序(在运行时,如果不设置该条件,则会存在无法匹配到拥有长程序名的程序)。
最短编辑距离变体
最短编辑距离变体算法由editSubstrDistance
函数实现,左边为内存中存储的字符串,而右边为用户输入的字符串。
该函数的计算流程是:当用户输入的字符串中,最少经过[添加一个字符|删除一个字符|修改一个字符]几次后可以变为左边的子字符串。并且计算修改的次数占总输入数据长度的比值 x x x(通过除法,将值映射为[0-1]区间内)。并使用 y ( x ) = 25 ∗ 3 3 ∗ x − 2 y(x)=25*3^{3*x-2} y(x)=25∗33∗x−2函数将其映射到 [ 2.7 , 75 ] [2.7, 75] [2.7,75]之间。(该函数没有特别设计,关键是一个指数函数,且 y ( x ) y(x) y(x)足够小, y ( 1 ) y(1) y(1)足够大,凹函数)
至于为什么这样设计,可以这样理解:如果我输入了5个字符,比如steam
,但是这5个字符,需要修改4次,才可以成为deepl
的一个子字符串,那么有极大的概率查找的不是这个程序。相反,我输入的这5个字符无须修改就可以成为steam
的一个子字符串,那么说明我查找的程序很有可能就是这个程序。综上所述,steam
的programLevel
会比deepl
的programLevel
小。
以下是计算的过程,输入的字符串为steam
- 对于程序
steam
来说:修改0次(steam
->steam
),则比值min_operations
为 0 / 5 = 0 0/5=0 0/5=0。再经过函数的映射,可以得到directValue
为2.77
。 - 对于程序
deepl
来说:修改4次(steam
->deepl
),则比值min_operations
为 4 / 5 = 0.8 4/5=0.8 4/5=0.8。再经过函数的映射,可以得到directValue
为38.79
为什么要使用指数函数将其扩大?
- 为了方便后续的处理。当输入
steam
时,可能会匹配其他的选择,比如steam support center
。此时,如果单纯的使用该算法作为判断依据,则无法处理这个情况(两者的值一样)
KMP算法变体
KMP算法变体由kmp
函数实现,其具体的处理过程如下:
- 如果输入的字符串是一个程序的子字符串,则返回其输入长度的负值。
- 如果输入的字符串不但是程序的子字符串,同时程序还是以该字符串开头的,则返回2倍的负的输入长度
通过该算法可以实现:如果我输入command
,那么command prompt
的值会比developer command prompt for vs 2022
小,可以实现前者排在后者的上面,优先被选中。
注意:该函数返回的值是一个负值。相加时可以减少programLevel
的值,从而提高匹配程序的排名。
最长公共子序列变体
最长公共子序列算法求的是输入字符串与目标字符串的最长的公共子序列的长度。比如:
text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
以上数据来源:力扣
在本程序中,使用LCS
函数实现。当该函数计算完子序列的长度previous[n]
时,会计算该子序列相对于输入字符串与目标字符串的差值。即:m + n - 2 * previous[n]
。这样可以防止输入steam
时,steam
与steam support center
的值一样,可以优先匹配长度更短的字符串。同时,使用该算法可以弥补最短编辑距离变体的缺点。由于该算法对于结果的影响较大(比如以上例子,匹配steam support center
时,该函数运行的结果为15
,会对结果造成很大的影响,可能排在第二位的选项不是该程序而是其他完全不相关的程序),所以在融合算法中,差其权重设置为了0.2
,从而来减小影响。