Loading......

文章背景图

✍️ 2026-01-14 | 算法 4 道 + SQL 3 道

2026-01-14
8
-
- 分钟

一、今日任务完成情况

算法新题:4 道

  1. 直接插入排序

  2. 希尔排序

  3. 冒泡排序

  4. 快速排序

SQL 新题:3 道

  1. 计算用户的平均次日留存率

  2. 计算每天用户的次日留存率

  3. 每个月 Top3 的周杰伦歌曲

二、今日收获 & 明日计划

今日收获

  1. 掌握 4 种经典排序算法的核心逻辑、适用场景和复杂度分析,能区分稳定 / 不稳定排序;

  2. 吃透留存率计算的核心逻辑,理解左连接和 DISTINCT 在结果准确性中的作用;

  3. 掌握 SQL TopN 问题的通用解法,明确窗口函数的使用规则和 SQL 执行顺序;

  4. 能识别 SQL 逻辑漏洞(如分母错误、日期缺失)并给出针对性改进方案。

三、解题思路

1. 算法题目

1.1 直接插入排序

题目说明:实现直接插入排序,核心逻辑为遍历数组时,将当前元素与前面已排序的元素逐一比较,找到对应位置插入,使已排序区间始终保持有序。

解题思路

  1. 复制原数组避免修改原始数据,获取数组长度;

  2. 遍历数组每个元素,暂存当前待插入元素;

  3. 向前遍历已排序区间,若已排序元素大于当前元素,则将该元素向后移动;

  4. 找到合适位置(j+1)后插入当前元素,完成一次插入操作;

  5. 遍历完成后返回排序后的数组。

def insert_sort(arr):
    arr_sorted = arr.copy()
    n = len(arr_sorted)
    for i in range(n):
        current = arr_sorted[i]
        j = i-1
        while j >= 0 and arr_sorted[j] > current:
            arr_sorted[j+1] = arr_sorted[j]
            j -= 1
        arr_sorted[j+1] = current
    return arr_sorted

1.2 希尔排序

题目说明:实现希尔排序(插入排序改进版),核心逻辑为按指定步长将数组拆分为多个子表,对每个子表执行直接插入排序,逐步缩小步长直至为 0,最终完成整体排序。

解题思路

  1. 复制原数组,获取数组长度,初始化步长为数组长度的一半;

  2. 当步长大于 0 时,按步长遍历数组元素,对每个元素执行子表内的插入排序;

  3. 子表插入排序逻辑与直接插入排序一致,仅比较和移动的步长为当前设定步长;

  4. 每轮排序完成后将步长折半,重复排序直至步长为 0;

  5. 返回排序后的数组。

def shell_sort(arr):
    arr_sorted = arr.copy()
    n = len(arr_sorted)
    step = n // 2
    while step > 0:
        for i in range(step,n):
            current = arr_sorted[i]
            j = i - step
            while j >= 0 and arr_sorted[j] > current:
                arr_sorted[j+step] = arr_sorted[j]
                j -= step
            arr_sorted[j+step] = current
        step = step // 2
    return arr_sorted

1.3 冒泡排序

题目说明:实现冒泡排序,核心逻辑为从前往后两两对比相邻元素,将较大的元素逐步 “冒泡” 到数组末尾,若某一轮无元素交换则提前终止排序。

解题思路

  1. 复制原数组,获取数组长度;

  2. 外层循环控制排序轮数,内层循环遍历未排序区间的相邻元素;

  3. 若前一个元素大于后一个元素则交换两者,标记本轮发生了交换;

  4. 若某一轮未发生任何交换,说明数组已完全有序,提前终止循环;

  5. 遍历完成后返回排序后的数组。

def bubble_sort(arr):
    arr_sorted = arr.copy()
    n = len(arr_sorted)
    for i in range(n):
        swapped = False
        for j in range(0,n-i-1):
            if arr_sorted[j] > arr_sorted[j+1]:
                arr_sorted[j],arr_sorted[j+1] = arr_sorted[j+1],arr_sorted[j]
                swapped = True
        if not swapped:
            break
    return arr_sorted

1.4 快速排序

题目说明:实现快速排序(分治思想),核心逻辑为选择基准值,将数组分为 “小于基准、等于基准、大于基准” 三部分,递归排序左右两部分后合并结果。

解题思路

  1. 递归终止条件:若数组为空则返回空数组;

  2. 选择数组第一个元素作为基准值;

  3. 遍历数组,将元素分别归入 “小于基准”“等于基准”“大于基准” 三个列表;

  4. 递归对 “小于基准” 和 “大于基准” 的列表执行快速排序;

  5. 合并递归结果与 “等于基准” 的列表,返回最终有序数组。

def quick_sort(arr):
    if not arr:
        return []
    pivot = arr[0]
    left = []
    equal = []
    right = []
    for num in arr:
        if num < pivot:
            left.append(num)
        elif num == pivot:
            equal.append(num)
        else:
            right.append(num)
    return quick_sort(left) + equal + quick_sort(right)

2. SQL 题目

2.1 计算用户的平均次日留存率

题目说明:从用户刷题记录表(question_practice_detail)中,计算用户在某天刷题后第二天还会再来刷题的平均次日留存率。

解题思路

  1. 第一步(CTE t1):去重获取用户 - 刷题日期的唯一记录,避免同一用户同一天多次刷题导致重复计数;

  2. 第二步(CTE t2):通过自连接匹配出 “用户当天刷题且次日也刷题” 的记录,筛选条件为用户 ID 相同、次日日期等于当天日期 + 1 天;

  3. 第三步:计算留存率,分子为有次日留存的 “用户 - 日期” 数,分母为所有 “用户 - 日期” 数,结果保留 4 位小数。

with t1 as (
    select
        distinct
        device_id,
        date
    from question_practice_detail
)
,t2 as (
    select
        b.device_id,
        b.date
    from t1 a left join t1 b
    on a.device_id = b.device_id
    where a.date < b.date and date_add(a.date,interval 1 day) = b.date
)
select
    round(count(distinct t2.device_id,t2.date)/count(distinct t1.device_id,t1.date),4) as avg_ret
from t1 left join t2 on t1.device_id = t2.device_id;

2.2 计算每天用户的次日留存率

题目说明:从用户登录表(user_login)中,计算每一天登录用户的次日留存率(次日仍登录的用户数 / 当日登录总用户数),要求保留所有日期(无留存的日期留存率为 0)。

解题思路

  1. 第一步(CTE t1):去重获取用户 - 登录日期的唯一记录,确保每个用户每天只计数一次;

  2. 第二步(CTE t2):自连接匹配出 “当日登录且次日也登录” 的用户,关联条件为用户 ID 相同、次日日期 = 当日日期 + 1 天,同时记录当日日期作为统计日期;

  3. 第三步:左连接 t1 和 t2,按登录日期分组,计算每组的留存率(分子为次日留存的用户数,分母为当日登录总用户数),结果保留 2 位小数,保证所有日期都被统计。

WITH t1 AS (
    SELECT DISTINCT
        user_id,
        login_date
    FROM user_login
),
t2 AS (
    SELECT
        a.login_date AS stat_date,
        a.user_id
    FROM t1 a
    JOIN t1 b
        ON a.user_id = b.user_id
        AND b.login_date = DATE_ADD(a.login_date, INTERVAL 1 DAY)
)
SELECT
    t1.login_date AS stat_date,
    ROUND(
        COUNT(DISTINCT t2.user_id) / COUNT(DISTINCT t1.user_id),
        2
    ) AS ret
FROM t1
LEFT JOIN t2
    ON t1.login_date = t2.stat_date
    AND t1.user_id = t2.user_id
GROUP BY t1.login_date
ORDER BY t1.login_date;

错题归因

  • 错误类型:逻辑错

  • 原因分析:

    1. 分母逻辑错误:误用 “次日留存的用户数” 替代 “当日总登录用户数” 作为留存率分母,违背 “留存率 = 留存数 / 总登录数” 的核心公式;

    2. 计数逻辑错误:关联后 count (last)/count (first) 结果恒为 1,无法正确计算比例;

    3. 结果完整性问题:未用左连接保留无留存的日期,导致留存率为 0 的日期被遗漏。

  • 改进方案:

    1. 明确分母为 “当日所有登录用户数”,分子为 “次日仍登录的用户数”;

    2. 使用左连接关联统计日期和留存记录,保证所有日期都被统计;

    3. 用 COUNT (DISTINCT t2.user_id)/COUNT (DISTINCT t1.user_id) 精准计算留存比例。

  • 核心考点:留存率的核心计算逻辑;左连接在日期完整性中的作用;DISTINCT 去重避免重复计数用户。

2.3 每个月 Top3 的周杰伦歌曲

题目说明:从听歌流水表(play_log)、歌曲信息表(song_info)、用户信息表(user_info)中,筛选 18-25 岁用户在 2022 年每个月播放次数排名前三的周杰伦歌曲。

解题思路

  1. 第一步(CTE t1):多表联查筛选基础数据,关联用户、歌曲、听歌记录,筛选条件为 18-25 岁用户、2022 年、周杰伦的歌曲,提取月份和歌曲名;

  2. 第二步(CTE t2):按月份和歌曲名分组,统计每首歌曲每月的播放次数;

  3. 第三步(CTE t3):按月份分区,对播放次数降序排序,用 ROW_NUMBER () 生成排名;

  4. 第四步:筛选排名≤3 的记录,得到每月播放量前三的歌曲。

with
    t1 as (
        select
            month(a.fdate) as month,
            b.song_name
        from
            play_log a
            join song_info b on a.song_id = b.song_id
            join user_info c on a.user_id = c.user_id
        where
            c.age between 18 and 25
            and year(a.fdate) = 2022
            and b.singer_name = '周杰伦'
    ),
    t2 as (
        select
            month,
            song_name,
            count(*) as play_pv
        from
            t1
        group by
            month,
            song_name
    ),
    t3 as (
        select
            month,
            row_number() over (
                partition by
                    month
                order by
                    play_pv desc
            ) as ranking,
            song_name,
            play_pv
        from
            t2
    )
select
    *
from
    t3
where
    ranking <= 3;

错题归因

  • 错误类型:语法 / 执行顺序错

  • 原因分析:

    1. 违反 SQL 执行顺序:将 row_number () 窗口函数直接写在 WHERE 子句中(WHERE 执行早于窗口函数);

    2. 隐式类型转换:年份筛选时用单引号包裹数值(year (a.fdate) = '2022'),降低查询效率。

  • 改进方案:

    1. 将窗口函数放在 CTE 中先计算排名,再在外层 WHERE 筛选 ranking <= 3;

    2. 年份筛选直接用数值匹配(year (a.fdate) = 2022),避免隐式转换。

  • 核心考点:SQL 执行顺序(FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY);窗口函数的合法使用位置;TopN 问题的三种排名函数选择。

四、总结

  1. 算法层面:插入 / 冒泡排序适合小数据量场景,快速排序适合大规模数据,希尔排序是插入排序的高效改进版,需掌握各算法的核心步骤和复杂度;

  2. SQL 层面:留存率计算核心是 “当日总活跃为分母、次日留存为分子”,需用左连接保证日期完整性;TopN 问题需先通过窗口函数计算排名,再筛选结果,且要遵循 SQL 执行顺序;

  3. 错题改进核心:留存率计算要避免分母错误和日期缺失,窗口函数不能直接用在 WHERE 子句中。

评论交流