数据结构复习提纲

一、绪论

数据结构三要素

  1. 逻辑结构

    • 定义:逻辑结构指数据元素之间的逻辑关系,是对数据在计算机内存中组织和存储的抽象。

    • 分类

      • 集合结构:元素之间除了同属于一个集合外,没有其他关系。

      • 线性结构:元素之间存在一对一的关系(如数组、链表)。

      • 树形结构:元素之间存在一对多的层次关系(如二叉树)。

      • 图形结构:元素之间存在多对多的关系(如图、网络)。

  2. 存储结构(物理结构):

    • 定义:存储结构指数据在计算机内存中的表示形式。

    • 分类

      • 顺序存储:数据元素按顺序存放在连续的存储单元中(如数组)。

      • 链式存储:数据元素存放在任意存储单元中,通过指针相互链接(如链表)。

      • 索引存储:在存储数据的同时,建立附加的索引表来加快查找速度。

      • 散列存储:根据关键字直接计算存储地址(如哈希表)。

  3. 基本操作

    • 定义:基本操作指在特定的数据结构上进行的基本运算。

    • 常见操作:插入、删除、查找、更新、遍历等。

1. 基本概念

  1. 数据

    • 定义:所有能输入到计算机中并被处理的符号的总称。

  2. 数据元素

    • 定义:数据的基本单位,在计算机中进行处理的最小单元。

    • 例子:在数组中,每一个元素都是一个数据元素。

  3. 数据对象

    • 定义:性质相同的数据元素的集合,是数据与数据元素的集合。

    • 例子:整数数据对象,包括所有整数。

  4. 数据类型

    • 定义:数据及其在计算机中的存储形式和能施加在这些数据上的运算。

    • 分类

      • 原子类型:不可再分的数据类型(如整型、字符型)。

      • 结构类型:可以再分的数据类型(如数组、结构体)。

  5. 数据结构

    • 定义:相互之间存在一种或多种特定关系的数据元素的集合。

    • 常见数据结构:数组、链表、栈、队列、树、图。

2. 算法分析

  1. 时间复杂度

    • 定义:衡量算法的运行时间随输入规模增长的变化率。

    • 常见符号:大O符号表示,常见的有O(1)、O(n)、O(log n)、O(n²)等。

    • 分析方法:通过分析算法的语句执行次数,得出时间复杂度。

  2. 空间复杂度

    • 定义:衡量算法在运行过程中所需存储空间的大小。

    • 常见符号:同样使用大O符号,常见的有O(1)、O(n)、O(n²)等。

    • 分析方法:通过分析算法中所需的辅助空间(如数组、变量等),得出空间复杂度。

好的,让我们详细探讨线性表的各个知识点。

二、线性表

线性表是一种最基本的数据结构,其特点是数据元素之间存在线性关系。

1. 顺序表

顺序表是线性表的一种顺序存储方式,即用一组地址连续的存储单元依次存储线性表的数据元素。

a) 插入/删除过程,平均移动元素个数
  1. 插入操作

    • 过程

      1. 确定插入位置。

      2. 从插入位置起,将插入位置后的元素依次向后移动一位。

      3. 在插入位置插入新的元素。

    • 时间复杂度:在最坏情况下(插入到第一个位置),需要移动n个元素;平均情况下,需要移动n/2个元素,因此平均时间复杂度为O(n)。

  2. 删除操作

    • 过程

      1. 确定删除位置。

      2. 从删除位置起,将删除位置后的元素依次向前移动一位,覆盖删除位置的元素。

    • 时间复杂度:在最坏情况下(删除第一个元素),需要移动n-1个元素;平均情况下,需要移动(n-1)/2个元素,因此平均时间复杂度为O(n)。

2. 单链表

单链表是线性表的一种链式存储方式。每个节点包含数据和指向下一个节点的指针。

a) 插入/删除操作
  1. 插入操作

    • 过程

      1. 创建一个新节点。

      2. 将新节点的指针指向插入位置后的节点。

      3. 将插入位置前一个节点的指针指向新节点。

    • 时间复杂度:在已知插入位置的前一个节点的情况下,时间复杂度为O(1)。若需查找插入位置,最坏情况下时间复杂度为O(n)。

  2. 删除操作

    • 过程

      1. 将待删除节点前一个节点的指针指向待删除节点的下一个节点。

      2. 释放待删除节点的内存。

    • 时间复杂度:在已知待删除节点的前一个节点的情况下,时间复杂度为O(1)。若需查找待删除节点,最坏情况下时间复杂度为O(n)。

3. 基本操作:增删改查

  1. 增(插入)

    • 顺序表:插入时需要移动插入位置后的元素,时间复杂度为O(n)。

    • 单链表:在已知插入位置的前一个节点的情况下,插入操作时间复杂度为O(1);否则为O(n)。

  2. 删(删除)

    • 顺序表:删除时需要移动删除位置后的元素,时间复杂度为O(n)。

    • 单链表:在已知删除位置的前一个节点的情况下,删除操作时间复杂度为O(1);否则为O(n)。

  3. 改(修改)

    • 顺序表:直接通过索引访问并修改元素,时间复杂度为O(1)。

    • 单链表:需要遍历链表找到待修改的节点,最坏情况下时间复杂度为O(n)。

  4. 查(查找)

    • 顺序表:直接通过索引访问元素,时间复杂度为O(1)。

    • 单链表:需要遍历链表找到目标元素,最坏情况下时间复杂度为O(n)。

让我们详细探讨栈与队列的各个知识点。

三、栈与队列

1. 栈 (Stack)

栈是一种后进先出(FILO, First In Last Out)的数据结构,即最后压入栈的元素最先弹出。

a) 出入栈操作
  1. 基本操作

    • 入栈(Push):将元素压入栈顶。

    • 出栈(Pop):将栈顶元素弹出。

    • 获取栈顶元素(Top/Peek):返回栈顶元素但不弹出。

  2. 实现细节

    • 栈顶指针:通常使用一个指针(如top)来指示栈顶位置。初始时,栈为空,top设为-1。

    • 入栈操作:将top加1,然后在top位置插入元素。

    • 出栈操作:读取top位置的元素,然后将top减1。

b) 栈的可能出栈序列

栈的出栈顺序是受其入栈顺序和出栈时的操作顺序共同决定的。对于一个给定的入栈序列,可能的出栈序列取决于具体的出栈顺序。例如,对于入栈序列1, 2, 3,可能的出栈序列有:

  • 1, 2, 3

  • 1, 3, 2

  • 2, 1, 3

  • 2, 3, 1

  • 3, 2, 1

2. 队列 (Queue)

队列是一种先进先出(FIFO, First In First Out)的数据结构,即最先进入队列的元素最先被移除。

a) 循环队列

循环队列是一种通过数组实现的队列,采用循环的方式来利用数组空间,以解决顺序队列因数据移动导致的效率低下问题。

  1. 基本操作

    • 入队(Enqueue):将元素插入队尾。

    • 出队(Dequeue):将队首元素移除。

  2. 实现细节

    • 队首指针(front):指示队列的第一个元素位置。

    • 队尾指针(rear):指示队列最后一个元素的下一个位置。

    • 入队操作:在rear位置插入元素,然后将rear加1(若超出数组末尾则回到数组开头)。

    • 出队操作:读取front位置的元素,然后将front加1(若超出数组末尾则回到数组开头)。

b) 判空/判满条件
  1. 判空条件

    • front等于rear时,队列为空。

  2. 判满条件

    • 通常会留一个空位以区分队列满和队列空的情况。当(rear + 1) % 数组长度 == front时,队列为满。

总结如下:

// 队列判空
bool isEmpty() {
    return front == rear;
}
​
// 队列判满
bool isFull() {
    return (rear + 1) % capacity == front;
}
​
// 入队操作
void enqueue(int element) {
    if (!isFull()) {
        data[rear] = element;
        rear = (rear + 1) % capacity;
    } else {
        // 处理队列满的情况
    }
}
​
// 出队操作
int dequeue() {
    if (!isEmpty()) {
        int element = data[front];
        front = (front + 1) % capacity;
        return element;
    } else {
        // 处理队列空的情况
        return -1; // 或抛出异常
    }
}

四、串

字符串(串)是一种特殊的线性表,数据元素是字符。

串的模式匹配

模式匹配(Pattern Matching)是指在一个文本串(Text)中寻找一个模式串(Pattern)的出现位置。常用的两种模式匹配算法是传统的暴力匹配法和KMP(Knuth-Morris-Pratt)算法。

1. 传统方法(暴力匹配)

暴力匹配是最简单的模式匹配算法,它逐个字符比较模式串和文本串的字符,直到找到匹配或所有位置都尝试过。

  • 算法描述

    1. 从文本串的第一个字符开始,逐个字符与模式串的第一个字符比较。

    2. 如果匹配,则继续比较模式串的下一个字符和文本串的下一个字符。

    3. 如果某个字符不匹配,则回到文本串的下一个字符重新开始比较。

    4. 重复上述步骤,直到找到匹配或所有字符都比较完。

  • 时间复杂度:最坏情况下为O(m*n),其中m是文本串的长度,n是模式串的长度。

2. KMP模式匹配

KMP算法通过预处理模式串,生成部分匹配表(Next数组),在匹配过程中利用这个表来避免重复比较,从而提高效率。

  • Next数组:Next数组记录了模式串中各个位置的部分匹配信息。Next[j]表示模式串在位置j之前的子串中,最长的相同前后缀的长度。

  • Next数组的计算

    • 初始化:Next[0] = -1。

    • 遍历模式串,根据已知的部分匹配信息,计算每个位置的Next值。

  • Next数组计算示例(模式串 "goodgoogle"):

    模式串: g o o d g o o g l e
    Next值: 0 1 1 1 2 3 4 1 1 1

    由于Next数组的计算涉及到模式串本身的匹配关系,这里简化为常见的模式串示例。

  • KMP算法描述

    1. 预处理模式串,计算Next数组。

    2. 在文本串中逐个字符比较模式串和文本串的字符。

    3. 如果某个字符不匹配,则利用Next数组调整模式串的位置,避免重复比较。

    4. 重复上述步骤,直到找到匹配或所有字符都比较完。

  • 时间复杂度:O(m+n),其中m是文本串的长度,n是模式串的长度。

KMP算法的实现
#include <iostream>
#include <vector>
#include <string>
​
// 计算Next数组
void computeNextArray(const std::string &pattern, std::vector<int> &next) {
    int m = pattern.length();
    int j = 0; // 前缀末尾
    next[0] = 0; // next数组的第一个值为0
    for (int i = 1; i < m; ++i) {
        while (j > 0 && pattern[i] != pattern[j]) {
            j = next[j - 1]; // 利用next数组跳转
        }
        if (pattern[i] == pattern[j]) {
            ++j;
        }
        next[i] = j;
    }
}
​
// KMP算法
void KMP(const std::string &text, const std::string &pattern) {
    int n = text.length();
    int m = pattern.length();
    std::vector<int> next(m, 0);
​
    // 计算next数组
    computeNextArray(pattern, next);
​
    int j = 0; // 模式串指针
    for (int i = 0; i < n; ++i) {
        while (j > 0 && text[i] != pattern[j]) {
            j = next[j - 1]; // 利用next数组跳转
        }
        if (text[i] == pattern[j]) {
            ++j;
        }
        if (j == m) { // 找到一个匹配
            std::cout << "Pattern found at index " << i - m + 1 << std::endl;
            j = next[j - 1]; // 寻找下一个匹配
        }
    }
}
​
int main() {
    std::string text = "goodgoogle";
    std::string pattern = "google";
    KMP(text, pattern);
    return 0;
}

好的,让我们详细探讨数组与广义表的各个知识点。

五、数组与广义表

1. 数组

数组是一种线性数据结构,用于存储相同类型的数据元素,具有随机访问的特点。

a) 特殊矩阵压缩存储

对于特殊矩阵,可以采用压缩存储方式以节省存储空间。常见的特殊矩阵有三角矩阵、带状矩阵和稀疏矩阵。

  1. 三角矩阵

    • 上三角矩阵:只存储主对角线及其上方的元素,其余元素均为零。

      • 存储方法:将上三角矩阵的非零元素按行或列存储在一个一维数组中。

      • 存储公式(按行存储):A[i][j](i ≤ j)存储在一维数组B中的位置为i*n + j - i*(i+1)/2

    • 下三角矩阵:只存储主对角线及其下方的元素,其余元素均为零。

      • 存储方法:将下三角矩阵的非零元素按行或列存储在一个一维数组中。

      • 存储公式(按行存储):A[i][j](i ≥ j)存储在一维数组B中的位置为i*(i+1)/2 + j

  2. 带状矩阵(带状矩阵)

    • 带状矩阵是在主对角线及其附近带状区域内有非零元素,带区以外的元素均为零。

    • 存储方法:仅存储带区内的元素,将带区按行或列存储在一个一维数组中。

    • 存储公式取决于带宽和具体带区位置。

  3. 稀疏矩阵

    • 稀疏矩阵是指非零元素相对较少的矩阵。

    • 存储方法:采用三元组(行索引,列索引,非零元素值)或压缩稀疏行(CSR)格式存储。

    • 三元组存储:将每个非零元素的信息存储为一个三元组。

b) 二维数组元素位置关系

二维数组在内存中通常按行优先或列优先顺序存储。假设二维数组Amn列,且按行优先存储。

  • 行优先存储公式

    • A[i][j]在内存中的位置为:L0 + (i * n + j) * size,其中L0是数组首地址,size是元素的大小。

2. 广义表

广义表(Generalized List)是一种递归的数据结构,可以包含原子(单个元素)和子表(广义表本身)。

a) Head/Tail
  1. Head

    • 广义表的第一个元素。如果这个元素是原子,则Head是这个原子;如果是子表,则Head是这个子表。

  2. Tail

    • 广义表中除了第一个元素以外的部分。如果原广义表只有一个元素,则Tail为空表。

  • 广义表的例子

    • 广义表L = (a, (b, c), d)

      • Head(L) = a

      • Tail(L) = ((b, c), d)

  • 广义表的递归定义

    1. 若广义表为空表,则为空表。

    2. 若广义表非空,则可表示为(Head, Tail),其中Head是第一个元素,Tail是剩余元素构成的广义表。

好的,让我们详细探讨树与二叉树的各个知识点。

六、树与二叉树

1. 二叉树基本概念与性质

基本概念
  • 二叉树:每个节点最多有两个子节点的树结构,通常称为左子节点和右子节点。

  • 节点的度:节点的子节点数目。

  • 叶子节点:没有子节点的节点。

  • 内部节点:至少有一个子节点的节点。

  • 树的高度:树中节点的最大层次。

二叉树的五大性质
  1. 性质1:在二叉树的第 (i) 层上,最多有 (2^{i-1}) 个节点((i \geq 1))。

  2. 性质2:深度为 (k) 的二叉树至多有 (2^k - 1) 个节点((k \geq 1))。

  3. 性质3:对任何一棵二叉树,如果其叶子节点数为 (n_0),度为2的节点数为 (n_2),则 (n_0 = n_2 + 1)。

  4. 性质4:具有 (n) 个节点的二叉树的高度最少为 (\lceil \log_2 (n + 1) \rceil)。

  5. 性质5:如果在二叉树中有 (n) 个节点,其中度为1的节点数为 (n_1),度为2的节点数为 (n_2),则 (n = n_0 + n_1 + n_2)。

2. 二叉树的遍历

遍历是指按一定的顺序访问树中的每一个节点。

前序遍历(Preorder)

顺序:根节点 -> 左子树 -> 右子树

void preorderTraversal(Node* root) {
    if (root) {
        visit(root);
        preorderTraversal(root->left);
        preorderTraversal(root->right);
    }
}
中序遍历(Inorder)

顺序:左子树 -> 根节点 -> 右子树

void inorderTraversal(Node* root) {
    if (root) {
        inorderTraversal(root->left);
        visit(root);
        inorderTraversal(root->right);
    }
}
后序遍历(Postorder)

顺序:左子树 -> 右子树 -> 根节点

void postorderTraversal(Node* root) {
    if (root) {
        postorderTraversal(root->left);
        postorderTraversal(root->right);
        visit(root);
    }
}

3. 树、二叉树、森林的转换

树、二叉树、森林的定义
  • :一种包含n个节点的有序集合,其中每个节点有零个或多个子节点。

  • 二叉树:每个节点最多有两个子节点的树。

  • 森林:由 (m) 棵互不相交的树的集合。

左孩子右兄弟表示法

将树或森林转换为二叉树的一种方法。

  • 转换规则

    1. 树的每个节点的左子树是该节点的第一个子节点。

    2. 树的每个节点的右子树是该节点的右兄弟。

遍历关系
  • 树的前根遍历等价于二叉树的前序遍历

  • 树的后根遍历等价于二叉树的中序遍历

  • 森林的前序遍历等价于二叉树的前序遍历

  • 森林的中序遍历等价于二叉树的中序遍历

4. 哈夫曼树

哈夫曼树(Huffman Tree)是一种最优二叉树,用于构造哈夫曼编码以进行无损数据压缩。

构造哈夫曼树
  1. 步骤

    1. 根据给定的字符及其频率,将每个字符视为一个独立的节点。

    2. 将所有节点按频率升序排列。

    3. 选择频率最小的两个节点作为新节点的左右子节点,构造一个新节点,其频率为左右子节点频率之和。

    4. 将新节点加入序列中,重新按频率排列。

    5. 重复上述步骤,直到所有节点合并为一棵哈夫曼树。

哈夫曼编码
  1. 编码规则

    • 从哈夫曼树根节点出发,向左子节点标记为0,向右子节点标记为1。

    • 从根节点到叶子节点的路径即为该叶子节点(字符)的哈夫曼编码。

平均编码长度

平均编码长度是各字符编码长度的加权平均值,权值为字符的频率。

[ \text{平均编码长度} = \sum_{i=1}^n (f_i \times l_i) ]

其中 ( f_i ) 是字符 ( i ) 的频率,( l_i ) 是字符 ( i ) 的哈夫曼编码长度。

示例代码
#include <iostream>
#include <queue>
#include <vector>
​
using namespace std;
​
struct HuffmanNode {
    char data;
    int frequency;
    HuffmanNode* left;
    HuffmanNode* right;
    HuffmanNode(char d, int f) : data(d), frequency(f), left(nullptr), right(nullptr) {}
};
​
struct Compare {
    bool operator()(HuffmanNode* a, HuffmanNode* b) {
        return a->frequency > b->frequency;
    }
};
​
void printHuffmanCodes(HuffmanNode* root, string code) {
    if (!root) return;
    if (root->data != '$') cout << root->data << ": " << code << endl;
    printHuffmanCodes(root->left, code + "0");
    printHuffmanCodes(root->right, code + "1");
}
​
void buildHuffmanTree(vector<char>& chars, vector<int>& freqs) {
    priority_queue<HuffmanNode*, vector<HuffmanNode*>, Compare> minHeap;
    for (int i = 0; i < chars.size(); ++i) {
        minHeap.push(new HuffmanNode(chars[i], freqs[i]));
    }
​
    while (minHeap.size() > 1) {
        HuffmanNode* left = minHeap.top(); minHeap.pop();
        HuffmanNode* right = minHeap.top(); minHeap.pop();
        HuffmanNode* newNode = new HuffmanNode('$', left->frequency + right->frequency);
        newNode->left = left;
        newNode->right = right;
        minHeap.push(newNode);
    }
​
    HuffmanNode* root = minHeap.top();
    printHuffmanCodes(root, "");
}
​
int main() {
    vector<char> chars = {'a', 'b', 'c', 'd', 'e', 'f'};
    vector<int> freqs = {5, 9, 12, 13, 16, 45};
    buildHuffmanTree(chars, freqs);
    return 0;
}

以上代码展示了构造哈夫曼树及生成哈夫曼编码的过程。

好的,让我们详细探讨图的各个知识点。

七、图

1. 基本概念

  • 有向图(Directed Graph, DG):由一组顶点和一组有方向的边组成,边表示从一个顶点到另一个顶点的方向。

  • 无向图(Undirected Graph, UDG):由一组顶点和一组无方向的边组成,边表示顶点之间的连接。

  • 有向网(Directed Network, DN):有向图中,边具有权值。

  • 无向网(Undirected Network, UDN):无向图中,边具有权值。

  • 完全有向图:每两个顶点之间都存在方向相反的两条边。

  • 完全无向图:每两个顶点之间都存在一条边。

  • 连通图:在无向图中,任意两个顶点之间都存在路径。

  • 强连通图:在有向图中,任意两个顶点之间都存在双向路径。

2. 邻接矩阵/邻接表

邻接矩阵

邻接矩阵是一种表示图的方法,用一个二维数组来表示顶点之间的连接关系。

  • 定义:如果图中有 ( n ) 个顶点,使用一个 ( n \times n ) 的二维数组 ( A ),其中 ( Ai ) 表示顶点 ( i ) 和顶点 ( j ) 之间的关系。

    • 对于无向图:( Ai = 1 ) 表示顶点 ( i ) 和顶点 ( j ) 之间有边,( 0 ) 表示没有边。

    • 对于有向图:( Ai = 1 ) 表示从顶点 ( i ) 到顶点 ( j ) 有一条边,( 0 ) 表示没有边。

    • 对于带权图:( Ai ) 可以表示边的权值。

邻接表

邻接表是一种更节省空间的表示图的方法,特别适用于稀疏图。

  • 定义:对于图中的每个顶点,使用一个链表来存储与该顶点相邻的所有顶点。

    • 无向图中,顶点 ( i ) 和顶点 ( j ) 之间有边,则在顶点 ( i ) 的链表中包含顶点 ( j ),在顶点 ( j ) 的链表中包含顶点 ( i )。

    • 有向图中,顶点 ( i ) 到顶点 ( j ) 有边,则在顶点 ( i ) 的链表中包含顶点 ( j )。

3. 图的遍历

深度优先搜索(DFS)

深度优先搜索是一种图的遍历算法,从起始顶点开始,尽可能深入每一个未访问的邻接顶点,直到所有顶点都被访问。

void DFS(Graph &G, int v, vector<bool> &visited) {
    visit(v);
    visited[v] = true;
    for (int w : G.adj(v)) {
        if (!visited[w]) {
            DFS(G, w, visited);
        }
    }
}
广度优先搜索(BFS)

广度优先搜索是一种图的遍历算法,从起始顶点开始,首先访问所有相邻顶点,然后再访问这些顶点的相邻顶点,依次类推。

void BFS(Graph &G, int v) {
    queue<int> q;
    vector<bool> visited(G.V(), false);
    visit(v);
    visited[v] = true;
    q.push(v);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int w : G.adj(u)) {
            if (!visited[w]) {
                visit(w);
                visited[w] = true;
                q.push(w);
            }
        }
    }
}

4. 图的应用

a) 最小生成树

普里姆算法(Prim's Algorithm)

普里姆算法从一个起始顶点开始,逐步扩展生成树,每次将具有最小权值的边加入生成树。

void Prim(Graph &G, int start) {
    vector<bool> inMST(G.V(), false);
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
    pq.push({0, start});
    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();
        if (inMST[u]) continue;
        inMST[u] = true;
        for (auto &[v, weight] : G.adj(u)) {
            if (!inMST[v]) {
                pq.push({weight, v});
            }
        }
    }
}

克鲁斯卡尔算法(Kruskal's Algorithm)

克鲁斯卡尔算法通过排序所有边,按权值从小到大选择边,加入生成树中,确保不形成环。

void Kruskal(Graph &G) {
    vector<Edge> edges = G.getAllEdges();
    sort(edges.begin(), edges.end());
    UnionFind uf(G.V());
    for (Edge e : edges) {
        if (!uf.connected(e.u, e.v)) {
            uf.union(e.u, e.v);
            mst.push_back(e);
        }
    }
}
b) AOV网拓扑排序

拓扑排序是对有向无环图(AOV网)顶点的一种线性排序,使得对于每一条有向边 ( u \rightarrow v ),顶点 ( u ) 在排序中出现在顶点 ( v ) 之前。

void topologicalSort(Graph &G) {
    vector<int> inDegree(G.V(), 0);
    for (int u = 0; u < G.V(); ++u) {
        for (int v : G.adj(u)) {
            inDegree[v]++;
        }
    }
    queue<int> q;
    for (int i = 0; i < G.V(); ++i) {
        if (inDegree[i] == 0) {
            q.push(i);
        }
    }
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        cout << u << " ";
        for (int v : G.adj(u)) {
            if (--inDegree[v] == 0) {
                q.push(v);
            }
        }
    }
}
c) 最短路径

迪杰斯特拉算法(Dijkstra's Algorithm)

迪杰斯特拉算法用于单源最短路径,计算从起始顶点到其他所有顶点的最短路径。

void Dijkstra(Graph &G, int src) {
    vector<int> dist(G.V(), INT_MAX);
    dist[src] = 0;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
    pq.push({0, src});
    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();
        for (auto &[v, weight] : G.adj(u)) {
            if (dist[u] + weight < dist[v]) {
                dist[v] = dist[u] + weight;
                pq.push({dist[v], v});
            }
        }
    }
}

弗洛伊德算法(Floyd-Warshall Algorithm)

弗洛伊德算法用于计算所有顶点对之间的最短路径。

void FloydWarshall(Graph &G) {
    vector<vector<int>> dist(G.V(), vector<int>(G.V(), INT_MAX));
    for (int u = 0; u < G.V(); ++u) {
        for (int v : G.adj(u)) {
            dist[u][v] = G.weight(u, v);
        }
        dist[u][u] = 0;
    }
    for (int k = 0; k < G.V(); ++k) {
        for (int i = 0; i < G.V(); ++i) {
            for (int j = 0; j < G.V(); ++j) {
                if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX) {
                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
                }
            }
        }
    }
}

八、查找

1. 顺序查找

顺序查找(Sequential Search),又称线性查找,是最简单的一种查找算法。它从数据结构的第一个元素开始,逐个比较,直到找到目标元素或遍历完整个结构。

算法实现
int sequentialSearch(int arr[], int n, int target) {
    for (int i = 0; i < n; ++i) {
        if (arr[i] == target) {
            return i;  // 找到目标,返回其索引
        }
    }
    return -1;  // 未找到目标,返回-1
}
时间复杂度
  • 最好情况:目标元素在第一个位置,时间复杂度为 (O(1))。

  • 最坏情况:目标元素在最后一个位置或不存在,时间复杂度为 (O(n))。

  • 平均情况:时间复杂度为 (O(n))。

2. 二分查找

二分查找(Binary Search)是一种效率较高的查找算法,适用于有序数组。它通过逐步将查找范围缩小一半,快速定位目标元素。

算法实现
int binarySearch(int arr[], int left, int right, int target) {
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid;  // 找到目标,返回其索引
        } else if (arr[mid] < target) {
            left = mid + 1;  // 目标在右半部分
        } else {
            right = mid - 1;  // 目标在左半部分
        }
    }
    return -1;  // 未找到目标,返回-1
}
时间复杂度
  • 最好情况:目标元素正好位于中间位置,时间复杂度为 (O(1))。

  • 最坏情况:需要多次折半,时间复杂度为 (O(\log n))。

  • 平均情况:时间复杂度为 (O(\log n))。

3. 平均查找长度(ASL)

平均查找长度(Average Search Length, ASL)是评价查找算法性能的一个重要指标,表示查找到目标元素时,所需比较的平均次数。

顺序查找的ASL

对于顺序查找,假设数组中有 (n) 个元素,目标元素在各个位置的概率相等,则:

[ \text{ASL} = \frac{1 + 2 + 3 + \cdots + n}{n} = \frac{n(n+1)}{2n} = \frac{n+1}{2} ]

二分查找的ASL

对于二分查找,查找成功时的ASL主要取决于查找次数(比较次数)。假设有 (n) 个元素:

[ \text{ASL} \approx \log_2(n+1) ]

因为二分查找每次将查找范围减半,因此查找次数与数组的对数成正比。

4. 总结

  • 顺序查找适用于无序或小规模的数据结构,但时间复杂度较高。

  • 二分查找适用于有序数组,具有较高的查找效率。

  • ASL可以用来衡量不同查找算法的平均性能。

好的,让我们详细探讨各种内部排序算法,包括插入类、交换类、选择类排序以及归并排序,并分析它们的时间复杂度、空间复杂度和稳定性。

九、内部排序

插入类排序

1. 直接插入排序

直接插入排序是一种简单的排序算法,通过将每个元素插入到已排好序的部分中。

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; ++i) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}
  • 时间复杂度:平均 (O(n^2)),最好 (O(n)),最坏 (O(n^2))

  • 空间复杂度:(O(1))

  • 稳定性:稳定

2. 希尔排序

希尔排序(Shell Sort)是直接插入排序的改进,通过分组进行插入排序,然后逐渐缩小间隔进行排序。

void shellSort(int arr[], int n) {
    for (int gap = n / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < n; ++i) {
            int temp = arr[i];
            int j;
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap];
            }
            arr[j] = temp;
        }
    }
}
  • 时间复杂度:平均 (O(n \log n)) 到 (O(n^{1.5})),最好 (O(n \log^2 n)),最坏 (O(n^2))

  • 空间复杂度:(O(1))

  • 稳定性:不稳定

交换类排序

1. 冒泡排序

冒泡排序通过多次遍历数组,每次比较相邻元素并交换,将较大的元素逐渐移动到数组末尾。

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; ++i) {
        for (int j = 0; j < n - i - 1; ++j) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}
  • 时间复杂度:平均 (O(n^2)),最好 (O(n)),最坏 (O(n^2))

  • 空间复杂度:(O(1))

  • 稳定性:稳定

2. 快速排序

快速排序通过选择一个“基准”元素,将数组分成两部分,一部分比基准小,另一部分比基准大,然后递归排序。

int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; ++j) {
        if (arr[j] <= pivot) {
            ++i;
            swap(arr[i], arr[j]);
        }
    }
    swap(arr[i + 1], arr[high]);
    return i + 1;
}
​
void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}
  • 时间复杂度:平均 (O(n \log n)),最好 (O(n \log n)),最坏 (O(n^2))

  • 空间复杂度:平均 (O(\log n))

  • 稳定性:不稳定

选择类排序

1. 简单选择排序

简单选择排序每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾。

void selectionSort(int arr[], int n) {
    for (int i = 0; i < n - 1; ++i) {
        int minIdx = i;
        for (int j = i + 1; j < n; ++j) {
            if (arr[j] < arr[minIdx]) {
                minIdx = j;
            }
        }
        swap(arr[minIdx], arr[i]);
    }
}
  • 时间复杂度:(O(n^2))

  • 空间复杂度:(O(1))

  • 稳定性:不稳定

2. 堆排序

堆排序利用堆这种数据结构来排序,通过构建最大堆或最小堆,每次取出堆顶元素实现排序。

void heapify(int arr[], int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    if (left < n && arr[left] > arr[largest]) largest = left;
    if (right < n && arr[right] > arr[largest]) largest = right;
    if (largest != i) {
        swap(arr[i], arr[largest]);
        heapify(arr, n, largest);
    }
}
​
void heapSort(int arr[], int n) {
    for (int i = n / 2 - 1; i >= 0; --i) {
        heapify(arr, n, i);
    }
    for (int i = n - 1; i > 0; --i) {
        swap(arr[0], arr[i]);
        heapify(arr, i, 0);
    }
}
  • 时间复杂度:(O(n \log n))

  • 空间复杂度:(O(1))

  • 稳定性:不稳定

归并排序

归并排序利用分治法,将数组递归地分成两半,然后合并两个有序子数组。

void merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;
    int L[n1], R[n2];
    for (int i = 0; i < n1; ++i) L[i] = arr[left + i];
    for (int i = 0; i < n2; ++i) R[i] = arr[mid + 1 + i];
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}
​
void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}
  • 时间复杂度:(O(n \log n))

  • 空间复杂度:(O(n))

  • 稳定性:稳定

排序算法总结表

排序算法平均时间复杂度最好时间复杂度最坏时间复杂度空间复杂度稳定性
直接插入排序(O(n^2))(O(n))(O(n^2))(O(1))稳定
希尔排序(O(n \log n))(O(n \log^2 n))(O(n^2))(O(1))不稳定
冒泡排序(O(n^2))(O(n))(O(n^2))(O(1))稳定
快速排序(O(n \log n))(O(n \log n))(O(n^2))(O(\log n))不稳定
简单选择排序(O(n^2))(O(n^2))(O(n^2))(O(1))不稳定
堆排序(O(n \log n))(O(n \log n))(O(n \log n))(O(1))不稳定
归并排序(O(n \log n))(O(n \log n))(O(n \log n))(O(n))稳定
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值