排序算法和查找算法 - 软考备战(十五)

数据结构(七)

参考资料:

十大经典排序算法详解与动态图解:从入门到精通 - 知乎

查找算法图解与可视化 - 算法导航

算法中的七大查找方法 - 白露~ - 博客园

【算法】排序算法之归并排序 - 知乎

【算法】排序算法之希尔排序 - 知乎

【算法】排序算法之选择排序 - 知乎

【算法】排序算法之堆排序 - 知乎

冒泡排序算法(超级详细,图文并茂) - C语言中文网

基数排序算法(非常详细,图文并茂) - C语言中文网

快速排序算法(超级详细,图文并茂) - C语言中文网


目录

数据结构(七)

[3.1 排序](#3.1 排序)

[3.1.1 排序的定义和稳定性](#3.1.1 排序的定义和稳定性)

[3.1.2 插入排序](#3.1.2 插入排序)

[1. 直接插入排序](#1. 直接插入排序)

[2. 希尔排序](#2. 希尔排序)

[3.1.3 选择排序](#3.1.3 选择排序)

[1. 直接选择排序](#1. 直接选择排序)

[2. 堆排序](#2. 堆排序)

[3.1.4 交换排序](#3.1.4 交换排序)

[1. 冒泡排序](#1. 冒泡排序)

[2. 快速排序](#2. 快速排序)

[3.1.6 归并排序](#3.1.6 归并排序)

[3.1.7 基数排序](#3.1.7 基数排序)

[3.1.8 算法复杂性比较(必背核心表)](#3.1.8 算法复杂性比较(必背核心表))

[3.2 查找](#3.2 查找)

[3.2.1 查找的定义和平均查找长度 (ASL)](#3.2.1 查找的定义和平均查找长度 (ASL))

[3.2.2 顺序查找](#3.2.2 顺序查找)

原理

适用

[ASL 计算(默认等概率)](#ASL 计算(默认等概率))

[3.2.3 二分法查找 / 折半查找](#3.2.3 二分法查找 / 折半查找)

前提条件

原理

判定树

[ASL 计算](#ASL 计算)

缺点

[3.2.4 分块查找 / 索引顺序查找](#3.2.4 分块查找 / 索引顺序查找)

原理

过程

[ASL 计算(设 n 个元素分 b 块,每块 s 个元素,n = b * s)](#ASL 计算(设 n 个元素分 b 块,每块 s 个元素,n = b * s))

[3.2.5 散列表](#3.2.5 散列表)

[1. 定义](#1. 定义)

[2. 散列函数(构造方法)](#2. 散列函数(构造方法))

直接定址法

除留余数法(最常考、最常用)

[3. 冲突处理](#3. 冲突处理)

[① 开放定址法](#① 开放定址法)

[② 拉链法 / 链地址法](#② 拉链法 / 链地址法)

[4. 探查方式](#4. 探查方式)

线性探查法

二次探查法(平方探查法)

双重散列法

[散列表的 ASL 计算题模板](#散列表的 ASL 计算题模板)

[3.2.6 总结对比表](#3.2.6 总结对比表)

3.1 排序

3.1.1 排序的定义和稳定性

排序

将一组数据按关键字递增(或递减)重新排列。

稳定性

若待排序序列中存在多个关键字相同的记录,排序后这些记录的相对次序保持不变,则称该排序方法是稳定的;否则称为不稳定的。

口诀:"不稳定的大神(快、希、选、堆)"

  • -稳定的:冒泡、直接插入、归并、基数
  • -不稳定的:快速、希尔、选择、堆

3.1.2 插入排序

1. 直接插入排序

原理:

将待排记录按关键字大小插入到前面已排好序的子序列的适当位置。

过程:

默认第一个元素有序,从第二个开始,从后往前比较,找到位置后整体后移插入。

性能:

  • 时间复杂度:O(n^2) (最好 O(n),数组已有序时)
  • 空间复杂度:O(1)

稳定性:稳定

适用:基本有序、数据量小的场景。

2. 希尔排序

原理:

又称"缩小增量排序"。

按一定增量分组,组内进行直接插入排序,不断缩小增量直到为1。

本质:跳跃式的直接插入排序。

性能:

  • 时间复杂度:约 O(n^{1.3}) (与增量序列有关,最坏 O(n^2))
  • 空间复杂度:O(1)

稳定性:不稳定(相同元素可能被分到不同组中)

适用:中等规模数据,是对直接插入排序的优化。

3.1.3 选择排序

1. 直接选择排序

原理:

每趟从待排序列中选出最小(或最大)的元素,放在已排序列的末尾。

性能:

  • 时间复杂度:O(n^2) (无论初始状态如何,比较次数都是 n(n-1)/2)
  • 空间复杂度:O(1)

稳定性:不稳定(如 [5, 5, 2],选2和第一个5交换,两个5的相对顺序破坏)

特点:移动次数少,最多 3(n-1) 次。

2. 堆排序

原理:

将数组看作完全二叉树,构建大根堆(或小根堆),堆顶为最大值(或最小值)。

将堆顶与末尾交换,剩下的元素重新调整堆,反复执行。

关键操作:

建堆:从最后一个非叶子节点 n/2 - 1 向上调整,O(n)。

调整堆:向下调整,O(\log n)。

性能:

  • 时间复杂度:O(n \log n) (最好、最坏、平均都是)
  • 空间复杂度:O(1)

稳定性:不稳定

适用:数据量大、且对内存要求严格(不需要递归栈空间)的场景。

3.1.4 交换排序

1. 冒泡排序

原理:

相邻元素两两比较,如果逆序则交换。每趟将最大(或最小)元素"冒"到末尾。

优化:

加一个标志位 flag,如果某一趟没有发生交换,说明已经有序,直接结束。

性能:

  • 时间复杂度:O(n^2) (最好 O(n),优化后数组已有序时)
  • 空间复杂度:O(1)

稳定性:稳定(相邻相等时不交换)

2. 快速排序

原理:

分治法。

任取一个元素作为基准,通过一趟排序将序列分为两部分(左边都小于基准,右边都大于基准),然后递归对左右子序列排序。

性能:

  • 时间复杂度:平均 O(n \log n),最坏 O(n^2)(每次选的基准最大或最小,即数组已有序时)。
  • 空间复杂度:O(\log n)(递归调用栈的深度,最坏退化为 O(n))

稳定性:不稳定

适用:目前内部排序中平均性能最高的算法。常考优化方法:随机取基准、三数取中法。

3.1.6 归并排序

原理:

分治法。

将序列递归地分成两半,分别排序,然后将两个有序子序列合并成一个有序序列。

性能:

  • 时间复杂度:O(n \log n) (最好、最坏、平均都是,与初始状态无关)
  • 空间复杂度:O(n) (需要额外的辅助数组)

稳定性:稳定

适用:数据量大且要求稳定的场景;链表排序的首选(不需要额外空间 O(1))。

3.1.7 基数排序

原理:

非比较排序。

按关键字的各位值,从最低位(LSD)到最高位依次进行"分配"和"收集"。

(类似于扑克牌按花色和面值整理)。

性能:

  • 时间复杂度:O(d(n + r)) (d是位数,n是元素个数,r是基数,如十进制 r=10)
  • 空间复杂度:O(r) (需要 r 个队列/桶)

稳定性:稳定

适用:适用于字符串排序或位数不多的整数排序。

3.1.8 算法复杂性比较(必背核心表)

这是整个排序章节最重要的总结:

|----------|-----------------------|-----------------------|-----------------------------------|-----------------------------|---------|---------------------|
| 排序算法 | 平均时间 | 最好时间 | 最坏时间 | 空间复杂度 | 稳定性 | 备注 / 特点 |
| 直接插入 | O(n2) | O(n) | O (n 2) | O (1) | 稳定 | 基本有序时极快 |
| 希尔排序 | O(n1.3) | O(n) | O (n 2) | O (1) | 不稳定 | 缩小增量分组插入 |
| 直接选择 | O (n 2) | O(n2) | O (n 2) | O (1) | 不稳定 | 比较次数固定,移动少 |
| 堆排序 | O (n logn ) | O (n logn ) | O (n logn ) | O (1) | 不稳定 | 大根堆/小根堆,无最坏退化 |
| 冒泡排序 | O(n2) | O (n ) | O (n 2) | O (1) | 稳定 | 加标志位可提前结束 |
| 快速排序 | O(nlogn) | O (n logn ) | O ( n 2 ) | O (logn ) | 不稳定 | 平均最快 ,最差退化为有序 |
| 归并排序 | O (n logn ) | O (n logn ) | O (n logn ) | O ( n ) | 稳定 | 需要额外空间,无最坏退化 |
| 基数排序 | O (d (n +r )) | O (d (n +r )) | O (d (n +r )) | O (r ) | 稳定 | 不是比较排序,按位分配收集 |

易错点辨析:

  1. 问"哪个最快":没加条件选快排(平均 O(n\log n) 且常数小);如果有"最坏情况下也要快",选堆排或归并。
  2. 问"既快又稳定":选归并排序(只有归并和基数满足 O(n\log n) 且稳定)。
  3. 问"空间开销最小":选堆排序(同为 O(n\log n) 的快排要 O(\log n) 栈空间,归并要 O(n) 辅助空间)。

判断稳定性记忆法:

  • 凡是存在跨越式交换或移动的,都不稳定。

(希尔跨组、选择跨区域、快排基准跨越、堆排父子跨越)。

  • 只有相邻比较交换的(冒泡、插入)和合并的(归并、基数)才是稳定的。
python 复制代码
import random
from collections import deque

# ============================================================
#  辅助:生成测试数组
# ============================================================
def make_arr(size=10, seed=42):
    random.seed(seed)
    return [random.randint(1, 99) for _ in range(size)]

def print_step(arr, tag=""):
    print(f"  {tag:>14}: {arr}")

def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]


# ============================================================
#  1. 直接插入排序
# ============================================================
def insert_sort(arr):
    """
    从第2个元素起,将其插入到前方已排好序的子序列的正确位置
    时间: O(n²)  空间: O(1)  稳定
    """
    a = arr[:]
    n = len(a)
    for i in range(1, n):
        temp = a[i]
        j = i - 1
        while j >= 0 and a[j] > temp:     # 后移腾位
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = temp
    return a


# ============================================================
#  2. 希尔排序(缩小增量排序)
# ============================================================
def shell_sort(arr):
    """
    按增量 gap 分组,组内直接插入排序;gap 逐次缩小直到 1
    时间: ~O(n^1.3)  空间: O(1)  不稳定
    """
    a = arr[:]
    n = len(a)
    gap = n // 2
    while gap > 0:
        for i in range(gap, n):
            temp = a[i]
            j = i - gap
            while j >= 0 and a[j] > temp:
                a[j + gap] = a[j]
                j -= gap
            a[j + gap] = temp
        gap //= 2
    return a


# ============================================================
#  3. 直接选择排序
# ============================================================
def select_sort(arr):
    """
    每趟从待排区间选出最小值,放到已排区间末尾
    时间: O(n²)  空间: O(1)  不稳定
    """
    a = arr[:]
    n = len(a)
    for i in range(n - 1):
        min_idx = i
        for j in range(i + 1, n):
            if a[j] < a[min_idx]:
                min_idx = j
        if min_idx != i:
            swap(a, i, min_idx)
    return a


# ============================================================
#  4. 堆排序
# ============================================================
def heap_sort(arr):
    """
    建大根堆 → 堆顶(最大值)与末尾交换 → 调整堆 → 重复
    时间: O(n log n)  空间: O(1)  不稳定
    """
    a = arr[:]
    n = len(a)

    def sift_down(size, root):
        """将 root 为根的子树调整为大根堆"""
        largest = root
        left = 2 * root + 1
        right = 2 * root + 2
        if left < size and a[left] > a[largest]:
            largest = left
        if right < size and a[right] > a[largest]:
            largest = right
        if largest != root:
            swap(a, root, largest)
            sift_down(size, largest)

    # 建堆:从最后一个非叶子节点向上调整
    for i in range(n // 2 - 1, -1, -1):
        sift_down(n, i)

    # 排序:每次把堆顶最大值交换到末尾,缩小堆
    for end in range(n - 1, 0, -1):
        swap(a, 0, end)
        sift_down(end, 0)

    return a


# ============================================================
#  5. 冒泡排序
# ============================================================
def bubble_sort(arr):
    """
    相邻元素两两比较,逆序则交换,每趟把最大值"冒"到末尾
    时间: O(n²)  空间: O(1)  稳定
    """
    a = arr[:]
    n = len(a)
    for i in range(n - 1):
        swapped = False
        for j in range(0, n - 1 - i):
            if a[j] > a[j + 1]:
                swap(a, j, j + 1)
                swapped = True
        if not swapped:                  # 本趟无交换 → 已有序,提前结束
            break
    return a


# ============================================================
#  6. 快速排序
# ============================================================
def quick_sort(arr):
    """
    取基准 pivot,将序列分为 < pivot 和 > pivot 两部分,递归排序
    时间: 平均 O(n log n),最坏 O(n²)  空间: O(log n)  不稳定
    """
    a = arr[:]

    def _quick(lo, hi):
        if lo >= hi:
            return
        pivot = a[hi]                    # 选最右为基准
        i = lo                           # i 指向"小于区"的下一个位置
        for j in range(lo, hi):
            if a[j] < pivot:
                swap(a, i, j)
                i += 1
        swap(a, i, hi)                   # 基准归位
        _quick(lo, i - 1)
        _quick(i + 1, hi)

    _quick(0, len(a) - 1)
    return a


# ============================================================
#  7. 归并排序
# ============================================================
def merge_sort(arr):
    """
    递归拆分为两半 → 分别排序 → 合并两个有序子序列
    时间: O(n log n)  空间: O(n)  稳定
    """
    a = arr[:]

    def _merge(left, right):
        """合并两个有序列表"""
        res = []
        i = j = 0
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:      # <= 保证稳定
                res.append(left[i]); i += 1
            else:
                res.append(right[j]); j += 1
        res.extend(left[i:])
        res.extend(right[j:])
        return res

    if len(a) <= 1:
        return a
    mid = len(a) // 2
    return _merge(merge_sort(a[:mid]), merge_sort(a[mid:]))


# ============================================================
#  8. 基数排序(LSD 最低位优先)
# ============================================================
def radix_sort(arr):
    """
    按个位、十位、百位...依次进行"分配-收集"
    时间: O(d(n+r))  空间: O(n+r)  稳定
    """
    if not arr:
        return arr
    a = [x for x in arr]                 # 复制
    max_val = max(a)
    exp = 1                              # 当前位(个位=1, 十位=10, ...)

    while max_val // exp > 0:
        # 10 个桶 (0~9)
        buckets = [[] for _ in range(10)]
        # 分配
        for num in a:
            digit = (num // exp) % 10
            buckets[digit].append(num)
        # 收集
        a = [num for bucket in buckets for num in bucket]
        exp *= 10

    return a


# ============================================================
#  排序算法汇总测试
# ============================================================
if __name__ == "__main__":
    original = make_arr()
    print(f"原始数组: {original}\n")

    algorithms = [
        ("直接插入排序", insert_sort),
        ("希尔排序  ", shell_sort),
        ("直接选择排序", select_sort),
        ("堆排序    ", heap_sort),
        ("冒泡排序  ", bubble_sort),
        ("快速排序  ", quick_sort),
        ("归并排序  ", merge_sort),
        ("基数排序  ", radix_sort),
    ]

    print("=" * 35)
    for name, func in algorithms:
        result = func(original)
        print(f"  {name}: {result}")
    print("=" * 35)

运行结果:
*

原始数组: [82, 15, 4, 95, 36, 2, 69, 32, 54, 85]

===================================

直接插入排序: [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

希尔排序 : [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

直接选择排序: [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

堆排序 : [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

冒泡排序 : [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

快速排序 : [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

归并排序 : [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

基数排序 : [2, 4, 15, 32, 36, 54, 69, 82, 85, 95]

===================================

3.2 查找

3.2.1 查找的定义和平均查找长度 (ASL)

查找

给定一个值 K,在含有 n 个元素的表中找出关键字等于 K 的记录。

平均查找长度

查找过程中,关键字比较次数的期望值。

公式:

3.2.2 顺序查找

原理

从头到尾(或从尾到头)逐个比较。

一般把哨兵设在下标 0 处,从后往前找,免去每次判断是否越界。

适用

无序表、线性链表。

ASL 计算(默认等概率)

3.2.3 二分法查找 / 折半查找

前提条件

线性表必须有序,且必须是顺序存储结构(数组)。

原理

取中间元素比较,相等则成功;

小于则在左半区查找;

大于则在右半区查找。

判定树

二分查找的过程可以用一棵二叉树(平衡二叉排序树)来描述。

查找成功时的比较次数 = 目标结点在判定树中的层数。

树的形态只与表的长度 n 有关,与表中元素的具体值无关。

ASL 计算
缺点

不适合频繁插入删除的场景

(因为要保持有序和顺序存储,移动元素代价大)。

3.2.4 分块查找 / 索引顺序查找

原理

将 n 个元素分成 b 块。

块内无序,块间有序(例如第2块所有元素都大于第1块)。

建一个索引表,存储每块的最大关键字和起始地址。

过程

先在索引表中二分或顺序查找确定目标在哪一块,然后在该块内顺序查找。

ASL 计算(设 n 个元素分 b 块,每块 s 个元素,n = b * s)

3.2.5 散列表

1. 定义

根据关键字直接计算出存储地址的方法。

记录的存储位置 f(key) 称为散列函数/哈希函数。

冲突:

,这两个key称为同义词。

2. 散列函数(构造方法)
直接定址法

不会发生冲突,适合关键字分布连续。

除留余数法(最常考、最常用)

注意:

p 必须是不大于表长 m 的最大素数

(或者最小素数,按教材来,通常是不大于表长的最大素数),这样能最大程度减少冲突。

3. 冲突处理
① 开放定址法
② 拉链法 / 链地址法

所有同义词用一个单链表链接起来。

散列表的每个单元存放链表的头指针。

优点:

处理冲突简单,无堆积现象。

适合经常进行插入删除的场景(不用像开放定址法那样移动元素)。

适用于表长不确定的情况。

缺点:

指针占用额外空间。

4. 探查方式
线性探查法

缺点:

容易产生"堆积/聚集"现象

(非同义词抢占了同一个后续地址)。

二次探查法(平方探查法)

特点:

可以避免堆积,但不能探测到表的所有位置

(至少能探测到一半位置)。

双重散列法
散列表的 ASL 计算题模板

给一组数字、表长 m、散列函数、处理冲突方法,求 ASL_{成功} 和 ASL_{失败}。

第一步:画表填数

严格按照规则填入散列表。如果是线性探查,遇到冲突就往后挪;如果是拉链法,画链表。

第二步:计算 ASL_{成功}

关键点:分母是元素的个数 n(不是表长 m)。

分子是每个元素插入时比较的次数之和。

例如:

某元素算出地址为5,发现被占了,和6比,发现也被占了,和7比,放入7。那么这个元素的比较次数是 3。*

第三步:计算 ASL_{失败}(极易错)

关键点:分母是散列函数的取值种类(即表长 m)。

分子是从第 0 个位置到第 m-1 个位置,假设查找不存在的关键字,分别需要比较几次才能确定失败。

(1)线性探查法中:

如果某个位置为空,则查找该位置失败只需比较 1 次;

如果某个位置及后面连续 3 个位置都有元素,则查找该位置失败需要比较 4 次(直到遇到第一个空隙才算失败)。

(2)拉链法中:

如果某个位置的链表有 3 个结点,查找该位置失败需要比较 3 次(从头比到尾的 NULL);

如果位置为空,比较 0 次。

3.2.6 总结对比表

|----------|-----------|-----------|---------------------------------|-------------------|
| 查找算法 | 前提条件 | 存储结构 | ASL 成功 ASL 成功 | 优缺点 |
| 顺序查找 | 无 | 顺序表/链表 | (n+1)/2 | 最慢,但最通用 |
| 二分查找 | 必须有序 | 仅限顺序表 | ≈log2(n+1)−1 | 最快(静态),但不支持动态增删 |
| 分块查找 | 块间有序,块内无序 | 顺序表+索引表 | 根号下n+1 | 介于顺序和二分之间 |
| 散列查找 | 选好散列函数 | 顺序表/链表 | 接近 O(1) | 极快,但有冲突开销,不支持范围查找 |

python 复制代码
from collections import deque

# ============================================================
#  1. 顺序查找
# ============================================================
def seq_search(arr, key):
    """
    从头到尾逐个比较
    时间: O(n)  无需有序  支持任意存储结构
    """
    for i, val in enumerate(arr):
        if val == key:
            return i          # 返回下标(从 0 开始,即比较 i+1 次)
    return -1                 # 未找到


# ============================================================
#  2. 二分法查找 / 折半查找
# ============================================================
def binary_search(arr, key):
    """
    前提:数组必须有序(从小到大)
    时间: O(log n)  仅限顺序存储
    返回: (找到的下标, 比较次数)  未找到时下标为 -1
    """
    lo, hi = 0, len(arr) - 1
    cnt = 0
    while lo <= hi:
        cnt += 1
        mid = (lo + hi) // 2
        if arr[mid] == key:
            return mid, cnt
        elif arr[mid] > key:
            hi = mid - 1
        else:
            lo = mid + 1
    return -1, cnt


# ============================================================
#  3. 分块查找(索引顺序查找)
# ============================================================
def block_search(arr, index, key):
    """
    块间有序、块内无序。先在索引表中确定块号,再块内顺序查找
    参数:
        arr   : 原始数组
        index : 索引表 [(块内最大值, 块起始下标), ...]
        key   : 查找关键字
    返回: (下标, 索引比较次数, 块内比较次数)
    """
    # --- 第一步:在索引表中查找目标块(顺序查找) ---
    block_idx = -1
    idx_cmp = 0
    for i, (max_val, start) in enumerate(index):
        idx_cmp += 1
        if key <= max_val:
            block_idx = i
            break
    if block_idx == -1:
        return -1, idx_cmp, 0      # 超出最大块

    # --- 第二步:块内顺序查找 ---
    _, blk_start = index[block_idx]
    # 确定块结束位置
    if block_idx + 1 < len(index):
        blk_end = index[block_idx + 1][1]
    else:
        blk_end = len(arr)

    blk_cmp = 0
    for j in range(blk_start, blk_end):
        blk_cmp += 1
        if arr[j] == key:
            return j, idx_cmp, blk_cmp
    return -1, idx_cmp, blk_cmp


# ============================================================
#  4. 散列表 ------ 开放定址法(线性探查)
# ============================================================
class HashTableOpen:
    """
    散列函数: H(key) = key % p  (p 为不大于表长的最大素数)
    冲突处理: 线性探查法 d_i = 1, 2, 3, ...
    """

    def __init__(self, size, p=None):
        """
        :param size: 散列表长 m
        :param p:    除留余数法的 p(默认取 <= size 的最大素数)
        """
        self.m = size
        self.p = p or self._max_prime_le(size)
        self.table = [None] * size

    @staticmethod
    def _max_prime_le(n):
        """求不超过 n 的最大素数"""
        if n < 2:
            return 2
        for x in range(n, 1, -1):
            if all(x % i != 0 for i in range(2, int(x**0.5) + 1)):
                return x
        return 2

    def hash(self, key):
        return key % self.p

    def insert(self, key):
        """插入,返回插入时的比较次数(即探查次数)"""
        addr = self.hash(key)
        cmp_cnt = 1                       # 第一次探查
        # 逐步向后线性探查
        while self.table[addr] is not None and self.table[addr] != -1:
            if self.table[addr] == key:   # 已存在,不重复插入
                return cmp_cnt
            addr = (addr + 1) % self.m
            cmp_cnt += 1
        self.table[addr] = key
        return cmp_cnt

    def search(self, key):
        """查找,返回 (找到的下标, 比较次数)"""
        addr = self.hash(key)
        cmp_cnt = 1
        first_deleted = None
        start = addr
        while self.table[addr] is not None:
            if self.table[addr] == key:
                return addr, cmp_cnt
            if self.table[addr] == -1 and first_deleted is None:
                first_deleted = addr
            addr = (addr + 1) % self.m
            cmp_cnt += 1
            if addr == start:              # 绕了一圈,表满
                break
        return -1, cmp_cnt

    def asl_success(self, keys):
        """计算查找成功的 ASL(keys 为实际插入的关键字列表)"""
        total = 0
        for k in keys:
            _, cnt = self.search(k)
            total += cnt
        return round(total / len(keys), 2)

    def asl_failure(self):
        """计算查找失败的 ASL(对每个散列地址 0~m-1 计算探查到空位的次数)"""
        total = 0
        for i in range(self.m):
            addr = i
            cnt = 1
            while self.table[addr] is not None:
                addr = (addr + 1) % self.m
                cnt += 1
            total += cnt
        return round(total / self.m, 2)

    def __repr__(self):
        return str(self.table)


# ============================================================
#  5. 散列表 ------ 拉链法(链地址法)
# ============================================================
class HashTableChain:
    """
    散列函数: H(key) = key % p
    冲突处理: 拉链法(同义词链在同一槽位的单链表中)
    """

    def __init__(self, size, p=None):
        self.m = size
        self.p = p or self._max_prime_le(size)
        self.table = [[] for _ in range(size)]     # 每个槽位一个链表

    @staticmethod
    def _max_prime_le(n):
        if n < 2: return 2
        for x in range(n, 1, -1):
            if all(x % i != 0 for i in range(2, int(x**0.5) + 1)):
                return x
        return 2

    def hash(self, key):
        return key % self.p

    def insert(self, key):
        addr = self.hash(key)
        if key not in self.table[addr]:            # 去重
            self.table[addr].append(key)

    def search(self, key):
        """返回 (是否找到, 比较次数)"""
        addr = self.hash(key)
        for i, k in enumerate(self.table[addr]):
            if k == key:
                return True, i + 1                 # 第 i+1 次比较命中
        return False, len(self.table[addr])        # 比较了链表全部元素

    def asl_success(self, keys):
        total = 0
        for k in keys:
            _, cnt = self.search(k)
            total += cnt
        return round(total / len(keys), 2)

    def asl_failure(self):
        """
        查找失败:对每个散列地址,探查次数 = 该槽位链表长度
        (空链表探查 0 次即可确定失败)
        """
        total = sum(len(chain) for chain in self.table)
        return round(total / self.m, 2)

    def __repr__(self):
        lines = []
        for i, chain in enumerate(self.table):
            lines.append(f"  [{i:>2}]: {chain}")
        return "\n".join(lines)


# ============================================================
#  查找算法汇总测试
# ============================================================
if __name__ == "__main__":

    # ---------- 1. 顺序查找 ----------
    print("=" * 50)
    print("1. 顺序查找")
    arr1 = [3, 9, 1, 7, 5, 2, 8, 4, 6]
    key = 5
    idx = seq_search(arr1, key)
    print(f"  数组: {arr1}")
    print(f"  查找 {key}: 下标={idx}, 比较次数={idx + 1}")
    print(f"  ASL_成功 = ({len(arr1)}+1)/2 = {(len(arr1)+1)/2}")

    # ---------- 2. 二分查找 ----------
    print("\n" + "=" * 50)
    print("2. 二分查找")
    arr2 = [2, 4, 7, 11, 15, 19, 23, 28, 33, 40]
    key = 23
    idx, cnt = binary_search(arr2, key)
    print(f"  有序数组: {arr2}")
    print(f"  查找 {key}: 下标={idx}, 比较次数={cnt}")
    # ASL 计算(验证)
    total_cmp = sum(binary_search(arr2, x)[1] for x in arr2)
    print(f"  ASL_成功 = {total_cmp}/{len(arr2)} = {round(total_cmp/len(arr2), 2)}")

    # ---------- 3. 分块查找 ----------
    print("\n" + "=" * 50)
    print("3. 分块查找")
    arr3 = [22, 12, 13, 9, 8, 33, 42, 44, 38, 45, 60, 58, 73, 77, 71]
    # 分 3 块,每块 5 个,块间有序
    index3 = [(22, 0), (45, 5), (77, 10)]   # (块内最大值, 起始下标)
    print(f"  原始数组: {arr3}")
    print(f"  索引表: {index3}")
    for k in [42, 71, 50]:
        idx, ic, bc = block_search(arr3, index3, k)
        status = f"下标={idx}" if idx != -1 else "未找到"
        print(f"  查找 {k}: {status}, 索引比较={ic}次, 块内比较={bc}次")

    # ---------- 4. 散列表 - 开放定址法 ----------
    print("\n" + "=" * 50)
    print("4. 散列表 ------ 开放定址法(线性探查)")
    keys4 = [19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79]
    ht_open = HashTableOpen(size=13, p=13)
    print(f"  关键字: {keys4}")
    print(f"  表长 m=13, p=13, H(key) = key % 13\n")
    for k in keys4:
        ht_open.insert(k)
    print(f"  散列表: {ht_open.table}")
    print(f"  ASL_成功 = {ht_open.asl_success(keys4)}")
    print(f"  ASL_失败 = {ht_open.asl_failure()}")

    # ---------- 5. 散列表 - 拉链法 ----------
    print("\n" + "=" * 50)
    print("5. 散列表 ------ 拉链法(链地址法)")
    keys5 = [19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79]
    ht_chain = HashTableChain(size=13, p=13)
    print(f"  关键字: {keys5}")
    print(f"  表长 m=13, p=13, H(key) = key % 13\n")
    for k in keys5:
        ht_chain.insert(k)
    print(f"  散列表:")
    print(ht_chain)
    print(f"\n  ASL_成功 = {ht_chain.asl_success(keys5)}")
    print(f"  ASL_失败 = {ht_chain.asl_failure()}")

运行结果:

==================================================

  1. 顺序查找

数组: [3, 9, 1, 7, 5, 2, 8, 4, 6]

查找 5: 下标=4, 比较次数=5

ASL_成功 = (9+1)/2 = 5.0

==================================================

  1. 二分查找

有序数组: [2, 4, 7, 11, 15, 19, 23, 28, 33, 40]

查找 23: 下标=6, 比较次数=2

ASL_成功 = 29/10 = 2.9

==================================================

  1. 分块查找

原始数组: [22, 12, 13, 9, 8, 33, 42, 44, 38, 45, 60, 58, 73, 77, 71]

索引表: [(22, 0), (45, 5), (77, 10)]

查找 42: 下标=6, 索引比较=2次, 块内比较=2次

查找 71: 下标=14, 索引比较=3次, 块内比较=5次

查找 50: 未找到, 索引比较=3次, 块内比较=5次

==================================================

  1. 散列表 ------ 开放定址法(线性探查)

关键字: [19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79]

表长 m=13, p=13, H(key) = key % 13

散列表: [1, 14, 68, 55, 84, 19, 20, 79, 33, 23, 11, 10, 27]

ASL_成功 = 2.0

ASL_失败 = 6.62

==================================================

  1. 散列表 ------ 拉链法(链地址法)

关键字: [19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79]

表长 m=13, p=13, H(key) = key % 13

散列表:

0\]: \[

1\]: \[1, 14, 27

2\]: \[68

3\]: \[55

4\]: \[84

5\]: \[19

6\]: \[20

7\]: \[79

8\]: \[33

9\]: \[23

10\]: \[10, 23

11\]: \[11

12\]: \[

ASL_成功 = 1.33

ASL_失败 = 1.08

相关推荐
旖-旎2 小时前
分治(交易逆序对的总数)(6)
c++·算法·leetcode·排序算法·归并排序
北顾笙9802 小时前
day14-数据结构力扣
数据结构·算法·leetcode
lifallen2 小时前
Flink Agents:从 DataStream 到 Agent 算子的接入与装配
java·大数据·人工智能·python·语言模型·flink
Ln5x9qZC22 小时前
尾递归与Continuation
算法
一路向北he2 小时前
esp32库依赖
c语言·c++·算法
老四啊laosi2 小时前
[双指针] 6. 查找总价为目标值的两个商品
算法·力扣·总价为目标值得两商品
做cv的小昊2 小时前
【conda】打包已有conda环境并在其他服务器上搭建
运维·服务器·python·conda·运维开发·pip·开发
Hommy882 小时前
【开源剪映小助手-客户端】目录扫描功能
python·开源·aigc·剪映小助手
Pocker_Spades_A2 小时前
Python快速入门专业版(五十六)——爬虫会话管理:Cookie与Session原理及实战(保持登录状态)
开发语言·爬虫·python