44、基础数据结构:数组、栈与二叉搜索树

基础数据结构:数组、栈与二叉搜索树

在计算机科学中,选择合适的算法和数据结构对于解决计算问题至关重要。算法的效率通常取决于输入数据的存储和处理方式,特别是所选择的特定数据结构。下面将详细介绍几种基础的数据结构,包括数组、栈和二叉搜索树。

1. 算法选择与数据结构的重要性

计算问题通常可以通过多种算法来解决,选择特定算法时主要考虑两个因素:时间复杂度和实现难度,其中时间复杂度更为重要。例如,对于图上的问题,即使知道该问题可以用多项式时间复杂度的算法解决,使用时间复杂度为 $O(N^2)$ 的算法与使用 $O(N^3)$ 的算法相比,在求解时间上也会有显著差异。

算法的效率往往取决于输入数据的存储和处理方式,特别是所选择的数据结构。以计算图中所有 $N$ 个节点的度为例,如果图以 $N \times N$ 的邻接矩阵存储,计算节点度的最简单算法的时间复杂度为 $O(N^2)$;而使用稀疏矩阵表示,在某些情况下可以将时间复杂度降低到 $O(K)$ 或 $O(N)$。

2. 数组

数组是最基本的数据结构,是一块连续的内存区域,能够存储多个相同类型的变量,通常称为数组的组件或元素。在 C 语言中,数组的索引从 0 开始,例如长度为 $N$ 的数组,第一个元素的索引为 0,最后一个元素的索引为 $N - 1$。数组可以是多维的,用于表示矩阵和张量。

数组的操作时间复杂度如下:
- 读取特定元素 :已知元素在数组中的位置时,读取其值的时间复杂度为 $O(1)$,即常数时间,与数组的大小无关。
- 添加元素 :在现有大小为 $N$ 的数组末尾添加一个元素,平均时间复杂度为 $O(N)$。这是因为可能需要操作系统扩展数组的内存空间,如果内存不足,需要找到一个足够大的连续内存区域,并将现有数组的值复制到新位置。
- 删除元素 :删除数组中的一个元素,平均时间复杂度也为 $O(N)$。在最坏情况下,需要将删除元素后面的所有元素向前移动。
- 搜索元素 :使用简单的线性搜索算法在大小为 $N$ 的数组中搜索特定元素,平均时间复杂度为 $O(N)$。

为了提高搜索效率,可以对数组进行排序,然后使用二分搜索算法。二分搜索的时间复杂度为 $O(log N)$,其伪代码如下:

Algorithm 2 binary_search()
Input: x, N, v[] (sorted in ascending order)
Output: position of x, or -1 if x is not in v[]
1: high ← N - 1
2: low ← 0
3: cur ← ⌊(low + high) / 2⌋
4: while low < high do
5:
    if x > v[cur] then
6:
        low ← cur + 1
7:
    else
8:
        if x ≤ v[cur] then
9:
            high ← cur
10:
        end if
11:
    end if
12: end while
13: if x = v[cur] then
14:
    return cur
15: else
16:
    return -1
17: end if

二分搜索的基本思想是将搜索范围不断缩小一半,直到找到目标元素或确定目标元素不存在。

排序数组的常用算法是快速排序,其平均时间复杂度为 $O(N log N)$。快速排序是一个递归过程,包括以下三个步骤:
- 选择一个元素作为枢轴(pivot)。
- 将数组划分为两个子数组,一个包含小于枢轴的元素,另一个包含大于枢轴的元素。
- 递归地对两个子数组应用快速排序算法。

快速排序的伪代码如下:

Algorithm 3 quick_sort()
Input: A, low, high
Output: the array A, sorted in-place
1: if low < high then
2:
    p ← partition(A, low, high)
3:
    quick_sort(A, low, p - 1)
4:
    quick_sort(A, p + 1, high)
5: end if

数组适用于元素数量预先固定或在计算过程中变化不大的情况,并且典型操作是使用索引读取或写入元素。当数组元素不经常变化,且需要进行大量搜索操作时,使用二分搜索可以提高效率。

3. 栈

栈是一种能够容纳同类元素的数据结构,但其访问元素的方式独特。栈遵循后进先出(LIFO)的访问策略,即每次只能访问栈顶的元素。栈的操作主要有两个: push() 用于在栈顶插入新元素, pop() 用于移除栈顶元素并将其返回给用户。

栈可以通过数组和一个变量 top 来实现, top 表示栈顶的索引。当栈为空时, top 通常设置为 -1。栈的插入和删除操作的时间复杂度均为 $O(1)$,但只能访问栈顶元素,不支持随机访问和元素搜索。

栈的操作流程如下:
1. 初始化栈,将 top 设置为 -1。
2. 执行 push() 操作时,将元素放入 s[top + 1] 位置,并将 top 加 1。
3. 执行 pop() 操作时,返回 s[top] 的值,并将 top 减 1。

栈在记录算法的执行进度方面非常有用,现代操作系统使用栈来跟踪子程序和函数的调用。例如,在枚举图的所有循环和贪心模块化优化的程序中都使用了栈的实现。

4. 二叉搜索树

二叉搜索树(BST)是一种专门为高效插入、删除和搜索元素而设计的数据结构。每个元素表示为树的一个节点,并且满足以下五个属性:
1. 树的每个节点都与一个唯一的数字键相关联。
2. 树有且只有一个节点被标记为根节点。
3. 除根节点外,每个节点都有一个父节点,即离根节点更近的邻居。
4. 每个节点最多有两个子节点,分别称为左子节点和右子节点。
5. 每个节点的键必须大于其左子树中所有节点的键,并且小于其右子树中所有节点的键。

构建二叉搜索树的方法是:第一个数字作为根节点的键,后续数字根据其与根节点键的大小关系,作为左子节点或右子节点插入树中。例如,从序列 9, 5, 27, 18, 23, 1, 32, 12 构建的二叉搜索树,根节点的键为 9,5 作为左子节点,27 作为右子节点,依此类推。

在二叉搜索树中搜索元素的递归过程 bst_search() 的伪代码如下:

Algorithm 4 bst_search()
Input: node, t
Output: {TRUE | FALSE}
1: if key[node] = t then
2:
    return TRUE
3: else
4:
    if key[node] > t then
5:
        if node has left child then
6:
            bst_search(left[node], t)
7:
        else
8:
            return FALSE
9:
        end if
10:
    else
11:
        if node has right child then
12:
            bst_search(right[node], t)
13:
        else
14:
            return FALSE
15:
        end if
16:
    end if
17: end if

二叉搜索树的搜索机制与二分搜索算法类似,每次递归调用都会排除一个子树,将搜索范围缩小。然而,二叉搜索树的搜索时间复杂度很大程度上取决于树的形状。不同形状的二叉搜索树在搜索元素时所需的迭代次数不同,例如,形状更紧凑的树搜索效率更高。

综上所述,不同的数据结构适用于不同的应用场景。数组适用于元素数量固定且主要进行索引访问的情况;栈适用于需要后进先出访问模式的场景;二叉搜索树则适用于需要频繁插入、删除和搜索元素的情况。在实际应用中,应根据具体需求选择合适的数据结构,以提高算法的效率。

下面用 mermaid 流程图展示二分搜索算法的流程:

graph TD;
    A[开始] --> B[初始化 high = N - 1, low = 0, cur = ⌊(low + high) / 2⌋];
    B --> C{low < high};
    C -- 是 --> D{x > v[cur]};
    D -- 是 --> E[low = cur + 1];
    D -- 否 --> F{x ≤ v[cur]};
    F -- 是 --> G[high = cur];
    E --> H[更新 cur = ⌊(low + high) / 2⌋];
    G --> H;
    H --> C;
    C -- 否 --> I{x = v[cur]};
    I -- 是 --> J[返回 cur];
    I -- 否 --> K[返回 -1];

再用表格总结一下这几种数据结构的操作时间复杂度:
| 数据结构 | 读取元素 | 添加元素 | 删除元素 | 搜索元素 |
| ---- | ---- | ---- | ---- | ---- |
| 数组 | $O(1)$ | $O(N)$ | $O(N)$ | 线性搜索 $O(N)$,二分搜索 $O(log N)$ |
| 栈 | 仅栈顶 $O(1)$ | $O(1)$ | $O(1)$ | 不支持 |
| 二叉搜索树 | - | $O(log N)$ | $O(log N)$ | $O(log N)$(平均) |

基础数据结构:数组、栈与二叉搜索树

5. 不同数据结构的应用场景分析

在实际的编程和算法设计中,选择合适的数据结构对于提高程序的性能至关重要。下面我们将详细分析数组、栈和二叉搜索树在不同场景下的应用。

5.1 数组的应用场景
  • 图像处理 :在图像处理中,图像通常可以表示为二维数组。每个像素的颜色信息可以存储在数组的元素中。例如,一个灰度图像可以用一个二维整数数组表示,其中每个元素代表一个像素的灰度值。使用数组可以方便地对图像进行遍历和修改,如调整亮度、对比度等操作。
  • 矩阵运算 :矩阵是线性代数中的重要概念,在很多科学计算和工程领域都有广泛应用。矩阵可以用二维数组来表示,通过数组的操作可以实现矩阵的加法、乘法等运算。例如,在机器学习中,矩阵运算常用于神经网络的前向传播和反向传播过程。
5.2 栈的应用场景
  • 表达式求值 :在计算数学表达式时,栈可以用于处理运算符和操作数。例如,在计算后缀表达式(逆波兰表达式)时,我们可以使用栈来存储操作数,遇到运算符时从栈中弹出相应数量的操作数进行计算,并将结果压入栈中。以下是一个简单的后缀表达式求值的 Python 代码示例:
def evaluate_postfix(expression):
    stack = []
    for token in expression:
        if token.isdigit():
            stack.append(int(token))
        else:
            operand2 = stack.pop()
            operand1 = stack.pop()
            if token == '+':
                result = operand1 + operand2
            elif token == '-':
                result = operand1 - operand2
            elif token == '*':
                result = operand1 * operand2
            elif token == '/':
                result = operand1 / operand2
            stack.append(result)
    return stack.pop()

expression = ['3', '4', '+', '2', '*']
print(evaluate_postfix(expression))  
  • 函数调用栈 :现代操作系统使用栈来跟踪函数的调用和返回。当一个函数被调用时,系统会将当前的执行上下文(包括局部变量、返回地址等)压入栈中;当函数返回时,系统会从栈中弹出相应的执行上下文,恢复之前的执行状态。
5.3 二叉搜索树的应用场景
  • 数据库索引 :在数据库中,为了提高数据的查询效率,常常使用索引。二叉搜索树可以作为一种索引结构,通过对数据的键进行排序,使得查询操作可以在 $O(log N)$ 的时间复杂度内完成。例如,在 MySQL 数据库中,B+ 树(一种改进的二叉搜索树)被广泛用于索引的实现。
  • 文件系统 :文件系统中的目录结构可以用树来表示,而二叉搜索树可以用于快速查找文件和目录。例如,在 Linux 系统中,文件系统的目录结构可以看作是一棵多叉树,通过对文件名进行排序,可以使用二叉搜索树的思想来提高文件查找的效率。
6. 数据结构的性能比较与选择策略

为了更直观地比较数组、栈和二叉搜索树的性能,我们可以用表格来总结它们的操作时间复杂度:
| 数据结构 | 读取元素 | 添加元素 | 删除元素 | 搜索元素 |
| ---- | ---- | ---- | ---- | ---- |
| 数组 | $O(1)$ | $O(N)$ | $O(N)$ | 线性搜索 $O(N)$,二分搜索 $O(log N)$ |
| 栈 | 仅栈顶 $O(1)$ | $O(1)$ | $O(1)$ | 不支持 |
| 二叉搜索树 | - | $O(log N)$ | $O(log N)$ | $O(log N)$(平均) |

根据以上表格,我们可以得出以下选择策略:
- 如果需要随机访问元素 :数组是最好的选择,因为数组可以通过索引在 $O(1)$ 的时间复杂度内访问任意元素。
- 如果需要后进先出的访问模式 :栈是最合适的,栈的插入和删除操作都可以在 $O(1)$ 的时间复杂度内完成。
- 如果需要频繁进行插入、删除和搜索操作 :二叉搜索树是一个不错的选择,其插入、删除和搜索操作的平均时间复杂度都为 $O(log N)$。

7. 总结

本文详细介绍了数组、栈和二叉搜索树三种基础数据结构。数组是最基本的数据结构,适用于元素数量固定或变化不大的情况,通过索引可以快速访问元素;栈遵循后进先出的原则,在记录算法执行进度和处理函数调用方面非常有用;二叉搜索树则专门为高效插入、删除和搜索元素而设计,其性能取决于树的形状。

在实际应用中,我们需要根据具体的需求和场景来选择合适的数据结构。通过合理选择数据结构,可以提高算法的效率,减少程序的运行时间和空间开销。同时,我们也可以根据不同数据结构的特点,将它们组合使用,以实现更加复杂和高效的算法。

下面用 mermaid 流程图展示栈的操作流程:

graph TD;
    A[初始化栈,top = -1] --> B{执行 push() 操作?};
    B -- 是 --> C[将元素放入 s[top + 1],top 加 1];
    C --> B;
    B -- 否 --> D{执行 pop() 操作?};
    D -- 是 --> E[返回 s[top],top 减 1];
    E --> B;
    D -- 否 --> F[结束];

希望本文能够帮助读者更好地理解和应用这三种基础数据结构,在编程和算法设计中做出更明智的选择。

先展示下效果 https://pan.quark.cn/s/a4b39357ea24 遗传算法 - 简书 遗传算法的理论是根据达尔文进化论而设计出来的算法: 人类是朝着好的方向(最优解)进化,进化过程中,会自动选择优良基因,淘汰劣等基因。 遗传算法(英语:genetic algorithm (GA) )是计算数学中用于解决最佳化的搜索算法,是进化算法的一种。 进化算法最初是借鉴了进化生物学中的一些现象而发展起来的,这些现象包括遗传、突变、自然选择、杂交等。 搜索算法的共同特征为: 首先组成一组候选解 依据某些适应性条件测算这些候选解的适应度 根据适应度保留某些候选解,放弃其他候选解 对保留的候选解进行某些操作,生成新的候选解 遗传算法流程 遗传算法的一般步骤 my_fitness函数 评估每条染色体所对应个体的适应度 升序排列适应度评估值,选出 前 parent_number 个 个体作为 待选 parent 种群(适应度函数的值越小越好) 从 待选 parent 种群 中随机选择 2 个个体作为父方和母方。 抽取父母双方的染色体,进行交叉,产生 2 个子代。 (交叉概率) 对子代(parent + 生成的 child)的染色体进行变异。 (变异概率) 重复3,4,5步骤,直到新种群(parentnumber + childnumber)的产生。 循环以上步骤直至找到满意的解。 名词解释 交叉概率:两个个体进行交配的概率。 例如,交配概率为0.8,则80%的“夫妻”会生育后代。 变异概率:所有的基因中发生变异的占总体的比例。 GA函数 适应度函数 适应度函数由解决的问题决定。 举一个平方和的例子。 简单的平方和问题 求函数的最小值,其中每个变量的取值区间都是 [-1, ...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值