源程序的相似性分析 —— 基于Python实现哈希表

一、问题描述

对于两个C++语言的源程序代码,用哈希表的方法分别统计两个程序中使用C++语言关键字的情况,并最终按定量的计算结果,得出两份程序的相似性。

二、需求分析

建立C++语言关键字的哈希表,统计在每个源程序中C++关键字出现的频度, 得到两个向量X1和X2,通过计算向量X1和X2的相对距离来判断两个源程序的相似性。

例如:

    关键字       Void  Int  For  Char  if  else  while  do  break  class 
程序1关键字频度    4    3      0    4    3    0    7      0    0      2 
程序2关键字频度    4    2      0    5    4    0    5      2    0      1 
         X1=[4,3,0,4,3,0,7,0,0,2] 
         X2=[4,2,0,5,4,0,5,2,0,1] 

设s是向量X1和X2的相对距离,s=sqrt(∑(x1[i]-x2[i])2),当X1=X2时,s=0, 反映出可能是同一个程序;s值越大,则两个程序的差别可能也越大。

测试数据
选择若干组编译和运行都无误的C++程序,程序之间有相近的和差别大的,用上述方法求s, 对比两个程序的相似性。

选作内容
建立源代码用户标识符表,比较两个源代码用户标识符出现的频度,综合关键字频度和用户标识符频度判断两个程序的相似性。

三、概要设计

为了实现上述功能,需要使用哈希表,Python中已经有基于哈希表实现的字典数据结构,可以直接使用,但是为了锻炼自己的编程能力,还是决定手动实现一个哈希表。

哈希表是根据关键字直接进行访问的数据结构,是一组元素的集合,元素的存储方式使我们可以在O(1)的时间内找到它们。哈希表的每个位置(通常称为槽)可以容纳一个元素,通过哈希函数建立了关键字和存储地址之间的一种直接映射关系,插入数据时,如果为空则直接插入,发生哈希碰撞时,如果key相等,并且key所对应的位置上有数据,则直接进行替换,如果key不相等,则利用线性探测的开放寻址法,找到下一个槽位并插入。

为了更加深刻的理解CPython字典对象的内部构造和实现,我去翻阅了CPython的内核源码,并打算以不同版本的CPython字典对象的C文件为测试数据,计算并分析其差异。

在Python3.6之前的版本,针对以稀疏的哈希表存储的字典对象是非常不友好的,如下图是一个指向Python内部字典对象的入口指针:
在这里插入图片描述

基于这种形式,如果应用场景下有较多的字典对象时,会浪费很多的内存空间,为了用更紧凑的方式来实现哈希表,Python3.6以后的版本采用了将哈希索引和真正的键值对分开存放的方式:
在这里插入图片描述
这么做的好处是空间利用率得到了较大的提升,以64位操作系统为例,每个指针的长度为8个字节,原本需要838=192个字节,现在变成了833+1*8=80个字节,节省了58%左右的内存空间。

本次实验以Python 2.7、3.6、3.7三个版本的dictobject.c文件为输入,处理文件过程包括筛除注释和空行等,然后以正则表达式切分单词并做词频统计,之后再根据词频向量计算不同文件之间的欧氏距离和余弦相似度,最后输出实验结果。

四、详细设计

1.哈希表

Python中的字典是一种关联数据类型,可以在其中存储键-值数据,通过键可以查找关联的数据值,这个想法也被称为map,抽象数据类型的定义如下:

  • Map()创建一个新的空映射。它返回一个空的映射集合。
  • put(key,val)向映射添加新的键值对。如果键已经在映射中,则用新值替换旧值。
  • get(key)给定一个键,返回存储在映射中的值,否则返回None
  • del使用del map[key]格式的语句从映射中删除键值对。
  • len()返回存储在映射中的键值对的数目。
  • in如果给定键在映射中,则返回True,否则返回False。

为了减少哈希冲突,哈希表的长度最好选择质数。假设哈希表的长度设置为15,则3的倍数或5的倍数的整数将分别散列为3和5的索引,换句话说,每个与长度共享一个公因子的整数都将被散列到该因子倍数的索引中。对于非随机数据,素数长度的哈希表将产生最广泛的整数分布到索引。因此,选择将哈希表的长度设置为质数将大大减少冲突的发生。

2.读取+处理文件

测试文件为Python 2.7、3.6、3.7三个版本的dictobject.c文件,使用Python读取文件比较简单,复杂的是对文件内容的处理,正规的C语言文件包含大量的注释,因此首先就要过滤到文件中的注释。

C语言文件的注释包括单行注释、多行注释和行内注释,并且还要去除空行。首先使用open()打开文件并返回一个file object,针对每一行单独进行处理。如果当前行是空行、以//开头或者以/*开头以*/结尾,则直接跳过。对于多行注释的判断可以借助于一个标志位isNotes,如果isNotes为False并且当前行以/*开头,则说明当前行是多行注释的开始行,设置isNotes为True,直到当前行以*/结尾,说明当前行是多行注释的结束行,设置isNotes为False。经过以上两次过滤之后,接下来要处理的就是行内注释,可以针对行内的///*进行索引和切片去除行内注释。

接下来具体处理C语言程序的每一行代码,首先通过正则表达式pattern = r'[\s,\.?;!:"]+'切分单词,切分符包括空白符号、英文逗号、英文句号、英文问号、英文感叹号、英文冒号、英文分号,最后的加号表示如果这些符号是连续挨着的则当成一个分隔符切分,切分后针对每个单词调用get函数获取值,然后在哈希表中查找是否存在该单词是否为C语言关键字,如果存在则该关键字则对应的频度加1。

3.计算欧式距离和余弦相似度

欧式距离也称欧几里得距离,是最常见的距离度量,衡量的是多维空间中两个点之间的绝对距离。也可以理解为:m维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。在二维和三维空间中的欧氏距离就是两点之间的实际距离,公式为:s=sqrt(∑(x1[i]-x2[i])2)。

对于如何计算两个向量的相似程度问题,可以把它们想象成空间中的两个线段,都是从原点(0, 0, …)出发,指向不同的方向,两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着方向形成直角;如果夹角为180度,意味着方向正好相反。因此,可以通过夹角的大小来判断向量的相似程度,夹角越小,就代表越相似。

两个线段A和B的夹角θ的余弦:
在这里插入图片描述
余弦值的范围在[-1,1]之间,值越趋近于1,代表两个向量的方向越接近;越趋近于-1,他们的方向越相反;接近于0,表示两个向量近乎于正交。一般情况下,相似度都是归一化到[0,1]区间内,因此余弦相似度表示为cosineSIM = 0.5cosθ + 0.5

五、编码实现

1.哈希表

我使用两个列表来创建一个实现映射抽象数据类型的哈希表类,一个名为slot的列表保存密钥,另一个名为data的并行列表保存数据值。

class HashTable:
    def __init__(self, size):
        self.size = size
        self.slot = [None] * self.size
        self.data = [None] * self.size

哈希函数实现了简单的余数方法,为了处理基于字符的元素(字符串),取其每个字母的序数值的和进行取余,通过线性探测再哈希函数解决碰撞问题。

    def put(self, key, data):
        hashValue = self.hashFunction(key, len(self.slot))
        # 如果slot内是empty,就存进去
        if self.slot[hashValue] is None:
            self.slot[hashValue] = key
            self.data[hashValue] = data
        else:
            # 如果已有值等于key,更新data
            if self.slot[hashValue] == key:
                self.data[hashValue] = data
            else:
                # 如果slot不等于key,找下一个为None的地方
                nextSlot = self.rehash(hashValue, len(self.slot))
                while self.slot[nextSlot] is not None and self.slot[nextSlot] != key:
                    nextSlot = self.rehash(nextSlot, len(self.slot))
                if self.slot[nextSlot] is None:
                    self.slot[nextSlot] = key
                    self.data[nextSlot] = data
                else:
                    self.data[nextSlot] = data

    @staticmethod
    def hashFunction(key, size):
        return sum([ord(ch) for ch in key]) % size

    @staticmethod
    def rehash(oldHash, size):
        return (oldHash + 3) % size

同样,get函数首先计算初始哈希值,如果该值不在初始槽中,则使用rehash来定位下一个可能的位置。

    def get(self, key):
        startSlot = self.hashFunction(key, len(self.slot))
        data, stop, found, position = None, False, False, startSlot
        while self.slot[position] is not None and not found and not stop:
            if self.slot[position] == key:
                found = True
                data = self.data[position]
            else:
                position = self.rehash(position, len(self.slot))
                # 如果回到开始位置说明没找到
                if position == startSlot:
                    stop = True
        return data

HashTable类提供了额外的字典功能,通过重载__getitem__和__setitem__方法以允许使用[]进行访问,这意味着创建的哈希表可以通过熟悉的索引操作符使用。

    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, data):
        self.put(key, data)

2.读取+处理文件

读取三个版本的dictobject.c文件并过滤空行和注释。

def readFile(path):
    file, isNotes = [], False
    with open(path, "r") as fp:
        for line in fp.readlines():
            line = line.strip()
            # 过滤C语言文件中的空行、单行注释、多行注释和行内注释
            if isNotes and line.endswith("*/"):
                isNotes = False
                continue
            if not line or line.startswith("//") or (line.startswith("/*") and line.endswith("*/")) or isNotes:
                continue
            if not isNotes and line.startswith("/*"):
                isNotes = True
                continue
            noteIndex = line.find("//") if line.find("/*") == -1 else line.find("/*")
            if noteIndex != -1:
                line = line[:noteIndex]

            file.append(line)
    return file


def readFiles():
    return readFile("dictobject-2.7.c"), readFile("dictobject-3.6.c"), readFile("dictobject-3.7.c")
    
file1, file2, file3 = readFiles()

通过正则表达式处理文件并统计关键字词频。

def processFile(file, table: HashTable):
    for line in file:
        pattern = r'[\s,\.?;!:"]+'
        words = re.split(pattern, line)
        for word in words:
            if table.get(word) is not None:
                table.put(word, table.get(word) + 1)

3.计算欧式距离和余弦相似度

欧几里得距离计算公式:s=sqrt(∑(x1[i]-x2[i])2)。

def computeEuclideanDistance(array1, array2):
    return math.sqrt(sum([(array1[i] - array2[i]) ** 2 for i in range(len(array1))]))

根据余弦定理计算余弦相似度:

def computeCosineSimilarity(x, y):
    zeroList = [0] * len(x)
    if x == zeroList or y == zeroList:
        return float(1) if x == y else float(0)

    dotProduct, squareSumX, squareSumY = 0, 0, 0
    for i in range(len(x)):
        dotProduct += x[i] * y[i]
        squareSumX += x[i] * x[i]
        squareSumY += y[i] * y[i]
    cos = dotProduct / (math.sqrt(squareSumX) * math.sqrt(squareSumY))

    return 0.5 * cos + 0.5

六、实验结果与分析

+----------+-----------------------+-----------------------+-----------------------+
| Keyword  | File 1 Word Frequency | File 2 Word Frequency | File 3 Word Frequency |
+----------+-----------------------+-----------------------+-----------------------+
|   auto   |           0           |           0           |           0           |
|  static  |           97          |          101          |          102          |
| register |           54          |           1           |           1           |
|  extern  |           3           |           0           |           0           |
|   char   |           4           |           4           |           4           |
|  double  |           0           |           0           |           0           |
|   enum   |           0           |           0           |           0           |
|  float   |           0           |           0           |           0           |
|   int    |           56          |           68          |           68          |
|   long   |           24          |           0           |           0           |
|  short   |           0           |           0           |           0           |
|  signed  |           0           |           0           |           0           |
|  struct  |           2           |           4           |           4           |
|  union   |           0           |           0           |           0           |
| unsigned |           0           |           0           |           0           |
|   void   |           14          |           16          |           15          |
|   for    |           23          |           39          |           40          |
|    do    |           3           |           1           |           1           |
|  while   |           13          |           19          |           16          |
|  break   |           8           |           9           |           9           |
| continue |           4           |           5           |           3           |
|    if    |          293          |          420          |          385          |
|   else   |           36          |           67          |           64          |
|   goto   |           33          |           32          |           31          |
|  switch  |           0           |           0           |           0           |
|   case   |           7           |           7           |           7           |
| default  |           0           |           0           |           0           |
|  const   |           3           |           5           |           6           |
|  sizeof  |           0           |           0           |           0           |
| typedef  |           2           |           1           |           1           |
| volatile |           0           |           0           |           0           |
|  return  |          246          |          321          |          307          |
+----------+-----------------------+-----------------------+-----------------------+
dictobject-2.7.c和dictobject-3.6.c的欧式距离:163.03680566056244,余弦相似度为:0.9929622255286137
dictobject-3.6.c和dictobject-3.7.c的欧式距离: 38.05259518088089,余弦相似度为:0.9998047476290259
dictobject-2.7.c和dictobject-3.7.c的欧式距离:129.82680770934792,余弦相似度为:0.9936477207981805

Process finished with exit code 0

在这里插入图片描述
分析:实验结果显示,虽然由于文件比较大,导致欧氏距离和余弦相似度比较大,Python2.7版本的dictobject.c文件和Python3.6版本的dictobject.c文件欧氏距离最大,相似度也相对较小,Python3.6版本的dictobject.c文件和Python3.7版本的dictobject.c文件欧氏距离最小,相似度也最大。实际情况下,Python2.7和Python3.6差了一个大版本,而Python3.6和Python3.7版本差了一个小版本,符合实验结果。

七、总结

哈希表,就是一种结构数据,它以牺牲空间为代价节约时间,哈希表的运用感觉还是挺简单的,不过对于具体的题目,以什么值为下标,储存其值,如何去查找还需要思考,如果单方面的看哈希表是简单的,就是在于如何放在题目中运用。

总结下来,哈希散列的两大问题:一个就是散列策略,另一个就是解决冲突。而且不难发现,哈希表的空间越大,冲突的频率就越低,这样时间复杂度就越接近O(1)。所以我们经常将哈希表的大小稍微设置得大一些,用空间来换时间也是挺值得的。

通过本次课程设计,让我更进一步的体会到了数据结构与算法的重要性,学会从问题入手,分析和研究计算机加工的数据结构特性,为应用数据选择适当的逻辑结构、存储结构及其相应的操作算法,并初步掌握算法的性能分析技术。

Alex 007 CSDN认证博客专家 机器学习 NLP TensorFlow
我是 Alex 007,一个热爱计算机编程和硬件设计的小白。
为啥是007呢?因为叫 Alex 的人太多了,再加上每天007的生活,Alex 007就诞生了。
如果你喜欢我的文章的话,给个三连吧!
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值