用数据驱动Leetcode刷题效率

写在前面的话

在这里插入图片描述
相信所有经常使用CSDN的读者也都对Leetcode不陌生,Leetcode上汇集了各种关于算法和数据结构的题目,系统性的学习Leetcode上题目的解题思路有助于熟悉各种数据结构和培养编程思维。截至目前Leetcode上已经有1842道题相信随着时间的流逝这个数目会只增不减,那到底怎么以一种最有效率的方式来刷Leetcode呢?读完本文后你可能也会有自己的答案。

数据预处理

本次分析使用的原始数据中包含了1959道Leetcode题目,数据集如下图所示:
在这里插入图片描述
这个数据集提供了相当全面的数据,但还有一项数据是我比较关心但原数据集并没有提供的,那就是每道题的标签。在我自己刷题时每道题的标签对我产生思路有很重要的作用而且标签也表明了每道题包含的数据结构或算法知识点。为了获取每道题的标签我找到了Github上一个爬取Leetcode题目的开源项目(https://github.com/gcyml/leetcode-crawler)并针对我的应用做了相应的修改。
首先是本次分析需要用的所有库:

# imports
import pandas as pd
import json
import re
import matplotlib.pyplot as plt
import numpy as np
from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator
import itertools
from collections import Counter
from collections import OrderedDict
from mlxtend.frequent_patterns import apriori, association_rules
from sklearn.preprocessing import MultiLabelBinarizer
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

下面就是爬取每道题标签的自定义函数:

# 读取原始数据集
leetcode_data=pd.read_csv('leetcode.csv')
user_agent = r'Mozilla/5.0 (X11; Windows x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'
# 获取每个问题的标签
def get_tags(problem_url):
    # 正则表达式从url里提取问题标题
    problem_title=re.findall(r'[^/]+(?=/$|$)',problem_url)[0]
    session = requests.Session()
    headers = {'User-Agent': user_agent, 'Connection': 
        'keep-alive', 'Content-Type': 'application/json',
        'Referer': 'https://leetcode.com/problems/'+problem_title}

    url = "https://leetcode.com/graphql"
    params = {'operationName': "getQuestionDetail",
        'variables': {'titleSlug': problem_title},
        'query': '''query getQuestionDetail($titleSlug: String!) {
            question(titleSlug: $titleSlug) {
                questionId
                questionFrontendId
                questionTitle
                questionTitleSlug
                content
                difficulty
                stats
                similarQuestions
                categoryTitle
                topicTags {
                name
                slug
            }
        }
    }'''
    }

    json_data = json.dumps(params).encode('utf8')
        
    question_detail = ()
    resp = session.post(url, data = json_data, headers = headers, timeout = 3)
    content = resp.json()
    questionId = content['data']['question']['questionId']
    tags = []
    for tag in content['data']['question']['topicTags']:
        tags.append(tag['name'])
    return tags
# 储存爬取的标签
problem_tags=[]
for url in leetcode_data['link']:
    tags=get_tags(url)
    problem_tags.append(tags)
# 把爬取的标签加入数据集
leetcode_data['tags']=problem_tags

现在的数据集中就包含了每道题对应的标签:
在这里插入图片描述
因为数据集中没有其他缺失值(除了video这一项,实际分析中没有用到该特征)所以不需要对数据集进行其他处理。

探索性数据分析

作为一个Leetcode的新用户我经常面对庞大的题库无从下手不知道是该正序刷题,倒序刷题还是随机刷题,那么我们来看看大家是怎么做的。下图显示了每道题的提交次数和通过次数,从图中可以清楚的看到题号越靠前的题目提交次数和通过次数越多,而到了750道题后面提交次数和通过次数几乎一样,说明大部分Leetcode的新用户或者对编程不是很熟悉的用户都是从前往后刷题的。通过后面的题目提交和通过次数相近可以看出后面的题目尝试的人数很少,而大部分用户在刷到250题左右的时候就不再使用Leetcode了。

# 题目顺序
question_order=leetcode_data['question_id'].values
# 总提交次数
num_submitted=leetcode_data['total Submitted'].values
# 总接受次数
num_accepted=leetcode_data['total Accepted'].values
plt.figure(figsize=(8,5))
# 题目顺序和总提交次数
plt.plot(question_order,num_submitted,label='Total Submitted')
# 题目顺序和总接受次数
plt.plot(question_order,num_accepted,label='Total Accepted')
plt.legend()
plt.xlabel('Question ID')
plt.ylabel('Number of times')

在这里插入图片描述
接下来我们来看看不同难度的题目通过率怎么样。下面显示了整体和不同难易程度的题目的通过率,从图中我们可以发现简单题通过率最高,中等题其次,困难题最后,这很符合常理。而且中等难度的题目的通过率分布和整体的通过率分布及其相似,是不是因为中等难度的题目最多呢?

# 整体接受率
accepted_rate=num_accepted/num_submitted
# 容易题目接受率
easy_num_accepted=leetcode_data[leetcode_data['difficulty']==1]['total Accepted']
easy_num_submitted=leetcode_data[leetcode_data['difficulty']==1]['total Submitted']
easy_accepted_rate=easy_num_accepted/easy_num_submitted
# 中等题目接受率
medium_num_accepted=leetcode_data[leetcode_data['difficulty']==2]['total Accepted']
medium_num_submitted=leetcode_data[leetcode_data['difficulty']==2]['total Submitted']
medium_accepted_rate=medium_num_accepted/medium_num_submitted
# 困难题目接受率
hard_num_accepted=leetcode_data[leetcode_data['difficulty']==3]['total Accepted']
hard_num_submitted=leetcode_data[leetcode_data['difficulty']==3]['total Submitted']
hard_accepted_rate=hard_num_accepted/hard_num_submitted
# 不同程度题目接受率
fig,ax=plt.subplots()
box_plot=ax.boxplot([accepted_rate,easy_accepted_rate,medium_accepted_rate,hard_accepted_rate],notch=True,patch_artist = True)
colors = ['gray', 'green', 
          'blue', 'red']
for patch, color in zip(box_plot['boxes'], colors):
    patch.set_facecolor(color)
ax.set_xticklabels(['Overall','Easy','Medium','Hard'])
ax.set_xlabel('Question Difficulty')
ax.set_ylabel('Acceptance Rate')

在这里插入图片描述
下面的饼图告诉了我们之前的猜测是正确的,在所有题目中中等难度的题目占了一半还多,所以中等难度的题目最能代表整体的水平。

# 不同程度题目数量
easy_count=sum(leetcode_data['difficulty']==1)
medium_count=sum(leetcode_data['difficulty']==2)
hard_count=sum(leetcode_data['difficulty']==3)
# 不同程度题目数量
plt.figure(figsize=(8,5))
difficulty_labels=['Easy','Medium','Hard']
explode=[0.1,0.1,0.1]
plt.pie([easy_count,medium_count,hard_count],labels=difficulty_labels,autopct='%1.1f%%',explode=explode,shadow=True)
plt.title('Questions with different level of difficulties')

在这里插入图片描述
接下来我们再来看看所有的题目中需要付费订阅才能解锁的题目占了多少。从下图可以看出付费解锁题目只占了总题目的不到16%,也就是说即使不付费还是依然能够学习大部分的题目的,因为Leetcode大部分的用户都是学生,所以这一点还是对用户很友好的。

# 付费与免费题目数量
paid_count=sum(leetcode_data['isPaid']==True)
free_count=sum(leetcode_data['isPaid']==False)
# 付费与免费题目数量
plt.figure(figsize=(8,5))
paid_labels=['Paid','Free']
explode=[0.1,0.1]
plt.pie([paid_count,free_count],labels=paid_labels,autopct='%1.1f%%',explode=explode,shadow=True)
plt.title('Percentage of free and paid questions')

在这里插入图片描述
从之前的分析我们知道大部分的题目都是中等难度,那付费的题目中题目难度是怎么分布的呢?从下图可以看出来,付费题目在各个难度的题目中分布还是很均匀的,这也可能是Leetcode的开发者们有意为之,可以说是对用户们非常体贴了。

# 计算不同难度付费与免费问题数量
paid_count_with_difficulty=[]
for d in [1,2,3]:
    for flag in [False,True]:
        index=np.where((leetcode_data['difficulty']==d) & (leetcode_data['isPaid']==flag))[0]
        paid_count_with_difficulty.append(len(index))
a, b, c=[plt.cm.Blues, plt.cm.Reds, plt.cm.Greens]

# 外圈题目难易程度
fig, ax = plt.subplots()
ax.axis('equal')
mypie, _ = ax.pie([easy_count,medium_count,hard_count], radius=1.3, labels=difficulty_labels, colors=[a(0.7), b(0.7), c(0.7)] )
plt.setp( mypie, width=0.3, edgecolor='white')
 
# 内圈付费或免费
mypie2, _ = ax.pie(paid_count_with_difficulty, radius=1.3-0.3, labels=['Free','Paid','Free','Paid','Free','Paid'], labeldistance=0.7, colors=[a(0.6), a(0.3),  b(0.6), b(0.3), c(0.6), c(0.3)])
plt.setp( mypie2, width=0.4, edgecolor='white')
plt.margins(0,0)

在这里插入图片描述
接下来我们来看看题目的标题们有什么特点。从下面的词云中可以看出,Leetcode中最常出现的题目就是关于字符串,数字,数组,求和,最大,最小,计数等,通过这些最常见的数据结构和操作来考察不同的算法的用法。

# 标题
plt.figure(figsize=(12,12))
titles_text=' '.join(leetcode_data['title']).lower()
stopwords=set(STOPWORDS)
wordcloud = WordCloud(width=500,height=300,stopwords=stopwords, background_color="white").generate(titles_text)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")

在这里插入图片描述
接下来就是我们爬取的各个题目的标签。下图展示了每个标签出现的次数,如果下图绘制的结果完全符合各位读者平时刷题的经验,说明你刷题的数量已经非常庞大了。

# 合并所有标签
tags=[]
for t in leetcode_data['tags']:
    tags+=t
# 计算每个标签出现次数
tag_count=Counter(tags)
# 排序
tag_count=OrderedDict(sorted(tag_count.items(), key=lambda x: x[1]))
# 条形图
plt.figure(figsize=(8,12))
y_pos=np.arange(len(tag_count.values()))
plt.barh(y_pos,tag_count.values())
plt.yticks(y_pos,list(tag_count.keys()))
plt.xlabel('Count')
plt.title('Number of times each tag appears')
plt.tight_layout()
plt.savefig('Tag count.jpg')

在这里插入图片描述

数据建模

在这部分我会使用一个Apriori模型和一个决策树来模型来从不同的方面分析Leetcode中题目的特点。
关联规则
关联规则是用来挖掘不同个体之间潜在关系的一个常用算法,而Apriori可能是关联规则最简单也是最直观的应用。在Apriori算法中最关键的三个概念分别是:support, confidence和lift。我们可以把我们要寻找关联规则的个体看成我们购物清单上的一个个商品,对于本次分析使用的数据集,购物清单上的商品就是每个题目的标签,而一个题目就是一张购物小票。简单来说,support表示在所有的购物小票中每个商品出现的概率,confidence表示在所有包含商品A的购物小票中商品B也出现在购物小票的概率,而lift则表示如果商品A出现在购物小票上那么商品B更有可能出现的概率。通过Apriori算法我们可以从统计学的角度来分析不同个体之间的关系。在本次分析中,我使用的阈值是confidence=0.2。通过返回的结果我们可以观察到树结构多和递归,深度优先搜索和广度优先搜索同时出现,数组则多和双指针,排序,二分查找同时出现。也许通过关联规则的挖掘以后看到不同数据结构的题目是可以更快的锁定使用的算法。

# 提取独立标签
unique_tags=list(tag_count.keys())
# 构建一个mxn矩阵,m为题目数量,n为标签数量
tag_matrix=np.zeros((leetcode_data.shape[0],len(unique_tags)))
# 遍历每个题目的标签
# 在对应的标签下填充1
for i in range(leetcode_data.shape[0]):
    tag=leetcode_data['tags'][i]
    for t in tag:
        # 找到标签对应的index
        index=unique_tags.index(t)
        # 填充1
        tag_matrix[i][index]=1
# 构建一个dataframe
tag_df=pd.DataFrame(tag_matrix,columns=unique_tags)
# 频繁项集
freq_items = apriori(tag_df, min_support=0.01,use_colnames=True)
# 关联规则
rules = association_rules(freq_items, metric="confidence", min_threshold=0.2)
rules

在这里插入图片描述
下面我们使用决策树分类模型来看看题目本身能不能对我们锁定对应的算法有所帮助。这里我自定义了一个函数,每次传入一个标签就可以返回训练出的决策树在测试集上的准确率和决策树中最重要的十个特征(单词),这样我们就可以看出题目中的哪些词有助于我们更快的解题。

# 转为小写
titles=leetcode_data['title'].str.lower()
# 去掉特殊字符
titles=[re.sub('[^A-Za-z0-9-]+', ' ', title) for title in titles]
# NLTK停用词
my_stopwords=stopwords.words('english')
# 使用TF-IDF向量化标题
vectorizer=TfidfVectorizer(max_features=1000,stop_words=my_stopwords)
tf_idf_titles=vectorizer.fit_transform(titles)
def dt_tag_classifier(tag_string):
    # 所有带标签的题目目标值为1其余为0
    target=[]
    for tag in leetcode_data['tags']:
        if tag_string in tag:
            target.append(1)
        else:
            target.append(0)
    # 分割训练集和测试集
    X_train,X_test,y_train,y_test=train_test_split(tf_idf_titles,target,test_size=0.2)
    # 实例化决策树
    dt=DecisionTreeClassifier()
    # 训练决策树
    dt.fit(X_train,y_train)
    # 对测试集数据做预测
    prediction=dt.predict(X_test)
    # 准确率
    accuracy=accuracy_score(y_test,prediction)
    print('Accuracy score on test set: {}'.format(round(accuracy,2)))
    # 提取模型特征重要性
    feature_importance=dt.feature_importances_
    # 最重要的十个特征的索引值
    top_10_features=np.argsort(feature_importance)[::-1][:10]
    # 提取对应特征和重要性
    features=[]
    importance=[]
    for index in top_10_features:
        features.append(vectorizer.get_feature_names()[index])
        importance.append(feature_importance[index])
    # 绘制条形图输出结果
    plt.bar(height=importance,x=features)
    plt.xticks(rotation=45,ha='right')
    plt.ylabel('Feature Importance')
    plt.title('Top 10 Important Words for {}'.format(tag_string))

下面是对不同算法的结果,希望大家可以从结果中找到自己今后解题的思路。

# 动态规划
dt_tag_classifier('Dynamic Programming')

在这里插入图片描述

# 深度优先搜索
dt_tag_classifier('Depth-first Search')

在这里插入图片描述

# 广度优先搜索
dt_tag_classifier('Breadth-first Search')

在这里插入图片描述

# 递归
dt_tag_classifier('Recursion')

在这里插入图片描述

# 二分查找
dt_tag_classifier('Binary Search')

在这里插入图片描述

# 双指针
dt_tag_classifier('Two Pointers')

在这里插入图片描述

写在最后的话

作者处于数据分析及机器学习初学阶段,本文中难免有错误或纰漏,希望各位读者可以及时指出

希望各位可以对本篇提出宝贵意见

转载请注明出处

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值